From 0bd8f901df4ca2259b9114f6c4cb986ae012c71d Mon Sep 17 00:00:00 2001 From: bilal alpaslan <47563997+BilalAlpaslan@users.noreply.github.com> Date: Tue, 26 Oct 2021 21:13:34 +0300 Subject: [PATCH 0001/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/python-types.md`=20(#3926)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/python-types.md | 314 +++++++++++++++++++++++++++++++++++ docs/tr/mkdocs.yml | 1 + 2 files changed, 315 insertions(+) create mode 100644 docs/tr/docs/python-types.md diff --git a/docs/tr/docs/python-types.md b/docs/tr/docs/python-types.md new file mode 100644 index 000000000..7e46bd031 --- /dev/null +++ b/docs/tr/docs/python-types.md @@ -0,0 +1,314 @@ +# Python Veri Tiplerine Giriş + +Python isteğe bağlı olarak "tip belirteçlerini" destekler. + + **"Tip belirteçleri"** bir değişkenin tipinin belirtilmesine olanak sağlayan özel bir sözdizimidir. + +Değişkenlerin tiplerini belirterek editör ve araçlardan daha fazla destek alabilirsiniz. + +Bu pythonda tip belirteçleri için **hızlı bir başlangıç / bilgi tazeleme** rehberidir . Bu rehber **FastAPI** kullanmak için gereken minimum konuyu kapsar ki bu da çok az bir miktardır. + +**FastAPI' nin** tamamı bu tür tip belirteçleri ile donatılmıştır ve birçok avantaj sağlamaktadır. + +**FastAPI** kullanmayacak olsanız bile tür belirteçleri hakkında bilgi edinmenizde fayda var. + +!!! not + Python uzmanıysanız ve tip belirteçleri ilgili her şeyi zaten biliyorsanız, sonraki bölüme geçin. + +## Motivasyon + +Basit bir örnek ile başlayalım: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Programın çıktısı: + +``` +John Doe +``` + +Fonksiyon sırayla şunları yapar: + +* `first_name` ve `last_name` değerlerini alır. +* `title()` ile değişkenlerin ilk karakterlerini büyütür. +* Değişkenleri aralarında bir boşlukla beraber Birleştirir. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Düzenle + +Bu çok basit bir program. + +Ama şimdi sıfırdan yazdığınızı hayal edin. + +Bir noktada fonksiyonun tanımına başlayacaktınız, parametreleri hazır hale getirdiniz... + +Ama sonra "ilk harfi büyük harfe dönüştüren yöntemi" çağırmanız gerekir. + + `upper` mıydı ? Yoksa `uppercase`' mi? `first_uppercase`? `capitalize`? + +Ardından, programcıların en iyi arkadaşı olan otomatik tamamlama ile denediniz. + +'first_name', ardından bir nokta ('.') yazıp otomatik tamamlamayı tetiklemek için 'Ctrl+Space' tuşlarına bastınız. + +Ancak, ne yazık ki, yararlı hiçbir şey elde edemediniz: + + + +### Tipleri ekle + +Önceki sürümden sadece bir satırı değiştirelim. + +Tam olarak bu parçayı, işlevin parametrelerini değiştireceğiz: + +```Python + first_name, last_name +``` + +ve bu hale getireceğiz: + +```Python + first_name: str, last_name: str +``` + +Bu kadar. + +İşte bunlar "tip belirteçleri": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Bu, aşağıdaki gibi varsayılan değerleri bildirmekle aynı şey değildir: + +```Python + first_name="john", last_name="doe" +``` + +Bu tamamen farklı birşey + +İki nokta üst üste (`:`) kullanıyoruz , eşittir (`=`) değil. + +Normalde tip belirteçleri eklemek, kod üzerinde olacakları değiştirmez. + +Şimdi programı sıfırdan birdaha yazdığınızı hayal edin. + +Aynı noktada, `Ctrl+Space` ile otomatik tamamlamayı tetiklediniz ve şunu görüyorsunuz: + + + +Aradığınızı bulana kadar seçenekleri kaydırabilirsiniz: + + + +## Daha fazla motivasyon + +Bu fonksiyon, zaten tür belirteçlerine sahip: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Editör değişkenlerin tiplerini bildiğinden, yalnızca otomatik tamamlama değil, hata kontrolleri de sağlar: + + + +Artık `age` değişkenini `str(age)` olarak kullanmanız gerektiğini biliyorsunuz: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Tip bildirme + +Az önce tip belirteçlerinin en çok kullanıldığı yeri gördünüz. + + **FastAPI**ile çalışırken tip belirteçlerini en çok kullanacağımız yer yine fonksiyonlardır. + +### Basit tipler + +Yalnızca `str` değil, tüm standart Python tiplerinin bildirebilirsiniz. + +Örneğin şunları kullanabilirsiniz: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Tip parametreleri ile Generic tipler + +"dict", "list", "set" ve "tuple" gibi diğer değerleri içerebilen bazı veri yapıları vardır. Ve dahili değerlerinin de tip belirtecleri olabilir. + +Bu tipleri ve dahili tpileri bildirmek için standart Python modülünü "typing" kullanabilirsiniz. + +Bu tür tip belirteçlerini desteklemek için özel olarak mevcuttur. + +#### `List` + +Örneğin `str` değerlerden oluşan bir `list` tanımlayalım. + +From `typing`, import `List` (büyük harf olan `L` ile): + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +Değişkenin tipini yine iki nokta üstüste (`:`) ile belirleyin. + +tip olarak `List` kullanın. + +Liste, bazı dahili tipleri içeren bir tür olduğundan, bunları köşeli parantez içine alırsınız: + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +!!! ipucu + Köşeli parantez içindeki bu dahili tiplere "tip parametreleri" denir. + + Bu durumda `str`, `List`e iletilen tür parametresidir. + +Bunun anlamı şudur: "`items` değişkeni bir `list`tir ve bu listedeki öğelerin her biri bir `str`dir". + +Bunu yaparak, düzenleyicinizin listedeki öğeleri işlerken bile destek sağlamasını sağlayabilirsiniz: + + + +Tip belirteçleri olmadan, bunu başarmak neredeyse imkansızdır. + +`item` değişkeninin `items` listesindeki öğelerden biri olduğuna dikkat edin. + +Ve yine, editör bunun bir `str` ​​olduğunu biliyor ve bunun için destek sağlıyor. + +#### `Tuple` ve `Set` + +`Tuple` ve `set`lerin tiplerini bildirmek için de aynısını yapıyoruz: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +Bu şu anlama geliyor: + +* `items_t` değişkeni sırasıyla `int`, `int`, ve `str` tiplerinden oluşan bir `tuple` türündedir . +* `items_s` ise her öğesi `bytes` türünde olan bir `set` örneğidir. + +#### `Dict` + +Bir `dict` tanımlamak için virgülle ayrılmış iki parametre verebilirsiniz. + +İlk tip parametresi `dict` değerinin `key` değeri içindir. + +İkinci parametre ise `dict` değerinin `value` değeri içindir: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +Bu şu anlama gelir: + +* `prices` değişkeni `dict` tipindedir: + * `dict` değişkeninin `key` değeri `str` tipindedir (herbir item'ın "name" değeri). + * `dict` değişkeninin `value` değeri `float` tipindedir (lherbir item'ın "price" değeri). + +#### `Optional` + +`Optional` bir değişkenin `str`gibi bir tipi olabileceğini ama isteğe bağlı olarak tipinin `None` olabileceğini belirtir: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +`str` yerine `Optional[str]` kullanmak editorün bu değerin her zaman `str` tipinde değil bazen `None` tipinde de olabileceğini belirtir ve hataları tespit etmemizde yardımcı olur. + +#### Generic tipler + +Köşeli parantez içinde tip parametreleri alan bu türler, örneğin: + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Optional` +* ...and others. + +**Generic types** yada **Generics** olarak adlandırılır. + +### Tip olarak Sınıflar + +Bir değişkenin tipini bir sınıf ile bildirebilirsiniz. + +Diyelim ki `name` değerine sahip `Person` sınıfınız var: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Sonra bir değişkeni 'Person' tipinde tanımlayabilirsiniz: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Ve yine bütün editör desteğini alırsınız: + + + +## Pydantic modelleri + +Pydantic veri doğrulaması yapmak için bir Python kütüphanesidir. + +Verilerin "biçimini" niteliklere sahip sınıflar olarak düzenlersiniz. + +Ve her niteliğin bir türü vardır. + +Sınıfın bazı değerlerle bir örneğini oluşturursunuz ve değerleri doğrular, bunları uygun türe dönüştürür ve size tüm verileri içeren bir nesne verir. + +Ve ortaya çıkan nesne üzerindeki bütün editör desteğini alırsınız. + +Resmi Pydantic dokümanlarından alınmıştır: + +```Python +{!../../../docs_src/python_types/tutorial011.py!} +``` + +!!! info + Daha fazla şey öğrenmek için Pydantic'i takip edin. + +**FastAPI** tamamen Pydantic'e dayanmaktadır. + +Daha fazlasini görmek için [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +## **FastAPI** tip belirteçleri + +**FastAPI** birkaç şey yapmak için bu tür tip belirteçlerinden faydalanır. + +**FastAPI** ile parametre tiplerini bildirirsiniz ve şunları elde edersiniz: + +* **Editor desteği**. +* **Tip kontrolü**. + +...ve **FastAPI** aynı belirteçleri şunlar için de kullanıyor: + +* **Gereksinimleri tanımlama**: request path parameters, query parameters, headers, bodies, dependencies, ve benzeri gereksinimlerden +* **Verileri çevirme**: Gönderilen veri tipinden istenilen veri tipine çevirme. +* **Verileri doğrulama**: Her gönderilen verinin: + * doğrulanması ve geçersiz olduğunda **otomatik hata** oluşturma. +* OpenAPI kullanarak apinizi **Belgeleyin** : + * bu daha sonra otomatik etkileşimli dokümantasyon kullanıcı arayüzü tarafından kullanılır. + +Bütün bunlar kulağa soyut gelebilir. Merak etme. Tüm bunları çalışırken göreceksiniz. [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +Önemli olan, standart Python türlerini tek bir yerde kullanarak (daha fazla sınıf, dekoratör vb. eklemek yerine), **FastAPI**'nin bizim için işi yapmasını sağlamak. + +!!! info + Tüm öğreticiyi zaten okuduysanız ve türler hakkında daha fazla bilgi için geri döndüyseniz, iyi bir kaynak: the "cheat sheet" from `mypy`. diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 6fe2b0b62..5ef8fd6a7 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -57,6 +57,7 @@ nav: - zh: /zh/ - features.md - fastapi-people.md +- python-types.md markdown_extensions: - toc: permalink: true From 3c93e803d55b67701f14c25a6da8d9b1f5291696 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Oct 2021 18:14:17 +0000 Subject: [PATCH 0002/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 31953a786..e2e00f912 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). ## 0.70.0 From a859497a72a11e9545ca84a1f87b423a0b9f1cca Mon Sep 17 00:00:00 2001 From: Sam Courtemanche Date: Tue, 26 Oct 2021 20:15:30 +0200 Subject: [PATCH 0003/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/query-params.md`=20(#3556)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruidy --- docs/fr/docs/tutorial/query-params.md | 198 ++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 199 insertions(+) create mode 100644 docs/fr/docs/tutorial/query-params.md diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md new file mode 100644 index 000000000..f1f2a605d --- /dev/null +++ b/docs/fr/docs/tutorial/query-params.md @@ -0,0 +1,198 @@ +# Paramètres de requête + +Quand vous déclarez des paramètres dans votre fonction de chemin qui ne font pas partie des paramètres indiqués dans le chemin associé, ces paramètres sont automatiquement considérés comme des paramètres de "requête". + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +La partie appelée requête (ou **query**) dans une URL est l'ensemble des paires clés-valeurs placées après le `?` , séparées par des `&`. + +Par exemple, dans l'URL : + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...les paramètres de requête sont : + +* `skip` : avec une valeur de`0` +* `limit` : avec une valeur de `10` + +Faisant partie de l'URL, ces valeurs sont des chaînes de caractères (`str`). + +Mais quand on les déclare avec des types Python (dans l'exemple précédent, en tant qu'`int`), elles sont converties dans les types renseignés. + +Toutes les fonctionnalités qui s'appliquent aux paramètres de chemin s'appliquent aussi aux paramètres de requête : + +* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. +* "Parsing" de données. +* Validation de données. +* Annotations d'API et documentation automatique. + +## Valeurs par défaut + +Les paramètres de requête ne sont pas une partie fixe d'un chemin, ils peuvent être optionnels et avoir des valeurs par défaut. + +Dans l'exemple ci-dessus, ils ont des valeurs par défaut qui sont `skip=0` et `limit=10`. + +Donc, accéder à l'URL : + +``` +http://127.0.0.1:8000/items/ +``` + +serait équivalent à accéder à l'URL : + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +Mais si vous accédez à, par exemple : + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +Les valeurs des paramètres de votre fonction seront : + +* `skip=20` : car c'est la valeur déclarée dans l'URL. +* `limit=10` : car `limit` n'a pas été déclaré dans l'URL, et que la valeur par défaut était `10`. + +## Paramètres optionnels + +De la même façon, vous pouvez définir des paramètres de requête comme optionnels, en leur donnant comme valeur par défaut `None` : + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial002.py!} +``` + +Ici, le paramètre `q` sera optionnel, et aura `None` comme valeur par défaut. + +!!! check "Remarque" + On peut voir que **FastAPI** est capable de détecter que le paramètre de chemin `item_id` est un paramètre de chemin et que `q` n'en est pas un, c'est donc un paramètre de requête. + +!!! note + **FastAPI** saura que `q` est optionnel grâce au `=None`. + + Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre editeur de texte pour détecter des erreurs dans votre code. + + +## Conversion des types des paramètres de requête + +Vous pouvez aussi déclarer des paramètres de requête comme booléens (`bool`), **FastAPI** les convertira : + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial003.py!} +``` + +Avec ce code, en allant sur : + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la première lettre en majuscule, etc.), votre fonction considérera le paramètre `short` comme ayant une valeur booléenne à `True`. Sinon la valeur sera à `False`. + +## Multiples paramètres de chemin et de requête + +Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête dans la même fonction, **FastAPI** saura comment les gérer. + +Et vous n'avez pas besoin de les déclarer dans un ordre spécifique. + +Ils seront détectés par leurs noms : + +```Python hl_lines="8 10" +{!../../../docs_src/query_params/tutorial004.py!} +``` + +## Paramètres de requête requis + +Quand vous déclarez une valeur par défaut pour un paramètre qui n'est pas un paramètre de chemin (actuellement, nous n'avons vu que les paramètres de requête), alors ce paramètre n'est pas requis. + +Si vous ne voulez pas leur donner de valeur par défaut mais juste les rendre optionnels, utilisez `None` comme valeur par défaut. + +Mais si vous voulez rendre un paramètre de requête obligatoire, vous pouvez juste ne pas y affecter de valeur par défaut : + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +Ici le paramètre `needy` est un paramètre requis (ou obligatoire) de type `str`. + +Si vous ouvrez une URL comme : + +``` +http://127.0.0.1:8000/items/foo-item +``` + +...sans ajouter le paramètre requis `needy`, vous aurez une erreur : + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +La présence de `needy` étant nécessaire, vous auriez besoin de l'insérer dans l'URL : + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...ce qui fonctionnerait : + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +Et bien sur, vous pouvez définir certains paramètres comme requis, certains avec des valeurs par défaut et certains entièrement optionnels : + +```Python hl_lines="10" +{!../../../docs_src/query_params/tutorial006.py!} +``` + +Ici, on a donc 3 paramètres de requête : + +* `needy`, requis et de type `str`. +* `skip`, un `int` avec comme valeur par défaut `0`. +* `limit`, un `int` optionnel. + +!!! tip "Astuce" + Vous pouvez utiliser les `Enum`s de la même façon qu'avec les [Paramètres de chemin](path-params.md#valeurs-predefinies){.internal-link target=_blank}. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 940e4ff69..4973d170e 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -59,6 +59,7 @@ nav: - fastapi-people.md - python-types.md - Tutoriel - Guide utilisateur: + - tutorial/query-params.md - tutorial/body.md - tutorial/background-tasks.md - async.md From b2c9574fc60bd5fa6606851f29d03f6b32facbe8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Oct 2021 18:16:12 +0000 Subject: [PATCH 0004/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e2e00f912..38791f2a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). * 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). ## 0.70.0 From 0d5e0ba5d590ca8de7814ae4ad926c891033530f Mon Sep 17 00:00:00 2001 From: Sam Courtemanche Date: Tue, 26 Oct 2021 20:33:34 +0200 Subject: [PATCH 0005/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/path-params.md`=20(#3548)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/fr/docs/tutorial/path-params.md | 254 +++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 255 insertions(+) create mode 100644 docs/fr/docs/tutorial/path-params.md diff --git a/docs/fr/docs/tutorial/path-params.md b/docs/fr/docs/tutorial/path-params.md new file mode 100644 index 000000000..58f53e008 --- /dev/null +++ b/docs/fr/docs/tutorial/path-params.md @@ -0,0 +1,254 @@ +# Paramètres de chemin + +Vous pouvez déclarer des "paramètres" ou "variables" de chemin avec la même syntaxe que celle utilisée par le +formatage de chaîne Python : + + +```Python hl_lines="6-7" +{!../../../docs_src/path_params/tutorial001.py!} +``` + +La valeur du paramètre `item_id` sera transmise à la fonction dans l'argument `item_id`. + +Donc, si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/foo, +vous verrez comme réponse : + +```JSON +{"item_id":"foo"} +``` + +## Paramètres de chemin typés + +Vous pouvez déclarer le type d'un paramètre de chemin dans la fonction, en utilisant les annotations de type Python : + + +```Python hl_lines="7" +{!../../../docs_src/path_params/tutorial002.py!} +``` + +Ici, `item_id` est déclaré comme `int`. + +!!! hint "Astuce" + Ceci vous permettra d'obtenir des fonctionnalités de l'éditeur dans votre fonction, telles + que des vérifications d'erreur, de l'auto-complétion, etc. + +## Conversion de données + +Si vous exécutez cet exemple et allez sur http://127.0.0.1:8000/items/3, vous aurez comme réponse : + +```JSON +{"item_id":3} +``` + +!!! hint "Astuce" + Comme vous l'avez remarqué, la valeur reçue par la fonction (et renvoyée ensuite) est `3`, + en tant qu'entier (`int`) Python, pas la chaîne de caractères (`string`) `"3"`. + + Grâce aux déclarations de types, **FastAPI** fournit du + "parsing" automatique. + +## Validation de données + +Si vous allez sur http://127.0.0.1:8000/items/foo, vous aurez une belle erreur HTTP : + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +car le paramètre de chemin `item_id` possède comme valeur `"foo"`, qui ne peut pas être convertie en entier (`int`). + +La même erreur se produira si vous passez un nombre flottant (`float`) et non un entier, comme ici +http://127.0.0.1:8000/items/4.2. + + +!!! hint "Astuce" + Donc, avec ces mêmes déclarations de type Python, **FastAPI** vous fournit de la validation de données. + + Notez que l'erreur mentionne le point exact où la validation n'a pas réussi. + + Ce qui est incroyablement utile au moment de développer et débugger du code qui interagit avec votre API. + +## Documentation + +Et quand vous vous rendez sur http://127.0.0.1:8000/docs, vous verrez la +documentation générée automatiquement et interactive : + + + +!!! info + À nouveau, en utilisant uniquement les déclarations de type Python, **FastAPI** vous fournit automatiquement une documentation interactive (via Swagger UI). + + On voit bien dans la documentation que `item_id` est déclaré comme entier. + +## Les avantages d'avoir une documentation basée sur une norme, et la documentation alternative. + +Le schéma généré suivant la norme OpenAPI, +il existe de nombreux outils compatibles. + +Grâce à cela, **FastAPI** lui-même fournit une documentation alternative (utilisant ReDoc), qui peut être lue +sur http://127.0.0.1:8000/redoc : + + + +De la même façon, il existe bien d'autres outils compatibles, y compris des outils de génération de code +pour de nombreux langages. + +## Pydantic + +Toute la validation de données est effectué en arrière-plan avec Pydantic, +dont vous bénéficierez de tous les avantages. Vous savez donc que vous êtes entre de bonnes mains. + +## L'ordre importe + +Quand vous créez des *fonctions de chemins*, vous pouvez vous retrouver dans une situation où vous avez un chemin fixe. + +Tel que `/users/me`, disons pour récupérer les données sur l'utilisateur actuel. + +Et vous avez un second chemin : `/users/{user_id}` pour récupérer de la donnée sur un utilisateur spécifique grâce à son identifiant d'utilisateur + +Les *fonctions de chemin* étant évaluées dans l'ordre, il faut s'assurer que la fonction correspondant à `/users/me` est déclarée avant celle de `/users/{user_id}` : + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003.py!} +``` + +Sinon, le chemin `/users/{user_id}` correspondrait aussi à `/users/me`, la fonction "croyant" qu'elle a reçu un paramètre `user_id` avec pour valeur `"me"`. + +## Valeurs prédéfinies + +Si vous avez une *fonction de chemin* qui reçoit un *paramètre de chemin*, mais que vous voulez que les valeurs possibles des paramètres soient prédéfinies, vous pouvez utiliser les `Enum` de Python. + +### Création d'un `Enum` + +Importez `Enum` et créez une sous-classe qui hérite de `str` et `Enum`. + +En héritant de `str` la documentation sera capable de savoir que les valeurs doivent être de type `string` et pourra donc afficher cette `Enum` correctement. + +Créez ensuite des attributs de classe avec des valeurs fixes, qui seront les valeurs autorisées pour cette énumération. + +```Python hl_lines="1 6-9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info + Les énumérations (ou enums) sont disponibles en Python depuis la version 3.4. + +!!! tip "Astuce" + Pour ceux qui se demandent, "AlexNet", "ResNet", et "LeNet" sont juste des noms de modèles de Machine Learning. + +### Déclarer un paramètre de chemin + +Créez ensuite un *paramètre de chemin* avec une annotation de type désignant l'énumération créée précédemment (`ModelName`) : + +```Python hl_lines="16" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +### Documentation + +Les valeurs disponibles pour le *paramètre de chemin* sont bien prédéfinies, la documentation les affiche correctement : + + + +### Manipuler les *énumérations* Python + +La valeur du *paramètre de chemin* sera un des "membres" de l'énumération. + +#### Comparer les *membres d'énumération* + +Vous pouvez comparer ce paramètre avec les membres de votre énumération `ModelName` : + +```Python hl_lines="17" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +#### Récupérer la *valeur de l'énumération* + +Vous pouvez obtenir la valeur réel d'un membre (une chaîne de caractères ici), avec `model_name.value`, ou en général, `votre_membre_d'enum.value` : + +```Python hl_lines="20" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip "Astuce" + Vous pouvez aussi accéder la valeur `"lenet"` avec `ModelName.lenet.value`. + +#### Retourner des *membres d'énumération* + +Vous pouvez retourner des *membres d'énumération* dans vos *fonctions de chemin*, même imbriquée dans un JSON (e.g. un `dict`). + +Ils seront convertis vers leurs valeurs correspondantes (chaînes de caractères ici) avant d'être transmis au client : + +```Python hl_lines="18 21 23" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +Le client recevra une réponse JSON comme celle-ci : + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + +## Paramètres de chemin contenant des chemins + +Disons que vous avez une *fonction de chemin* liée au chemin `/files/{file_path}`. + +Mais que `file_path` lui-même doit contenir un *chemin*, comme `home/johndoe/myfile.txt` par exemple. + +Donc, l'URL pour ce fichier pourrait être : `/files/home/johndoe/myfile.txt`. + +### Support d'OpenAPI + +OpenAPI ne supporte pas de manière de déclarer un paramètre de chemin contenant un *chemin*, cela pouvant causer des scénarios difficiles à tester et définir. + +Néanmoins, cela reste faisable dans **FastAPI**, via les outils internes de Starlette. + +Et la documentation fonctionne quand même, bien qu'aucune section ne soit ajoutée pour dire que la paramètre devrait contenir un *chemin*. + +### Convertisseur de *chemin* + +En utilisant une option de Starlette directement, vous pouvez déclarer un *paramètre de chemin* contenant un *chemin* avec une URL comme : + +``` +/files/{file_path:path} +``` + +Dans ce cas, le nom du paramètre est `file_path`, et la dernière partie, `:path`, indique à Starlette que le paramètre devrait correspondre à un *chemin*. + +Vous pouvez donc l'utilisez comme tel : + +```Python hl_lines="6" +{!../../../docs_src/path_params/tutorial004.py!} +``` + +!!! tip "Astuce" + Vous pourriez avoir besoin que le paramètre contienne `/home/johndoe/myfile.txt`, avec un slash au début (`/`). + + Dans ce cas, l'URL serait : `/files//home/johndoe/myfile.txt`, avec un double slash (`//`) entre `files` et `home`. + +## Récapitulatif + +Avec **FastAPI**, en utilisant les déclarations de type rapides, intuitives et standards de Python, vous bénéficiez de : + +* Support de l'éditeur : vérification d'erreurs, auto-complétion, etc. +* "Parsing" de données. +* Validation de données. +* Annotations d'API et documentation automatique. + +Et vous n'avez besoin de le déclarer qu'une fois. + +C'est probablement l'avantage visible principal de **FastAPI** comparé aux autres *frameworks* (outre les performances pures). diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 4973d170e..01cf8d5e0 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -59,6 +59,7 @@ nav: - fastapi-people.md - python-types.md - Tutoriel - Guide utilisateur: + - tutorial/path-params.md - tutorial/query-params.md - tutorial/body.md - tutorial/background-tasks.md From 08410ca5688e81963d681cbeb8aeef69486942eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Oct 2021 18:34:11 +0000 Subject: [PATCH 0006/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 38791f2a1..d15ca6103 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/tutorial/path-params.md`. PR [#3548](https://github.com/tiangolo/fastapi/pull/3548) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). * 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). From 652cf4bb6bf82a03361838f098ccde358ca71c56 Mon Sep 17 00:00:00 2001 From: Sam Courtemanche Date: Tue, 26 Oct 2021 20:47:01 +0200 Subject: [PATCH 0007/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20Tutorial=20-=20First=20steps=20(#3455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/fr/docs/tutorial/first-steps.md | 334 +++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 335 insertions(+) create mode 100644 docs/fr/docs/tutorial/first-steps.md diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md new file mode 100644 index 000000000..3a81362f6 --- /dev/null +++ b/docs/fr/docs/tutorial/first-steps.md @@ -0,0 +1,334 @@ +# Démarrage + +Le fichier **FastAPI** le plus simple possible pourrait ressembler à cela : + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Copiez ce code dans un fichier nommé `main.py`. + +Démarrez le serveur : + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + La commande `uvicorn main:app` fait référence à : + + * `main` : le fichier `main.py` (le module Python). + * `app` : l'objet créé dans `main.py` via la ligne `app = FastAPI()`. + * `--reload` : l'option disant à uvicorn de redémarrer le serveur à chaque changement du code. À ne pas utiliser en production ! + +Vous devriez voir dans la console, une ligne semblable à la suivante : + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Cette ligne montre l'URL par laquelle l'app est actuellement accessible, sur votre machine locale. + +### Allez voir le résultat + +Ouvrez votre navigateur à l'adresse http://127.0.0.1:8000. + +Vous obtiendrez cette réponse JSON : + +```JSON +{"message": "Hello World"} +``` + +### Documentation interactive de l'API + +Rendez-vous sur http://127.0.0.1:8000/docs. + +Vous verrez la documentation interactive de l'API générée automatiquement (via Swagger UI) : + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Documentation alternative + +Ensuite, rendez-vous sur http://127.0.0.1:8000/redoc. + +Vous y verrez la documentation alternative (via ReDoc) : + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** génère un "schéma" contenant toute votre API dans le standard de définition d'API **OpenAPI**. + +#### "Schéma" + +Un "schéma" est une définition ou une description de quelque chose. Pas le code qui l'implémente, uniquement une description abstraite. + +#### "Schéma" d'API + +Ici, OpenAPI est une spécification qui dicte comment définir le schéma de votre API. + +Le schéma inclut les chemins de votre API, les paramètres potentiels de chaque chemin, etc. + +#### "Schéma" de données + +Le terme "schéma" peut aussi faire référence à la forme de la donnée, comme un contenu JSON. + +Dans ce cas, cela signifierait les attributs JSON, ainsi que les types de ces attributs, etc. + +#### OpenAPI et JSON Schema + +**OpenAPI** définit un schéma d'API pour votre API. Il inclut des définitions (ou "schémas") de la donnée envoyée et reçue par votre API en utilisant **JSON Schema**, le standard des schémas de données JSON. + +#### Allez voir `openapi.json` + +Si vous êtes curieux d'à quoi ressemble le schéma brut **OpenAPI**, **FastAPI** génère automatiquement un (schéma) JSON avec les descriptions de toute votre API. + +Vous pouvez le voir directement à cette adresse : http://127.0.0.1:8000/openapi.json. + +Le schéma devrait ressembler à ceci : + + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### À quoi sert OpenAPI + +Le schéma **OpenAPI** est ce qui alimente les deux systèmes de documentation interactive. + +Et il existe des dizaines d'alternatives, toutes basées sur **OpenAPI**. Vous pourriez facilement ajouter n'importe laquelle de ces alternatives à votre application **FastAPI**. + +Vous pourriez aussi l'utiliser pour générer du code automatiquement, pour les clients qui communiquent avec votre API. Comme par exemple, des applications frontend, mobiles ou IOT. + +## Récapitulatif, étape par étape + +### Étape 1 : import `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` est une classe Python qui fournit toutes les fonctionnalités nécessaires au lancement de votre API. + +!!! note "Détails techniques" + `FastAPI` est une classe héritant directement de `Starlette`. + + Vous pouvez donc aussi utiliser toutes les fonctionnalités de Starlette depuis `FastAPI`. + +### Étape 2 : créer une "instance" `FastAPI` + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Ici la variable `app` sera une "instance" de la classe `FastAPI`. + +Ce sera le point principal d'interaction pour créer toute votre API. + +Cette `app` est la même que celle à laquelle fait référence `uvicorn` dans la commande : + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Si vous créez votre app avec : + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Et la mettez dans un fichier `main.py`, alors vous appeleriez `uvicorn` avec : + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Étape 3: créer une *opération de chemin* + +#### Chemin + +Chemin, ou "path" fait référence ici à la dernière partie de l'URL démarrant au premier `/`. + +Donc, dans un URL tel que : + +``` +https://example.com/items/foo +``` + +...le "path" serait : + +``` +/items/foo +``` + +!!! info + Un chemin, ou "path" est aussi souvent appelé route ou "endpoint". + + +#### Opération + +"Opération" fait référence à une des "méthodes" HTTP. + +Une de : + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...ou une des plus exotiques : + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +Dans le protocol HTTP, vous pouvez communiquer avec chaque chemin en utilisant une (ou plus) de ces "méthodes". + +--- + +En construisant des APIs, vous utilisez généralement ces méthodes HTTP spécifiques pour effectuer une action précise. + +Généralement vous utilisez : + +* `POST` : pour créer de la donnée. +* `GET` : pour lire de la donnée. +* `PUT` : pour mettre à jour de la donnée. +* `DELETE` : pour supprimer de la donnée. + +Donc, dans **OpenAPI**, chaque méthode HTTP est appelée une "opération". + +Nous allons donc aussi appeler ces dernières des "**opérations**". + + +#### Définir un *décorateur d'opération de chemin* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Le `@app.get("/")` dit à **FastAPI** que la fonction en dessous est chargée de gérer les requêtes qui vont sur : + +* le chemin `/` +* en utilisant une opération get + +!!! info "`@décorateur` Info" + Cette syntaxe `@something` en Python est appelée un "décorateur". + + Vous la mettez au dessus d'une fonction. Comme un joli chapeau décoratif (j'imagine que ce terme vient de là 🤷🏻‍♂). + + Un "décorateur" prend la fonction en dessous et en fait quelque chose. + + Dans notre cas, ce décorateur dit à **FastAPI** que la fonction en dessous correspond au **chemin** `/` avec l'**opération** `get`. + + C'est le "**décorateur d'opération de chemin**". + +Vous pouvez aussi utiliser les autres opérations : + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Tout comme celles les plus exotiques : + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip "Astuce" + Vous êtes libres d'utiliser chaque opération (méthode HTTP) comme vous le désirez. + + **FastAPI** n'impose pas de sens spécifique à chacune d'elle. + + Les informations qui sont présentées ici forment une directive générale, pas des obligations. + + Par exemple, quand l'on utilise **GraphQL**, toutes les actions sont effectuées en utilisant uniquement des opérations `POST`. + +### Étape 4 : définir la **fonction de chemin**. + +Voici notre "**fonction de chemin**" (ou fonction d'opération de chemin) : + +* **chemin** : `/`. +* **opération** : `get`. +* **fonction** : la fonction sous le "décorateur" (sous `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +C'est une fonction Python. + +Elle sera appelée par **FastAPI** quand une requête sur l'URL `/` sera reçue via une opération `GET`. + +Ici, c'est une fonction asynchrone (définie avec `async def`). + +--- + +Vous pourriez aussi la définir comme une fonction classique plutôt qu'avec `async def` : + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + Si vous ne connaissez pas la différence, allez voir la section [Concurrence : *"Vous êtes pressés ?"*](../async.md#vous-etes-presses){.internal-link target=_blank}. + +### Étape 5 : retourner le contenu + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Vous pouvez retourner un dictionnaire (`dict`), une liste (`list`), des valeurs seules comme des chaines de caractères (`str`) et des entiers (`int`), etc. + +Vous pouvez aussi retourner des models **Pydantic** (qui seront détaillés plus tard). + +Il y a de nombreux autres objets et modèles qui seront automatiquement convertis en JSON. Essayez d'utiliser vos favoris, il est fort probable qu'ils soient déjà supportés. + +## Récapitulatif + +* Importez `FastAPI`. +* Créez une instance d'`app`. +* Ajoutez une **décorateur d'opération de chemin** (tel que `@app.get("/")`). +* Ajoutez une **fonction de chemin** (telle que `def root(): ...` comme ci-dessus). +* Lancez le serveur de développement (avec `uvicorn main:app --reload`). diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 01cf8d5e0..6242a88fd 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -59,6 +59,7 @@ nav: - fastapi-people.md - python-types.md - Tutoriel - Guide utilisateur: + - tutorial/first-steps.md - tutorial/path-params.md - tutorial/query-params.md - tutorial/body.md From fb1f34123124641850d5cfbe1d247a654ab51586 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Oct 2021 18:47:42 +0000 Subject: [PATCH 0008/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d15ca6103..cec6c2749 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for Tutorial - First steps. PR [#3455](https://github.com/tiangolo/fastapi/pull/3455) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/path-params.md`. PR [#3548](https://github.com/tiangolo/fastapi/pull/3548) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). * 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). From 0a61a6c8656da738ee6438c1fa927e3cbad64bdd Mon Sep 17 00:00:00 2001 From: "weekwith.me" <63915557+0417taehyun@users.noreply.github.com> Date: Wed, 27 Oct 2021 03:53:40 +0900 Subject: [PATCH 0009/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20`docs/tutoria?= =?UTF-8?q?l/dependencies/classes-as-dependencies`:=20Add=20type=20of=20qu?= =?UTF-8?q?ery=20parameters=20in=20a=20description=20of=20`Classes=20as=20?= =?UTF-8?q?dependencies`=20(#4015)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/tutorial/dependencies/classes-as-dependencies.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 8c00374bf..7747e3e1b 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -91,9 +91,9 @@ Those parameters are what **FastAPI** will use to "solve" the dependency. In both cases, it will have: -* an optional `q` query parameter. -* a `skip` query parameter, with a default of `0`. -* a `limit` query parameter, with a default of `100`. +* An optional `q` query parameter that is a `str`. +* A `skip` query parameter that is an `int`, with a default of `0`. +* A `limit` query parameter that is an `int`, with a default of `100`. In both cases the data will be converted, validated, documented on the OpenAPI schema, etc. From 58ab733f19846b4875c5b79bfb1f4d1cb7f4823f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 26 Oct 2021 18:54:22 +0000 Subject: [PATCH 0010/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cec6c2749..72f2e9b58 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update `docs/tutorial/dependencies/classes-as-dependencies`: Add type of query parameters in a description of `Classes as dependencies`. PR [#4015](https://github.com/tiangolo/fastapi/pull/4015) by [@0417taehyun](https://github.com/0417taehyun). * 🌐 Add French translation for Tutorial - First steps. PR [#3455](https://github.com/tiangolo/fastapi/pull/3455) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/path-params.md`. PR [#3548](https://github.com/tiangolo/fastapi/pull/3548) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). From 146ff28d9cd226783aac1d65336ee694471455f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Dec 2021 16:04:04 +0100 Subject: [PATCH 0011/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20CryptAPI=20spons?= =?UTF-8?q?or=20(#4264)?= 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 + docs/en/docs/img/sponsors/cryptapi-banner.svg | 1375 +++++++++++++++++ docs/en/docs/img/sponsors/cryptapi.svg | 1216 +++++++++++++++ docs/en/overrides/main.html | 6 + 6 files changed, 2602 insertions(+) create mode 100644 docs/en/docs/img/sponsors/cryptapi-banner.svg create mode 100644 docs/en/docs/img/sponsors/cryptapi.svg diff --git a/README.md b/README.md index 830031581..53de38bd9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 4949e6c56..baa2e440d 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -2,6 +2,9 @@ gold: - url: https://bit.ly/2QSouzH title: "Jina: build neural search-as-a-service for any kind of data in just minutes." img: https://fastapi.tiangolo.com/img/sponsors/jina.svg + - url: https://cryptapi.io/ + title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." + img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 0496c6279..759748728 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -6,3 +6,4 @@ logins: - mikeckennedy - koaning - deepset-ai + - cryptapi diff --git a/docs/en/docs/img/sponsors/cryptapi-banner.svg b/docs/en/docs/img/sponsors/cryptapi-banner.svg new file mode 100644 index 000000000..29cd772da --- /dev/null +++ b/docs/en/docs/img/sponsors/cryptapi-banner.svg @@ -0,0 +1,1375 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/cryptapi.svg b/docs/en/docs/img/sponsors/cryptapi.svg new file mode 100644 index 000000000..db4e09347 --- /dev/null +++ b/docs/en/docs/img/sponsors/cryptapi.svg @@ -0,0 +1,1216 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index aa381faa3..70b0253bd 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -40,6 +40,12 @@ + {% endblock %} From f282c0e20718e50063a4c4ea164e83f13d020872 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:04:50 +0000 Subject: [PATCH 0012/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 72f2e9b58..d37959dcc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add CryptAPI sponsor. PR [#4264](https://github.com/tiangolo/fastapi/pull/4264) by [@tiangolo](https://github.com/tiangolo). * 📝 Update `docs/tutorial/dependencies/classes-as-dependencies`: Add type of query parameters in a description of `Classes as dependencies`. PR [#4015](https://github.com/tiangolo/fastapi/pull/4015) by [@0417taehyun](https://github.com/0417taehyun). * 🌐 Add French translation for Tutorial - First steps. PR [#3455](https://github.com/tiangolo/fastapi/pull/3455) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/tutorial/path-params.md`. PR [#3548](https://github.com/tiangolo/fastapi/pull/3548) by [@Smlep](https://github.com/Smlep). From dabb4898f140d98f5e5a4668fdbcd0361459fa8d Mon Sep 17 00:00:00 2001 From: kimjaeyoonn <87240205+kimjaeyoonn@users.noreply.github.com> Date: Thu, 9 Dec 2021 00:32:24 +0900 Subject: [PATCH 0013/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/index.md`=20(#4193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index a18a9ffc7..622aad1aa 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -2,11 +2,11 @@ 이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. -각 섹션은 이전 섹션을 기반해서 점진적으로 만들어 졌지만, 주제를 구분하여 구성 되었기 때문에 특정 API 요구사항을 해결하기 위해 어떤 특정 항목이던지 직접 이동할 수 있습니다. +각 섹션은 이전 섹션을 기반해서 점진적으로 만들어 졌지만, 주제에 따라 다르게 구성되었기 때문에 특정 API 요구사항을 해결하기 위해서라면 어느 특정 항목으로던지 직접 이동할 수 있습니다. 또한 향후 참조가 될 수 있도록 만들어졌습니다. -그러므로 다시 돌아와서 정확히 필요한 것을 볼 수 있습니다. +그러므로 다시 돌아와서 정확히 필요한 것을 확인할 수 있습니다. ## 코드 실행하기 @@ -30,7 +30,7 @@ $ uvicorn main:app --reload 코드를 작성하거나 복사, 편집할 때, 로컬에서 실행하는 것을 **강력히 장려**합니다. -편집기에서 이렇게 사용하면, 모든 타입 검사, 자동완성 등 작성해야 하는 코드가 얼마나 적은지 보면서 FastAPI의 장점을 실제로 확인할 수 있습니다. +편집기에서 이렇게 사용한다면, 모든 타입 검사와 자동완성 등 작성해야 하는 코드가 얼마나 적은지 보면서 FastAPI의 장점을 실제로 확인할 수 있습니다. --- @@ -77,4 +77,4 @@ $ pip install fastapi[all] 하지만 (지금 읽고 있는) **자습서 - 사용자 안내서**를 먼저 읽는게 좋습니다. -**자습서 - 사용자 안내서**만으로 완전한 애플리케이션을 구축한 다음, **고급 사용자 안내서**의 몇 가지 추가 아이디어를 사용하여 필요에 따라 다양한 방식으로 확장할 수 있도록 설계되었습니다. +**자습서 - 사용자 안내서**만으로도 완전한 애플리케이션을 구축할 수 있으며, 필요에 따라 **고급 사용자 안내서**에서 제공하는 몇 가지 추가적인 기능을 사용하여 다양한 방식으로 확장할 수 있도록 설계되었습니다. From cace5a79f77c7518eecb92ab1909a7d14cf466ff Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:33:09 +0000 Subject: [PATCH 0014/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d37959dcc..efb84de47 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix Korean translation for `docs/ko/docs/tutorial/index.md`. PR [#4193](https://github.com/tiangolo/fastapi/pull/4193) by [@kimjaeyoonn](https://github.com/kimjaeyoonn). * 🔧 Add CryptAPI sponsor. PR [#4264](https://github.com/tiangolo/fastapi/pull/4264) by [@tiangolo](https://github.com/tiangolo). * 📝 Update `docs/tutorial/dependencies/classes-as-dependencies`: Add type of query parameters in a description of `Classes as dependencies`. PR [#4015](https://github.com/tiangolo/fastapi/pull/4015) by [@0417taehyun](https://github.com/0417taehyun). * 🌐 Add French translation for Tutorial - First steps. PR [#3455](https://github.com/tiangolo/fastapi/pull/3455) by [@Smlep](https://github.com/Smlep). From 446d194c4675e8b3e0f6315dce31af22a229aeca Mon Sep 17 00:00:00 2001 From: daehyeon kim <87962045+DevDae@users.noreply.github.com> Date: Thu, 9 Dec 2021 00:37:30 +0900 Subject: [PATCH 0015/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/deployment/versions.md`=20(#4121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> --- docs/ko/docs/deployment/versions.md | 88 +++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/ko/docs/deployment/versions.md diff --git a/docs/ko/docs/deployment/versions.md b/docs/ko/docs/deployment/versions.md new file mode 100644 index 000000000..4c1bcdc2e --- /dev/null +++ b/docs/ko/docs/deployment/versions.md @@ -0,0 +1,88 @@ +# FastAPI 버전들에 대하여 + +**FastAPI** 는 이미 많은 응용 프로그램과 시스템들을 만드는데 사용되고 있습니다. 그리고 100%의 테스트 정확성을 가지고 있습니다. 하지만 이것은 아직까지도 빠르게 발전하고 있습니다. + +새로운 특징들이 빈번하게 추가되고, 오류들이 지속적으로 수정되고 있습니다. 그리고 코드가 계속적으로 향상되고 있습니다. + +이것이 아직도 최신 버전이 `0.x.x`인 이유입니다. 이것은 각각의 버전들이 잠재적으로 변할 수 있다는 것을 보여줍니다. 이는 유의적 버전 관습을 따릅니다. + +지금 바로 **FastAPI**로 응용 프로그램을 만들 수 있습니다. 이때 (아마 지금까지 그래 왔던 것처럼), 사용하는 버전이 코드와 잘 맞는지 확인해야합니다. + +## `fastapi` 버전을 표시 + +가장 먼저 해야할 것은 응용 프로그램이 잘 작동하는 가장 최신의 구체적인 **FastAPI** 버전을 표시하는 것입니다. + +예를 들어, 응용 프로그램에 `0.45.0` 버전을 사용했다고 가정합니다. + +만약에 `requirements.txt` 파일을 사용했다면, 다음과 같이 버전을 명세할 수 있습니다: + +```txt +fastapi==0.45.0 +``` + +이것은 `0.45.0` 버전을 사용했다는 것을 의미합니다. + +또는 다음과 같이 표시할 수 있습니다: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +이것은 `0.45.0` 버전과 같거나 높으면서 `0.46.0` 버전 보다는 낮은 버전을 사용했다는 것을 의미합니다. 예를 들어, `0.45.2` 버전과 같은 경우는 해당 조건을 만족합니다. + +만약에 Poetry, Pipenv, 또는 그밖의 다양한 설치 도구를 사용한다면, 패키지에 구체적인 버전을 정의할 수 있는 방법을 가지고 있을 것입니다. + +## 이용가능한 버전들 + +[Release Notes](../release-notes.md){.internal-link target=_blank}를 통해 사용할 수 있는 버전들을 확인할 수 있습니다.(예를 들어, 가장 최신의 버전을 확인할 수 있습니다.) + + +## 버전들에 대해 + +유의적 버전 관습을 따라서, `1.0.0` 이하의 모든 버전들은 잠재적으로 급변할 수 있습니다. + +FastAPI는 오류를 수정하고, 일반적인 변경사항을 위해 "패치"버전의 관습을 따릅니다. + +!!! tip "팁" + 여기서 말하는 "패치"란 버전의 마지막 숫자로, 예를 들어 `0.2.3` 버전에서 "패치"는 `3`을 의미합니다. + +따라서 다음과 같이 버전을 표시할 수 있습니다: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +수정된 사항과 새로운 요소들이 "마이너" 버전에 추가되었습니다. + +!!! tip "팁" + "마이너"란 버전 넘버의 가운데 숫자로, 예를 들어서 `0.2.3`의 "마이너" 버전은 `2`입니다. + +## FastAPI 버전의 업그레이드 + +응용 프로그램을 검사해야합니다. + +(Starlette 덕분에), **FastAPI** 를 이용하여 굉장히 쉽게 할 수 있습니다. [Testing](../tutorial/testing.md){.internal-link target=_blank}문서를 확인해 보십시오: + +검사를 해보고 난 후에, **FastAPI** 버전을 더 최신으로 업그레이드 할 수 있습니다. 그리고 코드들이 테스트에 정상적으로 작동하는지 확인을 해야합니다. + +만약에 모든 것이 정상 작동하거나 필요한 부분을 변경하고, 모든 검사를 통과한다면, 새로운 버전의 `fastapi`를 표시할 수 있습니다. + +## Starlette에 대해 + +`starlette`의 버전은 표시할 수 없습니다. + +서로다른 버전의 **FastAPI**가 구체적이고 새로운 버전의 Starlette을 사용할 것입니다. + +그러므로 **FastAPI**가 알맞은 Starlette 버전을 사용하도록 하십시오. + +## Pydantic에 대해 + +Pydantic은 **FastAPI** 를 위한 검사를 포함하고 있습니다. 따라서, 새로운 버전의 Pydantic(`1.0.0`이상)은 항상 FastAPI와 호환됩니다. + +작업을 하고 있는 `1.0.0` 이상의 모든 버전과 `2.0.0` 이하의 Pydantic 버전을 표시할 수 있습니다. + +예를 들어 다음과 같습니다: + +```txt +pydantic>=1.2.0,<2.0.0 +``` From 8e416875ce98e89b05da53cf95fccd40818c903f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:38:10 +0000 Subject: [PATCH 0016/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 efb84de47..0f0edaa7b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/ko/docs/deployment/versions.md`. PR [#4121](https://github.com/tiangolo/fastapi/pull/4121) by [@DevDae](https://github.com/DevDae). * 🌐 Fix Korean translation for `docs/ko/docs/tutorial/index.md`. PR [#4193](https://github.com/tiangolo/fastapi/pull/4193) by [@kimjaeyoonn](https://github.com/kimjaeyoonn). * 🔧 Add CryptAPI sponsor. PR [#4264](https://github.com/tiangolo/fastapi/pull/4264) by [@tiangolo](https://github.com/tiangolo). * 📝 Update `docs/tutorial/dependencies/classes-as-dependencies`: Add type of query parameters in a description of `Classes as dependencies`. PR [#4015](https://github.com/tiangolo/fastapi/pull/4015) by [@0417taehyun](https://github.com/0417taehyun). From a5d697b9b56443ac1abfc09f358e9eb56031422e Mon Sep 17 00:00:00 2001 From: Spike Date: Thu, 9 Dec 2021 00:41:26 +0900 Subject: [PATCH 0017/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20Tutorial=20-=20Path=20Parameters=20and=20Numeric?= =?UTF-8?q?=20Validations=20(#2432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../path-params-numeric-validations.md | 122 ++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 123 insertions(+) create mode 100644 docs/ko/docs/tutorial/path-params-numeric-validations.md diff --git a/docs/ko/docs/tutorial/path-params-numeric-validations.md b/docs/ko/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..abb9d03db --- /dev/null +++ b/docs/ko/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,122 @@ +# 경로 매개변수와 숫자 검증 + +`Query`를 사용하여 쿼리 매개변수에 더 많은 검증과 메타데이터를 선언하는 방법과 동일하게 `Path`를 사용하여 경로 매개변수에 검증과 메타데이터를 같은 타입으로 선언할 수 있습니다. + +## 경로 임포트 + +먼저 `fastapi`에서 `Path`를 임포트합니다: + +```Python hl_lines="3" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +## 메타데이터 선언 + +`Query`에 동일한 매개변수를 선언할 수 있습니다. + +예를 들어, `title` 메타데이터 값을 경로 매개변수 `item_id`에 선언하려면 다음과 같이 입력할 수 있습니다: + +```Python hl_lines="10" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +!!! note "참고" + 경로 매개변수는 경로의 일부여야 하므로 언제나 필수적입니다. + + 즉, `...`로 선언해서 필수임을 나타내는게 좋습니다. + + 그럼에도 `None`으로 선언하거나 기본값을 지정할지라도 아무 영향을 끼치지 않으며 언제나 필수입니다. + +## 필요한 경우 매개변수 정렬하기 + +`str` 형인 쿼리 매개변수 `q`를 필수로 선언하고 싶다고 해봅시다. + +해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 `Query`를 정말로 써야할 필요는 없습니다. + +하지만 `item_id` 경로 매개변수는 여전히 `Path`를 사용해야 합니다. + +파이썬은 "기본값"이 없는 값 앞에 "기본값"이 있는 값을 입력하면 불평합니다. + +그러나 매개변수들을 재정렬함으로써 기본값(쿼리 매개변수 `q`)이 없는 값을 처음 부분에 위치 할 수 있습니다. + +**FastAPI**에서는 중요하지 않습니다. 이름, 타입 그리고 선언구(`Query`, `Path` 등)로 매개변수를 감지하며 순서는 신경 쓰지 않습니다. + +따라서 함수를 다음과 같이 선언 할 수 있습니다: + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## 필요한 경우 매개변수 정렬하기, 트릭 + +`Query`나 아무런 기본값으로도 `q` 경로 매개변수를 선언하고 싶지 않지만 `Path`를 사용하여 경로 매개변수를 `item_id` 다른 순서로 선언하고 싶다면, 파이썬은 이를 위한 작고 특별한 문법이 있습니다. + +`*`를 함수의 첫 번째 매개변수로 전달하세요. + +파이썬은 `*`으로 아무런 행동도 하지 않지만, 따르는 매개변수들은 kwargs로도 알려진 키워드 인자(키-값 쌍)여야 함을 인지합니다. 기본값을 가지고 있지 않더라도 그렇습니다. + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## 숫자 검증: 크거나 같음 + +`Query`와 `Path`(나중에 볼 다른 것들도)를 사용하여 문자열 뿐만 아니라 숫자의 제약을 선언할 수 있습니다. + +여기서 `ge=1`인 경우, `item_id`는 `1`보다 "크거나(`g`reater) 같은(`e`qual)" 정수형 숫자여야 합니다. + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## 숫자 검증: 크거나 같음 및 작거나 같음 + +동일하게 적용됩니다: + +* `gt`: 크거나(`g`reater `t`han) +* `le`: 작거나 같은(`l`ess than or `e`qual) + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## 숫자 검증: 부동소수, 크거나 및 작거나 + +숫자 검증은 `float` 값에도 동작합니다. + +여기에서 ge뿐만 아니라 gt를 선언 할 수있는 것이 중요해집니다. 예를 들어 필요한 경우, 값이 `1`보다 작더라도 반드시 `0`보다 커야합니다. + +즉, `0.5`는 유효한 값입니다. 그러나 `0.0` 또는 `0`은 그렇지 않습니다. + +lt 역시 마찬가지입니다. + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## 요약 + +`Query`, `Path`(아직 보지 못한 다른 것들도)를 사용하면 [쿼리 매개변수와 문자열 검증](query-params-str-validations.md){.internal-link target=_blank}에서와 마찬가지로 메타데이터와 문자열 검증을 선언할 수 있습니다. + +그리고 숫자 검증 또한 선언할 수 있습니다: + +* `gt`: 크거나(`g`reater `t`han) +* `ge`: 크거나 같은(`g`reater than or `e`qual) +* `lt`: 작거나(`l`ess `t`han) +* `le`: 작거나 같은(`l`ess than or `e`qual) + +!!! info "정보" + `Query`, `Path`, 그리고 나중에게 보게될 것들은 (여러분이 사용할 필요가 없는) 공통 `Param` 클래스의 서브 클래스입니다. + + 그리고 이들 모두는 여태까지 본 추가 검증과 메타데이터의 동일한 모든 매개변수를 공유합니다. + +!!! note "기술 세부사항" + `fastapi`에서 `Query`, `Path` 등을 임포트 할 때, 이것들은 실제로 함수입니다. + + 호출되면 동일한 이름의 클래스의 인스턴스를 반환합니다. + + 즉, 함수인 `Query`를 임포트한 겁니다. 그리고 호출하면 `Query`라는 이름을 가진 클래스의 인스턴스를 반환합니다. + + 편집기에서 타입에 대한 오류를 표시하지 않도록 하기 위해 (클래스를 직접 사용하는 대신) 이러한 함수들이 있습니다. + + 이렇게 하면 오류를 무시하기 위한 사용자 설정을 추가하지 않고도 일반 편집기와 코딩 도구를 사용할 수 있습니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 0bf1a9713..0cd0e2f9f 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -61,6 +61,7 @@ nav: - tutorial/path-params.md - tutorial/query-params.md - tutorial/header-params.md + - tutorial/path-params-numeric-validations.md markdown_extensions: - toc: permalink: true From da69a1c4c0460ab475e3d2ad351bac3a12bf6690 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:42:12 +0000 Subject: [PATCH 0018/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0f0edaa7b..907df6430 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for Tutorial - Path Parameters and Numeric Validations. PR [#2432](https://github.com/tiangolo/fastapi/pull/2432) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/deployment/versions.md`. PR [#4121](https://github.com/tiangolo/fastapi/pull/4121) by [@DevDae](https://github.com/DevDae). * 🌐 Fix Korean translation for `docs/ko/docs/tutorial/index.md`. PR [#4193](https://github.com/tiangolo/fastapi/pull/4193) by [@kimjaeyoonn](https://github.com/kimjaeyoonn). * 🔧 Add CryptAPI sponsor. PR [#4264](https://github.com/tiangolo/fastapi/pull/4264) by [@tiangolo](https://github.com/tiangolo). From 461e0d4cce8542cbed53791fbc80f5731883c061 Mon Sep 17 00:00:00 2001 From: Kwang Soo Jeong Date: Thu, 9 Dec 2021 00:43:31 +0900 Subject: [PATCH 0019/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20Tutorial=20-=20JSON=20Compatible=20Encoder=20(#315?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/encoder.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/ko/docs/tutorial/encoder.md diff --git a/docs/ko/docs/tutorial/encoder.md b/docs/ko/docs/tutorial/encoder.md new file mode 100644 index 000000000..8b5fdb8b7 --- /dev/null +++ b/docs/ko/docs/tutorial/encoder.md @@ -0,0 +1,34 @@ +# JSON 호환 가능 인코더 + +데이터 유형(예: Pydantic 모델)을 JSON과 호환된 형태로 반환해야 하는 경우가 있습니다. (예: `dict`, `list` 등) + +예를 들면, 데이터베이스에 저장해야하는 경우입니다. + +이를 위해, **FastAPI** 에서는 `jsonable_encoder()` 함수를 제공합니다. + +## `jsonable_encoder` 사용 + +JSON 호환 가능 데이터만 수신하는 `fake_db` 데이터베이스가 존재한다고 가정하겠습니다. + +예를 들면, `datetime` 객체는 JSON과 호환되는 데이터가 아니므로 이 데이터는 받아들여지지 않습니다. + +따라서 `datetime` 객체는 ISO format 데이터를 포함하는 `str`로 변환되어야 합니다. + +같은 방식으로 이 데이터베이스는 Pydantic 모델(속성이 있는 객체)을 받지 않고, `dict` 만을 받습니다. + +이를 위해 `jsonable_encoder` 를 사용할 수 있습니다. + +Pydantic 모델과 같은 객체를 받고 JSON 호환 가능한 버전으로 반환합니다: + +```Python hl_lines="5 22" +{!../../../docs_src/encoder/tutorial001.py!} +``` + +이 예시는 Pydantic 모델을 `dict`로, `datetime` 형식을 `str`로 변환합니다. + +이렇게 호출한 결과는 파이썬 표준인 `json.dumps()`로 인코딩 할 수 있습니다. + +길이가 긴 문자열 형태의 JSON 형식(문자열)의 데이터가 들어있는 상황에서는 `str`로 반환하지 않습니다. JSON과 모두 호환되는 값과 하위 값이 있는 Python 표준 데이터 구조 (예: `dict`)를 반환합니다. + +!!! note "참고" + 실제로 `jsonable_encoder`는 **FastAPI** 에서 내부적으로 데이터를 변환하는 데 사용하지만, 다른 많은 곳에서도 이는 유용합니다. From dd463d0dc2d48777bd9149d6938a672e8f81b203 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:44:10 +0000 Subject: [PATCH 0020/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 907df6430..31034c847 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). * 🌐 Add Korean translation for Tutorial - Path Parameters and Numeric Validations. PR [#2432](https://github.com/tiangolo/fastapi/pull/2432) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/deployment/versions.md`. PR [#4121](https://github.com/tiangolo/fastapi/pull/4121) by [@DevDae](https://github.com/DevDae). * 🌐 Fix Korean translation for `docs/ko/docs/tutorial/index.md`. PR [#4193](https://github.com/tiangolo/fastapi/pull/4193) by [@kimjaeyoonn](https://github.com/kimjaeyoonn). From 39dbee9d563967aeab15637974d40a374e6df85c Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Thu, 9 Dec 2021 00:45:37 +0900 Subject: [PATCH 0021/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/response-status-code.md`=20(#3742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ko/docs/tutorial/response-status-code.md | 89 +++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 90 insertions(+) create mode 100644 docs/ko/docs/tutorial/response-status-code.md diff --git a/docs/ko/docs/tutorial/response-status-code.md b/docs/ko/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..d201867a1 --- /dev/null +++ b/docs/ko/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# 응답 상태 코드 + +응답 모델과 같은 방법으로, 어떤 *경로 작동*이든 `status_code` 매개변수를 사용하여 응답에 대한 HTTP 상태 코드를 선언할 수 있습니다. + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* 기타 + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "참고" + `status_code` 는 "데코레이터" 메소드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수들과 본문처럼 *경로 작동 함수*가 아닙니다. + +`status_code` 매개변수는 HTTP 상태 코드를 숫자로 입력받습니다. + +!!! info "정보" + `status_code` 는 파이썬의 `http.HTTPStatus` 와 같은 `IntEnum` 을 입력받을 수도 있습니다. + +`status_code` 매개변수는: + +* 응답에서 해당 상태 코드를 반환합니다. +* 상태 코드를 OpenAPI 스키마(및 사용자 인터페이스)에 문서화 합니다. + + + +!!! note "참고" + 어떤 응답 코드들은 해당 응답에 본문이 없다는 것을 의미하기도 합니다 (다음 항목 참고). + + 이에 따라 FastAPI는 응답 본문이 없음을 명시하는 OpenAPI를 생성합니다. + +## HTTP 상태 코드에 대하여 + +!!! note "참고" + 만약 HTTP 상태 코드에 대하여 이미 알고있다면, 다음 항목으로 넘어가십시오. + +HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다. + +이 상태 코드들은 각자를 식별할 수 있도록 지정된 이름이 있으나, 중요한 것은 숫자 코드입니다. + +요약하자면: + +* `**1xx**` 상태 코드는 "정보"용입니다. 이들은 직접적으로는 잘 사용되지는 않습니다. 이 상태 코드를 갖는 응답들은 본문을 가질 수 없습니다. +* `**2xx**` 상태 코드는 "성공적인" 응답을 위해 사용됩니다. 가장 많이 사용되는 유형입니다. + * `200` 은 디폴트 상태 코드로, 모든 것이 "성공적임"을 의미합니다. + * 다른 예로는 `201` "생성됨"이 있습니다. 일반적으로 데이터베이스에 새로운 레코드를 생성한 후 사용합니다. + * 단, `204` "내용 없음"은 특별한 경우입니다. 이것은 클라이언트에게 반환할 내용이 없는 경우 사용합니다. 따라서 응답은 본문을 가질 수 없습니다. +* `**3xx**` 상태 코드는 "리다이렉션"용입니다. 본문을 가질 수 없는 `304` "수정되지 않음"을 제외하고, 이 상태 코드를 갖는 응답에는 본문이 있을 수도, 없을 수도 있습니다. +* `**4xx**` 상태 코드는 "클라이언트 오류" 응답을 위해 사용됩니다. 이것은 아마 가장 많이 사용하게 될 두번째 유형입니다. + * 일례로 `404` 는 "찾을 수 없음" 응답을 위해 사용합니다. + * 일반적인 클라이언트 오류의 경우 `400` 을 사용할 수 있습니다. +* `**5xx**` 상태 코드는 서버 오류에 사용됩니다. 이것들을 직접 사용할 일은 거의 없습니다. 응용 프로그램 코드나 서버의 일부에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환합니다. + +!!! tip "팁" + 각각의 상태 코드와 이들이 의미하는 내용에 대해 더 알고싶다면 MDN HTTP 상태 코드에 관한 문서 를 확인하십시오. + +## 이름을 기억하는 쉬운 방법 + +상기 예시 참고: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` 은 "생성됨"를 의미하는 상태 코드입니다. + +하지만 모든 상태 코드들이 무엇을 의미하는지 외울 필요는 없습니다. + +`fastapi.status` 의 편의 변수를 사용할 수 있습니다. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +이것은 단순히 작업을 편리하게 하기 위한 것으로, HTTP 상태 코드와 동일한 번호를 갖고있지만, 이를 사용하면 편집기의 자동완성 기능을 사용할 수 있습니다: + + + +!!! note "기술적 세부사항" + `from starlette import status` 역시 사용할 수 있습니다. + + **FastAPI**는 개발자인 당신의 편의를 위해 `fastapi.status` 와 동일한 `starlette.status` 도 제공합니다. 하지만 이것은 Starlette로부터 직접 제공됩니다. + +## 기본값 변경 + +추후 여기서 선언하는 기본 상태 코드가 아닌 다른 상태 코드를 반환하는 방법을 [숙련된 사용자 지침서](https://fastapi.tiangolo.com/ko/advanced/response-change-status-code/){.internal-link target=_blank}에서 확인할 수 있습니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 0cd0e2f9f..dd2d6a7ac 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -62,6 +62,7 @@ nav: - tutorial/query-params.md - tutorial/header-params.md - tutorial/path-params-numeric-validations.md + - tutorial/response-status-code.md markdown_extensions: - toc: permalink: true From fb9c4b31b91fe9f9601302046997e007fe35ef95 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Dec 2021 15:46:19 +0000 Subject: [PATCH 0022/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 31034c847..ca7553855 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). * 🌐 Add Korean translation for Tutorial - Path Parameters and Numeric Validations. PR [#2432](https://github.com/tiangolo/fastapi/pull/2432) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/deployment/versions.md`. PR [#4121](https://github.com/tiangolo/fastapi/pull/4121) by [@DevDae](https://github.com/DevDae). From 10e3a02582cfbf58b9c93dd782b4c2cb7a615da5 Mon Sep 17 00:00:00 2001 From: Leandro de Souza <85115541+leandrodesouzadev@users.noreply.github.com> Date: Wed, 8 Dec 2021 12:50:35 -0300 Subject: [PATCH 0023/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/tutorial/query-params-str-validations.md?= =?UTF-8?q?`=20(#3965)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mário Victor Ribeiro Silva Co-authored-by: Leandro de Souza Co-authored-by: Sebastián Ramírez --- .../tutorial/query-params-str-validations.md | 303 ++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 304 insertions(+) create mode 100644 docs/pt/docs/tutorial/query-params-str-validations.md diff --git a/docs/pt/docs/tutorial/query-params-str-validations.md b/docs/pt/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..baac5f493 --- /dev/null +++ b/docs/pt/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,303 @@ +# Parâmetros de consulta e validações de texto + +O **FastAPI** permite que você declare informações adicionais e validações aos seus parâmetros. + +Vamos utilizar essa aplicação como exemplo: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial001.py!} +``` + +O parâmetro de consulta `q` é do tipo `Optional[str]`, o que significa que é do tipo `str` mas que também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. + +!!! note "Observação" + O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. + + O `Optional` em `Optional[str]` não é usado pelo FastAPI, mas permitirá que seu editor lhe dê um melhor suporte e detecte erros. + +## Validação adicional + +Nós iremos forçar que mesmo o parâmetro `q` seja opcional, sempre que informado, **seu tamanho não exceda 50 caracteres**. + +### Importe `Query` + +Para isso, primeiro importe `Query` de `fastapi`: + +```Python hl_lines="3" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +## Use `Query` como o valor padrão + +Agora utilize-o como valor padrão do seu parâmetro, definindo o parâmetro `max_length` para 50: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +Note que substituímos o valor padrão de `None` para `Query(None)`, o primeiro parâmetro de `Query` serve para o mesmo propósito: definir o valor padrão do parâmetro. + +Então: + +```Python +q: Optional[str] = Query(None) +``` + +...Torna o parâmetro opcional, da mesma maneira que: + +```Python +q: Optional[str] = None +``` + +Mas o declara explicitamente como um parâmetro de consulta. + +!!! info "Informação" + Tenha em mente que o FastAPI se preocupa com a parte: + + ```Python + = None + ``` + + Ou com: + + ```Python + = Query(None) + ``` + + E irá utilizar o `None` para detectar que o parâmetro de consulta não é obrigatório. + + O `Optional` é apenas para permitir que seu editor de texto lhe dê um melhor suporte. + +Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a textos: + +```Python +q: str = Query(None, max_length=50) +``` + +Isso irá validar os dados, mostrar um erro claro quando os dados forem inválidos, e documentar o parâmetro na *operação de rota* do esquema OpenAPI.. + +## Adicionando mais validações + +Você também pode incluir um parâmetro `min_length`: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial003.py!} +``` + +## Adicionando expressões regulares + +Você pode definir uma expressão regular que combine com um padrão esperado pelo parâmetro: + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial004.py!} +``` + +Essa expressão regular específica verifica se o valor recebido no parâmetro: + +* `^`: Inicia com os seguintes caracteres, ou seja, não contém caracteres anteriores. +* `fixedquery`: contém o valor exato `fixedquery`. +* `$`: termina aqui, não contém nenhum caractere após `fixedquery`. + +Se você se sente perdido com todo esse assunto de **"expressão regular"**, não se preocupe. Esse é um assunto complicado para a maioria das pessoas. Você ainda pode fazer muitas coisas sem utilizar expressões regulares. + +Mas assim que você precisar e já tiver aprendido sobre, saiba que você poderá usá-las diretamente no **FastAPI**. + +## Valores padrão + +Da mesma maneira que você utiliza `None` como o primeiro argumento para ser utilizado como um valor padrão, você pode usar outros valores. + +Vamos dizer que você queira que o parâmetro de consulta `q` tenha um `min_length` de `3`, e um valor padrão de `"fixedquery"`, então declararíamos assim: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note "Observação" + O parâmetro torna-se opcional quando possui um valor padrão. + +## Torne-o obrigatório + +Quando você não necessita de validações ou de metadados adicionais, podemos fazer com que o parâmetro de consulta `q` seja obrigatório por não declarar um valor padrão, dessa forma: + +```Python +q: str +``` + +em vez desta: + +```Python +q: Optional[str] = None +``` + +Mas agora nós o estamos declarando como `Query`, conforme abaixo: + +```Python +q: Optional[str] = Query(None, min_length=3) +``` + +Então, quando você precisa declarar um parâmetro obrigatório utilizando o `Query`, você pode utilizar `...` como o primeiro argumento: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +!!! info "Informação" + Se você nunca viu os `...` antes: é um valor único especial, faz parte do Python e é chamado "Ellipsis". + +Dessa forma o **FastAPI** saberá que o parâmetro é obrigatório. + +## Lista de parâmetros de consulta / múltiplos valores + +Quando você declara explicitamente um parâmetro com `Query` você pode declará-lo para receber uma lista de valores, ou podemos dizer, que irá receber mais de um valor. + +Por exemplo, para declarar que o parâmetro `q` pode aparecer diversas vezes na URL, você escreveria: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial011.py!} +``` + +Então, com uma URL assim: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +você receberá os múltiplos *parâmetros de consulta* `q` com os valores (`foo` e `bar`) em uma lista (`list`) Python dentro da *função de operação de rota*, no *parâmetro da função* `q`. + +Assim, a resposta para essa URL seria: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "Dica" + Para declarar um parâmetro de consulta com o tipo `list`, como no exemplo acima, você precisa usar explicitamente o `Query`, caso contrário será interpretado como um corpo da requisição. + +A documentação interativa da API irá atualizar de acordo, permitindo múltiplos valores: + + + +### Lista de parâmetros de consulta / múltiplos valores por padrão + +E você também pode definir uma lista (`list`) de valores padrão caso nenhum seja informado: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial012.py!} +``` + +Se você for até: + +``` +http://localhost:8000/items/ +``` + +O valor padrão de `q` será: `["foo", "bar"]` e sua resposta será: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Usando `list` + +Você também pode utilizar o tipo `list` diretamente em vez de `List[str]`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note "Observação" + Tenha em mente que neste caso, o FastAPI não irá validar os conteúdos da lista. + + Por exemplo, um `List[int]` iria validar (e documentar) que os contéudos da lista são números inteiros. Mas apenas `list` não. + +## Declarando mais metadados + +Você pode adicionar mais informações sobre o parâmetro. + +Essa informações serão inclusas no esquema do OpenAPI e utilizado pela documentação interativa e ferramentas externas. + +!!! note "Observação" + Tenha em mente que cada ferramenta oferece diferentes níveis de suporte ao OpenAPI. + + Algumas delas não exibem todas as informações extras que declaramos, ainda que na maioria dos casos, esses recursos estão planejados para desenvolvimento. + +Você pode adicionar um `title`: + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial007.py!} +``` + +E uma `description`: + +```Python hl_lines="13" +{!../../../docs_src/query_params_str_validations/tutorial008.py!} +``` + +## Apelidos (alias) de parâmetros + +Imagine que você queira que um parâmetro tenha o nome `item-query`. + +Desta maneira: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Mas o nome `item-query` não é um nome de váriavel válido no Python. + +O que mais se aproxima é `item_query`. + +Mas ainda você precisa que o nome seja exatamente `item-query`... + +Então você pode declarar um `alias`, e esse apelido (alias) que será utilizado para encontrar o valor do parâmetro: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial009.py!} +``` + +## Parâmetros descontinuados + +Agora vamos dizer que você não queria mais utilizar um parâmetro. + +Você tem que deixá-lo ativo por um tempo, já que existem clientes o utilizando. Mas você quer que a documentação deixe claro que este parâmetro será descontinuado. + +Então você passa o parâmetro `deprecated=True` para `Query`: + +```Python hl_lines="18" +{!../../../docs_src/query_params_str_validations/tutorial010.py!} +``` + +Na documentação aparecerá assim: + + + +## Recapitulando + +Você pode adicionar validações e metadados adicionais aos seus parâmetros. + +Validações genéricas e metadados: + +* `alias` +* `title` +* `description` +* `deprecated` + +Validações específicas para textos: + +* `min_length` +* `max_length` +* `regex` + +Nesses exemplos você viu como declarar validações em valores do tipo `str`. + +Leia os próximos capítulos para ver como declarar validação de outros tipos, como números. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 219f41b81..ea4af852e 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -62,6 +62,7 @@ nav: - tutorial/first-steps.md - tutorial/path-params.md - tutorial/body-fields.md + - tutorial/query-params-str-validations.md - Segurança: - tutorial/security/index.md - Guia de Usuário Avançado: From fa5639cb3564a475335f468b1d2d80ae5d8845d0 Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Thu, 9 Dec 2021 00:52:01 +0900 Subject: [PATCH 0024/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/request-files.md`=20(#3743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Spike Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ko/docs/tutorial/request-files.md | 144 +++++++++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 145 insertions(+) create mode 100644 docs/ko/docs/tutorial/request-files.md diff --git a/docs/ko/docs/tutorial/request-files.md b/docs/ko/docs/tutorial/request-files.md new file mode 100644 index 000000000..769a676cd --- /dev/null +++ b/docs/ko/docs/tutorial/request-files.md @@ -0,0 +1,144 @@ +# 파일 요청 + +`File`을 사용하여 클라이언트가 업로드할 파일들을 정의할 수 있습니다. + +!!! info "정보" + 업로드된 파일을 전달받기 위해 먼저 `python-multipart`를 설치해야합니다. + + 예시) `pip install python-multipart`. + + 업로드된 파일들은 "폼 데이터"의 형태로 전송되기 때문에 이 작업이 필요합니다. + +## `File` 임포트 + +`fastapi` 에서 `File` 과 `UploadFile` 을 임포트 합니다: + +```Python hl_lines="1" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +## `File` 매개변수 정의 + +`Body` 및 `Form` 과 동일한 방식으로 파일의 매개변수를 생성합니다: + +```Python hl_lines="7" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +!!! info "정보" + `File` 은 `Form` 으로부터 직접 상속된 클래스입니다. + + 하지만 `fastapi`로부터 `Query`, `Path`, `File` 등을 임포트 할 때, 이것들은 특별한 클래스들을 반환하는 함수라는 것을 기억하기 바랍니다. + +!!! tip "팁" + File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본문(JSON) 매개변수로 해석되는 것을 방지하기 위해 `File` 을 사용해야합니다. + +파일들은 "폼 데이터"의 형태로 업로드 됩니다. + +*경로 작동 함수*의 매개변수를 `bytes` 로 선언하는 경우 **FastAPI**는 파일을 읽고 `bytes` 형태의 내용을 전달합니다. + +이것은 전체 내용이 메모리에 저장된다는 것을 의미한다는 걸 염두하기 바랍니다. 이는 작은 크기의 파일들에 적합합니다. + +어떤 경우에는 `UploadFile` 을 사용하는 것이 더 유리합니다. + +## `File` 매개변수와 `UploadFile` + +`File` 매개변수를 `UploadFile` 타입으로 정의합니다: + +```Python hl_lines="12" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +`UploadFile` 을 사용하는 것은 `bytes` 과 비교해 다음과 같은 장점이 있습니다: + +* "스풀 파일"을 사용합니다. + * 최대 크기 제한까지만 메모리에 저장되며, 이를 초과하는 경우 디스크에 저장됩니다. +* 따라서 이미지, 동영상, 큰 이진코드와 같은 대용량 파일들을 많은 메모리를 소모하지 않고 처리하기에 적합합니다. +* 업로드 된 파일의 메타데이터를 얻을 수 있습니다. +* file-like `async` 인터페이스를 갖고 있습니다. +* file-like object를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 파이썬 `SpooledTemporaryFile` 객체를 반환합니다. + +### `UploadFile` + +`UploadFile` 은 다음과 같은 어트리뷰트가 있습니다: + +* `filename` : 문자열(`str`)로 된 업로드된 파일의 파일명입니다 (예: `myimage.jpg`). +* `content_type` : 문자열(`str`)로 된 파일 형식(MIME type / media type)입니다 (예: `image/jpeg`). +* `file` : `SpooledTemporaryFile` (파일류 객체)입니다. 이것은 "파일류" 객체를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일입니다. + +`UploadFile` 에는 다음의 `async` 메소드들이 있습니다. 이들은 내부적인 `SpooledTemporaryFile` 을 사용하여 해당하는 파일 메소드를 호출합니다. + +* `write(data)`: `data`(`str` 또는 `bytes`)를 파일에 작성합니다. +* `read(size)`: 파일의 바이트 및 글자의 `size`(`int`)를 읽습니다. +* `seek(offset)`: 파일 내 `offset`(`int`) 위치의 바이트로 이동합니다. + * 예) `await myfile.seek(0)` 를 사용하면 파일의 시작부분으로 이동합니다. + * `await myfile.read()` 를 사용한 후 내용을 다시 읽을 때 유용합니다. +* `close()`: 파일을 닫습니다. + +상기 모든 메소드들이 `async` 메소드이기 때문에 “await”을 사용하여야 합니다. + +예를들어, `async` *경로 작동 함수*의 내부에서 다음과 같은 방식으로 내용을 가져올 수 있습니다: + +```Python +contents = await myfile.read() +``` + +만약 일반적인 `def` *경로 작동 함수*의 내부라면, 다음과 같이 `UploadFile.file` 에 직접 접근할 수 있습니다: + +```Python +contents = myfile.file.read() +``` + +!!! note "`async` 기술적 세부사항" + `async` 메소드들을 사용할 때 **FastAPI**는 스레드풀에서 파일 메소드들을 실행하고 그들을 기다립니다. + +!!! note "Starlette 기술적 세부사항" + **FastAPI**의 `UploadFile` 은 **Starlette**의 `UploadFile` 을 직접적으로 상속받지만, **Pydantic** 및 FastAPI의 다른 부분들과의 호환성을 위해 필요한 부분들이 추가되었습니다. + +## "폼 데이터"란 + +HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용합니다. + +**FastAPI**는 JSON 대신 올바른 위치에서 데이터를 읽을 수 있도록 합니다. + +!!! note "기술적 세부사항" + 폼의 데이터는 파일이 포함되지 않은 경우 일반적으로 "미디어 유형" `application/x-www-form-urlencoded` 을 사용해 인코딩 됩니다. + + 하지만 파일이 포함된 경우, `multipart/form-data`로 인코딩됩니다. `File`을 사용하였다면, **FastAPI**는 본문의 적합한 부분에서 파일을 가져와야 한다는 것을 인지합니다. + + 인코딩과 폼 필드에 대해 더 알고싶다면, POST에 관한MDN웹 문서 를 참고하기 바랍니다,. + +!!! warning "주의" + 다수의 `File` 과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json` 가 아닌 `multipart/form-data` 로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. + + 이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. + +## 다중 파일 업로드 + +여러 파일을 동시에 업로드 할 수 있습니다. + +그들은 "폼 데이터"를 사용하여 전송된 동일한 "폼 필드"에 연결됩니다. + +이 기능을 사용하기 위해 , `bytes` 의 `List` 또는 `UploadFile` 를 선언하기 바랍니다: + +```Python hl_lines="10 15" +{!../../../docs_src/request_files/tutorial002.py!} +``` + +선언한대로, `bytes` 의 `list` 또는 `UploadFile` 들을 전송받을 것입니다. + +!!! note "참고" + 2019년 4월 14일부터 Swagger UI가 하나의 폼 필드로 다수의 파일을 업로드하는 것을 지원하지 않습니다. 더 많은 정보를 원하면, #4276#3641을 참고하세요. + + 그럼에도, **FastAPI**는 표준 Open API를 사용해 이미 호환이 가능합니다. + + 따라서 Swagger UI 또는 기타 그 외의 OpenAPI를 지원하는 툴이 다중 파일 업로드를 지원하는 경우, 이들은 **FastAPI**와 호환됩니다. + +!!! note "기술적 세부사항" + `from starlette.responses import HTMLResponse` 역시 사용할 수 있습니다. + + **FastAPI**는 개발자의 편의를 위해 `fastapi.responses` 와 동일한 `starlette.responses` 도 제공합니다. 하지만 대부분의 응답들은 Starlette로부터 직접 제공됩니다. + +## 요약 + +폼 데이터로써 입력 매개변수로 업로드할 파일을 선언할 경우 `File` 을 사용하기 바랍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index dd2d6a7ac..124e7b1d3 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -63,6 +63,7 @@ nav: - tutorial/header-params.md - tutorial/path-params-numeric-validations.md - tutorial/response-status-code.md + - tutorial/request-files.md markdown_extensions: - toc: permalink: true From eb79441a7f23c0df42f13547af67159d037f43c5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:40:14 +0000 Subject: [PATCH 0025/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ca7553855..053244434 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). * 🌐 Add Korean translation for Tutorial - Path Parameters and Numeric Validations. PR [#2432](https://github.com/tiangolo/fastapi/pull/2432) by [@hard-coders](https://github.com/hard-coders). From aa4a69f790fb9a78ba7ed7767273f8734a5d028d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:45:00 +0000 Subject: [PATCH 0026/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 053244434..b96e81c76 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for Tutorial - JSON Compatible Encoder. PR [#3152](https://github.com/tiangolo/fastapi/pull/3152) by [@NEONKID](https://github.com/NEONKID). From 062efcbb5d19efb075ed1b50cb3f01d813c04460 Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Thu, 9 Dec 2021 19:47:26 +0900 Subject: [PATCH 0027/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/request-forms-and-files.md`=20(#374?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> --- .../docs/tutorial/request-forms-and-files.md | 35 +++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 36 insertions(+) create mode 100644 docs/ko/docs/tutorial/request-forms-and-files.md diff --git a/docs/ko/docs/tutorial/request-forms-and-files.md b/docs/ko/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..6750c7b23 --- /dev/null +++ b/docs/ko/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,35 @@ +# 폼 및 파일 요청 + +`File` 과 `Form` 을 사용하여 파일과 폼을 함께 정의할 수 있습니다. + +!!! info "정보" + 파일과 폼 데이터를 함께, 또는 각각 업로드하기 위해 먼저 `python-multipart`를 설치해야합니다. + + 예 ) `pip install python-multipart`. + +## `File` 및 `Form` 업로드 + +```Python hl_lines="1" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +## `File` 및 `Form` 매개변수 정의 + +`Body` 및 `Query`와 동일한 방식으로 파일과 폼의 매개변수를 생성합니다: + +```Python hl_lines="8" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +파일과 폼 필드는 폼 데이터 형식으로 업로드되어 파일과 폼 필드로 전달됩니다. + +어떤 파일들은 `bytes`로, 또 어떤 파일들은 `UploadFile`로 선언할 수 있습니다. + +!!! warning "주의" + 다수의 `File`과 `Form` 매개변수를 한 *경로 작동*에 선언하는 것이 가능하지만, 요청의 본문이 `application/json`가 아닌 `multipart/form-data`로 인코딩 되기 때문에 JSON으로 받아야하는 `Body` 필드를 함께 선언할 수는 없습니다. + + 이는 **FastAPI**의 한계가 아니라, HTTP 프로토콜에 의한 것입니다. + +## 요약 + +하나의 요청으로 데이터와 파일들을 받아야 할 경우 `File`과 `Form`을 함께 사용하기 바랍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 124e7b1d3..7a75940a3 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -64,6 +64,7 @@ nav: - tutorial/path-params-numeric-validations.md - tutorial/response-status-code.md - tutorial/request-files.md + - tutorial/request-forms-and-files.md markdown_extensions: - toc: permalink: true From 0a87bc88b85445018d3ced9d07ac6a37f9e6d8f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Dec 2021 10:48:10 +0000 Subject: [PATCH 0028/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b96e81c76..271aab5d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). * 🌐 Add Korean translation for `docs/tutorial/response-status-code.md`. PR [#3742](https://github.com/tiangolo/fastapi/pull/3742) by [@NinaHwang](https://github.com/NinaHwang). From b0cd4d7e7ebdbab130d663d73d5474361116ce63 Mon Sep 17 00:00:00 2001 From: Eric Jolibois Date: Sun, 12 Dec 2021 12:28:35 +0100 Subject: [PATCH 0029/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20?= =?UTF-8?q?for=20dataclasses,=20supporting=20the=20fixes=20in=20Pydantic?= =?UTF-8?q?=201.9=20(#4272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../test_dataclasses/test_tutorial002.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 10d8d227d..34aeb0be5 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -29,7 +31,7 @@ openapi_schema = { "schemas": { "Item": { "title": "Item", - "required": ["name", "price", "tags"], + "required": ["name", "price"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, @@ -51,7 +53,18 @@ openapi_schema = { def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == openapi_schema + # TODO: remove this once Pydantic 1.9 is released + # Ref: https://github.com/samuelcolvin/pydantic/pull/2557 + data = response.json() + alternative_data1 = deepcopy(data) + alternative_data2 = deepcopy(data) + alternative_data1["components"]["schemas"]["Item"]["required"] = ["name", "price"] + alternative_data2["components"]["schemas"]["Item"]["required"] = [ + "name", + "price", + "tags", + ] + assert alternative_data1 == openapi_schema or alternative_data2 == openapi_schema def test_get_item(): From 6d642ef5fb60eb48d178de2846657cf4d90e882a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 12 Dec 2021 11:29:36 +0000 Subject: [PATCH 0030/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 271aab5d1..2a0d27360 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). From e7158bc59212ddc5ecd6a45a83e6d99f8c6bb3d4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Dec 2021 12:34:18 +0100 Subject: [PATCH 0031/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#4274)?= 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 | 258 ++++++++++++++++++++-------------------- 1 file changed, 127 insertions(+), 131 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 8f1710d33..89c6f8dc3 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo answers: 1230 - prs: 260 + prs: 269 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 299 + count: 315 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: dmontagu @@ -30,41 +30,45 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: ArcLightSlavik - count: 68 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik +- login: raphaelauv + count: 63 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv - login: falkben - count: 57 + count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen count: 48 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: raphaelauv - count: 48 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv +- login: Dustyposa + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa - login: includeamin count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: Dustyposa - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa +- login: insomnes + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff +- login: STeveShary + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: krishnardt count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: insomnes - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -73,6 +77,14 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 +- login: adriangb + count: 26 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb +- login: ghandic + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: dbanty count: 25 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 @@ -85,13 +97,9 @@ experts: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- 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 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 url: https://github.com/acnebs - login: nsidnev count: 22 @@ -101,22 +109,18 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: panla + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: retnikt 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 @@ -125,26 +129,30 @@ 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: acidjunk + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar -- login: acidjunk +- login: dstlny count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 url: https://github.com/David-Lor +- login: lowercase00 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/21188280?v=4 + url: https://github.com/lowercase00 - login: zamiramir count: 11 avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4 @@ -153,10 +161,6 @@ experts: 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 @@ -173,43 +177,27 @@ experts: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 url: https://github.com/hellocoldworld +- login: oligond + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 + url: https://github.com/oligond last_month_active: +- login: raphaelauv + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv +- login: oligond + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 + url: https://github.com/oligond - login: Kludex - count: 13 + count: 5 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex -- login: ghandic - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic -- login: STeveShary - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary -- login: Honda-a +- login: Dustyposa count: 3 - 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/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 -- login: panla - count: 3 - 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 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa top_contributors: - login: waynerv count: 25 @@ -227,14 +215,18 @@ top_contributors: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: jaystone776 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl -- login: jaystone776 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 +- login: Smlep + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -248,8 +240,8 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: hard-coders - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders - login: wshayes count: 5 @@ -263,10 +255,6 @@ 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 @@ -275,13 +263,21 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 url: https://github.com/jfunez +- login: ycd + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 + url: https://github.com/ycd - login: komtaki count: 4 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki +- login: NinaHwang + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=36ca24be1fc3daa967b0f409bab0e17132d8c80c&v=4 + url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 90 + count: 91 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 url: https://github.com/Kludex - login: waynerv @@ -297,29 +293,37 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: ycd - count: 43 + count: 44 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: AdrianDeAnda - count: 32 + count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4 url: https://github.com/AdrianDeAnda - login: ArcLightSlavik - count: 27 + count: 31 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 url: https://github.com/dmontagu +- login: cassiobotaro + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 + url: https://github.com/cassiobotaro - login: komtaki count: 21 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: cassiobotaro +- login: hard-coders count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 - url: https://github.com/cassiobotaro + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + url: https://github.com/hard-coders +- login: 0417taehyun + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 + url: https://github.com/0417taehyun - login: yanever count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 @@ -328,10 +332,14 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc -- login: hard-coders +- login: Smlep count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=f2d3d2038c55d86d7f9348f4e6c5e30191e4ee8b&v=4 - url: https://github.com/hard-coders + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep +- login: DevDae + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 + url: https://github.com/DevDae - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -340,10 +348,6 @@ 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: 14 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 @@ -372,18 +376,38 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo +- login: yezz123 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 +- login: graingert + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 + url: https://github.com/graingert - login: PandaHun count: 9 avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4 url: https://github.com/PandaHun +- login: kty4119 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 + url: https://github.com/kty4119 - login: bezaca count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca +- login: solomein-sv + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + url: https://github.com/solomein-sv - login: blt232018 count: 8 avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 url: https://github.com/blt232018 +- login: ComicShrimp + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: Serrones count: 7 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -396,10 +420,6 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: graingert - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 - url: https://github.com/graingert - login: NastasiaSaby count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 @@ -408,14 +428,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause -- login: solomein-sv +- login: krocdort 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 + avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 + url: https://github.com/krocdort - login: jovicon count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 @@ -424,6 +440,10 @@ top_reviewers: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan +- login: diogoduartec + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 + url: https://github.com/diogoduartec - login: nimctl count: 5 avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 @@ -452,27 +472,3 @@ 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 - url: https://github.com/aviramha -- login: Cajuteq - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/26676532?u=8ee0422981810e51480855de1c0d67b6b79cd3f2&v=4 - url: https://github.com/Cajuteq -- login: Zxilly - 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 - url: https://github.com/Bluenix2 From 9c25f9615c037cdfdeccb06b002e6ed8fd50d903 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 12 Dec 2021 11:34:58 +0000 Subject: [PATCH 0032/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2a0d27360..2d3a0f4e4 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 [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). From 3efb4f7edff99fdc12802a85ae9d140ec4772497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 12 Dec 2021 12:39:32 +0100 Subject: [PATCH 0033/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?70.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 +++++++++++++- fastapi/__init__.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2d3a0f4e4..5995ecca6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,8 +2,16 @@ ## Latest Changes -* 👥 Update FastAPI People. PR [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). +## 0.70.1 + +There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 + +### Fixes + * 🐛 Fix JSON Schema for dataclasses, supporting the fixes in Pydantic 1.9. PR [#4272](https://github.com/tiangolo/fastapi/pull/4272) by [@PrettyWood](https://github.com/PrettyWood). + +### Translations + * 🌐 Add Korean translation for `docs/tutorial/request-forms-and-files.md`. PR [#3744](https://github.com/tiangolo/fastapi/pull/3744) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Korean translation for `docs/tutorial/request-files.md`. PR [#3743](https://github.com/tiangolo/fastapi/pull/3743) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add portuguese translation for `docs/tutorial/query-params-str-validations.md`. PR [#3965](https://github.com/tiangolo/fastapi/pull/3965) by [@leandrodesouzadev](https://github.com/leandrodesouzadev). @@ -19,6 +27,10 @@ * 🌐 Add French translation for `docs/tutorial/query-params.md`. PR [#3556](https://github.com/tiangolo/fastapi/pull/3556) by [@Smlep](https://github.com/Smlep). * 🌐 Add Turkish translation for `docs/python-types.md`. PR [#3926](https://github.com/tiangolo/fastapi/pull/3926) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). +### Internal + +* 👥 Update FastAPI People. PR [#4274](https://github.com/tiangolo/fastapi/pull/4274) by [@github-actions[bot]](https://github.com/apps/github-actions). + ## 0.70.0 This release just upgrades Starlette to the latest version, `0.16.0`, which includes several bug fixes and some small breaking changes. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 4d4d4333d..8bb6ce15b 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.70.0" +__version__ = "0.70.1" from starlette import status as status From 2b10ca1cc47146b07b6598ac9473cbe8cac50cc0 Mon Sep 17 00:00:00 2001 From: simondale00 <33907262+simondale00@users.noreply.github.com> Date: Fri, 7 Jan 2022 04:34:28 -0500 Subject: [PATCH 0034/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20to=200.17.1=20(#4145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dima Tisnek Co-authored-by: simond Co-authored-by: Samuel Colvin Co-authored-by: Samuel Colvin --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50e0afe87..4feae5524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] requires = [ - "starlette ==0.16.0", + "starlette ==0.17.1", "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" From 4e9f75912f75f09816328d5f8a28060c9d1ddd69 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 09:35:10 +0000 Subject: [PATCH 0035/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5995ecca6..add51ca59 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 From 764ecae2d477c48ba2e61dda44ee3d663d54b84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 11:24:00 +0100 Subject: [PATCH 0036/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20MkDocs=20Materi?= =?UTF-8?q?al=20and=20configs=20(#4385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 9 +++++---- docs/de/mkdocs.yml | 9 +++++---- docs/en/mkdocs.yml | 9 +++++---- docs/es/mkdocs.yml | 9 +++++---- docs/fr/mkdocs.yml | 9 +++++---- docs/id/mkdocs.yml | 9 +++++---- docs/it/mkdocs.yml | 9 +++++---- docs/ja/mkdocs.yml | 9 +++++---- docs/ko/mkdocs.yml | 9 +++++---- docs/pl/mkdocs.yml | 9 +++++---- docs/pt/mkdocs.yml | 9 +++++---- docs/ru/mkdocs.yml | 9 +++++---- docs/sq/mkdocs.yml | 9 +++++---- docs/tr/mkdocs.yml | 9 +++++---- docs/uk/mkdocs.yml | 9 +++++---- docs/zh/mkdocs.yml | 9 +++++---- pyproject.toml | 2 +- 17 files changed, 81 insertions(+), 65 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 72ca92a96..66220f63e 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 621305d20..360fa8c4a 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -71,8 +68,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index aa2d0451d..5bdd2c54a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -176,8 +173,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index ef722715b..a4bc41154 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -80,8 +77,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 6242a88fd..ff16e1d78 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -85,8 +82,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index f9cae22cc..d70d2b3c3 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index f16210608..e6d01fbde 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 237bc9654..39fd8a211 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -110,8 +107,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 7a75940a3..1d4d30913 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -80,8 +77,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 220008607..3c1351a12 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index ea4af852e..f202f306d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -90,8 +87,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index e15a36bf2..6e17c287e 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index a4879521e..d9c3dad4c 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5ef8fd6a7..f6ed7f5b9 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -73,8 +70,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 7f5785e3e..d0de8cc0e 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -70,8 +67,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 07ffa367f..a929e3388 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -29,9 +29,6 @@ theme: repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' -google_analytics: -- UA-133183413-1 -- auto plugins: - search - markdownextradata: @@ -120,8 +117,12 @@ markdown_extensions: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed +- pymdownx.tabbed: + alternate_style: true extra: + analytics: + provider: google + property: UA-133183413-1 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/pyproject.toml b/pyproject.toml index 4feae5524..e6148831b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ test = [ ] doc = [ "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=7.1.9,<8.0.0", + "mkdocs-material >=8.1.4,<9.0.0", "mdx-include >=1.4.1,<2.0.0", "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", "typer-cli >=0.0.12,<0.0.13", From 83f67810371f48c22b7955de926d86e42ac30416 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 10:24:43 +0000 Subject: [PATCH 0037/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 add51ca59..ce1d3144d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 From d08a062ee2121f446537f06c8425cdeb1209e4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 15:11:31 +0100 Subject: [PATCH 0038/1881] =?UTF-8?q?=E2=9C=A8=20Add=20docs=20and=20tests?= =?UTF-8?q?=20for=20Python=203.9=20and=20Python=203.10=20(#3712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Grainger --- .github/workflows/test.yml | 2 +- docs/az/mkdocs.yml | 1 - docs/de/mkdocs.yml | 1 - docs/en/docs/python-types.md | 208 ++++++++-- docs/en/docs/tutorial/background-tasks.md | 14 +- docs/en/docs/tutorial/body-fields.md | 28 +- docs/en/docs/tutorial/body-multiple-params.md | 76 +++- docs/en/docs/tutorial/body-nested-models.md | 208 ++++++++-- docs/en/docs/tutorial/body-updates.md | 80 +++- docs/en/docs/tutorial/body.md | 84 +++- docs/en/docs/tutorial/cookie-params.md | 28 +- .../dependencies/classes-as-dependencies.md | 98 ++++- docs/en/docs/tutorial/dependencies/index.md | 42 +- .../tutorial/dependencies/sub-dependencies.md | 48 ++- docs/en/docs/tutorial/encoder.md | 14 +- docs/en/docs/tutorial/extra-data-types.md | 28 +- docs/en/docs/tutorial/extra-models.md | 86 +++- docs/en/docs/tutorial/header-params.md | 63 ++- .../tutorial/path-operation-configuration.md | 100 ++++- .../path-params-numeric-validations.md | 30 +- .../tutorial/query-params-str-validations.md | 182 +++++++-- docs/en/docs/tutorial/query-params.md | 61 ++- docs/en/docs/tutorial/request-files.md | 16 +- docs/en/docs/tutorial/response-model.md | 158 +++++-- docs/en/docs/tutorial/schema-extra-example.md | 56 ++- .../tutorial/security/get-current-user.md | 72 +++- docs/en/docs/tutorial/security/oauth2-jwt.md | 56 ++- .../docs/tutorial/security/simple-oauth2.md | 70 +++- docs/en/docs/tutorial/sql-databases.md | 164 ++++++-- docs/en/docs/tutorial/testing.md | 20 +- docs/en/mkdocs.yml | 1 - docs/es/mkdocs.yml | 1 - docs/fr/mkdocs.yml | 1 - docs/id/mkdocs.yml | 1 - docs/it/mkdocs.yml | 1 - docs/ja/mkdocs.yml | 1 - docs/ko/mkdocs.yml | 1 - docs/pl/mkdocs.yml | 1 - docs/pt/mkdocs.yml | 1 - docs/ru/mkdocs.yml | 1 - docs/sq/mkdocs.yml | 1 - docs/tr/mkdocs.yml | 1 - docs/uk/mkdocs.yml | 1 - docs/zh/mkdocs.yml | 1 - docs_src/app_testing/app_b/__init__.py | 0 .../app_testing/{main_b.py => app_b/main.py} | 0 .../{test_main_b.py => app_b/test_main.py} | 2 +- docs_src/app_testing/app_b_py310/__init__.py | 0 docs_src/app_testing/app_b_py310/main.py | 36 ++ docs_src/app_testing/app_b_py310/test_main.py | 65 +++ .../background_tasks/tutorial002_py310.py | 24 ++ docs_src/body/tutorial001_py310.py | 17 + docs_src/body/tutorial002_py310.py | 21 + docs_src/body/tutorial003_py310.py | 17 + docs_src/body/tutorial004_py310.py | 20 + docs_src/body_fields/tutorial001_py310.py | 19 + .../body_multiple_params/tutorial001_py310.py | 26 ++ .../body_multiple_params/tutorial002_py310.py | 22 + .../body_multiple_params/tutorial003_py310.py | 24 ++ .../body_multiple_params/tutorial004_py310.py | 31 ++ .../body_multiple_params/tutorial005_py310.py | 17 + .../body_nested_models/tutorial001_py310.py | 18 + .../body_nested_models/tutorial002_py310.py | 18 + .../body_nested_models/tutorial002_py39.py | 20 + .../body_nested_models/tutorial003_py310.py | 18 + .../body_nested_models/tutorial003_py39.py | 20 + .../body_nested_models/tutorial004_py310.py | 24 ++ .../body_nested_models/tutorial004_py39.py | 26 ++ .../body_nested_models/tutorial005_py310.py | 24 ++ .../body_nested_models/tutorial005_py39.py | 26 ++ .../body_nested_models/tutorial006_py310.py | 24 ++ .../body_nested_models/tutorial006_py39.py | 26 ++ .../body_nested_models/tutorial007_py310.py | 30 ++ .../body_nested_models/tutorial007_py39.py | 32 ++ .../body_nested_models/tutorial008_py39.py | 14 + .../body_nested_models/tutorial009_py39.py | 8 + docs_src/body_updates/tutorial001_py310.py | 32 ++ docs_src/body_updates/tutorial001_py39.py | 34 ++ docs_src/body_updates/tutorial002_py310.py | 35 ++ docs_src/body_updates/tutorial002_py39.py | 37 ++ docs_src/cookie_params/tutorial001_py310.py | 8 + docs_src/dependencies/tutorial001_py310.py | 17 + docs_src/dependencies/tutorial002_py310.py | 23 ++ docs_src/dependencies/tutorial003_py310.py | 23 ++ docs_src/dependencies/tutorial004_py310.py | 23 ++ docs_src/dependencies/tutorial005_py310.py | 20 + docs_src/encoder/tutorial001_py310.py | 22 + .../extra_data_types/tutorial001_py310.py | 27 ++ docs_src/extra_models/tutorial001_py310.py | 41 ++ docs_src/extra_models/tutorial002_py310.py | 39 ++ docs_src/extra_models/tutorial003_py310.py | 35 ++ docs_src/extra_models/tutorial004_py39.py | 20 + docs_src/extra_models/tutorial005_py39.py | 8 + docs_src/header_params/tutorial001_py310.py | 8 + docs_src/header_params/tutorial002_py310.py | 10 + docs_src/header_params/tutorial003_py310.py | 8 + docs_src/header_params/tutorial003_py39.py | 10 + .../tutorial001.py | 2 +- .../tutorial001_py310.py | 17 + .../tutorial001_py39.py | 19 + .../tutorial002.py | 2 +- .../tutorial002_py310.py | 27 ++ .../tutorial002_py39.py | 29 ++ .../tutorial003.py | 2 +- .../tutorial003_py310.py | 22 + .../tutorial003_py39.py | 24 ++ .../tutorial004.py | 2 +- .../tutorial004_py310.py | 26 ++ .../tutorial004_py39.py | 28 ++ .../tutorial005.py | 2 +- .../tutorial005_py310.py | 31 ++ .../tutorial005_py39.py | 33 ++ .../tutorial001_py310.py | 14 + docs_src/python_types/tutorial006_py39.py | 3 + docs_src/python_types/tutorial007_py39.py | 2 + docs_src/python_types/tutorial008.py | 5 +- docs_src/python_types/tutorial008b.py | 5 + docs_src/python_types/tutorial008b_py310.py | 2 + docs_src/python_types/tutorial009_py310.py | 5 + docs_src/python_types/tutorial009b.py | 8 + docs_src/python_types/tutorial011_py310.py | 22 + docs_src/python_types/tutorial011_py39.py | 23 ++ docs_src/query_params/tutorial002_py310.py | 10 + docs_src/query_params/tutorial003_py310.py | 15 + docs_src/query_params/tutorial004_py310.py | 17 + docs_src/query_params/tutorial006_py310.py | 11 + docs_src/query_params/tutorial006b.py | 13 + .../tutorial001_py310.py | 11 + .../tutorial002_py310.py | 11 + .../tutorial003_py310.py | 11 + .../tutorial004_py310.py | 13 + .../tutorial007_py310.py | 11 + .../tutorial008_py310.py | 19 + .../tutorial009_py310.py | 11 + .../tutorial010_py310.py | 23 ++ .../tutorial011_py310.py | 9 + .../tutorial011_py39.py | 11 + .../tutorial012_py39.py | 9 + docs_src/request_files/tutorial002_py39.py | 31 ++ docs_src/response_model/tutorial001_py310.py | 17 + docs_src/response_model/tutorial001_py39.py | 19 + docs_src/response_model/tutorial002_py310.py | 17 + docs_src/response_model/tutorial003_py310.py | 22 + docs_src/response_model/tutorial004_py310.py | 24 ++ docs_src/response_model/tutorial004_py39.py | 26 ++ docs_src/response_model/tutorial005_py310.py | 37 ++ docs_src/response_model/tutorial006_py310.py | 37 ++ .../schema_extra_example/tutorial001_py310.py | 27 ++ .../schema_extra_example/tutorial002_py310.py | 17 + .../schema_extra_example/tutorial003_py310.py | 28 ++ .../schema_extra_example/tutorial004_py310.py | 50 +++ docs_src/security/tutorial002_py310.py | 30 ++ docs_src/security/tutorial003_py310.py | 88 ++++ docs_src/security/tutorial004_py310.py | 137 +++++++ docs_src/security/tutorial005_py310.py | 172 ++++++++ docs_src/security/tutorial005_py39.py | 173 ++++++++ .../sql_databases/sql_app_py310/__init__.py | 0 .../sql_databases/sql_app_py310/alt_main.py | 60 +++ docs_src/sql_databases/sql_app_py310/crud.py | 36 ++ .../sql_databases/sql_app_py310/database.py | 13 + docs_src/sql_databases/sql_app_py310/main.py | 53 +++ .../sql_databases/sql_app_py310/models.py | 26 ++ .../sql_databases/sql_app_py310/schemas.py | 35 ++ .../sql_app_py310/tests/__init__.py | 0 .../sql_app_py310/tests/test_sql_app.py | 47 +++ .../sql_databases/sql_app_py39/__init__.py | 0 .../sql_databases/sql_app_py39/alt_main.py | 60 +++ docs_src/sql_databases/sql_app_py39/crud.py | 36 ++ .../sql_databases/sql_app_py39/database.py | 13 + docs_src/sql_databases/sql_app_py39/main.py | 53 +++ docs_src/sql_databases/sql_app_py39/models.py | 26 ++ .../sql_databases/sql_app_py39/schemas.py | 37 ++ .../sql_app_py39/tests/__init__.py | 0 .../sql_app_py39/tests/test_sql_app.py | 47 +++ pyproject.toml | 1 + .../test_tutorial002_py310.py | 21 + .../test_body/test_tutorial001_py310.py | 292 +++++++++++++ .../test_tutorial001_py310.py | 176 ++++++++ .../test_tutorial001_py310.py | 155 +++++++ .../test_tutorial003_py310.py | 206 ++++++++++ .../test_tutorial009_py39.py | 113 ++++++ .../test_tutorial001_py310.py | 172 ++++++++ .../test_tutorial001_py39.py | 172 ++++++++ .../test_tutorial001_py310.py | 100 +++++ .../test_tutorial001_py310.py | 157 +++++++ .../test_tutorial004_py310.py | 152 +++++++ .../test_tutorial001_py310.py | 146 +++++++ .../test_tutorial003_py310.py | 135 ++++++ .../test_tutorial004_py39.py | 69 ++++ .../test_tutorial005_py39.py | 53 +++ .../test_tutorial001_py310.py | 94 +++++ .../test_tutorial005_py310.py | 121 ++++++ .../test_tutorial005_py39.py | 121 ++++++ .../test_tutorial006_py310.py | 148 +++++++ .../test_tutorial001_py310.py | 132 ++++++ .../test_tutorial011_py310.py | 105 +++++ .../test_tutorial011_py39.py | 105 +++++ .../test_tutorial012_py39.py | 106 +++++ .../test_tutorial002_py39.py | 235 +++++++++++ .../test_tutorial003_py310.py | 129 ++++++ .../test_tutorial004_py310.py | 133 ++++++ .../test_tutorial004_py39.py | 133 ++++++ .../test_tutorial005_py310.py | 152 +++++++ .../test_tutorial006_py310.py | 152 +++++++ .../test_tutorial004_py310.py | 143 +++++++ .../test_security/test_tutorial003_py310.py | 192 +++++++++ .../test_security/test_tutorial005_py310.py | 375 +++++++++++++++++ .../test_security/test_tutorial005_py39.py | 375 +++++++++++++++++ .../test_sql_databases/test_sql_databases.py | 7 +- .../test_sql_databases_middleware_py310.py | 384 ++++++++++++++++++ .../test_sql_databases_middleware_py39.py | 384 ++++++++++++++++++ .../test_sql_databases_py310.py | 383 +++++++++++++++++ .../test_sql_databases_py39.py | 383 +++++++++++++++++ .../test_testing_databases.py | 9 +- .../test_testing_databases_py310.py | 26 ++ .../test_testing_databases_py39.py | 26 ++ .../test_tutorial/test_testing/test_main_b.py | 14 +- .../test_testing/test_main_b_py310.py | 13 + tests/utils.py | 3 + 219 files changed, 11562 insertions(+), 452 deletions(-) create mode 100644 docs_src/app_testing/app_b/__init__.py rename docs_src/app_testing/{main_b.py => app_b/main.py} (100%) rename docs_src/app_testing/{test_main_b.py => app_b/test_main.py} (98%) create mode 100644 docs_src/app_testing/app_b_py310/__init__.py create mode 100644 docs_src/app_testing/app_b_py310/main.py create mode 100644 docs_src/app_testing/app_b_py310/test_main.py create mode 100644 docs_src/background_tasks/tutorial002_py310.py create mode 100644 docs_src/body/tutorial001_py310.py create mode 100644 docs_src/body/tutorial002_py310.py create mode 100644 docs_src/body/tutorial003_py310.py create mode 100644 docs_src/body/tutorial004_py310.py create mode 100644 docs_src/body_fields/tutorial001_py310.py create mode 100644 docs_src/body_multiple_params/tutorial001_py310.py create mode 100644 docs_src/body_multiple_params/tutorial002_py310.py create mode 100644 docs_src/body_multiple_params/tutorial003_py310.py create mode 100644 docs_src/body_multiple_params/tutorial004_py310.py create mode 100644 docs_src/body_multiple_params/tutorial005_py310.py create mode 100644 docs_src/body_nested_models/tutorial001_py310.py create mode 100644 docs_src/body_nested_models/tutorial002_py310.py create mode 100644 docs_src/body_nested_models/tutorial002_py39.py create mode 100644 docs_src/body_nested_models/tutorial003_py310.py create mode 100644 docs_src/body_nested_models/tutorial003_py39.py create mode 100644 docs_src/body_nested_models/tutorial004_py310.py create mode 100644 docs_src/body_nested_models/tutorial004_py39.py create mode 100644 docs_src/body_nested_models/tutorial005_py310.py create mode 100644 docs_src/body_nested_models/tutorial005_py39.py create mode 100644 docs_src/body_nested_models/tutorial006_py310.py create mode 100644 docs_src/body_nested_models/tutorial006_py39.py create mode 100644 docs_src/body_nested_models/tutorial007_py310.py create mode 100644 docs_src/body_nested_models/tutorial007_py39.py create mode 100644 docs_src/body_nested_models/tutorial008_py39.py create mode 100644 docs_src/body_nested_models/tutorial009_py39.py create mode 100644 docs_src/body_updates/tutorial001_py310.py create mode 100644 docs_src/body_updates/tutorial001_py39.py create mode 100644 docs_src/body_updates/tutorial002_py310.py create mode 100644 docs_src/body_updates/tutorial002_py39.py create mode 100644 docs_src/cookie_params/tutorial001_py310.py create mode 100644 docs_src/dependencies/tutorial001_py310.py create mode 100644 docs_src/dependencies/tutorial002_py310.py create mode 100644 docs_src/dependencies/tutorial003_py310.py create mode 100644 docs_src/dependencies/tutorial004_py310.py create mode 100644 docs_src/dependencies/tutorial005_py310.py create mode 100644 docs_src/encoder/tutorial001_py310.py create mode 100644 docs_src/extra_data_types/tutorial001_py310.py create mode 100644 docs_src/extra_models/tutorial001_py310.py create mode 100644 docs_src/extra_models/tutorial002_py310.py create mode 100644 docs_src/extra_models/tutorial003_py310.py create mode 100644 docs_src/extra_models/tutorial004_py39.py create mode 100644 docs_src/extra_models/tutorial005_py39.py create mode 100644 docs_src/header_params/tutorial001_py310.py create mode 100644 docs_src/header_params/tutorial002_py310.py create mode 100644 docs_src/header_params/tutorial003_py310.py create mode 100644 docs_src/header_params/tutorial003_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial001_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial001_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial002_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial002_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial003_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial003_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial004_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial004_py39.py create mode 100644 docs_src/path_operation_configuration/tutorial005_py310.py create mode 100644 docs_src/path_operation_configuration/tutorial005_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial001_py310.py create mode 100644 docs_src/python_types/tutorial006_py39.py create mode 100644 docs_src/python_types/tutorial007_py39.py create mode 100644 docs_src/python_types/tutorial008b.py create mode 100644 docs_src/python_types/tutorial008b_py310.py create mode 100644 docs_src/python_types/tutorial009_py310.py create mode 100644 docs_src/python_types/tutorial009b.py create mode 100644 docs_src/python_types/tutorial011_py310.py create mode 100644 docs_src/python_types/tutorial011_py39.py create mode 100644 docs_src/query_params/tutorial002_py310.py create mode 100644 docs_src/query_params/tutorial003_py310.py create mode 100644 docs_src/query_params/tutorial004_py310.py create mode 100644 docs_src/query_params/tutorial006_py310.py create mode 100644 docs_src/query_params/tutorial006b.py create mode 100644 docs_src/query_params_str_validations/tutorial001_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial002_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial003_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial004_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial007_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial008_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial009_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial010_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial011_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial011_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial012_py39.py create mode 100644 docs_src/request_files/tutorial002_py39.py create mode 100644 docs_src/response_model/tutorial001_py310.py create mode 100644 docs_src/response_model/tutorial001_py39.py create mode 100644 docs_src/response_model/tutorial002_py310.py create mode 100644 docs_src/response_model/tutorial003_py310.py create mode 100644 docs_src/response_model/tutorial004_py310.py create mode 100644 docs_src/response_model/tutorial004_py39.py create mode 100644 docs_src/response_model/tutorial005_py310.py create mode 100644 docs_src/response_model/tutorial006_py310.py create mode 100644 docs_src/schema_extra_example/tutorial001_py310.py create mode 100644 docs_src/schema_extra_example/tutorial002_py310.py create mode 100644 docs_src/schema_extra_example/tutorial003_py310.py create mode 100644 docs_src/schema_extra_example/tutorial004_py310.py create mode 100644 docs_src/security/tutorial002_py310.py create mode 100644 docs_src/security/tutorial003_py310.py create mode 100644 docs_src/security/tutorial004_py310.py create mode 100644 docs_src/security/tutorial005_py310.py create mode 100644 docs_src/security/tutorial005_py39.py create mode 100644 docs_src/sql_databases/sql_app_py310/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py310/alt_main.py create mode 100644 docs_src/sql_databases/sql_app_py310/crud.py create mode 100644 docs_src/sql_databases/sql_app_py310/database.py create mode 100644 docs_src/sql_databases/sql_app_py310/main.py create mode 100644 docs_src/sql_databases/sql_app_py310/models.py create mode 100644 docs_src/sql_databases/sql_app_py310/schemas.py create mode 100644 docs_src/sql_databases/sql_app_py310/tests/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py create mode 100644 docs_src/sql_databases/sql_app_py39/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py39/alt_main.py create mode 100644 docs_src/sql_databases/sql_app_py39/crud.py create mode 100644 docs_src/sql_databases/sql_app_py39/database.py create mode 100644 docs_src/sql_databases/sql_app_py39/main.py create mode 100644 docs_src/sql_databases/sql_app_py39/models.py create mode 100644 docs_src/sql_databases/sql_app_py39/schemas.py create mode 100644 docs_src/sql_databases/sql_app_py39/tests/__init__.py create mode 100644 docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py create mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_body/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_body_updates/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial004_py39.py create mode 100644 tests/test_tutorial/test_extra_models/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_query_params/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial004_py39.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial006_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial003_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py create mode 100644 tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py create mode 100644 tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py create mode 100644 tests/test_tutorial/test_testing/test_main_b_py310.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ab0f1230..21ea7c1a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] fail-fast: false steps: diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 66220f63e..dbff7b26a 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 360fa8c4a..8f29ef316 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 9bbf955b9..76d442855 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -148,37 +148,62 @@ You can use, for example: There are some data structures that can contain other values, like `dict`, `list`, `set` and `tuple`. And the internal values can have their own type too. -To declare those types and the internal types, you can use the standard Python module `typing`. +These types that have internal types are called "**generic**" types. And it's possible to declare them, even with their internal types. -It exists specifically to support these type hints. +To declare those types and the internal types, you can use the standard Python module `typing`. It exists specifically to support these type hints. -#### `List` +#### Newer versions of Python + +The syntax using `typing` is **compatible** with all versions, from Python 3.6 to the latest ones, including Python 3.9, Python 3.10, etc. + +As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. + +If you can chose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. + +#### List For example, let's define a variable to be a `list` of `str`. -From `typing`, import `List` (with a capital `L`): +=== "Python 3.6 and above" -```Python hl_lines="1" -{!../../../docs_src/python_types/tutorial006.py!} -``` + From `typing`, import `List` (with a capital `L`): -Declare the variable, with the same colon (`:`) syntax. + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` -As the type, put the `List`. + Declare the variable, with the same colon (`:`) syntax. -As the list is a type that contains some internal types, you put them in square brackets: + As the type, put the `List` that you imported from `typing`. -```Python hl_lines="4" -{!../../../docs_src/python_types/tutorial006.py!} -``` + As the list is a type that contains some internal types, you put them in square brackets: -!!! tip + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "Python 3.9 and above" + + Declare the variable, with the same colon (`:`) syntax. + + As the type, put `list`. + + As the list is a type that contains some internal types, you put them in square brackets: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info Those internal types in the square brackets are called "type parameters". - In this case, `str` is the type parameter passed to `List`. + In this case, `str` is the type parameter passed to `List` (or `list` in Python 3.9 and above). That means: "the variable `items` is a `list`, and each of the items in this list is a `str`". +!!! tip + If you use Python 3.9 or above, you don't have to import `List` from `typing`, you can use the same regular `list` type instead. + By doing that, your editor can provide support even while processing items from the list: @@ -189,20 +214,28 @@ Notice that the variable `item` is one of the elements in the list `items`. And still, the editor knows it is a `str`, and provides support for that. -#### `Tuple` and `Set` +#### Tuple and Set You would do the same to declare `tuple`s and `set`s: -```Python hl_lines="1 4" -{!../../../docs_src/python_types/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` This means: * The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. * The variable `items_s` is a `set`, and each of its items is of type `bytes`. -#### `Dict` +#### Dict To define a `dict`, you pass 2 type parameters, separated by commas. @@ -210,9 +243,17 @@ The first type parameter is for the keys of the `dict`. The second type parameter is for the values of the `dict`: -```Python hl_lines="1 4" -{!../../../docs_src/python_types/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` This means: @@ -220,9 +261,33 @@ This means: * The keys of this `dict` are of type `str` (let's say, the name of each item). * The values of this `dict` are of type `float` (let's say, the price of each item). -#### `Optional` +#### Union -You can also use `Optional` to declare that a variable has a type, like `str`, but that it is "optional", which means that it could also be `None`: +You can declare that a variable can be any of **several types**, for example, an `int` or a `str`. + +In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. + +In Python 3.10 there's also an **alternative syntax** were you can put the possible types separated by a vertical bar (`|`). + +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +In both cases this means that `item` could be an `int` or a `str`. + +#### Possibly `None` + +You can declare that a value could have a type, like `str`, but that it could also be `None`. + +In Python 3.6 and above (including Python 3.10) you can declare it by importing and using `Optional` from the `typing` module. ```Python hl_lines="1 4" {!../../../docs_src/python_types/tutorial009.py!} @@ -230,18 +295,73 @@ You can also use `Optional` to declare that a variable has a type, like `str`, b Using `Optional[str]` instead of just `str` will let the editor help you detecting errors where you could be assuming that a value is always a `str`, when it could actually be `None` too. +`Optional[Something]` is actually a shortcut for `Union[Something, None]`, they are equivalent. + +This also means that in Python 3.10, you can use `Something | None`: + +=== "Python 3.6 and above" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.6 and above - alternative" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + #### Generic types -These types that take type parameters in square brackets, like: +These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: -* `List` -* `Tuple` -* `Set` -* `Dict` -* `Optional` -* ...and others. +=== "Python 3.6 and above" -are called **Generic types** or **Generics**. + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.9 and above" + + You can use the same builtin types as generics (with square brakets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.6, from the `typing` module: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.10 and above" + + You can use the same builtin types as generics (with square brakets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.6, from the `typing` module: + + * `Union` + * `Optional` (the same as with Python 3.6) + * ...and others. + + In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types. ### Classes as types @@ -275,11 +395,25 @@ Then you create an instance of that class with some values and it will validate And you get all the editor support with that resulting object. -Taken from the official Pydantic docs: +An example from the official Pydantic docs: -```Python -{!../../../docs_src/python_types/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` !!! info To learn more about Pydantic, check its docs. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 6002ce075..69aeb6712 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -57,9 +57,17 @@ Using `BackgroundTasks` also works with the dependency injection system, you can **FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards: -```Python hl_lines="13 15 22 25" -{!../../../docs_src/background_tasks/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` In this example, the messages will be written to the `log.txt` file *after* the response is sent. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 6b3fd8fb5..1f38a0c5c 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -6,9 +6,17 @@ The same way you can declare additional validation and metadata in *path operati First, you have to import it: -```Python hl_lines="4" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` !!! warning Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc). @@ -17,9 +25,17 @@ First, you have to import it: You can then use `Field` with model attributes: -```Python hl_lines="11-14" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index a4484147f..13de4c8ea 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -8,9 +8,17 @@ First, of course, you can mix `Path`, `Query` and request body parameter declara And you can also declare body parameters as optional, by setting the default to `None`: -```Python hl_lines="19-21" -{!../../../docs_src/body_multiple_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` !!! note Notice that, in this case, the `item` that would be taken from the body is optional. As it has a `None` default value. @@ -30,9 +38,17 @@ In the previous example, the *path operations* would expect a JSON body with the But you can also declare multiple body parameters, e.g. `item` and `user`: -```Python hl_lines="22" -{!../../../docs_src/body_multiple_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). @@ -71,14 +87,20 @@ If you declare it as is, because it is a singular value, **FastAPI** will assume But you can instruct **FastAPI** to treat it as another body key using `Body`: +=== "Python 3.6 and above" -```Python hl_lines="23" -{!../../../docs_src/body_multiple_params/tutorial003.py!} -``` + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` In this case, **FastAPI** will expect a body like: - ```JSON { "item": { @@ -107,12 +129,26 @@ As, by default, singular values are interpreted as query parameters, you don't h q: Optional[str] = None ``` -as in: +Or in Python 3.10 and above: -```Python hl_lines="28" -{!../../../docs_src/body_multiple_params/tutorial004.py!} +```Python +q: str | None = None ``` +For example: + +=== "Python 3.6 and above" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + !!! info `Body` also has all the same extra validation and metadata parameters as `Query`,`Path` and others you will see later. @@ -131,9 +167,17 @@ item: Item = Body(..., embed=True) as in: -```Python hl_lines="17" -{!../../../docs_src/body_multiple_params/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` In this case **FastAPI** will expect a body like: diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index 5bacf1666..fa38cfc48 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -6,9 +6,17 @@ With **FastAPI**, you can define, validate, document, and use arbitrarily deeply You can define an attribute to be a subtype. For example, a Python `list`: -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` This will make `tags` be a list of items. Although it doesn't declare the type of each of the items. @@ -18,19 +26,29 @@ But Python has a specific way to declare lists with internal types, or "type par ### Import typing's `List` -First, import `List` from standard Python's `typing` module: +In Python 3.9 and above you can use the standard `list` to declare these type annotations as we'll see below. 💡 + +But in Python versions before 3.9 (3.6 and above), you first need to import `List` from standard Python's `typing` module: ```Python hl_lines="1" -{!../../../docs_src/body_nested_models/tutorial002.py!} +{!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` -### Declare a `List` with a type parameter +### Declare a `list` with a type parameter To declare types that have type parameters (internal types), like `list`, `dict`, `tuple`: -* Import them from the `typing` module +* If you are in a Python version lower than 3.9, import their equivalent version from the `typing` module * Pass the internal type(s) as "type parameters" using square brackets: `[` and `]` +In Python 3.9 it would be: + +```Python +my_list: list[str] +``` + +In versions of Python before 3.9, it would be: + ```Python from typing import List @@ -43,9 +61,23 @@ Use that same standard syntax for model attributes with internal types. So, in our example, we can make `tags` be specifically a "list of strings": -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` ## Set types @@ -53,11 +85,25 @@ But then we think about it, and realize that tags shouldn't repeat, they would p And Python has a special data type for sets of unique items, the `set`. -Then we can import `Set` and declare `tags` as a `set` of `str`: +Then we can declare `tags` as a set of strings: -```Python hl_lines="1 14" -{!../../../docs_src/body_nested_models/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` With this, even if you receive a request with duplicate data, it will be converted to a set of unique items. @@ -79,17 +125,45 @@ All that, arbitrarily nested. For example, we can define an `Image` model: -```Python hl_lines="9-11" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` ### Use the submodel as a type And then we can use it as the type of an attribute: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` This would mean that **FastAPI** would expect a body similar to: @@ -122,9 +196,23 @@ To see all the options you have, checkout the docs for ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` The string will be checked to be a valid URL, and documented in JSON Schema / OpenAPI as such. @@ -132,9 +220,23 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op You can also use Pydantic models as subtypes of `list`, `set`, etc: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` This will expect (convert, validate, document, etc) a JSON body like: @@ -169,9 +271,23 @@ This will expect (convert, validate, document, etc) a JSON body like: You can define arbitrarily deeply nested models: -```Python hl_lines="9 14 20 23 27" -{!../../../docs_src/body_nested_models/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` !!! info Notice how `Offer` has a list of `Item`s, which in turn have an optional list of `Image`s @@ -184,11 +300,25 @@ If the top level value of the JSON body you expect is a JSON `array` (a Python ` images: List[Image] ``` +or in Python 3.9 and above: + +```Python +images: list[Image] +``` + as in: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` ## Editor support everywhere @@ -218,9 +348,17 @@ That's what we are going to see here. In this case, you would accept any `dict` as long as it has `int` keys with `float` values: -```Python hl_lines="9" -{!../../../docs_src/body_nested_models/tutorial009.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` !!! tip Have in mind that JSON only supports `str` as keys. diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 757d7bdbc..7d8675060 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -6,9 +6,23 @@ To update an item you can use the ../../../docs_src/body_updates/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ``` `PUT` is used to receive data that should replace the existing data. @@ -53,9 +67,23 @@ That would generate a `dict` with only the data that was set when creating the ` Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: -```Python hl_lines="34" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` ### Using Pydantic's `update` parameter @@ -63,9 +91,23 @@ Now, you can create a copy of the existing model using `.copy()`, and pass the ` Like `stored_item_model.copy(update=update_data)`: -```Python hl_lines="35" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` ### Partial updates recap @@ -82,9 +124,23 @@ In summary, to apply partial updates you would: * Save the data to your DB. * Return the updated model. -```Python hl_lines="30-37" -{!../../../docs_src/body_updates/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` !!! tip You can actually use this same technique with an HTTP `PUT` operation. diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index f18afaea6..81441b41e 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -19,9 +19,17 @@ To declare a **request** body, you use ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` ## Create your data model @@ -29,9 +37,17 @@ Then you declare your data model as a class that inherits from `BaseModel`. Use standard Python types for all the attributes: -```Python hl_lines="7-11" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` The same as when declaring query parameters, when a model attribute has a default value, it is not required. Otherwise, it is required. Use `None` to make it just optional. @@ -59,9 +75,17 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like To add it to your *path operation*, declare it the same way you declared path and query parameters: -```Python hl_lines="18" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` ...and declare its type as the model you created, `Item`. @@ -125,9 +149,17 @@ But you would get the same editor support with ../../../docs_src/body/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` ## Request body + path parameters @@ -135,9 +167,17 @@ You can declare path parameters and request body at the same time. **FastAPI** will recognize that the function parameters that match path parameters should be **taken from the path**, and that function parameters that are declared to be Pydantic models should be **taken from the request body**. -```Python hl_lines="17-18" -{!../../../docs_src/body/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` ## Request body + path + query parameters @@ -145,9 +185,17 @@ You can also declare **body**, **path** and **query** parameters, all at the sam **FastAPI** will recognize each of them and take the data from the correct place. -```Python hl_lines="18" -{!../../../docs_src/body/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` The function parameters will be recognized as follows: diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 9aa2300c4..221cdfffb 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -6,9 +6,17 @@ You can define Cookie parameters the same way you define `Query` and `Path` para First import `Cookie`: -```Python hl_lines="3" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` ## Declare `Cookie` parameters @@ -16,9 +24,17 @@ Then declare the cookie parameters using the same structure as with `Path` and ` The first value is the default value, you can pass all the extra validation or annotation parameters: -```Python hl_lines="9" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` !!! note "Technical Details" `Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class. diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 7747e3e1b..663fff15b 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,9 +6,17 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the In the previous example, we were returning a `dict` from our dependency ("dependable"): -```Python hl_lines="9" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` But then we get a `dict` in the parameter `commons` of the *path operation function*. @@ -71,21 +79,45 @@ That also applies to callables with no parameters at all. The same as it would b Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParams`: -```Python hl_lines="11-15" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` Pay attention to the `__init__` method used to create the instance of the class: -```Python hl_lines="12" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` ...it has the same parameters as our previous `common_parameters`: -```Python hl_lines="8" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` Those parameters are what **FastAPI** will use to "solve" the dependency. @@ -101,9 +133,17 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` **FastAPI** calls the `CommonQueryParams` class. This creates an "instance" of that class and the instance will be passed as the parameter `commons` to your function. @@ -143,9 +183,17 @@ commons = Depends(CommonQueryParams) ..as in: -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc: @@ -179,9 +227,17 @@ You declare the dependency as the type of the parameter, and you use `Depends()` The same example would then look like: -```Python hl_lines="19" -{!../../../docs_src/dependencies/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` ...and **FastAPI** will know what to do. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index cf0b5a92f..fe10facfb 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -31,9 +31,17 @@ Let's first focus on the dependency. It is just a function that can take all the same parameters that a *path operation function* can take: -```Python hl_lines="8-9" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` That's it. @@ -55,17 +63,33 @@ And then it just returns a `dict` containing those values. ### Import `Depends` -```Python hl_lines="3" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` ### Declare the dependency, in the "dependant" The same way you use `Body`, `Query`, etc. with your *path operation function* parameters, use `Depends` with a new parameter: -```Python hl_lines="13 18" -{!../../../docs_src/dependencies/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 097edb68b..51531228d 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -6,25 +6,41 @@ They can be as **deep** as you need them to be. **FastAPI** will take care of solving them. -### First dependency "dependable" +## First dependency "dependable" You could create a first dependency ("dependable") like: -```Python hl_lines="8-9" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` It declares an optional query parameter `q` as a `str`, and then it just returns it. This is quite simple (not very useful), but will help us focus on how the sub-dependencies work. -### Second dependency, "dependable" and "dependant" +## Second dependency, "dependable" and "dependant" Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too): -```Python hl_lines="13" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` Let's focus on the parameters declared: @@ -33,13 +49,21 @@ Let's focus on the parameters declared: * It also declares an optional `last_query` cookie, as a `str`. * If the user didn't provide any query `q`, we use the last query used, which we saved to a cookie before. -### Use the dependency +## Use the dependency Then we can use the dependency with: -```Python hl_lines="21" -{!../../../docs_src/dependencies/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` !!! info Notice that we are only declaring one dependency in the *path operation function*, the `query_or_cookie_extractor`. diff --git a/docs/en/docs/tutorial/encoder.md b/docs/en/docs/tutorial/encoder.md index a2cbe45d5..7a69c5474 100644 --- a/docs/en/docs/tutorial/encoder.md +++ b/docs/en/docs/tutorial/encoder.md @@ -20,9 +20,17 @@ You can use `jsonable_encoder` for that. It receives an object, like a Pydantic model, and returns a JSON compatible version: -```Python hl_lines="5 22" -{!../../../docs_src/encoder/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 995f0b972..a00bd3212 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -55,12 +55,28 @@ Here are some of the additional data types you can use: Here's an example *path operation* with parameters using some of the above types. -```Python hl_lines="1 3 12-16" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: -```Python hl_lines="18-19" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 4eced04ca..72fc74894 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -17,9 +17,17 @@ This is especially the case for user models, because: Here's a general idea of how the models could look like with their password fields and the places where they are used: -```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" -{!../../../docs_src/extra_models/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` ### About `**user_in.dict()` @@ -150,9 +158,17 @@ All the data conversion, validation, documentation, etc. will still work as norm That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): -```Python hl_lines="9 15-16 19-20 23-24" -{!../../../docs_src/extra_models/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` ## `Union` or `anyOf` @@ -165,19 +181,49 @@ To do that, use the standard Python type hint `Union`, include the most specific type first, followed by the less specific type. In the example below, the more specific `PlaneItem` comes before `CarItem` in `Union[PlaneItem, CarItem]`. -```Python hl_lines="1 14-15 18-20 33" -{!../../../docs_src/extra_models/tutorial003.py!} +=== "Python 3.6 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` in Python 3.10 + +In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`. + +Because we are passing it as a **value to an argument** instead of putting it in a **type annotation**, we have to use `Union` even in Python 3.10. + +If it was in a type annotation we could have used the vertical bar, as: + +```Python +some_variable: PlaneItem | CarItem ``` +But if we put that in `response_model=PlaneItem | CarItem` we would get an error, because Python would try to perform an **invalid operation** between `PlaneItem` and `CarItem` instead of interpreting that as a type annotation. + ## List of models The same way, you can declare responses of lists of objects. -For that, use the standard Python `typing.List`: +For that, use the standard Python `typing.List` (or just `list` in Python 3.9 and above): -```Python hl_lines="1 20" -{!../../../docs_src/extra_models/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` ## Response with arbitrary `dict` @@ -185,11 +231,19 @@ You can also declare a response using a plain arbitrary `dict`, declaring just t This is useful if you don't know the valid field/attribute names (that would be needed for a Pydantic model) beforehand. -In this case, you can use `typing.Dict`: +In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above): -```Python hl_lines="1 8" -{!../../../docs_src/extra_models/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 57570153f..8e294416c 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -6,9 +6,17 @@ You can define Header parameters the same way you define `Query`, `Path` and `Co First import `Header`: -```Python hl_lines="3" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` ## Declare `Header` parameters @@ -16,9 +24,17 @@ Then declare the header parameters using the same structure as with `Path`, `Que The first value is the default value, you can pass all the extra validation or annotation parameters: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` !!! note "Technical Details" `Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class. @@ -44,14 +60,21 @@ So, you can use `user_agent` as you normally would in Python code, instead of ne If for some reason you need to disable automatic conversion of underscores to hyphens, set the parameter `convert_underscores` of `Header` to `False`: -```Python hl_lines="10" -{!../../../docs_src/header_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` !!! warning Before setting `convert_underscores` to `False`, bear in mind that some HTTP proxies and servers disallow the usage of headers with underscores. - ## Duplicate headers It is possible to receive duplicate headers. That means, the same header with multiple values. @@ -62,9 +85,23 @@ You will receive all the values from the duplicate header as a Python `list`. For example, to declare a header of `X-Token` that can appear more than once, you can write: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` If you communicate with that *path operation* sending two HTTP headers like: diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 0d606331d..1ff448e76 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -13,9 +13,23 @@ You can pass directly the `int` code, like `404`. But if you don't remember what each number code is for, you can use the shortcut constants in `status`: -```Python hl_lines="3 17" -{!../../../docs_src/path_operation_configuration/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` That status code will be used in the response and will be added to the OpenAPI schema. @@ -28,9 +42,23 @@ That status code will be used in the response and will be added to the OpenAPI s You can add tags to your *path operation*, pass the parameter `tags` with a `list` of `str` (commonly just one `str`): -```Python hl_lines="17 22 27" -{!../../../docs_src/path_operation_configuration/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` They will be added to the OpenAPI schema and used by the automatic documentation interfaces: @@ -40,9 +68,23 @@ They will be added to the OpenAPI schema and used by the automatic documentation You can add a `summary` and `description`: -```Python hl_lines="20-21" -{!../../../docs_src/path_operation_configuration/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` ## Description from docstring @@ -50,9 +92,23 @@ As descriptions tend to be long and cover multiple lines, you can declare the *p You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). -```Python hl_lines="19-27" -{!../../../docs_src/path_operation_configuration/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` It will be used in the interactive docs: @@ -62,9 +118,23 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: -```Python hl_lines="21" -{!../../../docs_src/path_operation_configuration/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` !!! info Notice that `response_description` refers specifically to the response, the `description` refers to the *path operation* in general. diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 5da69a21b..31bf91a0e 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -6,9 +6,17 @@ The same way you can declare more validations and metadata for query parameters First, import `Path` from `fastapi`: -```Python hl_lines="3" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` ## Declare metadata @@ -16,13 +24,21 @@ You can declare all the same parameters as for `Query`. For example, to declare a `title` metadata value for the path parameter `item_id` you can type: -```Python hl_lines="10" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` !!! note A path parameter is always required as it has to be part of the path. - + So, you should declare it with `...` to mark it as required. Nevertheless, even if you declared it with `None` or set a default value, it would not affect anything, it would still be always required. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 8deccda0b..fcac1a4e0 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -4,11 +4,19 @@ Let's take this application as example: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial001.py!} -``` +=== "Python 3.6 and above" -The query parameter `q` is of type `Optional[str]`, that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +The query parameter `q` is of type `Optional[str]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. !!! note FastAPI will know that the value of `q` is not required because of the default value `= None`. @@ -23,17 +31,33 @@ We are going to enforce that even though `q` is optional, whenever it is provide To achieve that, first import `Query` from `fastapi`: -```Python hl_lines="3" -{!../../../docs_src/query_params_str_validations/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` ## Use `Query` as the default value And now use it as the default value of your parameter, setting the parameter `max_length` to 50: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value. @@ -49,10 +73,22 @@ q: Optional[str] = Query(None) q: Optional[str] = None ``` +And in Python 3.10 and above: + +```Python +q: str | None = Query(None) +``` + +...makes the parameter optional, the same as: + +```Python +q: str | None = None +``` + But it declares it explicitly as being a query parameter. !!! info - Have in mind that FastAPI cares about the part: + Have in mind that the most important part to make a parameter optional is the part: ```Python = None @@ -64,9 +100,9 @@ But it declares it explicitly as being a query parameter. = Query(None) ``` - and will use that `None` to detect that the query parameter is not required. + as it will use that `None` as the default value, and that way make the parameter **not required**. - The `Optional` part is only to allow your editor to provide better support. + The `Optional` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required. Then, we can pass more parameters to `Query`. In this case, the `max_length` parameter that applies to strings: @@ -80,17 +116,33 @@ This will validate the data, show a clear error when the data is not valid, and You can also add a parameter `min_length`: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` ## Add regular expressions You can define a regular expression that the parameter should match: -```Python hl_lines="10" -{!../../../docs_src/query_params_str_validations/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` This specific regular expression checks that the received parameter value: @@ -152,9 +204,23 @@ When you define a query parameter explicitly with `Query` you can also declare i For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` Then, with a URL like: @@ -186,9 +252,17 @@ The interactive API docs will update accordingly, to allow multiple values: And you can also define a default `list` of values if none are provided: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial012.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` If you go to: @@ -209,7 +283,7 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: #### Using `list` -You can also use `list` directly instead of `List[str]`: +You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): ```Python hl_lines="7" {!../../../docs_src/query_params_str_validations/tutorial013.py!} @@ -233,15 +307,31 @@ That information will be included in the generated OpenAPI and used by the docum You can add a `title`: -```Python hl_lines="10" -{!../../../docs_src/query_params_str_validations/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` And a `description`: -```Python hl_lines="13" -{!../../../docs_src/query_params_str_validations/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` ## Alias parameters @@ -261,9 +351,17 @@ But you still need it to be exactly `item-query`... Then you can declare an `alias`, and that alias is what will be used to find the parameter value: -```Python hl_lines="9" -{!../../../docs_src/query_params_str_validations/tutorial009.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` ## Deprecating parameters @@ -273,9 +371,17 @@ You have to leave it there a while because there are clients using it, but you w Then pass the parameter `deprecated=True` to `Query`: -```Python hl_lines="18" -{!../../../docs_src/query_params_str_validations/tutorial010.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` The docs will show it like this: diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 4401745ac..eec55502a 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -63,27 +63,38 @@ The parameter values in your function will be: The same way, you can declare optional query parameters, by setting their default to `None`: -```Python hl_lines="9" -{!../../../docs_src/query_params/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` In this case, the function parameter `q` will be optional, and will be `None` by default. !!! check Also notice that **FastAPI** is smart enough to notice that the path parameter `item_id` is a path parameter and `q` is not, so, it's a query parameter. -!!! note - FastAPI will know that `q` is optional because of the `= None`. - - The `Optional` in `Optional[str]` is not used by FastAPI (FastAPI will only use the `str` part), but the `Optional[str]` will let your editor help you finding errors in your code. - ## Query parameter type conversion You can also declare `bool` types, and they will be converted: -```Python hl_lines="9" -{!../../../docs_src/query_params/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` In this case, if you go to: @@ -126,9 +137,17 @@ And you don't have to declare them in any specific order. They will be detected by name: -```Python hl_lines="8 10" -{!../../../docs_src/query_params/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` ## Required query parameters @@ -184,9 +203,17 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy And of course, you can define some parameters as required, some as having a default value, and some entirely optional: -```Python hl_lines="10" -{!../../../docs_src/query_params/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` In this case, there are 3 query parameters: diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 68ea654c2..b7257c7eb 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -119,11 +119,19 @@ It's possible to upload several files at the same time. They would be associated to the same "form field" sent using "form data". -To use that, declare a `List` of `bytes` or `UploadFile`: +To use that, declare a list of `bytes` or `UploadFile`: -```Python hl_lines="10 15" -{!../../../docs_src/request_files/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` You will receive, as declared, a `list` of `bytes` or `UploadFile`s. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index e2b89ca9e..c751a9256 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -8,9 +8,23 @@ You can declare the model used for the response with the parameter `response_mod * `@app.delete()` * etc. -```Python hl_lines="17" -{!../../../docs_src/response_model/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` !!! note Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your *path operation function*, like all the parameters and body. @@ -35,15 +49,31 @@ But most importantly: Here we are declaring a `UserIn` model, it will contain a plaintext password: -```Python hl_lines="9 11" -{!../../../docs_src/response_model/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` And we are using this model to declare our input and the same model to declare our output: -```Python hl_lines="17-18" -{!../../../docs_src/response_model/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` Now, whenever a browser is creating a user with a password, the API will return the same password in the response. @@ -58,21 +88,45 @@ But if we use the same model for another *path operation*, we could be sending o We can instead create an input model with the plaintext password and an output model without it: -```Python hl_lines="9 11 16" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` Here, even though our *path operation function* is returning the same input user that contains the password: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: -```Python hl_lines="22" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). @@ -90,9 +144,23 @@ And both models will be used for the interactive API documentation: Your response model could have default values, like: -```Python hl_lines="11 13-14" -{!../../../docs_src/response_model/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` * `description: Optional[str] = None` has a default of `None`. * `tax: float = 10.5` has a default of `10.5`. @@ -106,9 +174,23 @@ For example, if you have models with many optional attributes in a NoSQL databas You can set the *path operation decorator* parameter `response_model_exclude_unset=True`: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` and those default values won't be included in the response, only the values actually set. @@ -185,9 +267,17 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan This also applies to `response_model_by_alias` that works similarly. -```Python hl_lines="31 37" -{!../../../docs_src/response_model/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` !!! tip The syntax `{"name", "description"}` creates a `set` with those two values. @@ -198,9 +288,17 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly: -```Python hl_lines="31 37" -{!../../../docs_src/response_model/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 71f9a3918..c69df51dc 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -8,9 +8,17 @@ Here are several ways to do it. You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: -```Python hl_lines="15-23" -{!../../../docs_src/schema_extra_example/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. @@ -25,9 +33,17 @@ When using `Field()` with Pydantic models, you can also declare extra info for t You can use this to add `example` for each field: -```Python hl_lines="4 10-13" -{!../../../docs_src/schema_extra_example/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` !!! warning Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. @@ -50,9 +66,17 @@ you can also declare a data `example` or a group of `examples` with additional i Here we pass an `example` of the data expected in `Body()`: -```Python hl_lines="21-26" -{!../../../docs_src/schema_extra_example/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21-26" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19-24" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` ### Example in the docs UI @@ -73,9 +97,17 @@ Each specific example `dict` in the `examples` can contain: * `value`: This is the actual example shown, e.g. a `dict`. * `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. -```Python hl_lines="22-48" -{!../../../docs_src/schema_extra_example/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="22-48" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="20-46" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` ### Examples in the docs UI diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index a41db2b67..13e867216 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -16,9 +16,17 @@ First, let's create a Pydantic user model. The same way we use Pydantic to declare bodies, we can use it anywhere else: -```Python hl_lines="5 12-16" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="3 10-14" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Create a `get_current_user` dependency @@ -30,25 +38,49 @@ Remember that dependencies can have sub-dependencies? The same as we were doing before in the *path operation* directly, our new dependency `get_current_user` will receive a `token` as a `str` from the sub-dependency `oauth2_scheme`: -```Python hl_lines="25" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Get the user `get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model: -```Python hl_lines="19-22 26-27" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-20 24-25" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Inject the current user So now we can use the same `Depends` with our `get_current_user` in the *path operation*: -```Python hl_lines="31" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="29" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` Notice that we declare the type of `current_user` as the Pydantic model `User`. @@ -64,7 +96,6 @@ This will help us inside of the function with all the completion and type checks We are not restricted to having only one dependency that can return that type of data. - ## Other models You can now get the current user directly in the *path operation functions* and deal with the security mechanisms at the **Dependency Injection** level, using `Depends`. @@ -81,7 +112,6 @@ You actually don't have users that log in to your application but robots, bots, Just use any kind of model, any kind of class, any kind of database that you need for your application. **FastAPI** has you covered with the dependency injection system. - ## Code size This example might seem verbose. Have in mind that we are mixing security, data models, utility functions and *path operations* in the same file. @@ -98,9 +128,17 @@ And all of them (or any portion of them that you want) can take the advantage of And all these thousands of *path operations* can be as small as 3 lines: -```Python hl_lines="30-32" -{!../../../docs_src/security/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="28-30" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index f88cf23c0..09557f1ac 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -109,9 +109,17 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. -```Python hl_lines="7 48 55-56 59-60 69-75" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 47 54-55 58-59 68-74" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` !!! note If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. @@ -144,9 +152,17 @@ Define a Pydantic Model that will be used in the token endpoint for the response Create a utility function to generate a new access token. -```Python hl_lines="6 12-14 28-30 78-86" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="5 11-13 27-29 77-85" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ## Update the dependencies @@ -156,9 +172,17 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. -```Python hl_lines="89-106" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="88-105" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ## Update the `/token` *path operation* @@ -166,9 +190,17 @@ Create a `timedelta` with the expiration time of the token. Create a real JWT access token and return it. -```Python hl_lines="115-128" -{!../../../docs_src/security/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="115-128" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="114-127" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` ### Technical details about the JWT "subject" `sub` diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 2aa747847..505b223b1 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -49,9 +49,17 @@ Now let's use the utilities provided by **FastAPI** to handle this. First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` in the *path operation* for `/token`: -```Python hl_lines="4 76" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="2 74" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` `OAuth2PasswordRequestForm` is a class dependency that declares a form body with: @@ -90,9 +98,17 @@ If there is no such user, we return an error saying "incorrect username or passw For the error, we use the exception `HTTPException`: -```Python hl_lines="3 77-79" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 75-77" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` ### Check the password @@ -118,9 +134,17 @@ If your database is stolen, the thief won't have your users' plaintext passwords So, the thief won't be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous). -```Python hl_lines="80-83" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="78-81" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` #### About `**user_dict` @@ -156,9 +180,17 @@ For this simple example, we are going to just be completely insecure and return But for now, let's focus on the specific details we need. -```Python hl_lines="85" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="83" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` !!! tip By the spec, you should return a JSON with an `access_token` and a `token_type`, the same as in this example. @@ -181,9 +213,17 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active: -```Python hl_lines="58-67 69-72 90" -{!../../../docs_src/security/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="55-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` !!! info The additional header `WWW-Authenticate` with value `Bearer` we are returning here is also part of the spec. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index e8ebb29c8..9dc2f64c6 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -248,9 +248,23 @@ So, the user will also have a `password` when creating it. But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user. -```Python hl_lines="3 6-8 11-12 23-24 27-28" -{!../../../docs_src/sql_databases/sql_app/schemas.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` #### SQLAlchemy style and Pydantic style @@ -278,9 +292,23 @@ The same way, when reading a user, we can now declare that `items` will contain Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`. -```Python hl_lines="15-17 31-34" -{!../../../docs_src/sql_databases/sql_app/schemas.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` !!! tip Notice that the `User`, the Pydantic *model* that will be used when reading a user (returning it from the API) doesn't include the `password`. @@ -293,9 +321,23 @@ This ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` !!! tip Notice it's assigning a value with `=`, like: @@ -425,9 +467,17 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part In a very simplistic way create the database tables: -```Python hl_lines="9" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` #### Alembic Note @@ -451,9 +501,17 @@ For that, we will create a new dependency with `yield`, as explained before in t Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished. -```Python hl_lines="15-20" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. @@ -468,9 +526,17 @@ And then, when using the dependency in a *path operation function*, we declare i This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`: -```Python hl_lines="24 32 38 47 53" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` !!! info "Technical Details" The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided. @@ -481,9 +547,17 @@ This will then give us better editor support inside the *path operation function Now, finally, here's the standard **FastAPI** *path operations* code. -```Python hl_lines="23-28 31-34 37-42 45-49 52-55" -{!../../../docs_src/sql_databases/sql_app/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards. @@ -566,9 +640,23 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` * `sql_app/crud.py`: @@ -578,9 +666,17 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` ## Check it @@ -629,9 +725,17 @@ A "middleware" is basically a function that is always executed for each request, The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished. -```Python hl_lines="14-22" -{!../../../docs_src/sql_databases/sql_app/alt_main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ``` !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 9c9a8270c..7e2ae84d0 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -65,7 +65,7 @@ Now let's extend this example and add more details to see how to test different ### Extended **FastAPI** app file -Let's say you have a file `main_b.py` with your **FastAPI** app. +Let's say that now the file `main.py` with your **FastAPI** app has some other **path operations**. It has a `GET` operation that could return an error. @@ -73,16 +73,24 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. -```Python -{!../../../docs_src/app_testing/main_b.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "Python 3.10 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` ### Extended testing file -You could then have a `test_main_b.py`, the same as before, with the extended tests: +You could then update `test_main.py` with the extended tests: ```Python -{!../../../docs_src/app_testing/test_main_b.py!} +{!> ../../../docs_src/app_testing/app_b/test_main.py!} ``` Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 5bdd2c54a..b3716f508 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index a4bc41154..06b015a1d 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index ff16e1d78..c42f92b8a 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index d70d2b3c3..0dc34a14f 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index e6d01fbde..18afbd547 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 39fd8a211..327fe862a 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 1d4d30913..5ffb71877 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 3c1351a12..724dc2725 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index f202f306d..f36ecf49d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 6e17c287e..eb2597de7 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index d9c3dad4c..5afe0a665 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index f6ed7f5b9..40ba6d522 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index d0de8cc0e..a116d90a3 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a929e3388..33d40129a 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -20,7 +20,6 @@ theme: features: - search.suggest - search.highlight - - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs_src/app_testing/app_b/__init__.py b/docs_src/app_testing/app_b/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/main_b.py b/docs_src/app_testing/app_b/main.py similarity index 100% rename from docs_src/app_testing/main_b.py rename to docs_src/app_testing/app_b/main.py diff --git a/docs_src/app_testing/test_main_b.py b/docs_src/app_testing/app_b/test_main.py similarity index 98% rename from docs_src/app_testing/test_main_b.py rename to docs_src/app_testing/app_b/test_main.py index 83cc7d255..d186b8ecb 100644 --- a/docs_src/app_testing/test_main_b.py +++ b/docs_src/app_testing/app_b/test_main.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from .main_b import app +from .main import app client = TestClient(app) diff --git a/docs_src/app_testing/app_b_py310/__init__.py b/docs_src/app_testing/app_b_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py new file mode 100644 index 000000000..d44ab9e7c --- /dev/null +++ b/docs_src/app_testing/app_b_py310/main.py @@ -0,0 +1,36 @@ +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: str = Header(...)): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: str = Header(...)): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_py310/test_main.py b/docs_src/app_testing/app_b_py310/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/background_tasks/tutorial002_py310.py b/docs_src/background_tasks/tutorial002_py310.py new file mode 100644 index 000000000..626af1358 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_py310.py @@ -0,0 +1,24 @@ +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: str = Depends(get_query) +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/body/tutorial001_py310.py b/docs_src/body/tutorial001_py310.py new file mode 100644 index 000000000..b71a52b62 --- /dev/null +++ b/docs_src/body/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + return item diff --git a/docs_src/body/tutorial002_py310.py b/docs_src/body/tutorial002_py310.py new file mode 100644 index 000000000..8928b72b8 --- /dev/null +++ b/docs_src/body/tutorial002_py310.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.post("/items/") +async def create_item(item: Item): + item_dict = item.dict() + if item.tax: + price_with_tax = item.price + item.tax + item_dict.update({"price_with_tax": price_with_tax}) + return item_dict diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py new file mode 100644 index 000000000..a936f28fd --- /dev/null +++ b/docs_src/body/tutorial003_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def create_item(item_id: int, item: Item): + return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py new file mode 100644 index 000000000..60cfd9610 --- /dev/null +++ b/docs_src/body/tutorial004_py310.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def create_item(item_id: int, item: Item, q: str | None = None): + result = {"item_id": item_id, **item.dict()} + if q: + result.update({"q": q}) + return result diff --git a/docs_src/body_fields/tutorial001_py310.py b/docs_src/body_fields/tutorial001_py310.py new file mode 100644 index 000000000..01e02a050 --- /dev/null +++ b/docs_src/body_fields/tutorial001_py310.py @@ -0,0 +1,19 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + None, title="The description of the item", max_length=300 + ) + price: float = Field(..., gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item = Body(..., embed=True)): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_py310.py b/docs_src/body_multiple_params/tutorial001_py310.py new file mode 100644 index 000000000..b08d397b3 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial002_py310.py b/docs_src/body_multiple_params/tutorial002_py310.py new file mode 100644 index 000000000..2c4eb824c --- /dev/null +++ b/docs_src/body_multiple_params/tutorial002_py310.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item, user: User): + results = {"item_id": item_id, "item": item, "user": user} + return results diff --git a/docs_src/body_multiple_params/tutorial003_py310.py b/docs_src/body_multiple_params/tutorial003_py310.py new file mode 100644 index 000000000..9ddbda3f7 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_py310.py @@ -0,0 +1,24 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: int = Body(...) +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py new file mode 100644 index 000000000..77321300e --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_py310.py @@ -0,0 +1,31 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: int = Body(..., gt=0), + q: str | None = None +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial005_py310.py b/docs_src/body_multiple_params/tutorial005_py310.py new file mode 100644 index 000000000..97b213b16 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_py310.py @@ -0,0 +1,17 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item = Body(..., embed=True)): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial001_py310.py b/docs_src/body_nested_models/tutorial001_py310.py new file mode 100644 index 000000000..d89be35d4 --- /dev/null +++ b/docs_src/body_nested_models/tutorial001_py310.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: list = [] + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial002_py310.py b/docs_src/body_nested_models/tutorial002_py310.py new file mode 100644 index 000000000..71340e9fd --- /dev/null +++ b/docs_src/body_nested_models/tutorial002_py310.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: list[str] = [] + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial002_py39.py b/docs_src/body_nested_models/tutorial002_py39.py new file mode 100644 index 000000000..af523a74e --- /dev/null +++ b/docs_src/body_nested_models/tutorial002_py39.py @@ -0,0 +1,20 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: list[str] = [] + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial003_py310.py b/docs_src/body_nested_models/tutorial003_py310.py new file mode 100644 index 000000000..194f2dc23 --- /dev/null +++ b/docs_src/body_nested_models/tutorial003_py310.py @@ -0,0 +1,18 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial003_py39.py b/docs_src/body_nested_models/tutorial003_py39.py new file mode 100644 index 000000000..931d92f88 --- /dev/null +++ b/docs_src/body_nested_models/tutorial003_py39.py @@ -0,0 +1,20 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial004_py310.py b/docs_src/body_nested_models/tutorial004_py310.py new file mode 100644 index 000000000..ab0b63db1 --- /dev/null +++ b/docs_src/body_nested_models/tutorial004_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Image(BaseModel): + url: str + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = [] + image: Image | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial004_py39.py b/docs_src/body_nested_models/tutorial004_py39.py new file mode 100644 index 000000000..19985ec7a --- /dev/null +++ b/docs_src/body_nested_models/tutorial004_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Image(BaseModel): + url: str + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = [] + image: Optional[Image] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial005_py310.py b/docs_src/body_nested_models/tutorial005_py310.py new file mode 100644 index 000000000..009628453 --- /dev/null +++ b/docs_src/body_nested_models/tutorial005_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + image: Image | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial005_py39.py b/docs_src/body_nested_models/tutorial005_py39.py new file mode 100644 index 000000000..504551883 --- /dev/null +++ b/docs_src/body_nested_models/tutorial005_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + image: Optional[Image] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial006_py310.py b/docs_src/body_nested_models/tutorial006_py310.py new file mode 100644 index 000000000..c87cda3c8 --- /dev/null +++ b/docs_src/body_nested_models/tutorial006_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + images: list[Image] | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial006_py39.py b/docs_src/body_nested_models/tutorial006_py39.py new file mode 100644 index 000000000..61898178e --- /dev/null +++ b/docs_src/body_nested_models/tutorial006_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + images: Optional[list[Image]] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_nested_models/tutorial007_py310.py b/docs_src/body_nested_models/tutorial007_py310.py new file mode 100644 index 000000000..665ac56e5 --- /dev/null +++ b/docs_src/body_nested_models/tutorial007_py310.py @@ -0,0 +1,30 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + images: list[Image] | None = None + + +class Offer(BaseModel): + name: str + description: str | None = None + price: float + items: list[Item] + + +@app.post("/offers/") +async def create_offer(offer: Offer): + return offer diff --git a/docs_src/body_nested_models/tutorial007_py39.py b/docs_src/body_nested_models/tutorial007_py39.py new file mode 100644 index 000000000..0c7d32fbb --- /dev/null +++ b/docs_src/body_nested_models/tutorial007_py39.py @@ -0,0 +1,32 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + images: Optional[list[Image]] = None + + +class Offer(BaseModel): + name: str + description: Optional[str] = None + price: float + items: list[Item] + + +@app.post("/offers/") +async def create_offer(offer: Offer): + return offer diff --git a/docs_src/body_nested_models/tutorial008_py39.py b/docs_src/body_nested_models/tutorial008_py39.py new file mode 100644 index 000000000..854a7a5a4 --- /dev/null +++ b/docs_src/body_nested_models/tutorial008_py39.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI +from pydantic import BaseModel, HttpUrl + +app = FastAPI() + + +class Image(BaseModel): + url: HttpUrl + name: str + + +@app.post("/images/multiple/") +async def create_multiple_images(images: list[Image]): + return images diff --git a/docs_src/body_nested_models/tutorial009_py39.py b/docs_src/body_nested_models/tutorial009_py39.py new file mode 100644 index 000000000..59c1e5082 --- /dev/null +++ b/docs_src/body_nested_models/tutorial009_py39.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.post("/index-weights/") +async def create_index_weights(weights: dict[int, float]): + return weights diff --git a/docs_src/body_updates/tutorial001_py310.py b/docs_src/body_updates/tutorial001_py310.py new file mode 100644 index 000000000..d275d7f84 --- /dev/null +++ b/docs_src/body_updates/tutorial001_py310.py @@ -0,0 +1,32 @@ +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str | None = None + description: str | None = None + price: float | None = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.put("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + update_item_encoded = jsonable_encoder(item) + items[item_id] = update_item_encoded + return update_item_encoded diff --git a/docs_src/body_updates/tutorial001_py39.py b/docs_src/body_updates/tutorial001_py39.py new file mode 100644 index 000000000..5d5388b56 --- /dev/null +++ b/docs_src/body_updates/tutorial001_py39.py @@ -0,0 +1,34 @@ +from typing import Optional + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.put("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + update_item_encoded = jsonable_encoder(item) + items[item_id] = update_item_encoded + return update_item_encoded diff --git a/docs_src/body_updates/tutorial002_py310.py b/docs_src/body_updates/tutorial002_py310.py new file mode 100644 index 000000000..349841496 --- /dev/null +++ b/docs_src/body_updates/tutorial002_py310.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str | None = None + description: str | None = None + price: float | None = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.patch("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + stored_item_data = items[item_id] + stored_item_model = Item(**stored_item_data) + update_data = item.dict(exclude_unset=True) + updated_item = stored_item_model.copy(update=update_data) + items[item_id] = jsonable_encoder(updated_item) + return updated_item diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py new file mode 100644 index 000000000..ab85bd5ae --- /dev/null +++ b/docs_src/body_updates/tutorial002_py39.py @@ -0,0 +1,37 @@ +from typing import Optional + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + price: Optional[float] = None + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item) +async def read_item(item_id: str): + return items[item_id] + + +@app.patch("/items/{item_id}", response_model=Item) +async def update_item(item_id: str, item: Item): + stored_item_data = items[item_id] + stored_item_model = Item(**stored_item_data) + update_data = item.dict(exclude_unset=True) + updated_item = stored_item_model.copy(update=update_data) + items[item_id] = jsonable_encoder(updated_item) + return updated_item diff --git a/docs_src/cookie_params/tutorial001_py310.py b/docs_src/cookie_params/tutorial001_py310.py new file mode 100644 index 000000000..d0b004631 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_py310.py @@ -0,0 +1,8 @@ +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: str | None = Cookie(None)): + return {"ads_id": ads_id} diff --git a/docs_src/dependencies/tutorial001_py310.py b/docs_src/dependencies/tutorial001_py310.py new file mode 100644 index 000000000..662af9b1b --- /dev/null +++ b/docs_src/dependencies/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return commons + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return commons diff --git a/docs_src/dependencies/tutorial002_py310.py b/docs_src/dependencies/tutorial002_py310.py new file mode 100644 index 000000000..23c048ab9 --- /dev/null +++ b/docs_src/dependencies/tutorial002_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_py310.py b/docs_src/dependencies/tutorial003_py310.py new file mode 100644 index 000000000..10d3771c1 --- /dev/null +++ b/docs_src/dependencies/tutorial003_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons=Depends(CommonQueryParams)): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_py310.py b/docs_src/dependencies/tutorial004_py310.py new file mode 100644 index 000000000..5245bb0f6 --- /dev/null +++ b/docs_src/dependencies/tutorial004_py310.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: CommonQueryParams = Depends()): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_py310.py b/docs_src/dependencies/tutorial005_py310.py new file mode 100644 index 000000000..5e1d7e0ef --- /dev/null +++ b/docs_src/dependencies/tutorial005_py310.py @@ -0,0 +1,20 @@ +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: str = Depends(query_extractor), last_query: str | None = Cookie(None) +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query(query_or_default: str = Depends(query_or_cookie_extractor)): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/encoder/tutorial001_py310.py b/docs_src/encoder/tutorial001_py310.py new file mode 100644 index 000000000..5baf13628 --- /dev/null +++ b/docs_src/encoder/tutorial001_py310.py @@ -0,0 +1,22 @@ +from datetime import datetime + +from fastapi import FastAPI +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +fake_db = {} + + +class Item(BaseModel): + title: str + timestamp: datetime + description: str | None = None + + +app = FastAPI() + + +@app.put("/items/{id}") +def update_item(id: str, item: Item): + json_compatible_item_data = jsonable_encoder(item) + fake_db[id] = json_compatible_item_data diff --git a/docs_src/extra_data_types/tutorial001_py310.py b/docs_src/extra_data_types/tutorial001_py310.py new file mode 100644 index 000000000..4a33481b7 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_py310.py @@ -0,0 +1,27 @@ +from datetime import datetime, time, timedelta +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: datetime | None = Body(None), + end_datetime: datetime | None = Body(None), + repeat_at: time | None = Body(None), + process_after: timedelta | None = Body(None), +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_models/tutorial001_py310.py b/docs_src/extra_models/tutorial001_py310.py new file mode 100644 index 000000000..669386ae6 --- /dev/null +++ b/docs_src/extra_models/tutorial001_py310.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +class UserOut(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserInDB(BaseModel): + username: str + hashed_password: str + email: EmailStr + full_name: str | None = None + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/docs_src/extra_models/tutorial002_py310.py b/docs_src/extra_models/tutorial002_py310.py new file mode 100644 index 000000000..5b8ed7de3 --- /dev/null +++ b/docs_src/extra_models/tutorial002_py310.py @@ -0,0 +1,39 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserBase(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserIn(UserBase): + password: str + + +class UserOut(UserBase): + pass + + +class UserInDB(UserBase): + hashed_password: str + + +def fake_password_hasher(raw_password: str): + return "supersecret" + raw_password + + +def fake_save_user(user_in: UserIn): + hashed_password = fake_password_hasher(user_in.password) + user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password) + print("User saved! ..not really") + return user_in_db + + +@app.post("/user/", response_model=UserOut) +async def create_user(user_in: UserIn): + user_saved = fake_save_user(user_in) + return user_saved diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py new file mode 100644 index 000000000..065439acc --- /dev/null +++ b/docs_src/extra_models/tutorial003_py310.py @@ -0,0 +1,35 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class BaseItem(BaseModel): + description: str + type: str + + +class CarItem(BaseItem): + type = "car" + + +class PlaneItem(BaseItem): + type = "plane" + size: int + + +items = { + "item1": {"description": "All my friends drive a low rider", "type": "car"}, + "item2": { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + }, +} + + +@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem]) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/extra_models/tutorial004_py39.py b/docs_src/extra_models/tutorial004_py39.py new file mode 100644 index 000000000..28cacde4d --- /dev/null +++ b/docs_src/extra_models/tutorial004_py39.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str + + +items = [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, +] + + +@app.get("/items/", response_model=list[Item]) +async def read_items(): + return items diff --git a/docs_src/extra_models/tutorial005_py39.py b/docs_src/extra_models/tutorial005_py39.py new file mode 100644 index 000000000..9da2a0a0f --- /dev/null +++ b/docs_src/extra_models/tutorial005_py39.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/keyword-weights/", response_model=dict[str, float]) +async def read_keyword_weights(): + return {"foo": 2.3, "bar": 3.4} diff --git a/docs_src/header_params/tutorial001_py310.py b/docs_src/header_params/tutorial001_py310.py new file mode 100644 index 000000000..b28463346 --- /dev/null +++ b/docs_src/header_params/tutorial001_py310.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: str | None = Header(None)): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_py310.py b/docs_src/header_params/tutorial002_py310.py new file mode 100644 index 000000000..98ab5a807 --- /dev/null +++ b/docs_src/header_params/tutorial002_py310.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: str | None = Header(None, convert_underscores=False) +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_py310.py b/docs_src/header_params/tutorial003_py310.py new file mode 100644 index 000000000..2dac2c13c --- /dev/null +++ b/docs_src/header_params/tutorial003_py310.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: list[str] | None = Header(None)): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_py39.py b/docs_src/header_params/tutorial003_py39.py new file mode 100644 index 000000000..359766527 --- /dev/null +++ b/docs_src/header_params/tutorial003_py39.py @@ -0,0 +1,10 @@ +from typing import Optional + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Optional[list[str]] = Header(None)): + return {"X-Token values": x_token} diff --git a/docs_src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py index 66ea442ea..1316d9237 100644 --- a/docs_src/path_operation_configuration/tutorial001.py +++ b/docs_src/path_operation_configuration/tutorial001.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) diff --git a/docs_src/path_operation_configuration/tutorial001_py310.py b/docs_src/path_operation_configuration/tutorial001_py310.py new file mode 100644 index 000000000..da078fdf5 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, status +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial001_py39.py b/docs_src/path_operation_configuration/tutorial001_py39.py new file mode 100644 index 000000000..5c04d8bac --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Optional + +from fastapi import FastAPI, status +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py index 01df041b8..2df537d86 100644 --- a/docs_src/path_operation_configuration/tutorial002.py +++ b/docs_src/path_operation_configuration/tutorial002.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, tags=["items"]) diff --git a/docs_src/path_operation_configuration/tutorial002_py310.py b/docs_src/path_operation_configuration/tutorial002_py310.py new file mode 100644 index 000000000..9a8af5432 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002_py310.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, tags=["items"]) +async def create_item(item: Item): + return item + + +@app.get("/items/", tags=["items"]) +async def read_items(): + return [{"name": "Foo", "price": 42}] + + +@app.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "johndoe"}] diff --git a/docs_src/path_operation_configuration/tutorial002_py39.py b/docs_src/path_operation_configuration/tutorial002_py39.py new file mode 100644 index 000000000..766d9fb0b --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002_py39.py @@ -0,0 +1,29 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, tags=["items"]) +async def create_item(item: Item): + return item + + +@app.get("/items/", tags=["items"]) +async def read_items(): + return [{"name": "Foo", "price": 42}] + + +@app.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "johndoe"}] diff --git a/docs_src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py index 29bcb4a22..269a1a253 100644 --- a/docs_src/path_operation_configuration/tutorial003.py +++ b/docs_src/path_operation_configuration/tutorial003.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post( diff --git a/docs_src/path_operation_configuration/tutorial003_py310.py b/docs_src/path_operation_configuration/tutorial003_py310.py new file mode 100644 index 000000000..3d94afe2c --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial003_py310.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + description="Create an item with all the information, name, description, price, tax and a set of unique tags", +) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial003_py39.py b/docs_src/path_operation_configuration/tutorial003_py39.py new file mode 100644 index 000000000..446198b5c --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial003_py39.py @@ -0,0 +1,24 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + description="Create an item with all the information, name, description, price, tax and a set of unique tags", +) +async def create_item(item: Item): + return item diff --git a/docs_src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_configuration/tutorial004.py index ac95cc4bb..de83be836 100644 --- a/docs_src/path_operation_configuration/tutorial004.py +++ b/docs_src/path_operation_configuration/tutorial004.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, summary="Create an item") diff --git a/docs_src/path_operation_configuration/tutorial004_py310.py b/docs_src/path_operation_configuration/tutorial004_py310.py new file mode 100644 index 000000000..4cb8bdd43 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial004_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial004_py39.py b/docs_src/path_operation_configuration/tutorial004_py39.py new file mode 100644 index 000000000..bf6005b95 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial004_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post("/items/", response_model=Item, summary="Create an item") +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py index c1b809887..0f62c3814 100644 --- a/docs_src/path_operation_configuration/tutorial005.py +++ b/docs_src/path_operation_configuration/tutorial005.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post( diff --git a/docs_src/path_operation_configuration/tutorial005_py310.py b/docs_src/path_operation_configuration/tutorial005_py310.py new file mode 100644 index 000000000..b176631d8 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial005_py310.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + response_description="The created item", +) +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_operation_configuration/tutorial005_py39.py b/docs_src/path_operation_configuration/tutorial005_py39.py new file mode 100644 index 000000000..5ef320405 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial005_py39.py @@ -0,0 +1,33 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: set[str] = set() + + +@app.post( + "/items/", + response_model=Item, + summary="Create an item", + response_description="The created item", +) +async def create_item(item: Item): + """ + Create an item with all the information: + + - **name**: each item must have a name + - **description**: a long description + - **price**: required + - **tax**: if the item doesn't have tax, you can omit this + - **tags**: a set of unique tag strings for this item + """ + return item diff --git a/docs_src/path_params_numeric_validations/tutorial001_py310.py b/docs_src/path_params_numeric_validations/tutorial001_py310.py new file mode 100644 index 000000000..b940a0949 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_py310.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: int = Path(..., title="The ID of the item to get"), + q: str | None = Query(None, alias="item-query"), +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial006_py39.py b/docs_src/python_types/tutorial006_py39.py new file mode 100644 index 000000000..486b67caf --- /dev/null +++ b/docs_src/python_types/tutorial006_py39.py @@ -0,0 +1,3 @@ +def process_items(items: list[str]): + for item in items: + print(item) diff --git a/docs_src/python_types/tutorial007_py39.py b/docs_src/python_types/tutorial007_py39.py new file mode 100644 index 000000000..ea96c7964 --- /dev/null +++ b/docs_src/python_types/tutorial007_py39.py @@ -0,0 +1,2 @@ +def process_items(items_t: tuple[int, int, str], items_s: set[bytes]): + return items_t, items_s diff --git a/docs_src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py index 9fb1043bb..a393385b0 100644 --- a/docs_src/python_types/tutorial008.py +++ b/docs_src/python_types/tutorial008.py @@ -1,7 +1,4 @@ -from typing import Dict - - -def process_items(prices: Dict[str, float]): +def process_items(prices: dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price) diff --git a/docs_src/python_types/tutorial008b.py b/docs_src/python_types/tutorial008b.py new file mode 100644 index 000000000..e52539ead --- /dev/null +++ b/docs_src/python_types/tutorial008b.py @@ -0,0 +1,5 @@ +from typing import Union + + +def process_item(item: Union[int, str]): + print(item) diff --git a/docs_src/python_types/tutorial008b_py310.py b/docs_src/python_types/tutorial008b_py310.py new file mode 100644 index 000000000..6bb437841 --- /dev/null +++ b/docs_src/python_types/tutorial008b_py310.py @@ -0,0 +1,2 @@ +def process_item(item: int | str): + print(item) diff --git a/docs_src/python_types/tutorial009_py310.py b/docs_src/python_types/tutorial009_py310.py new file mode 100644 index 000000000..8464c6ff0 --- /dev/null +++ b/docs_src/python_types/tutorial009_py310.py @@ -0,0 +1,5 @@ +def say_hi(name: str | None = None): + if name is not None: + print(f"Hey {name}!") + else: + print("Hello World") diff --git a/docs_src/python_types/tutorial009b.py b/docs_src/python_types/tutorial009b.py new file mode 100644 index 000000000..9f1a05bc0 --- /dev/null +++ b/docs_src/python_types/tutorial009b.py @@ -0,0 +1,8 @@ +from typing import Union + + +def say_hi(name: Union[str, None] = None): + if name is not None: + print(f"Hey {name}!") + else: + print("Hello World") diff --git a/docs_src/python_types/tutorial011_py310.py b/docs_src/python_types/tutorial011_py310.py new file mode 100644 index 000000000..7f173880f --- /dev/null +++ b/docs_src/python_types/tutorial011_py310.py @@ -0,0 +1,22 @@ +from datetime import datetime + +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name = "John Doe" + signup_ts: datetime | None = None + friends: list[int] = [] + + +external_data = { + "id": "123", + "signup_ts": "2017-06-01 12:22", + "friends": [1, "2", b"3"], +} +user = User(**external_data) +print(user) +# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +# > 123 diff --git a/docs_src/python_types/tutorial011_py39.py b/docs_src/python_types/tutorial011_py39.py new file mode 100644 index 000000000..af79e2df0 --- /dev/null +++ b/docs_src/python_types/tutorial011_py39.py @@ -0,0 +1,23 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class User(BaseModel): + id: int + name = "John Doe" + signup_ts: Optional[datetime] = None + friends: list[int] = [] + + +external_data = { + "id": "123", + "signup_ts": "2017-06-01 12:22", + "friends": [1, "2", b"3"], +} +user = User(**external_data) +print(user) +# > User id=123 name='John Doe' signup_ts=datetime.datetime(2017, 6, 1, 12, 22) friends=[1, 2, 3] +print(user.id) +# > 123 diff --git a/docs_src/query_params/tutorial002_py310.py b/docs_src/query_params/tutorial002_py310.py new file mode 100644 index 000000000..bedb58ce8 --- /dev/null +++ b/docs_src/query_params/tutorial002_py310.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_item(item_id: str, q: str | None = None): + if q: + return {"item_id": item_id, "q": q} + return {"item_id": item_id} diff --git a/docs_src/query_params/tutorial003_py310.py b/docs_src/query_params/tutorial003_py310.py new file mode 100644 index 000000000..1eec66d90 --- /dev/null +++ b/docs_src/query_params/tutorial003_py310.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_item(item_id: str, q: str | None = None, short: bool = False): + item = {"item_id": item_id} + if q: + item.update({"q": q}) + if not short: + item.update( + {"description": "This is an amazing item that has a long description"} + ) + return item diff --git a/docs_src/query_params/tutorial004_py310.py b/docs_src/query_params/tutorial004_py310.py new file mode 100644 index 000000000..8ec4338df --- /dev/null +++ b/docs_src/query_params/tutorial004_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/users/{user_id}/items/{item_id}") +async def read_user_item( + user_id: int, item_id: str, q: str | None = None, short: bool = False +): + item = {"item_id": item_id, "owner_id": user_id} + if q: + item.update({"q": q}) + if not short: + item.update( + {"description": "This is an amazing item that has a long description"} + ) + return item diff --git a/docs_src/query_params/tutorial006_py310.py b/docs_src/query_params/tutorial006_py310.py new file mode 100644 index 000000000..be2a44c31 --- /dev/null +++ b/docs_src/query_params/tutorial006_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_user_item( + item_id: str, needy: str, skip: int = 0, limit: int | None = None +): + item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} + return item diff --git a/docs_src/query_params/tutorial006b.py b/docs_src/query_params/tutorial006b.py new file mode 100644 index 000000000..f0dbfe08f --- /dev/null +++ b/docs_src/query_params/tutorial006b.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_user_item( + item_id: str, needy: str, skip: int = 0, limit: Union[int, None] = None +): + item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} + return item diff --git a/docs_src/query_params_str_validations/tutorial001_py310.py b/docs_src/query_params_str_validations/tutorial001_py310.py new file mode 100644 index 000000000..eeefddfa7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial001_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial002_py310.py b/docs_src/query_params_str_validations/tutorial002_py310.py new file mode 100644 index 000000000..fa3139d5a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, max_length=50)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_py310.py b/docs_src/query_params_str_validations/tutorial003_py310.py new file mode 100644 index 000000000..335858a40 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, min_length=3, max_length=50)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py new file mode 100644 index 000000000..518b779f7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str | None = Query(None, min_length=3, max_length=50, regex="^fixedquery$") +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_py310.py b/docs_src/query_params_str_validations/tutorial007_py310.py new file mode 100644 index 000000000..14ef4cb69 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, title="Query string", min_length=3)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_py310.py b/docs_src/query_params_str_validations/tutorial008_py310.py new file mode 100644 index 000000000..06bb02442 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str + | None = Query( + None, + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ) +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_py310.py b/docs_src/query_params_str_validations/tutorial009_py310.py new file mode 100644 index 000000000..e84c116f1 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(None, alias="item-query")): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py new file mode 100644 index 000000000..c35800858 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -0,0 +1,23 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: str + | None = Query( + None, + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ) +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial011_py310.py b/docs_src/query_params_str_validations/tutorial011_py310.py new file mode 100644 index 000000000..c3d992e62 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_py310.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: list[str] | None = Query(None)): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_py39.py b/docs_src/query_params_str_validations/tutorial011_py39.py new file mode 100644 index 000000000..38ba764d6 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_py39.py @@ -0,0 +1,11 @@ +from typing import Optional + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Optional[list[str]] = Query(None)): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_py39.py b/docs_src/query_params_str_validations/tutorial012_py39.py new file mode 100644 index 000000000..1900133d9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_py39.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: list[str] = Query(["foo", "bar"])): + query_items = {"q": q} + return query_items diff --git a/docs_src/request_files/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py new file mode 100644 index 000000000..26cd56769 --- /dev/null +++ b/docs_src/request_files/tutorial002_py39.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: list[bytes] = File(...)): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile] = File(...)): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/response_model/tutorial001_py310.py b/docs_src/response_model/tutorial001_py310.py new file mode 100644 index 000000000..59efecde4 --- /dev/null +++ b/docs_src/response_model/tutorial001_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: list[str] = [] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item): + return item diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py new file mode 100644 index 000000000..37b866864 --- /dev/null +++ b/docs_src/response_model/tutorial001_py39.py @@ -0,0 +1,19 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + tags: list[str] = [] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item): + return item diff --git a/docs_src/response_model/tutorial002_py310.py b/docs_src/response_model/tutorial002_py310.py new file mode 100644 index 000000000..29ab9c9d2 --- /dev/null +++ b/docs_src/response_model/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +# Don't do this in production! +@app.post("/user/", response_model=UserIn) +async def create_user(user: UserIn): + return user diff --git a/docs_src/response_model/tutorial003_py310.py b/docs_src/response_model/tutorial003_py310.py new file mode 100644 index 000000000..fc9693e3c --- /dev/null +++ b/docs_src/response_model/tutorial003_py310.py @@ -0,0 +1,22 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class UserIn(BaseModel): + username: str + password: str + email: EmailStr + full_name: str | None = None + + +class UserOut(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +@app.post("/user/", response_model=UserOut) +async def create_user(user: UserIn): + return user diff --git a/docs_src/response_model/tutorial004_py310.py b/docs_src/response_model/tutorial004_py310.py new file mode 100644 index 000000000..a3e21b434 --- /dev/null +++ b/docs_src/response_model/tutorial004_py310.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial004_py39.py b/docs_src/response_model/tutorial004_py39.py new file mode 100644 index 000000000..07ccbbf41 --- /dev/null +++ b/docs_src/response_model/tutorial004_py39.py @@ -0,0 +1,26 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: float = 10.5 + tags: list[str] = [] + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2}, + "baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []}, +} + + +@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True) +async def read_item(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial005_py310.py b/docs_src/response_model/tutorial005_py310.py new file mode 100644 index 000000000..acb3035b9 --- /dev/null +++ b/docs_src/response_model/tutorial005_py310.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, + "baz": { + "name": "Baz", + "description": "There goes my baz", + "price": 50.2, + "tax": 10.5, + }, +} + + +@app.get( + "/items/{item_id}/name", + response_model=Item, + response_model_include={"name", "description"}, +) +async def read_item_name(item_id: str): + return items[item_id] + + +@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"}) +async def read_item_public_data(item_id: str): + return items[item_id] diff --git a/docs_src/response_model/tutorial006_py310.py b/docs_src/response_model/tutorial006_py310.py new file mode 100644 index 000000000..9ccec324e --- /dev/null +++ b/docs_src/response_model/tutorial006_py310.py @@ -0,0 +1,37 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float = 10.5 + + +items = { + "foo": {"name": "Foo", "price": 50.2}, + "bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2}, + "baz": { + "name": "Baz", + "description": "There goes my baz", + "price": 50.2, + "tax": 10.5, + }, +} + + +@app.get( + "/items/{item_id}/name", + response_model=Item, + response_model_include=["name", "description"], +) +async def read_item_name(item_id: str): + return items[item_id] + + +@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude=["tax"]) +async def read_item_public_data(item_id: str): + return items[item_id] diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py new file mode 100644 index 000000000..77ceedd60 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + class Config: + schema_extra = { + "example": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + } + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py new file mode 100644 index 000000000..4f8f8304e --- /dev/null +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str = Field(..., example="Foo") + description: str | None = Field(None, example="A very nice Item") + price: float = Field(..., example=35.4) + tax: float | None = Field(None, example=3.2) + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py new file mode 100644 index 000000000..cf4c99dc0 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -0,0 +1,28 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Item = Body( + ..., + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py new file mode 100644 index 000000000..6f29c1a5c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -0,0 +1,50 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + ..., + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial002_py310.py b/docs_src/security/tutorial002_py310.py new file mode 100644 index 000000000..3c2018f54 --- /dev/null +++ b/docs_src/security/tutorial002_py310.py @@ -0,0 +1,30 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: User = Depends(get_current_user)): + return current_user diff --git a/docs_src/security/tutorial003_py310.py b/docs_src/security/tutorial003_py310.py new file mode 100644 index 000000000..af935e997 --- /dev/null +++ b/docs_src/security/tutorial003_py310.py @@ -0,0 +1,88 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user(current_user: User = Depends(get_current_user)): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py new file mode 100644 index 000000000..797d56d04 --- /dev/null +++ b/docs_src/security/tutorial004_py310.py @@ -0,0 +1,137 @@ +from datetime import datetime, timedelta + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user(current_user: User = Depends(get_current_user)): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items(current_user: User = Depends(get_current_active_user)): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py new file mode 100644 index 000000000..c6a095d2c --- /dev/null +++ b/docs_src/security/tutorial005_py310.py @@ -0,0 +1,172 @@ +from datetime import datetime, timedelta + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = f"Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: User = Security(get_current_user, scopes=["me"]) +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: User = Security(get_current_active_user, scopes=["items"]) +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: User = Depends(get_current_user)): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py new file mode 100644 index 000000000..d45c08ce6 --- /dev/null +++ b/docs_src/security/tutorial005_py39.py @@ -0,0 +1,173 @@ +from datetime import datetime, timedelta +from typing import Optional + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Optional[str] = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: Optional[str] = None + full_name: Optional[str] = None + disabled: Optional[bool] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme) +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = f"Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: User = Security(get_current_user, scopes=["me"]) +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me(current_user: User = Depends(get_current_active_user)): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: User = Security(get_current_active_user, scopes=["items"]) +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: User = Depends(get_current_user)): + return {"status": "ok"} diff --git a/docs_src/sql_databases/sql_app_py310/__init__.py b/docs_src/sql_databases/sql_app_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py310/alt_main.py b/docs_src/sql_databases/sql_app_py310/alt_main.py new file mode 100644 index 000000000..5de88ec3a --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/alt_main.py @@ -0,0 +1,60 @@ +from fastapi import Depends, FastAPI, HTTPException, Request, Response +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +@app.middleware("http") +async def db_session_middleware(request: Request, call_next): + response = Response("Internal server error", status_code=500) + try: + request.state.db = SessionLocal() + response = await call_next(request) + finally: + request.state.db.close() + return response + + +# Dependency +def get_db(request: Request): + return request.state.db + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py310/crud.py b/docs_src/sql_databases/sql_app_py310/crud.py new file mode 100644 index 000000000..679acdb5c --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/crud.py @@ -0,0 +1,36 @@ +from sqlalchemy.orm import Session + +from . import models, schemas + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def get_items(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): + db_item = models.Item(**item.dict(), owner_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item diff --git a/docs_src/sql_databases/sql_app_py310/database.py b/docs_src/sql_databases/sql_app_py310/database.py new file mode 100644 index 000000000..45a8b9f69 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/docs_src/sql_databases/sql_app_py310/main.py b/docs_src/sql_databases/sql_app_py310/main.py new file mode 100644 index 000000000..a9856d0b6 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/main.py @@ -0,0 +1,53 @@ +from fastapi import Depends, FastAPI, HTTPException +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py310/models.py b/docs_src/sql_databases/sql_app_py310/models.py new file mode 100644 index 000000000..62d8ab4aa --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + items = relationship("Item", back_populates="owner") + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="items") diff --git a/docs_src/sql_databases/sql_app_py310/schemas.py b/docs_src/sql_databases/sql_app_py310/schemas.py new file mode 100644 index 000000000..aea2e3f10 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/schemas.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: str | None = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class User(UserBase): + id: int + is_active: bool + items: list[Item] = [] + + class Config: + orm_mode = True diff --git a/docs_src/sql_databases/sql_app_py310/tests/__init__.py b/docs_src/sql_databases/sql_app_py310/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py b/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py new file mode 100644 index 000000000..c60c3356f --- /dev/null +++ b/docs_src/sql_databases/sql_app_py310/tests/test_sql_app.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..database import Base +from ..main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base.metadata.create_all(bind=engine) + + +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + +client = TestClient(app) + + +def test_create_user(): + response = client.post( + "/users/", + json={"email": "deadpool@example.com", "password": "chimichangas4life"}, + ) + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert "id" in data + user_id = data["id"] + + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert data["id"] == user_id diff --git a/docs_src/sql_databases/sql_app_py39/__init__.py b/docs_src/sql_databases/sql_app_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py39/alt_main.py b/docs_src/sql_databases/sql_app_py39/alt_main.py new file mode 100644 index 000000000..5de88ec3a --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/alt_main.py @@ -0,0 +1,60 @@ +from fastapi import Depends, FastAPI, HTTPException, Request, Response +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +@app.middleware("http") +async def db_session_middleware(request: Request, call_next): + response = Response("Internal server error", status_code=500) + try: + request.state.db = SessionLocal() + response = await call_next(request) + finally: + request.state.db.close() + return response + + +# Dependency +def get_db(request: Request): + return request.state.db + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py39/crud.py b/docs_src/sql_databases/sql_app_py39/crud.py new file mode 100644 index 000000000..679acdb5c --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/crud.py @@ -0,0 +1,36 @@ +from sqlalchemy.orm import Session + +from . import models, schemas + + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() + + +def create_user(db: Session, user: schemas.UserCreate): + fake_hashed_password = user.password + "notreallyhashed" + db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def get_items(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): + db_item = models.Item(**item.dict(), owner_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item diff --git a/docs_src/sql_databases/sql_app_py39/database.py b/docs_src/sql_databases/sql_app_py39/database.py new file mode 100644 index 000000000..45a8b9f69 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/database.py @@ -0,0 +1,13 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db" +# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() diff --git a/docs_src/sql_databases/sql_app_py39/main.py b/docs_src/sql_databases/sql_app_py39/main.py new file mode 100644 index 000000000..a9856d0b6 --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/main.py @@ -0,0 +1,53 @@ +from fastapi import Depends, FastAPI, HTTPException +from sqlalchemy.orm import Session + +from . import crud, models, schemas +from .database import SessionLocal, engine + +models.Base.metadata.create_all(bind=engine) + +app = FastAPI() + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.post("/users/", response_model=schemas.User) +def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return crud.create_user(db=db, user=user) + + +@app.get("/users/", response_model=list[schemas.User]) +def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + users = crud.get_users(db, skip=skip, limit=limit) + return users + + +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + if db_user is None: + raise HTTPException(status_code=404, detail="User not found") + return db_user + + +@app.post("/users/{user_id}/items/", response_model=schemas.Item) +def create_item_for_user( + user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db) +): + return crud.create_user_item(db=db, item=item, user_id=user_id) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): + items = crud.get_items(db, skip=skip, limit=limit) + return items diff --git a/docs_src/sql_databases/sql_app_py39/models.py b/docs_src/sql_databases/sql_app_py39/models.py new file mode 100644 index 000000000..62d8ab4aa --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/models.py @@ -0,0 +1,26 @@ +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) + + items = relationship("Item", back_populates="owner") + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True) + description = Column(String, index=True) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User", back_populates="items") diff --git a/docs_src/sql_databases/sql_app_py39/schemas.py b/docs_src/sql_databases/sql_app_py39/schemas.py new file mode 100644 index 000000000..a19f1cdfe --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/schemas.py @@ -0,0 +1,37 @@ +from typing import Optional + +from pydantic import BaseModel + + +class ItemBase(BaseModel): + title: str + description: Optional[str] = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + owner_id: int + + class Config: + orm_mode = True + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class User(UserBase): + id: int + is_active: bool + items: list[Item] = [] + + class Config: + orm_mode = True diff --git a/docs_src/sql_databases/sql_app_py39/tests/__init__.py b/docs_src/sql_databases/sql_app_py39/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py b/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py new file mode 100644 index 000000000..c60c3356f --- /dev/null +++ b/docs_src/sql_databases/sql_app_py39/tests/test_sql_app.py @@ -0,0 +1,47 @@ +from fastapi.testclient import TestClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from ..database import Base +from ..main import app, get_db + +SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base.metadata.create_all(bind=engine) + + +def override_get_db(): + try: + db = TestingSessionLocal() + yield db + finally: + db.close() + + +app.dependency_overrides[get_db] = override_get_db + +client = TestClient(app) + + +def test_create_user(): + response = client.post( + "/users/", + json={"email": "deadpool@example.com", "password": "chimichangas4life"}, + ) + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert "id" in data + user_id = data["id"] + + response = client.get(f"/users/{user_id}") + assert response.status_code == 200, response.text + data = response.json() + assert data["email"] == "deadpool@example.com" + assert data["id"] == user_id diff --git a/pyproject.toml b/pyproject.toml index e6148831b..fa399c492 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py new file mode 100644 index 000000000..6937c8fab --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py new file mode 100644 index 000000000..e292b5346 --- /dev/null +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -0,0 +1,292 @@ +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture +def client(): + from docs_src.body.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_missing = { + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + +price_not_float = { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] +} + +name_price_missing = { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +body_missing = { + "detail": [ + {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/", + {"name": "Foo", "price": 50.5}, + 200, + {"name": "Foo", "price": 50.5, "description": None, "tax": None}, + ), + ( + "/items/", + {"name": "Foo", "price": "50.5"}, + 200, + {"name": "Foo", "price": 50.5, "description": None, "tax": None}, + ), + ( + "/items/", + {"name": "Foo", "price": "50.5", "description": "Some Foo"}, + 200, + {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, + ), + ( + "/items/", + {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + 200, + {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, + ), + ("/items/", {"name": "Foo"}, 422, price_missing), + ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), + ("/items/", {}, 422, name_price_missing), + ("/items/", None, 422, body_missing), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.post(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_post_broken_body(client: TestClient): + response = client.post( + "/items/", + headers={"content-type": "application/json"}, + data="{some broken json}", + ) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + + +@needs_py310 +def test_post_form_for_json(client: TestClient): + response = client.post("/items/", data={"name": "Foo", "price": 50.5}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + + +@needs_py310 +def test_explicit_content_type(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "application/json"}, + ) + assert response.status_code == 200, response.text + + +@needs_py310 +def test_geo_json(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + headers={"Content-Type": "application/geo+json"}, + ) + assert response.status_code == 200, response.text + + +@needs_py310 +def test_no_content_type_is_json(client: TestClient): + response = client.post( + "/items/", + data='{"name": "Foo", "price": 50.5}', + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "description": None, + "price": 50.5, + "tax": None, + } + + +@needs_py310 +def test_wrong_headers(client: TestClient): + data = '{"name": "Foo", "price": 50.5}' + invalid_dict = { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + + response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"}) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + + response = client.post( + "/items/", data=data, headers={"Content-Type": "application/geo+json-seq"} + ) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + response = client.post( + "/items/", data=data, headers={"Content-Type": "application/not-really-json"} + ) + assert response.status_code == 422, response.text + assert response.json() == invalid_dict + + +@needs_py310 +def test_other_exceptions(client: TestClient): + with patch("json.loads", side_effect=Exception): + response = client.post("/items/", json={"test": "test2"}) + assert response.status_code == 400, response.text diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py new file mode 100644 index 000000000..d7a525ea7 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py new file mode 100644 index 000000000..85ba41ce6 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py new file mode 100644 index 000000000..f896f7bf5 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py new file mode 100644 index 000000000..17ca29ce5 --- /dev/null +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -0,0 +1,113 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_nested_models.tutorial009_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_body(client: TestClient): + data = {"2": 2.2, "3": 3.3} + response = client.post("/index-weights/", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +@needs_py39 +def test_post_invalid_body(client: TestClient): + data = {"foo": 2.2, "3": 3.3} + response = client.post("/index-weights/", json=data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py new file mode 100644 index 000000000..ca1d8c585 --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -0,0 +1,172 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +@needs_py310 +def test_put(client: TestClient): + response = client.put( + "/items/bar", json={"name": "Barz", "price": 3, "description": None} + ) + assert response.json() == { + "name": "Barz", + "description": None, + "price": 3, + "tax": 10.5, + "tags": [], + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py new file mode 100644 index 000000000..f2b184c4f --- /dev/null +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -0,0 +1,172 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get(client: TestClient): + response = client.get("/items/baz") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [], + } + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/bar", json={"name": "Barz", "price": 3, "description": None} + ) + assert response.json() == { + "name": "Barz", + "description": None, + "price": 3, + "tax": 10.5, + "tags": [], + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py new file mode 100644 index 000000000..587a328da --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -0,0 +1,100 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.cookie_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response, client: TestClient): + response = client.get(path, cookies=cookies) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py new file mode 100644 index 000000000..a7991170e --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py new file mode 100644 index 000000000..f66a36a99 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -0,0 +1,152 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial004_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py new file mode 100644 index 000000000..3d4c1d07d --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py new file mode 100644 index 000000000..185bc3a37 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_get_car(client: TestClient): + response = client.get("/items/item1") + assert response.status_code == 200, response.text + assert response.json() == { + "description": "All my friends drive a low rider", + "type": "car", + } + + +@needs_py310 +def test_get_plane(client: TestClient): + response = client.get("/items/item2") + assert response.status_code == 200, response.text + assert response.json() == { + "description": "Music is my aeroplane, it's my aeroplane", + "type": "plane", + "size": 5, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py new file mode 100644 index 000000000..7f4f5b9be --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -0,0 +1,69 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial004_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "Foo", "description": "There comes my hero"}, + {"name": "Red", "description": "It's my aeroplane"}, + ] diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py new file mode 100644 index 000000000..3bb5a99f1 --- /dev/null +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -0,0 +1,53 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_models.tutorial005_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_items(client: TestClient): + response = client.get("/keyword-weights/") + assert response.status_code == 200, response.text + assert response.json() == {"foo": 2.3, "bar": 3.4} diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py new file mode 100644 index 000000000..f5ee17428 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -0,0 +1,94 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py new file mode 100644 index 000000000..1f617da70 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -0,0 +1,121 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_configuration.tutorial005_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_query_params_str_validations(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 42}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "price": 42, + "description": None, + "tax": None, + "tags": [], + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py new file mode 100644 index 000000000..ffdf05081 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -0,0 +1,121 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_configuration.tutorial005_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_query_params_str_validations(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 42}) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Foo", + "price": 42, + "description": None, + "tax": None, + "tags": [], + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py new file mode 100644 index 000000000..1986d27d0 --- /dev/null +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -0,0 +1,148 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer"}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +query_required = { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params.tutorial006_py310 import app + + c = TestClient(app) + return c + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/openapi.json", 200, openapi_schema), + ( + "/items/foo?needy=very", + 200, + {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, + ), + ( + "/items/foo?skip=a&limit=b", + 422, + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + }, + ), + ], +) +def test(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py new file mode 100644 index 000000000..66b24017e --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py new file mode 100644 index 000000000..8894ee1b5 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py new file mode 100644 index 000000000..b10e70af7 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py new file mode 100644 index 000000000..a9cbce02a --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py new file mode 100644 index 000000000..bbdf25cd9 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -0,0 +1,235 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_files.tutorial002_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_files(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b" Date: Fri, 7 Jan 2022 14:12:16 +0000 Subject: [PATCH 0039/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ce1d3144d..f6e66d805 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). ## 0.70.1 From a1ede32f29366902033e701f09bfe77c29d0442e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 15:17:13 +0100 Subject: [PATCH 0040/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20FastAPI=20Trove?= =?UTF-8?q?=20Classifier=20for=20PyPI=20(#4386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index fa399c492..77c01322f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: AsyncIO", + "Framework :: FastAPI", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", From 4da33e303148cbabeb96bb3e17cd236ef4402d33 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 14:17:49 +0000 Subject: [PATCH 0041/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f6e66d805..9453909be 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). * ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). From 44f4885c663f498c5902ba1a951d6e0e3d318fa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Jan 2022 15:18:45 +0100 Subject: [PATCH 0042/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#4354)?= 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 | 98 ++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 89c6f8dc3..df088f39f 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -6,8 +6,8 @@ maintainers: url: https://github.com/tiangolo experts: - login: Kludex - count: 315 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + count: 316 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: dmontagu count: 262 @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: raphaelauv - count: 63 + count: 67 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: falkben @@ -45,6 +45,10 @@ experts: count: 48 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: insomnes + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: Dustyposa count: 42 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 @@ -53,10 +57,6 @@ experts: count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: insomnes - count: 37 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -78,7 +78,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 - login: adriangb - count: 26 + count: 28 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 url: https://github.com/adriangb - login: ghandic @@ -97,6 +97,10 @@ experts: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak +- login: panla + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -109,10 +113,6 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: panla - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 @@ -137,14 +137,14 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk +- login: dstlny + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar -- login: dstlny - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 - url: https://github.com/dstlny - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 @@ -182,22 +182,30 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 url: https://github.com/oligond last_month_active: +- login: insomnes + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: raphaelauv count: 6 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: oligond - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 - url: https://github.com/oligond +- login: jgould22 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: harunyasar + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 + url: https://github.com/harunyasar - login: Kludex - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 - url: https://github.com/Kludex -- login: Dustyposa count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 + url: https://github.com/Kludex +- login: panla + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla top_contributors: - login: waynerv count: 25 @@ -237,7 +245,7 @@ top_contributors: url: https://github.com/RunningIkkyu - login: Kludex count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: hard-coders count: 7 @@ -273,12 +281,12 @@ top_contributors: url: https://github.com/komtaki - login: NinaHwang count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=36ca24be1fc3daa967b0f409bab0e17132d8c80c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang top_reviewers: - login: Kludex count: 91 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=cf8455cb899806b774a3a71073f88583adec99f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: waynerv count: 47 @@ -364,6 +372,10 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: yezz123 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -376,10 +388,6 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: yezz123 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: graingert count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 @@ -420,6 +428,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv +- login: BilalAlpaslan + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: NastasiaSaby count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 @@ -436,10 +448,6 @@ top_reviewers: 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: diogoduartec count: 5 avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 @@ -448,6 +456,10 @@ top_reviewers: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 url: https://github.com/nimctl +- login: israteneda + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/20668624?u=d7b2961d330aca65fbce5bdb26a0800a3d23ed2d&v=4 + url: https://github.com/israteneda - login: juntatalor count: 5 avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 @@ -464,11 +476,7 @@ top_reviewers: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 url: https://github.com/oandersonmagalhaes -- login: euri10 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 - url: https://github.com/euri10 -- login: rkbeatss - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/23391143?u=56ab6bff50be950fa8cae5cf736f2ae66e319ff3&v=4 - url: https://github.com/rkbeatss +- login: qysfblog + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/52229895?v=4 + url: https://github.com/qysfblog From 66c27c3e0752b63bfe6e843dcf2af28d774caaa7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jan 2022 14:19:23 +0000 Subject: [PATCH 0043/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9453909be..02e2c8879 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 [#4354](https://github.com/tiangolo/fastapi/pull/4354) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). * ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). From 672c55ac626b20cc6219696023cdcc5871e5141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jan 2022 18:07:59 +0100 Subject: [PATCH 0044/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?71.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 ++++++++++++-- fastapi/__init__.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 02e2c8879..01df31267 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,11 +2,21 @@ ## Latest Changes +## 0.71.0 + +### Features + +* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). + * You can start with [Python Types Intro](https://fastapi.tiangolo.com/python-types/), it explains what changes between different Python versions, in Python 3.9 and in Python 3.10. + * All the FastAPI docs are updated. Each code example in the docs that could use different syntax in Python 3.9 or Python 3.10 now has all the alternatives in tabs. +* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). + +### Internal + * 👥 Update FastAPI People. PR [#4354](https://github.com/tiangolo/fastapi/pull/4354) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Add FastAPI Trove Classifier for PyPI as now there's one 🤷😁. PR [#4386](https://github.com/tiangolo/fastapi/pull/4386) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add docs and tests for Python 3.9 and Python 3.10. PR [#3712](https://github.com/tiangolo/fastapi/pull/3712) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade MkDocs Material and configs. PR [#4385](https://github.com/tiangolo/fastapi/pull/4385) by [@tiangolo](https://github.com/tiangolo). -* ⬆️ Upgrade Starlette to 0.17.1. PR [#4145](https://github.com/tiangolo/fastapi/pull/4145) by [@simondale00](https://github.com/simondale00). + ## 0.70.1 There's nothing interesting in this particular FastAPI release. It is mainly to enable/unblock the release of the next version of Pydantic that comes packed with features and improvements. 🤩 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 8bb6ce15b..5b735aed5 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.70.1" +__version__ = "0.71.0" from starlette import status as status From ca2b1dbb648b24ba776cf73f4dfa7026d5ff4a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 15:34:45 +0100 Subject: [PATCH 0045/1881] =?UTF-8?q?=F0=9F=94=A7=20Enable=20MkDocs=20Mate?= =?UTF-8?q?rial=20Insiders'=20`content.tabs.link`=20(#4399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 1 + docs/de/mkdocs.yml | 1 + docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/fr/mkdocs.yml | 1 + docs/id/mkdocs.yml | 1 + docs/it/mkdocs.yml | 1 + docs/ja/mkdocs.yml | 1 + docs/ko/mkdocs.yml | 1 + docs/pl/mkdocs.yml | 1 + docs/pt/mkdocs.yml | 1 + docs/ru/mkdocs.yml | 1 + docs/sq/mkdocs.yml | 1 + docs/tr/mkdocs.yml | 1 + docs/uk/mkdocs.yml | 1 + docs/zh/mkdocs.yml | 1 + 16 files changed, 16 insertions(+) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index dbff7b26a..66220f63e 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8f29ef316..360fa8c4a 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index b3716f508..5bdd2c54a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 06b015a1d..a4bc41154 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index c42f92b8a..ff16e1d78 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 0dc34a14f..d70d2b3c3 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 18afbd547..e6d01fbde 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 327fe862a..39fd8a211 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 5ffb71877..1d4d30913 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 724dc2725..3c1351a12 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index f36ecf49d..f202f306d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index eb2597de7..6e17c287e 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 5afe0a665..d9c3dad4c 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 40ba6d522..f6ed7f5b9 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index a116d90a3..d0de8cc0e 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 33d40129a..a929e3388 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -20,6 +20,7 @@ theme: features: - search.suggest - search.highlight + - content.tabs.link icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg From b8ae84d460f331b8adc4d25b5b6f0bcb40baaac8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 14:35:21 +0000 Subject: [PATCH 0046/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 01df31267..5bf2302b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 ### Features From 7fe79441c1cb7cf3baecc271a3e9680006c8f2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 15:44:08 +0100 Subject: [PATCH 0047/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20Python=20Type?= =?UTF-8?q?s=20docs,=20add=20missing=203.6=20/=203.9=20example=20(#4434)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/python-types.md | 2 +- docs_src/python_types/tutorial008.py | 5 ++++- docs_src/python_types/tutorial008_py39.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 docs_src/python_types/tutorial008_py39.py diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 76d442855..fe56dadec 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -252,7 +252,7 @@ The second type parameter is for the values of the `dict`: === "Python 3.9 and above" ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial008.py!} + {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` This means: diff --git a/docs_src/python_types/tutorial008.py b/docs_src/python_types/tutorial008.py index a393385b0..9fb1043bb 100644 --- a/docs_src/python_types/tutorial008.py +++ b/docs_src/python_types/tutorial008.py @@ -1,4 +1,7 @@ -def process_items(prices: dict[str, float]): +from typing import Dict + + +def process_items(prices: Dict[str, float]): for item_name, item_price in prices.items(): print(item_name) print(item_price) diff --git a/docs_src/python_types/tutorial008_py39.py b/docs_src/python_types/tutorial008_py39.py new file mode 100644 index 000000000..a393385b0 --- /dev/null +++ b/docs_src/python_types/tutorial008_py39.py @@ -0,0 +1,4 @@ +def process_items(prices: dict[str, float]): + for item_name, item_price in prices.items(): + print(item_name) + print(item_price) From 9af8cc69d509cd8538bba3d481687d9f85cb52b4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 14:44:43 +0000 Subject: [PATCH 0048/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5bf2302b2..b33e9dcfc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 From a85aa125d24acb225b7b722bb4f3e331a21ec267 Mon Sep 17 00:00:00 2001 From: John Riebold Date: Sun, 16 Jan 2022 11:26:24 -0800 Subject: [PATCH 0049/1881] =?UTF-8?q?=E2=9C=A8=20Enable=20configuring=20Sw?= =?UTF-8?q?agger=20UI=20parameters=20(#2568)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Artem Ivanov Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/extending-openapi.md | 79 ++++++++++++++++++ .../tutorial/extending-openapi/image02.png | Bin 0 -> 11292 bytes .../tutorial/extending-openapi/image03.png | Bin 0 -> 11176 bytes .../tutorial/extending-openapi/image04.png | Bin 0 -> 11180 bytes docs_src/extending_openapi/tutorial003.py | 8 ++ docs_src/extending_openapi/tutorial004.py | 8 ++ docs_src/extending_openapi/tutorial005.py | 8 ++ fastapi/applications.py | 3 + fastapi/openapi/docs.py | 22 +++-- .../test_tutorial003.py | 41 +++++++++ .../test_tutorial004.py | 44 ++++++++++ .../test_tutorial005.py | 44 ++++++++++ 12 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image02.png create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image03.png create mode 100644 docs/en/docs/img/tutorial/extending-openapi/image04.png create mode 100644 docs_src/extending_openapi/tutorial003.py create mode 100644 docs_src/extending_openapi/tutorial004.py create mode 100644 docs_src/extending_openapi/tutorial005.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial003.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial004.py create mode 100644 tests/test_tutorial/test_extending_openapi/test_tutorial005.py diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index d8d280ba6..d1b14bc00 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -233,3 +233,82 @@ Now, to be able to test that everything works, create a *path operation*: Now, you should be able to disconnect your WiFi, go to your docs at
http://127.0.0.1:8000/docs, and reload the page. And even without Internet, you would be able to see the docs for your API and interact with it. + +## Configuring Swagger UI + +You can configure some extra Swagger UI parameters. + +To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. + +`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. + +FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. + +### Disable Syntax Highlighting + +For example, you could disable syntax highlighting in Swagger UI. + +Without changing the settings, syntax highlighting is enabled by default: + + + +But you can disable it by setting `syntaxHighlight` to `False`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial003.py!} +``` + +...and then Swagger UI won't show the syntax highlighting anymore: + + + +### Change the Theme + +The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial004.py!} +``` + +That configuration would change the syntax highlighting color theme: + + + +### Change Default Swagger UI Parameters + +FastAPI includes some default configuration parameters appropriate for most of the use cases. + +It includes these default configurations: + +```Python +{!../../../fastapi/openapi/docs.py[ln:7-13]!} +``` + +You can override any of them by setting a different value in the argument `swagger_ui_parameters`. + +For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial005.py!} +``` + +### Other Swagger UI Parameters + +To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. + +### JavaScript-only settings + +Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). + +FastAPI also includes these JavaScript-only `presets` settings: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. + +If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/img/tutorial/extending-openapi/image02.png b/docs/en/docs/img/tutorial/extending-openapi/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..91453fb56f687b99fc725afa0a7fa07eb1955137 GIT binary patch literal 11292 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/extending-openapi/image04.png b/docs/en/docs/img/tutorial/extending-openapi/image04.png new file mode 100644 index 0000000000000000000000000000000000000000..394d2bb431ab5eadaa919087ce1618f996bd829e GIT binary patch literal 11180 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 None: self._debug: bool = debug @@ -120,6 +121,7 @@ class FastAPI(Starlette): self.redoc_url = redoc_url self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url self.swagger_ui_init_oauth = swagger_ui_init_oauth + self.swagger_ui_parameters = swagger_ui_parameters self.extra = extra self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} @@ -174,6 +176,7 @@ class FastAPI(Starlette): title=self.title + " - Swagger UI", oauth2_redirect_url=oauth2_redirect_url, init_oauth=self.swagger_ui_init_oauth, + swagger_ui_parameters=self.swagger_ui_parameters, ) self.add_route(self.docs_url, swagger_ui_html, include_in_schema=False) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index fd22e4e8c..1be90d188 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -4,6 +4,14 @@ from typing import Any, Dict, Optional from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse +swagger_ui_default_parameters = { + "dom_id": "#swagger-ui", + "layout": "BaseLayout", + "deepLinking": True, + "showExtensions": True, + "showCommonExtensions": True, +} + def get_swagger_ui_html( *, @@ -14,7 +22,11 @@ def get_swagger_ui_html( swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[Dict[str, Any]] = None, + swagger_ui_parameters: Optional[Dict[str, Any]] = None, ) -> HTMLResponse: + current_swagger_ui_parameters = swagger_ui_default_parameters.copy() + if swagger_ui_parameters: + current_swagger_ui_parameters.update(swagger_ui_parameters) html = f""" @@ -34,19 +46,17 @@ def get_swagger_ui_html( url: '{openapi_url}', """ + for key, value in current_swagger_ui_parameters.items(): + html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n" + if oauth2_redirect_url: html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}'," html += """ - dom_id: '#swagger-ui', - presets: [ + presets: [ SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset ], - layout: "BaseLayout", - deepLinking: true, - showExtensions: true, - showCommonExtensions: true })""" if init_oauth: diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py b/tests/test_tutorial/test_extending_openapi/test_tutorial003.py new file mode 100644 index 000000000..0184dd9f8 --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial003.py @@ -0,0 +1,41 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial003 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"syntaxHighlight": false' in response.text + ), "syntaxHighlight should be included and converted to JSON" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"deepLinking": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py b/tests/test_tutorial/test_extending_openapi/test_tutorial004.py new file mode 100644 index 000000000..4f7615126 --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial004.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial004 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"syntaxHighlight": false' not in response.text + ), "not used parameters should not be included" + assert ( + '"syntaxHighlight.theme": "obsidian"' in response.text + ), "parameters with middle dots should be included in a JSON compatible way" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"deepLinking": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py b/tests/test_tutorial/test_extending_openapi/test_tutorial005.py new file mode 100644 index 000000000..24aeb93db --- /dev/null +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial005.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient + +from docs_src.extending_openapi.tutorial005 import app + +client = TestClient(app) + + +def test_swagger_ui(): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert ( + '"deepLinking": false,' in response.text + ), "overridden configs should be preserved" + assert ( + '"deepLinking": true' not in response.text + ), "overridden configs should not include the old value" + assert ( + '"syntaxHighlight": false' not in response.text + ), "not used parameters should not be included" + assert ( + '"dom_id": "#swagger-ui"' in response.text + ), "default configs should be preserved" + assert "presets: [" in response.text, "default configs should be preserved" + assert ( + "SwaggerUIBundle.presets.apis," in response.text + ), "default configs should be preserved" + assert ( + "SwaggerUIBundle.SwaggerUIStandalonePreset" in response.text + ), "default configs should be preserved" + assert ( + '"layout": "BaseLayout",' in response.text + ), "default configs should be preserved" + assert ( + '"showExtensions": true,' in response.text + ), "default configs should be preserved" + assert ( + '"showCommonExtensions": true,' in response.text + ), "default configs should be preserved" + + +def test_get_users(): + response = client.get("/users/foo") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello foo"} From acbe79da371bc7e3673c86cafeb6d07ca64712f4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:27:03 +0000 Subject: [PATCH 0050/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b33e9dcfc..2ed9729d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). ## 0.71.0 From 5c62a59e7b99a49ce25c622747e510e211f22692 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Mon, 17 Jan 2022 03:34:28 +0800 Subject: [PATCH 0051/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs\tutorial\path-operation-configuration.md`?= =?UTF-8?q?=20(#3312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/path-operation-configuration.md | 101 ++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 102 insertions(+) create mode 100644 docs/zh/docs/tutorial/path-operation-configuration.md diff --git a/docs/zh/docs/tutorial/path-operation-configuration.md b/docs/zh/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..ad0e08d30 --- /dev/null +++ b/docs/zh/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,101 @@ +# 路径操作配置 + +*路径操作装饰器*支持多种配置参数。 + +!!! warning "警告" + + 注意:以下参数应直接传递给**路径操作装饰器**,不能传递给*路径操作函数*。 + +## `status_code` 状态码 + +`status_code` 用于定义*路径操作*响应中的 HTTP 状态码。 + +可以直接传递 `int` 代码, 比如 `404`。 + +如果记不住数字码的涵义,也可以用 `status` 的快捷常量: + +```Python hl_lines="3 17" +{!../../../docs_src/path_operation_configuration/tutorial001.py!} +``` + +状态码在响应中使用,并会被添加到 OpenAPI 概图。 + +!!! note "技术细节" + + 也可以使用 `from starlette import status` 导入状态码。 + + **FastAPI** 的`fastapi.status` 和 `starlette.status` 一样,只是快捷方式。实际上,`fastapi.status` 直接继承自 Starlette。 + +## `tags` 参数 + +`tags` 参数的值是由 `str` 组成的 `list` (一般只有一个 `str` ),`tags` 用于为*路径操作*添加标签: + +```Python hl_lines="17 22 27" +{!../../../docs_src/path_operation_configuration/tutorial002.py!} +``` + +OpenAPI 概图会自动添加标签,供 API 文档接口使用: + + + +## `summary` 和 `description` 参数 + +路径装饰器还支持 `summary` 和 `description` 这两个参数: + +```Python hl_lines="20-21" +{!../../../docs_src/path_operation_configuration/tutorial003.py!} +``` + +## 文档字符串(`docstring`) + +描述内容比较长且占用多行时,可以在函数的 docstring 中声明*路径操作*的描述,**FastAPI** 支持从文档字符串中读取描述内容。 + +文档字符串支持 Markdown,能正确解析和显示 Markdown 的内容,但要注意文档字符串的缩进。 + +```Python hl_lines="19-27" +{!../../../docs_src/path_operation_configuration/tutorial004.py!} +``` + +下图为 Markdown 文本在 API 文档中的显示效果: + + + +## 响应描述 + +`response_description` 参数用于定义响应的描述说明: + +```Python hl_lines="21" +{!../../../docs_src/path_operation_configuration/tutorial005.py!} +``` + +!!! info "说明" + + 注意,`response_description` 只用于描述响应,`description` 一般则用于描述*路径操作*。 + +!!! check "检查" + + OpenAPI 规定每个*路径操作*都要有响应描述。 + + 如果没有定义响应描述,**FastAPI** 则自动生成内容为 "Successful response" 的响应描述。 + + + +## 弃用*路径操作* + +`deprecated` 参数可以把*路径操作*标记为弃用,无需直接删除: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +API 文档会把该路径操作标记为弃用: + + + +下图显示了正常*路径操作*与弃用*路径操作* 的区别: + + + +## 小结 + +通过传递参数给*路径操作装饰器* ,即可轻松地配置*路径操作*、添加元数据。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a929e3388..1d050fddd 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -78,6 +78,7 @@ nav: - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md - tutorial/body-updates.md - 依赖项: - tutorial/dependencies/index.md From 436261b3ea75a095efbf67c4d537baa588331301 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:35:00 +0000 Subject: [PATCH 0052/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2ed9729d0..9066792d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). From 5c5b889288f6330e2a64ab74c9bf50c95e829190 Mon Sep 17 00:00:00 2001 From: MicroPanda123 Date: Sun, 16 Jan 2022 19:36:42 +0000 Subject: [PATCH 0053/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Polish=20transla?= =?UTF-8?q?tion=20for=20`docs/pl/docs/index.md`=20(#4245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Dawid Dutkiewicz Co-authored-by: Dima Tisnek Co-authored-by: Bart Skowron Co-authored-by: Bart Skowron --- docs/pl/docs/index.md | 306 +++++++++++++++++++++--------------------- 1 file changed, 150 insertions(+), 156 deletions(-) diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 95fb7ae21..4a300ae63 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -1,12 +1,8 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + FastAPI to szybki, prosty w nauce i gotowy do użycia w produkcji framework

@@ -22,29 +18,28 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**Dokumentacja**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Kod żródłowy**: https://github.com/tiangolo/fastapi --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.6+ bazujący na standardowym typowaniu Pythona. -The key features are: +Kluczowe cechy: -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Wydajność**: FastAPI jest bardzo wydajny, na równi z **NodeJS** oraz **Go** (dzięki Starlette i Pydantic). [Jeden z najszybszych dostępnych frameworków Pythonowych](#wydajnosc). +* **Szybkość kodowania**: Przyśpiesza szybkość pisania nowych funkcjonalności o około 200% do 300%. * +* **Mniejsza ilość błędów**: Zmniejsza ilość ludzkich (dewelopera) błędy o około 40%. * +* **Intuicyjność**: Wspaniałe wsparcie dla edytorów kodu. Dostępne wszędzie automatyczne uzupełnianie kodu. Krótszy czas debugowania. +* **Łatwość**: Zaprojektowany by być prosty i łatwy do nauczenia. Mniej czasu spędzonego na czytanie dokumentacji. +* **Kompaktowość**: Minimalizacja powtarzającego się kodu. Wiele funkcjonalności dla każdej deklaracji parametru. Mniej błędów. +* **Solidność**: Kod gotowy dla środowiska produkcyjnego. Wraz z automatyczną interaktywną dokumentacją. +* **Bazujący na standardach**: Oparty na (i w pełni kompatybilny z) otwartych standardach API: OpenAPI (wcześniej znane jako Swagger) oraz JSON Schema. -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. +* oszacowania bazowane na testach wykonanych przez wewnętrzny zespół deweloperów, budujących aplikacie używane na środowisku produkcyjnym. -* estimation based on tests on an internal development team, building production applications. - -## Sponsors +## Sponsorzy @@ -59,9 +54,9 @@ The key features are: -Other sponsors +Inni sponsorzy -## Opinions +## Opinie "_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" @@ -101,24 +96,24 @@ The key features are: --- -## **Typer**, the FastAPI of CLIs +## **Typer**, FastAPI aplikacji konsolowych -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Jeżeli tworzysz aplikacje CLI, która ma być używana w terminalu zamiast API, sprawdź **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** to młodsze rodzeństwo FastAPI. Jego celem jest pozostanie **FastAPI aplikacji konsolowych** . ⌨️ 🚀 -## Requirements +## Wymagania Python 3.6+ -FastAPI stands on the shoulders of giants: +FastAPI oparty jest na: -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette dla części webowej. +* Pydantic dla części obsługujących dane. -## Installation +## Instalacja

@@ -130,7 +125,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Na serwerze produkcyjnym będziesz także potrzebował serwera ASGI, np. Uvicorn lub Hypercorn.
@@ -142,11 +137,11 @@ $ pip install uvicorn[standard]
-## Example +## Przykład -### Create it +### Stwórz -* Create a file `main.py` with: +* Utwórz plik o nazwie `main.py` z: ```Python from typing import Optional @@ -167,9 +162,9 @@ def read_item(item_id: int, q: Optional[str] = None): ```
-Or use async def... +Albo użyj async def... -If your code uses `async` / `await`, use `async def`: +Jeżeli twój kod korzysta z `async` / `await`, użyj `async def`: ```Python hl_lines="9 14" from typing import Optional @@ -189,15 +184,15 @@ async def read_item(item_id: int, q: Optional[str] = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Przypis**: -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Jeżeli nie znasz, sprawdź sekcję _"In a hurry?"_ o `async` i `await` w dokumentacji.
-### Run it +### Uruchom -Run the server with: +Uruchom serwer używając:
@@ -214,54 +209,53 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +O komendzie uvicorn main:app --reload... +Komenda `uvicorn main:app` odnosi się do: -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main`: plik `main.py` ("moduł" w Pythonie). +* `app`: obiekt stworzony w `main.py` w lini `app = FastAPI()`. +* `--reload`: spraw by serwer resetował się po każdej zmianie w kodzie. Używaj tego tylko w środowisku deweloperskim.
-### Check it +### Wypróbuj -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Otwórz link http://127.0.0.1:8000/items/5?q=somequery w przeglądarce. -You will see the JSON response as: +Zobaczysz następującą odpowiedź JSON: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Właśnie stworzyłeś API które: -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* Otrzymuje żądania HTTP w _ścieżce_ `/` i `/items/{item_id}`. +* Obie _ścieżki_ używają operacji `GET` (znane także jako _metody_ HTTP). +* _Ścieżka_ `/items/{item_id}` ma _parametr ścieżki_ `item_id` który powinien być obiektem typu `int`. +* _Ścieżka_ `/items/{item_id}` ma opcjonalny _parametr zapytania_ typu `str` o nazwie `q`. -### Interactive API docs +### Interaktywna dokumentacja API -Now go to http://127.0.0.1:8000/docs. +Otwórz teraz stronę http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Zobaczysz automatyczną interaktywną dokumentację API (dostarczoną z pomocą Swagger UI): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Alternatywna dokumentacja API -And now, go to http://127.0.0.1:8000/redoc. +Otwórz teraz http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Zobaczysz alternatywną, lecz wciąż automatyczną dokumentację (wygenerowaną z pomocą ReDoc): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Aktualizacja przykładu -Now modify the file `main.py` to receive a body from a `PUT` request. +Zmodyfikuj teraz plik `main.py`, aby otrzmywał treść (body) żądania `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Zadeklaruj treść żądania, używając standardowych typów w Pythonie dzięki Pydantic. ```Python hl_lines="4 9-12 25-27" from typing import Optional @@ -293,175 +287,175 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +Serwer powinien przeładować się automatycznie (ponieważ dodałeś `--reload` do komendy `uvicorn` powyżej). -### Interactive API docs upgrade +### Zaktualizowana interaktywna dokumentacja API -Now go to http://127.0.0.1:8000/docs. +Wejdź teraz na http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* Interaktywna dokumentacja API zaktualizuje sie automatycznie, także z nową treścią żądania (body): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* Kliknij przycisk "Try it out" (wypróbuj), pozwoli Ci to wypełnić parametry i bezpośrednio użyć API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* Kliknij potem przycisk "Execute" (wykonaj), interfejs użytkownika połączy się z API, wyśle parametry, otrzyma odpowiedź i wyświetli ją na ekranie: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Zaktualizowana alternatywna dokumentacja API -And now, go to http://127.0.0.1:8000/redoc. +Otwórz teraz http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* Alternatywna dokumentacja również pokaże zaktualizowane parametry i treść żądania (body): ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### Podsumowanie -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +Podsumowując, musiałeś zadeklarować typy parametrów, treści żądania (body) itp. tylko **raz**, i są one dostępne jako parametry funkcji. -You do that with standard modern Python types. +Robisz to tak samo jak ze standardowymi typami w Pythonie. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Nie musisz sie uczyć żadnej nowej składni, metod lub klas ze specyficznych bibliotek itp. -Just standard **Python 3.6+**. +Po prostu standardowy **Python 3.6+**. -For example, for an `int`: +Na przykład, dla danych typu `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +albo dla bardziej złożonego obiektu `Item`: ```Python item: Item ``` -...and with that single declaration you get: +...i z pojedyńczą deklaracją otrzymujesz: -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* Wsparcie edytorów kodu, wliczając: + * Auto-uzupełnianie. + * Sprawdzanie typów. +* Walidacja danych: + * Automatyczne i przejrzyste błędy gdy dane są niepoprawne. + * Walidacja nawet dla głęboko zagnieżdżonych obiektów JSON. +* Konwersja danych wejściowych: przychodzących z sieci na Pythonowe typy. Pozwala na przetwarzanie danych: * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: + * Parametrów ścieżki. + * Parametrów zapytania. + * Dane cookies. + * Dane nagłówków (headers). + * Formularze. + * Pliki. +* Konwersja danych wyjściowych: wychodzących z Pythona do sieci (jako JSON): + * Przetwarzanie Pythonowych typów (`str`, `int`, `float`, `bool`, `list`, itp). + * Obiekty `datetime`. + * Obiekty `UUID`. + * Modele baz danych. + * ...i wiele więcej. +* Automatyczne interaktywne dokumentacje API, wliczając 2 alternatywne interfejsy użytkownika: * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +Wracając do poprzedniego przykładu, **FastAPI** : -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +* Potwierdzi, że w ścieżce jest `item_id` dla żądań `GET` i `PUT`. +* Potwierdzi, że `item_id` jest typu `int` dla żądań `GET` i `PUT`. + * Jeżeli nie jest, odbiorca zobaczy przydatną, przejrzystą wiadomość z błędem. +* Sprawdzi czy w ścieżce jest opcjonalny parametr zapytania `q` (np. `http://127.0.0.1:8000/items/foo?q=somequery`) dla żądania `GET`. + * Jako że parametr `q` jest zadeklarowany jako `= None`, jest on opcjonalny. + * Gdyby tego `None` nie było, parametr ten byłby wymagany (tak jak treść żądania w żądaniu `PUT`). +* Dla żądania `PUT` z ścieżką `/items/{item_id}`, odczyta treść żądania jako JSON: + * Sprawdzi czy posiada wymagany atrybut `name`, który powinien być typu `str`. + * Sprawdzi czy posiada wymagany atrybut `price`, który musi być typu `float`. + * Sprawdzi czy posiada opcjonalny atrybut `is_offer`, który (jeżeli obecny) powinien być typu `bool`. + * To wszystko będzie również działać dla głęboko zagnieżdżonych obiektów JSON. +* Automatycznie konwertuje z i do JSON. +* Dokumentuje wszystko w OpenAPI, które może być używane przez: + * Interaktywne systemy dokumentacji. + * Systemy automatycznego generowania kodu klienckiego, dla wielu języków. +* Dostarczy bezpośrednio 2 interaktywne dokumentacje webowe. --- -We just scratched the surface, but you already get the idea of how it all works. +To dopiero początek, ale już masz mniej-więcej pojęcie jak to wszystko działa. -Try changing the line with: +Spróbuj zmienić linijkę: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +...z: ```Python ... "item_name": item.name ... ``` -...to: +...na: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +...i zobacz jak edytor kodu automatycznie uzupełni atrybuty i będzie znał ich typy: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Dla bardziej kompletnych przykładów posiadających więcej funkcjonalności, zobacz Tutorial - User Guide. -**Spoiler alert**: the tutorial - user guide includes: +**Uwaga Spoiler**: tutorial - user guide zawiera: -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** +* Deklaracje **parametrów** z innych miejsc takich jak: **nagłówki**, **pliki cookies**, **formularze** i **pliki**. +* Jak ustawić **ograniczenia walidacyjne** takie jak `maksymalna długość` lub `regex`. +* Potężny i łatwy w użyciu system **Dependency Injection**. +* Zabezpieczenia i autentykacja, wliczając wsparcie dla **OAuth2** z **tokenami JWT** oraz autoryzacją **HTTP Basic**. +* Bardziej zaawansowane (ale równie proste) techniki deklarowania **głęboko zagnieżdżonych modeli JSON** (dzięki Pydantic). +* Wiele dodatkowych funkcji (dzięki Starlette) takie jak: + * **WebSockety** * **GraphQL** - * extremely easy tests based on `requests` and `pytest` + * bardzo proste testy bazujące na `requests` oraz `pytest` * **CORS** - * **Cookie Sessions** - * ...and more. + * **Sesje cookie** + * ...i więcej. -## Performance +## Wydajność -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +Niezależne benchmarki TechEmpower pokazują, że **FastAPI** (uruchomiony na serwerze Uvicorn) jest jednym z najszybszych dostępnych Pythonowych frameworków, zaraz po Starlette i Uvicorn (używanymi wewnątrznie przez FastAPI). (*) -To understand more about it, see the section Benchmarks. +Aby dowiedzieć się o tym więcej, zobacz sekcję Benchmarks. -## Optional Dependencies +## Opcjonalne zależności -Used by Pydantic: +Używane przez Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - dla szybszego "parsowania" danych JSON. +* email_validator - dla walidacji adresów email. -Used by Starlette: +Używane przez Starlette: -* requests - Required if you want to use the `TestClient`. -* aiofiles - Required if you want to use `FileResponse` or `StaticFiles`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. +* requests - Wymagane jeżeli chcesz korzystać z `TestClient`. +* aiofiles - Wymagane jeżeli chcesz korzystać z `FileResponse` albo `StaticFiles`. +* jinja2 - Wymagane jeżeli chcesz używać domyślnej konfiguracji szablonów. +* python-multipart - Wymagane jeżelich chcesz wsparcie "parsowania" formularzy, używając `request.form()`. +* itsdangerous - Wymagany dla wsparcia `SessionMiddleware`. +* pyyaml - Wymagane dla wsparcia `SchemaGenerator` z Starlette (z FastAPI prawdopodobnie tego nie potrzebujesz). +* graphene - Wymagane dla wsparcia `GraphQLApp`. +* ujson - Wymagane jeżeli chcesz korzystać z `UJSONResponse`. -Used by FastAPI / Starlette: +Używane przez FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - jako serwer, który ładuje i obsługuje Twoją aplikację. +* orjson - Wymagane jeżeli chcesz używać `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Możesz zainstalować wszystkie te aplikacje przy pomocy `pip install fastapi[all]`. -## License +## Licencja -This project is licensed under the terms of the MIT license. +Ten projekt jest na licencji MIT. From 24968937e5a788fd15f801c705da9afba35c2517 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:37:21 +0000 Subject: [PATCH 0054/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9066792d8..658fd4255 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). * 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). From 26e94116c12f46035ea3f4949c74d566aaa89116 Mon Sep 17 00:00:00 2001 From: kty4119 <49435654+kty4119@users.noreply.github.com> Date: Mon, 17 Jan 2022 04:41:13 +0900 Subject: [PATCH 0055/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/index.md`=20(#4195)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/ko/docs/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index ee3edded6..d0c236906 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -35,7 +35,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 * **직관적**: 훌륭한 편집기 지원. 모든 곳에서 자동완성. 적은 디버깅 시간. * **쉬움**: 쉽게 사용하고 배우도록 설계. 적은 문서 읽기 시간. * **짧음**: 코드 중복 최소화. 각 매개변수 선언의 여러 기능. 적은 버그. -* **견고함**: 준비된 프로덕션 용 코드를 얻으세요. 자동 대화형 문서와 함께. +* **견고함**: 준비된 프로덕션 용 코드를 얻으십시오. 자동 대화형 문서와 함께. * **표준 기반**: API에 대한 (완전히 호환되는) 개방형 표준 기반: OpenAPI (이전에 Swagger로 알려졌던) 및 JSON 스키마. * 내부 개발팀의 프로덕션 애플리케이션을 빌드한 테스트에 근거한 측정 @@ -89,9 +89,9 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 --- -"_REST API를 만들기 위해 **현대적인 프레임워크**를 찾고 있다면 **FastAPI**를 확인해 보세요. [...] 빠르고, 쓰기 쉽고, 배우기도 쉽습니다 [...]_" +"_REST API를 만들기 위해 **현대적인 프레임워크**를 찾고 있다면 **FastAPI**를 확인해 보십시오. [...] 빠르고, 쓰기 쉽고, 배우기도 쉽습니다 [...]_" -"_우리 **API**를 **FastAPI**로 바꿨습니다 [...] 아마 여러분도 좋아하실 겁니다 [...]_" +"_우리 **API**를 **FastAPI**로 바꿨습니다 [...] 아마 여러분도 좋아하실 것입니다 [...]_"
Ines Montani - Matthew Honnibal - Explosion AI 설립자 - spaCy 제작자 (ref) - (ref)
@@ -193,7 +193,7 @@ async def read_item(item_id: int, q: Optional[str] = None): ### 실행하기 -서버를 실행하세요: +서버를 실행하십시오:
@@ -239,7 +239,7 @@ INFO: Application startup complete. ### 대화형 API 문서 -이제 http://127.0.0.1:8000/docs로 가보세요. +이제 http://127.0.0.1:8000/docs로 가보십시오. 자동 대화형 API 문서를 볼 수 있습니다 (Swagger UI 제공): @@ -388,7 +388,7 @@ item: Item 우리는 그저 수박 겉핡기만 했을 뿐인데 여러분은 벌써 어떻게 작동하는지 알고 있습니다. -다음 줄을 바꿔보세요: +다음 줄을 바꿔보십시오: ```Python return {"item_name": item.name, "item_id": item_id} @@ -447,7 +447,7 @@ Starlette이 사용하는: * jinja2 - 기본 템플릿 설정을 사용하려면 필요. * python-multipart - `request.form()`과 함께 "parsing"의 지원을 원하면 필요. * itsdangerous - `SessionMiddleware` 지원을 위해 필요. -* pyyaml - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요가 없을 겁니다). +* pyyaml - Starlette의 `SchemaGenerator` 지원을 위해 필요 (FastAPI와 쓸때는 필요 없을 것입니다). * graphene - `GraphQLApp` 지원을 위해 필요. * ujson - `UJSONResponse`를 사용하려면 필요. From d23b295b96ca9a4c9ebef381a111de4435acd222 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:41:49 +0000 Subject: [PATCH 0056/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 658fd4255..15602b1e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). * ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). From e1c6d7d31083e3c637c352d36b7d9f8d93508b15 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Mon, 17 Jan 2022 03:41:59 +0800 Subject: [PATCH 0057/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/help-fastapi.md`=20(#3847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/help-fastapi.md | 164 ++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 61 deletions(-) diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md index 99e37b7c1..6f3f5159b 100644 --- a/docs/zh/docs/help-fastapi.md +++ b/docs/zh/docs/help-fastapi.md @@ -1,107 +1,149 @@ -# 帮助 FastAPI - 获取帮助 +# 帮助 FastAPI 与求助 -你喜欢 **FastAPI** 吗? +您喜欢 **FastAPI** 吗? -您愿意去帮助 FastAPI,帮助其他用户以及作者吗? +想帮助 FastAPI?其它用户?还有项目作者? -或者你想要获得有关 **FastAPI** 的帮助? +或要求助怎么使用 **FastAPI**? -下面是一些非常简单的方式去提供帮助(有些只需单击一两次链接)。 +以下几种帮助的方式都非常简单(有些只需要点击一两下鼠标)。 -以及几种获取帮助的途径。 +求助的渠道也很多。 -## 在 GitHub 上 Star **FastAPI** +## 订阅新闻邮件 -你可以在 GitHub 上 "star" FastAPI(点击右上角的 star 按钮):https://github.com/tiangolo/fastapi。 +您可以订阅 [**FastAPI 和它的小伙伴** 新闻邮件](/newsletter/){.internal-link target=_blank}(不会经常收到) -通过添加 star,其他用户将会更容易发现 FastAPI,并了解已经有许多人认为它有用。 +* FastAPI 及其小伙伴的新闻 🚀 +* 指南 📝 +* 功能 ✨ +* 破坏性更改 🚨 +* 开发技巧 ✅ -## Watch GitHub 仓库的版本发布 +## 在推特上关注 FastAPI -你可以在 GitHub 上 "watch" FastAPI(点击右上角的 watch 按钮):https://github.com/tiangolo/fastapi。 +在 **Twitter** 上关注 @fastapi 获取 **FastAPI** 的最新消息。🐦 -这时你可以选择 "Releases only" 选项。 +## 在 GitHub 上为 **FastAPI** 加星 -之后,只要有 **FastAPI** 的新版本(包含缺陷修复和新功能)发布,你都会(通过电子邮件)收到通知。 +您可以在 GitHub 上 **Star** FastAPI(只要点击右上角的星星就可以了): https://github.com/tiangolo/fastapi。⭐️ -## 加入聊天室 +**Star** 以后,其它用户就能更容易找到 FastAPI,并了解到已经有其他用户在使用它了。 -加入 Gitter 上的聊天室:https://gitter.im/tiangolo/fastapi。 +## 关注 GitHub 资源库的版本发布 -在这里你可以快速提问、帮助他人、分享想法等。 +您还可以在 GitHub 上 **Watch** FastAPI,(点击右上角的 **Watch** 按钮)https://github.com/tiangolo/fastapi。👀 -## 与作者联系 +您可以选择只关注发布(**Releases only**)。 -你可以联系 我 (Sebastián Ramírez / `tiangolo`) - FastAPI 的作者。 +这样,您就可以(在电子邮件里)接收到 **FastAPI** 新版发布的通知,及时了解 bug 修复与新功能。 -你可以: +## 联系作者 -* 在 **GitHub** 上关注我。 - * 查看我创建的其他的可能对你有帮助的开源项目。 - * 关注我以了解我创建的新开源项目。 -* 在 **Twitter** 上关注我。 - * 告诉我你是如何使用 FastAPI 的(我很乐意听到)。 - * 提出问题。 -* 在 **Linkedin** 上联系我。 - * 与我交流。 - * 认可我的技能或推荐我 :) -* 在 **Medium** 上阅读我写的文章(或关注我)。 - * 阅读我创建的其他想法,文章和工具。 - * 关注我以了解我发布的新内容。 +您可以联系项目作者,就是我(Sebastián Ramírez / `tiangolo`)。 -## 发布和 **FastAPI** 有关的推特 +您可以: - 发布和 **FastAPI** 有关的推特 让我和其他人知道你为什么喜欢它。 +* 在 **GitHub** 上关注我 + * 了解其它我创建的开源项目,或许对您会有帮助 + * 关注我什么时候创建新的开源项目 +* 在 **Twitter** 上关注我 + * 告诉我您使用 FastAPI(我非常乐意听到这种消息) + * 接收我发布公告或新工具的消息 + * 您还可以关注@fastapi on Twitter,这是个独立的账号 +* 在**领英**上联系我 + * 接收我发布公告或新工具的消息(虽然我用 Twitter 比较多) +* 阅读我在 **Dev.to****Medium** 上的文章,或关注我 + * 阅读我的其它想法、文章,了解我创建的工具 + * 关注我,这样就可以随时看到我发布的新文章 -## 告诉我你正在如何使用 **FastAPI** +## Tweet about **FastAPI** -我很乐意听到有关 **FastAPI** 被如何使用、你喜欢它的哪一点、被投入使用的项目/公司等等信息。 +Tweet about **FastAPI** 让我和大家知道您为什么喜欢 FastAPI。🎉 -你可以通过以下平台让我知道: - -* **Twitter**。 -* **Linkedin**。 -* **Medium**。 +知道有人使用 **FastAPI**,我会很开心,我也想知道您为什么喜欢 FastAPI,以及您在什么项目/哪些公司使用 FastAPI,等等。 ## 为 FastAPI 投票 -* 在 Slant 上为 **FastAPI** 投票。 +* 在 Slant 上为 **FastAPI** 投票 +* 在 AlternativeTo 上为 **FastAPI** 投票 -## 帮助他人解决 GitHub 的 issues +## 在 GitHub 上帮助其他人解决问题 -你可以查看 已有的 issues 并尝试帮助其他人。 +您可以查看现有 issues,并尝试帮助其他人解决问题,说不定您能解决这些问题呢。🤓 -## Watch GitHub 仓库 +如果帮助很多人解决了问题,您就有可能成为 [FastAPI 的官方专家](fastapi-people.md#experts){.internal-link target=_blank}。🎉 -你可以在 GitHub 上 "watch" FastAPI(点击右上角的 "watch" 按钮):https://github.com/tiangolo/fastapi。 +## 监听 GitHub 资源库 -如果你选择的是 "Watching" 而不是 "Releases only" 选项,你会在其他人创建了新的 issue 时收到通知。 +您可以在 GitHub 上「监听」FastAPI(点击右上角的 "watch" 按钮): https://github.com/tiangolo/fastapi. 👀 -然后你可以尝试帮助他们解决这些 issue。 +如果您选择 "Watching" 而不是 "Releases only",有人创建新 Issue 时,您会接收到通知。 -## 创建 issue +然后您就可以尝试并帮助他们解决问题。 -你可以在 GitHub 仓库中 创建一个新 issue 用来: +## 创建 Issue -* 报告 bug 或问题。 -* 提议新的特性。 -* 提问。 +您可以在 GitHub 资源库中创建 Issue,例如: -## 创建 Pull Request +* 提出**问题**或**意见** +* 提出新**特性**建议 -你可以 创建一个 Pull Request 用来: +**注意**:如果您创建 Issue,我会要求您也要帮助别的用户。😉 -* 纠正你在文档中发现的错别字。 -* 添加新的文档内容。 -* 修复已有的 bug 或问题。 -* 添加新的特性。 +## 创建 PR + +您可以创建 PR 为源代码做[贡献](contributing.md){.internal-link target=_blank},例如: + +* 修改文档错别字 +* 编辑这个文件,分享 FastAPI 的文章、视频、博客,不论是您自己的,还是您看到的都成 + * 注意,添加的链接要放在对应区块的开头 +* [翻译文档](contributing.md#translations){.internal-link target=_blank} + * 审阅别人翻译的文档 +* 添加新的文档内容 +* 修复现有问题/Bug +* 添加新功能 + +## 加入聊天 + +快加入 👥 Discord 聊天服务器 👥 和 FastAPI 社区里的小伙伴一起哈皮吧。 + +!!! tip "提示" + + 如有问题,请在 GitHub Issues 里提问,在这里更容易得到 [FastAPI 专家](fastapi-people.md#experts){.internal-link target=_blank}的帮助。 + + 聊天室仅供闲聊。 + +我们之前还使用过 Gitter chat,但它不支持频道等高级功能,聊天也比较麻烦,所以现在推荐使用 Discord。 + +### 别在聊天室里提问 + +注意,聊天室更倾向于“闲聊”,经常有人会提出一些笼统得让人难以回答的问题,所以在这里提问一般没人回答。 + +GitHub Issues 里提供了模板,指引您提出正确的问题,有利于获得优质的回答,甚至可能解决您还没有想到的问题。而且就算答疑解惑要耗费不少时间,我还是会尽量在 GitHub 里回答问题。但在聊天室里,我就没功夫这么做了。😅 + +聊天室里的聊天内容也不如 GitHub 里好搜索,聊天里的问答很容易就找不到了。只有在 GitHub Issues 里的问答才能帮助您成为 [FastAPI 专家](fastapi-people.md#experts){.internal-link target=_blank},在 GitHub Issues 中为您带来更多关注。 + +另一方面,聊天室里有成千上万的用户,在这里,您有很大可能遇到聊得来的人。😄 ## 赞助作者 -你还可以通过 GitHub sponsors 在经济上支持作者(我)。 +您还可以通过 GitHub 赞助商资助本项目的作者(就是我)。 -这样你可以给我买杯咖啡☕️以示谢意😄。 +给我买杯咖啡 ☕️ 以示感谢 😄 + +当然您也可以成为 FastAPI 的金牌或银牌赞助商。🏅🎉 + +## 赞助 FastAPI 使用的工具 + +如您在本文档中所见,FastAPI 站在巨人的肩膀上,它们分别是 Starlette 和 Pydantic。 + +您还可以赞助: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) --- -感谢! +谢谢!🚀 + From 93e4a19e3526369b2a9a0c5ef71101833faa2987 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Jan 2022 19:42:33 +0000 Subject: [PATCH 0058/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 15602b1e8..6ce21a8af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/help-fastapi.md`. PR [#3847](https://github.com/tiangolo/fastapi/pull/3847) by [@jaystone776](https://github.com/jaystone776). * 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). From 9e2f5c67b603d73a77c420b629e2ee4e7378de1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 21:08:04 +0100 Subject: [PATCH 0059/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6ce21a8af..090a344a6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,13 +2,25 @@ ## Latest Changes +### Features + +* ✨ Enable configuring Swagger UI parameters. Original PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). Here are the new docs: [Configuring Swagger UI](https://fastapi.tiangolo.com/advanced/extending-openapi/#configuring-swagger-ui). + +### Docs + +* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Update Chinese translation for `docs/help-fastapi.md`. PR [#3847](https://github.com/tiangolo/fastapi/pull/3847) by [@jaystone776](https://github.com/jaystone776). * 🌐 Fix Korean translation for `docs/ko/docs/index.md`. PR [#4195](https://github.com/tiangolo/fastapi/pull/4195) by [@kty4119](https://github.com/kty4119). * 🌐 Add Polish translation for `docs/pl/docs/index.md`. PR [#4245](https://github.com/tiangolo/fastapi/pull/4245) by [@MicroPanda123](https://github.com/MicroPanda123). * 🌐 Add Chinese translation for `docs\tutorial\path-operation-configuration.md`. PR [#3312](https://github.com/tiangolo/fastapi/pull/3312) by [@jaystone776](https://github.com/jaystone776). -* ✨ Enable configuring Swagger UI parameters. PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). -* 📝 Update Python Types docs, add missing 3.6 / 3.9 example. PR [#4434](https://github.com/tiangolo/fastapi/pull/4434) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 🔧 Enable MkDocs Material Insiders' `content.tabs.link`. PR [#4399](https://github.com/tiangolo/fastapi/pull/4399) by [@tiangolo](https://github.com/tiangolo). + ## 0.71.0 ### Features From f0388915a8b1cd9f3ae2259bace234ac6249c51a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Jan 2022 21:09:10 +0100 Subject: [PATCH 0060/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?72.0?= 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 090a344a6..2dfb47c12 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.72.0 + ### Features * ✨ Enable configuring Swagger UI parameters. Original PR [#2568](https://github.com/tiangolo/fastapi/pull/2568) by [@jmriebold](https://github.com/jmriebold). Here are the new docs: [Configuring Swagger UI](https://fastapi.tiangolo.com/advanced/extending-openapi/#configuring-swagger-ui). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5b735aed5..d83fe6fbd 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.71.0" +__version__ = "0.72.0" from starlette import status as status From a75d0801241dc59590d928c48da7665856a52963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 15:56:14 +0100 Subject: [PATCH 0061/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor=20Dropba?= =?UTF-8?q?se=20(#4465)?= 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 + docs/en/docs/img/sponsors/dropbase-banner.svg | 117 +++++++++++++++++ docs/en/docs/img/sponsors/dropbase.svg | 124 ++++++++++++++++++ docs/en/overrides/main.html | 6 + 6 files changed, 252 insertions(+) create mode 100644 docs/en/docs/img/sponsors/dropbase-banner.svg create mode 100644 docs/en/docs/img/sponsors/dropbase.svg diff --git a/README.md b/README.md index 53de38bd9..c9c69d3e8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index baa2e440d..b98e68b65 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,6 +5,9 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg + - url: https://www.dropbase.io/careers + title: Dropbase - seamlessly collect, clean, and centralize data. + img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 759748728..0c4e716d7 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -7,3 +7,4 @@ logins: - koaning - deepset-ai - cryptapi + - DropbaseHQ diff --git a/docs/en/docs/img/sponsors/dropbase-banner.svg b/docs/en/docs/img/sponsors/dropbase-banner.svg new file mode 100644 index 000000000..d65abf1d9 --- /dev/null +++ b/docs/en/docs/img/sponsors/dropbase-banner.svg @@ -0,0 +1,117 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/dropbase.svg b/docs/en/docs/img/sponsors/dropbase.svg new file mode 100644 index 000000000..d0defb4df --- /dev/null +++ b/docs/en/docs/img/sponsors/dropbase.svg @@ -0,0 +1,124 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 70b0253bd..0f452b515 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -46,6 +46,12 @@
+ {% endblock %} From 347e391271e09244c3d95ac46dd5493a9b472ee4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 14:56:44 +0000 Subject: [PATCH 0062/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2dfb47c12..41fa8493e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From ca5d57ea799028d771101bd711ca87a301dd45d8 Mon Sep 17 00:00:00 2001 From: Mark Date: Sun, 23 Jan 2022 23:54:59 +0800 Subject: [PATCH 0063/1881] =?UTF-8?q?=E2=9C=A8=20Allow=20hiding=20from=20O?= =?UTF-8?q?penAPI=20(and=20Swagger=20UI)=20`Query`,=20`Cookie`,=20`Header`?= =?UTF-8?q?,=20and=20`Path`=20parameters=20(#3144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../tutorial/query-params-str-validations.md | 16 ++ .../tutorial014.py | 15 ++ .../tutorial014_py310.py | 11 + fastapi/openapi/utils.py | 2 + fastapi/param_functions.py | 8 + fastapi/params.py | 10 + tests/test_param_include_in_schema.py | 239 ++++++++++++++++++ .../test_tutorial014.py | 82 ++++++ .../test_tutorial014_py310.py | 91 +++++++ 9 files changed, 474 insertions(+) create mode 100644 docs_src/query_params_str_validations/tutorial014.py create mode 100644 docs_src/query_params_str_validations/tutorial014_py310.py create mode 100644 tests/test_param_include_in_schema.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index fcac1a4e0..ee62b9718 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -387,6 +387,22 @@ The docs will show it like this: +## Exclude from OpenAPI + +To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter `include_in_schema` of `Query` to `False`: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + ## Recap You can declare additional validations and metadata for your parameters. diff --git a/docs_src/query_params_str_validations/tutorial014.py b/docs_src/query_params_str_validations/tutorial014.py new file mode 100644 index 000000000..fb50bc27b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014.py @@ -0,0 +1,15 @@ +from typing import Optional + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Optional[str] = Query(None, include_in_schema=False) +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_py310.py b/docs_src/query_params_str_validations/tutorial014_py310.py new file mode 100644 index 000000000..7ae39c7f9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(hidden_query: str | None = Query(None, include_in_schema=False)): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 0e73e21bf..aff76b15e 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -92,6 +92,8 @@ def get_openapi_operation_parameters( for param in all_route_params: field_info = param.field_info field_info = cast(Param, field_info) + if not field_info.include_in_schema: + continue parameter = { "name": param.alias, "in": field_info.in_.value, diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index ff65d7271..a553a1461 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -20,6 +20,7 @@ def Path( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Path( @@ -37,6 +38,7 @@ def Path( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -57,6 +59,7 @@ def Query( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Query( @@ -74,6 +77,7 @@ def Query( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -95,6 +99,7 @@ def Header( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Header( @@ -113,6 +118,7 @@ def Header( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) @@ -133,6 +139,7 @@ def Cookie( # noqa: N802 example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ) -> Any: return params.Cookie( @@ -150,6 +157,7 @@ def Cookie( # noqa: N802 example=example, examples=examples, deprecated=deprecated, + include_in_schema=include_in_schema, **extra, ) diff --git a/fastapi/params.py b/fastapi/params.py index 3cab98b78..042bbd42f 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -31,11 +31,13 @@ class Param(FieldInfo): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.deprecated = deprecated self.example = example self.examples = examples + self.include_in_schema = include_in_schema super().__init__( default, alias=alias, @@ -75,6 +77,7 @@ class Path(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.in_ = self.in_ @@ -93,6 +96,7 @@ class Path(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -117,6 +121,7 @@ class Query(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): super().__init__( @@ -134,6 +139,7 @@ class Query(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -159,6 +165,7 @@ class Header(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): self.convert_underscores = convert_underscores @@ -177,6 +184,7 @@ class Header(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) @@ -201,6 +209,7 @@ class Cookie(Param): example: Any = Undefined, examples: Optional[Dict[str, Any]] = None, deprecated: Optional[bool] = None, + include_in_schema: bool = True, **extra: Any, ): super().__init__( @@ -218,6 +227,7 @@ class Cookie(Param): deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, **extra, ) diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py new file mode 100644 index 000000000..4eaac72d8 --- /dev/null +++ b/tests/test_param_include_in_schema.py @@ -0,0 +1,239 @@ +from typing import Optional + +import pytest +from fastapi import Cookie, FastAPI, Header, Path, Query +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/hidden_cookie") +async def hidden_cookie( + hidden_cookie: Optional[str] = Cookie(None, include_in_schema=False) +): + return {"hidden_cookie": hidden_cookie} + + +@app.get("/hidden_header") +async def hidden_header( + hidden_header: Optional[str] = Header(None, include_in_schema=False) +): + return {"hidden_header": hidden_header} + + +@app.get("/hidden_path/{hidden_path}") +async def hidden_path(hidden_path: str = Path(..., include_in_schema=False)): + return {"hidden_path": hidden_path} + + +@app.get("/hidden_query") +async def hidden_query( + hidden_query: Optional[str] = Query(None, include_in_schema=False) +): + return {"hidden_query": hidden_query} + + +client = TestClient(app) + +openapi_shema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/hidden_cookie": { + "get": { + "summary": "Hidden Cookie", + "operationId": "hidden_cookie_hidden_cookie_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_header": { + "get": { + "summary": "Hidden Header", + "operationId": "hidden_header_hidden_header_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_path/{hidden_path}": { + "get": { + "summary": "Hidden Path", + "operationId": "hidden_path_hidden_path__hidden_path__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/hidden_query": { + "get": { + "summary": "Hidden Query", + "operationId": "hidden_query_hidden_query_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == openapi_shema + + +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ( + "/hidden_cookie", + {}, + 200, + {"hidden_cookie": None}, + ), + ( + "/hidden_cookie", + {"hidden_cookie": "somevalue"}, + 200, + {"hidden_cookie": "somevalue"}, + ), + ], +) +def test_hidden_cookie(path, cookies, expected_status, expected_response): + response = client.get(path, cookies=cookies) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ( + "/hidden_header", + {}, + 200, + {"hidden_header": None}, + ), + ( + "/hidden_header", + {"Hidden-Header": "somevalue"}, + 200, + {"hidden_header": "somevalue"}, + ), + ], +) +def test_hidden_header(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_hidden_path(): + response = client.get("/hidden_path/hidden_path") + assert response.status_code == 200 + assert response.json() == {"hidden_path": "hidden_path"} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ( + "/hidden_query", + 200, + {"hidden_query": None}, + ), + ( + "/hidden_query?hidden_query=somevalue", + 200, + {"hidden_query": "somevalue"}, + ), + ], +) +def test_hidden_query(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py new file mode 100644 index 000000000..98ae5a684 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -0,0 +1,82 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014 import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py new file mode 100644 index 000000000..33f3d5f77 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} From 85518bc58b131ddc8d7f251c9349f473d194a9b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 15:55:36 +0000 Subject: [PATCH 0064/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 41fa8493e..afd109d88 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From 3de0fb82bf17fa4179caa38c3126786a7d99cf67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 17:13:49 +0100 Subject: [PATCH 0065/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20docs=20dependenc?= =?UTF-8?q?ies=20cache,=20to=20get=20the=20latest=20Material=20for=20MkDoc?= =?UTF-8?q?s=20(#4466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index eba5fc57e..ccf964486 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -20,7 +20,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs-v2 - name: Install Flit if: steps.cache.outputs.cache-hit != 'true' run: python3.7 -m pip install flit From 699b5ef84198a352a332beea9953fe1db33315b6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:14:28 +0000 Subject: [PATCH 0066/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 afd109d88..997fb7529 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From d4608a00cf4855021dfb1a780556e24dedc94b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 17:32:04 +0100 Subject: [PATCH 0067/1881] =?UTF-8?q?=F0=9F=90=9B=20Prefer=20custom=20enco?= =?UTF-8?q?der=20over=20defaults=20if=20specified=20in=20`jsonable=5Fencod?= =?UTF-8?q?er`=20(#4467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vivek Sunder --- fastapi/encoders.py | 18 +++++++++--------- tests/test_jsonable_encoder.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 3f599c9fa..4b7ffe313 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -34,9 +34,17 @@ def jsonable_encoder( exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, - custom_encoder: Dict[Any, Callable[[Any], Any]] = {}, + custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None, sqlalchemy_safe: bool = True, ) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) if include is not None and not isinstance(include, (set, dict)): include = set(include) if exclude is not None and not isinstance(exclude, (set, dict)): @@ -118,14 +126,6 @@ def jsonable_encoder( ) return encoded_list - if custom_encoder: - if type(obj) in custom_encoder: - return custom_encoder[type(obj)](obj) - else: - for encoder_type, encoder in custom_encoder.items(): - if isinstance(obj, encoder_type): - return encoder(obj) - if type(obj) in ENCODERS_BY_TYPE: return ENCODERS_BY_TYPE[type(obj)](obj) for encoder, classes_tuple in encoders_by_class_tuples.items(): diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index e2aa8adf8..fa82b5ea8 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -161,6 +161,21 @@ def test_custom_encoders(): assert encoded_instance["dt_field"] == instance.dt_field.isoformat() +def test_custom_enum_encoders(): + def custom_enum_encoder(v: Enum): + return v.value.lower() + + class MyEnum(Enum): + ENUM_VAL_1 = "ENUM_VAL_1" + + instance = MyEnum.ENUM_VAL_1 + + encoded_instance = jsonable_encoder( + instance, custom_encoder={MyEnum: custom_enum_encoder} + ) + assert encoded_instance == custom_enum_encoder(instance) + + def test_encode_model_with_path(model_with_path): if isinstance(model_with_path.path, PureWindowsPath): expected = "\\foo\\bar" From f4963f05bf4295e02cce1a28386712a5e6776fc4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:32:35 +0000 Subject: [PATCH 0068/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 997fb7529..49747539d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From 6215fdd39e54422561d51d7e6159c219053d41cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 16:34:52 +0000 Subject: [PATCH 0070/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 49747539d..468d450a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). From 0f8349fcb7c57921d28e78296a7dc8d0504459e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 18:03:42 +0100 Subject: [PATCH 0071/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 468d450a8..54017ceb3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3,7 +3,7 @@ ## Latest Changes * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). -* 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). + * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). * ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). From 569afb4378c80e0bff5dc4a45f26d012e498eda6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 18:43:04 +0100 Subject: [PATCH 0072/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20tag?= =?UTF-8?q?s=20with=20Enums=20(#4468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/path-operation-configuration.md | 12 ++++ .../tutorial002b.py | 20 +++++++ fastapi/applications.py | 23 ++++---- fastapi/routing.py | 32 +++++------ .../test_tutorial002b.py | 56 +++++++++++++++++++ 5 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 docs_src/path_operation_configuration/tutorial002b.py create mode 100644 tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 1ff448e76..884a762e2 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -64,6 +64,18 @@ They will be added to the OpenAPI schema and used by the automatic documentation +### Tags with Enums + +If you have a big application, you might end up accumulating **several tags**, and you would want to make sure you always use the **same tag** for related *path operations*. + +In these cases, it could make sense to store the tags in an `Enum`. + +**FastAPI** supports that the same way as with plain strings: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + ## Summary and description You can add a `summary` and `description`: diff --git a/docs_src/path_operation_configuration/tutorial002b.py b/docs_src/path_operation_configuration/tutorial002b.py new file mode 100644 index 000000000..d53b4d817 --- /dev/null +++ b/docs_src/path_operation_configuration/tutorial002b.py @@ -0,0 +1,20 @@ +from enum import Enum + +from fastapi import FastAPI + +app = FastAPI() + + +class Tags(Enum): + items = "items" + users = "users" + + +@app.get("/items/", tags=[Tags.items]) +async def get_items(): + return ["Portal gun", "Plumbus"] + + +@app.get("/users/", tags=[Tags.users]) +async def read_users(): + return ["Rick", "Morty"] diff --git a/fastapi/applications.py b/fastapi/applications.py index d71d4190a..dbfd76fb9 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union from fastapi import routing @@ -219,7 +220,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -273,7 +274,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -342,7 +343,7 @@ class FastAPI(Starlette): router: routing.APIRouter, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, @@ -368,7 +369,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -419,7 +420,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -470,7 +471,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -521,7 +522,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -572,7 +573,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -623,7 +624,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -674,7 +675,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -725,7 +726,7 @@ class FastAPI(Starlette): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 63ad72964..f6d5370d6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1,9 +1,9 @@ import asyncio import dataclasses import email.message -import enum import inspect import json +from enum import Enum, IntEnum from typing import ( Any, Callable, @@ -305,7 +305,7 @@ class APIRoute(routing.Route): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -330,7 +330,7 @@ class APIRoute(routing.Route): openapi_extra: Optional[Dict[str, Any]] = None, ) -> None: # normalise enums e.g. http.HTTPStatus - if isinstance(status_code, enum.IntEnum): + if isinstance(status_code, IntEnum): status_code = int(status_code) self.path = path self.endpoint = endpoint @@ -438,7 +438,7 @@ class APIRouter(routing.Router): self, *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -466,7 +466,7 @@ class APIRouter(routing.Router): "/" ), "A path prefix must not end with '/', as the routes will start with '/'" self.prefix = prefix - self.tags: List[str] = tags or [] + self.tags: List[Union[str, Enum]] = tags or [] self.dependencies = list(dependencies or []) or [] self.deprecated = deprecated self.include_in_schema = include_in_schema @@ -483,7 +483,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -557,7 +557,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -634,7 +634,7 @@ class APIRouter(routing.Router): router: "APIRouter", *, prefix: str = "", - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, @@ -738,7 +738,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -790,7 +790,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -842,7 +842,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -894,7 +894,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -946,7 +946,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -998,7 +998,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1050,7 +1050,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, @@ -1102,7 +1102,7 @@ class APIRouter(routing.Router): *, response_model: Optional[Type[Any]] = None, status_code: Optional[int] = None, - tags: Optional[List[str]] = None, + tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, summary: Optional[str] = None, description: Optional[str] = None, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py new file mode 100644 index 000000000..be9f2afec --- /dev/null +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -0,0 +1,56 @@ +from fastapi.testclient import TestClient + +from docs_src.path_operation_configuration.tutorial002b import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == ["Portal gun", "Plumbus"] + + +def test_get_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] From 59b1f353b3fe77cf801242e3d120372ad8519710 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 17:43:36 +0000 Subject: [PATCH 0073/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 54017ceb3..7026d0eb1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). From 1bf55200a90b04229f665cd2ee83edde91e936e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 20:14:13 +0100 Subject: [PATCH 0074/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20dec?= =?UTF-8?q?laring=20`UploadFile`=20parameters=20without=20explicit=20`File?= =?UTF-8?q?()`=20(#4469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/request-files.md | 47 +++- docs_src/request_files/tutorial001.py | 2 +- docs_src/request_files/tutorial001_02.py | 21 ++ .../request_files/tutorial001_02_py310.py | 19 ++ docs_src/request_files/tutorial001_03.py | 15 ++ docs_src/request_files/tutorial002.py | 2 +- docs_src/request_files/tutorial002_py39.py | 2 +- docs_src/request_files/tutorial003.py | 37 +++ docs_src/request_files/tutorial003_py39.py | 35 +++ fastapi/datastructures.py | 6 +- fastapi/dependencies/utils.py | 28 +-- .../test_request_files/test_tutorial001_02.py | 157 ++++++++++++ .../test_tutorial001_02_py310.py | 169 +++++++++++++ .../test_request_files/test_tutorial001_03.py | 159 +++++++++++++ .../test_request_files/test_tutorial003.py | 194 +++++++++++++++ .../test_tutorial003_py39.py | 223 ++++++++++++++++++ 16 files changed, 1086 insertions(+), 30 deletions(-) create mode 100644 docs_src/request_files/tutorial001_02.py create mode 100644 docs_src/request_files/tutorial001_02_py310.py create mode 100644 docs_src/request_files/tutorial001_03.py create mode 100644 docs_src/request_files/tutorial003.py create mode 100644 docs_src/request_files/tutorial003_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_py39.py diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index b7257c7eb..ed2c8b6af 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -17,7 +17,7 @@ Import `File` and `UploadFile` from `fastapi`: {!../../../docs_src/request_files/tutorial001.py!} ``` -## Define `File` parameters +## Define `File` Parameters Create file parameters the same way you would for `Body` or `Form`: @@ -41,7 +41,7 @@ Have in mind that this means that the whole contents will be stored in memory. T But there are several cases in which you might benefit from using `UploadFile`. -## `File` parameters with `UploadFile` +## `File` Parameters with `UploadFile` Define a `File` parameter with a type of `UploadFile`: @@ -51,6 +51,7 @@ Define a `File` parameter with a type of `UploadFile`: Using `UploadFile` has several advantages over `bytes`: +* You don't have to use `File()` in the default value. * It uses a "spooled" file: * A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk. * This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory. @@ -113,7 +114,31 @@ The way HTML forms (`
`) sends the data to the server normally uses This is not a limitation of **FastAPI**, it's part of the HTTP protocol. -## Multiple file uploads +## Optional File Upload + +You can make a file optional by using standard type annotations: + +=== "Python 3.6 and above" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +## `UploadFile` with Additional Metadata + +You can also use `File()` with `UploadFile` to set additional parameters in `File()`, for example additional metadata: + +```Python hl_lines="13" +{!../../../docs_src/request_files/tutorial001_03.py!} +``` + +## Multiple File Uploads It's possible to upload several files at the same time. @@ -140,6 +165,22 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s. **FastAPI** provides the same `starlette.responses` as `fastapi.responses` just as a convenience for you, the developer. But most of the available responses come directly from Starlette. +### Multiple File Uploads with Additional Metadata + +And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`: + +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + ## Recap Use `File` to declare files to be uploaded as input parameters (as form data). diff --git a/docs_src/request_files/tutorial001.py b/docs_src/request_files/tutorial001.py index fffb56af8..0fb1dd571 100644 --- a/docs_src/request_files/tutorial001.py +++ b/docs_src/request_files/tutorial001.py @@ -9,5 +9,5 @@ async def create_file(file: bytes = File(...)): @app.post("/uploadfile/") -async def create_upload_file(file: UploadFile = File(...)): +async def create_upload_file(file: UploadFile): return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02.py b/docs_src/request_files/tutorial001_02.py new file mode 100644 index 000000000..26a4c9cbf --- /dev/null +++ b/docs_src/request_files/tutorial001_02.py @@ -0,0 +1,21 @@ +from typing import Optional + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Optional[bytes] = File(None)): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Optional[UploadFile] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_py310.py b/docs_src/request_files/tutorial001_02_py310.py new file mode 100644 index 000000000..0e576251b --- /dev/null +++ b/docs_src/request_files/tutorial001_02_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: bytes | None = File(None)): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03.py b/docs_src/request_files/tutorial001_03.py new file mode 100644 index 000000000..abcac9e4c --- /dev/null +++ b/docs_src/request_files/tutorial001_03.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: bytes = File(..., description="A file read as bytes")): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: UploadFile = File(..., description="A file read as UploadFile") +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002.py b/docs_src/request_files/tutorial002.py index 6fdf16a75..94abb7c6c 100644 --- a/docs_src/request_files/tutorial002.py +++ b/docs_src/request_files/tutorial002.py @@ -12,7 +12,7 @@ async def create_files(files: List[bytes] = File(...)): @app.post("/uploadfiles/") -async def create_upload_files(files: List[UploadFile] = File(...)): +async def create_upload_files(files: List[UploadFile]): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_files/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py index 26cd56769..2779618bd 100644 --- a/docs_src/request_files/tutorial002_py39.py +++ b/docs_src/request_files/tutorial002_py39.py @@ -10,7 +10,7 @@ async def create_files(files: list[bytes] = File(...)): @app.post("/uploadfiles/") -async def create_upload_files(files: list[UploadFile] = File(...)): +async def create_upload_files(files: list[UploadFile]): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_files/tutorial003.py b/docs_src/request_files/tutorial003.py new file mode 100644 index 000000000..4a91b7a8b --- /dev/null +++ b/docs_src/request_files/tutorial003.py @@ -0,0 +1,37 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: List[bytes] = File(..., description="Multiple files as bytes") +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: List[UploadFile] = File(..., description="Multiple files as UploadFile") +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_py39.py b/docs_src/request_files/tutorial003_py39.py new file mode 100644 index 000000000..d853f48d1 --- /dev/null +++ b/docs_src/request_files/tutorial003_py39.py @@ -0,0 +1,35 @@ +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: list[bytes] = File(..., description="Multiple files as bytes") +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: list[UploadFile] = File(..., description="Multiple files as UploadFile") +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b13171287..b20a25ab6 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Iterable, Type, TypeVar +from typing import Any, Callable, Dict, Iterable, Type, TypeVar from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 @@ -20,6 +20,10 @@ class UploadFile(StarletteUploadFile): raise ValueError(f"Expected UploadFile, received: {type(v)}") return v + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) + class DefaultPlaceholder: """ diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 35ba44aab..d4028d067 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -390,6 +390,8 @@ def get_param_field( field.required = required if not had_schema and not is_scalar_field(field=field): field.field_info = params.Body(field_info.default) + if not had_schema and lenient_issubclass(field.type_, UploadFile): + field.field_info = params.File(field_info.default) return field @@ -701,25 +703,6 @@ def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper: return missing_field_error -def get_schema_compatible_field(*, field: ModelField) -> ModelField: - out_field = field - if lenient_issubclass(field.type_, UploadFile): - use_type: type = bytes - if field.shape in sequence_shapes: - use_type = List[bytes] - out_field = create_response_field( - name=field.name, - type_=use_type, - class_validators=field.class_validators, - model_config=field.model_config, - default=field.default, - required=field.required, - alias=field.alias, - field_info=field.field_info, - ) - return out_field - - def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: flat_dependant = get_flat_dependant(dependant) if not flat_dependant.body_params: @@ -729,9 +712,8 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: embed = getattr(field_info, "embed", None) body_param_names_set = {param.name for param in flat_dependant.body_params} if len(body_param_names_set) == 1 and not embed: - final_field = get_schema_compatible_field(field=first_param) - check_file_field(final_field) - return final_field + check_file_field(first_param) + return first_param # If one field requires to embed, all have to be embedded # in case a sub-dependency is evaluated with a single unique body field # That is combined (embedded) with other body fields @@ -740,7 +722,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: model_name = "Body_" + name BodyModel: Type[BaseModel] = create_model(model_name) for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = get_schema_compatible_field(field=f) + BodyModel.__fields__[f.name] = f required = any(True for f in flat_dependant.body_params if f.required) BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None) diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py new file mode 100644 index 000000000..e852a1b31 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -0,0 +1,157 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py new file mode 100644 index 000000000..62e9f98d0 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py new file mode 100644 index 000000000..ec7509ea2 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py new file mode 100644 index 000000000..943b235ab --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -0,0 +1,194 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial003 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfiles/": { + "post": { + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as bytes", + } + }, + }, + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + "description": "Multiple files as UploadFile", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b" Date: Sun, 23 Jan 2022 19:14:47 +0000 Subject: [PATCH 0075/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7026d0eb1..b7593ee3c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). From f8d4d040155a58ecfdbc2ed58f0739b07e417516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 22:30:35 +0100 Subject: [PATCH 0076/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20and=20improve?= =?UTF-8?q?=20docs=20for=20Request=20Files=20(#4470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/request-files.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index ed2c8b6af..3ca471a91 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -41,9 +41,9 @@ Have in mind that this means that the whole contents will be stored in memory. T But there are several cases in which you might benefit from using `UploadFile`. -## `File` Parameters with `UploadFile` +## File Parameters with `UploadFile` -Define a `File` parameter with a type of `UploadFile`: +Define a file parameter with a type of `UploadFile`: ```Python hl_lines="12" {!../../../docs_src/request_files/tutorial001.py!} @@ -51,7 +51,7 @@ Define a `File` parameter with a type of `UploadFile`: Using `UploadFile` has several advantages over `bytes`: -* You don't have to use `File()` in the default value. +* You don't have to use `File()` in the default value of the parameter. * It uses a "spooled" file: * A file stored in memory up to a maximum size limit, and after passing this limit it will be stored in disk. * This means that it will work well for large files like images, videos, large binaries, etc. without consuming all the memory. @@ -116,7 +116,7 @@ The way HTML forms (`
`) sends the data to the server normally uses ## Optional File Upload -You can make a file optional by using standard type annotations: +You can make a file optional by using standard type annotations and setting a default value of `None`: === "Python 3.6 and above" @@ -132,7 +132,7 @@ You can make a file optional by using standard type annotations: ## `UploadFile` with Additional Metadata -You can also use `File()` with `UploadFile` to set additional parameters in `File()`, for example additional metadata: +You can also use `File()` with `UploadFile`, for example, to set additional metadata: ```Python hl_lines="13" {!../../../docs_src/request_files/tutorial001_03.py!} @@ -183,4 +183,4 @@ And the same way as before, you can use `File()` to set additional parameters, e ## Recap -Use `File` to declare files to be uploaded as input parameters (as form data). +Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data. From dba9ea81208078bdd91fbfff4fdbfe203dcc303f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 21:31:08 +0000 Subject: [PATCH 0077/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b7593ee3c..84908dda5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). From a698908ed65d887a17245a580ecbf3bf3c848406 Mon Sep 17 00:00:00 2001 From: Victor Benichoux Date: Sun, 23 Jan 2022 23:13:55 +0100 Subject: [PATCH 0078/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20bug=20preventing?= =?UTF-8?q?=20to=20use=20OpenAPI=20when=20using=20tuples=20(#3874)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/openapi/models.py | 2 +- tests/test_tuples.py | 267 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 tests/test_tuples.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 361c75005..9c6598d2d 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -123,7 +123,7 @@ class Schema(BaseModel): oneOf: Optional[List["Schema"]] = None anyOf: Optional[List["Schema"]] = None not_: Optional["Schema"] = Field(None, alias="not") - items: Optional["Schema"] = None + items: Optional[Union["Schema", List["Schema"]]] = None properties: Optional[Dict[str, "Schema"]] = None additionalProperties: Optional[Union["Schema", Reference, bool]] = None description: Optional[str] = None diff --git a/tests/test_tuples.py b/tests/test_tuples.py new file mode 100644 index 000000000..4cd5ee3af --- /dev/null +++ b/tests/test_tuples.py @@ -0,0 +1,267 @@ +from typing import List, Tuple + +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class ItemGroup(BaseModel): + items: List[Tuple[str, str]] + + +class Coordinate(BaseModel): + x: float + y: float + + +@app.post("/model-with-tuple/") +def post_model_with_tuple(item_group: ItemGroup): + return item_group + + +@app.post("/tuple-of-models/") +def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]): + return square + + +@app.post("/tuple-form/") +def hello(values: Tuple[int, int] = Form(...)): + return values + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model-with-tuple/": { + "post": { + "summary": "Post Model With Tuple", + "operationId": "post_model_with_tuple_model_with_tuple__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemGroup"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-of-models/": { + "post": { + "summary": "Post Tuple Of Models", + "operationId": "post_tuple_of_models_tuple_of_models__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-form/": { + "post": { + "summary": "Hello", + "operationId": "hello_tuple_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_hello_tuple_form__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_hello_tuple_form__post": { + "title": "Body_hello_tuple_form__post", + "required": ["values"], + "type": "object", + "properties": { + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + }, + }, + "Coordinate": { + "title": "Coordinate", + "required": ["x", "y"], + "type": "object", + "properties": { + "x": {"title": "X", "type": "number"}, + "y": {"title": "Y", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemGroup": { + "title": "ItemGroup", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"type": "string"}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_model_with_tuple_valid(): + data = {"items": [["foo", "bar"], ["baz", "whatelse"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_model_with_tuple_invalid(): + data = {"items": [["foo", "bar"], ["baz", "whatelse", "too", "much"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 422, response.text + + data = {"items": [["foo", "bar"], ["baz"]]} + response = client.post("/model-with-tuple/", json=data) + assert response.status_code == 422, response.text + + +def test_tuple_with_model_valid(): + data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 200, response.text + assert response.json() == data + + +def test_tuple_with_model_invalid(): + data = [{"x": 1, "y": 2}, {"x": 3, "y": 4}, {"x": 5, "y": 6}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 422, response.text + + data = [{"x": 1, "y": 2}] + response = client.post("/tuple-of-models/", json=data) + assert response.status_code == 422, response.text + + +def test_tuple_form_valid(): + response = client.post("/tuple-form/", data=[("values", "1"), ("values", "2")]) + assert response.status_code == 200, response.text + assert response.json() == [1, 2] + + +def test_tuple_form_invalid(): + response = client.post( + "/tuple-form/", data=[("values", "1"), ("values", "2"), ("values", "3")] + ) + assert response.status_code == 422, response.text + + response = client.post("/tuple-form/", data=[("values", "1")]) + assert response.status_code == 422, response.text From af18d5c49fde32e79e4dfd7a82819a2c642c6c17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 23 Jan 2022 22:14:28 +0000 Subject: [PATCH 0079/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 84908dda5..f9f3aabde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). * 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). From cbe8d552c1eee5b48f8ab0eab6b517e98ae8523b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 23:37:48 +0100 Subject: [PATCH 0080/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f9f3aabde..e75d46706 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,14 +2,25 @@ ## Latest Changes -* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). +### Features + +* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). New docs: [Request Files - File Parameters with UploadFile](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile). +* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). New docs: [Path Operation Configuration - Tags with Enums](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags-with-enums). +* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). New docs: [Query Parameters and String Validations - Exclude from OpenAPI](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + +### Docs + * 📝 Tweak and improve docs for Request Files. PR [#4470](https://github.com/tiangolo/fastapi/pull/4470) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for tags with Enums. PR [#4468](https://github.com/tiangolo/fastapi/pull/4468) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Fix bug preventing to use OpenAPI when using tuples. PR [#3874](https://github.com/tiangolo/fastapi/pull/3874) by [@victorbenichoux](https://github.com/victorbenichoux). * 🐛 Prefer custom encoder over defaults if specified in `jsonable_encoder`. PR [#2061](https://github.com/tiangolo/fastapi/pull/2061) by [@viveksunder](https://github.com/viveksunder). * 💚 Duplicate PR to trigger CI. PR [#4467](https://github.com/tiangolo/fastapi/pull/4467) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 🐛 Fix docs dependencies cache, to get the latest Material for MkDocs. PR [#4466](https://github.com/tiangolo/fastapi/pull/4466) by [@tiangolo](https://github.com/tiangolo). -* ✨ Allow hiding from OpenAPI (and Swagger UI) `Query`, `Cookie`, `Header`, and `Path` parameters. PR [#3144](https://github.com/tiangolo/fastapi/pull/3144) by [@astraldawn](https://github.com/astraldawn). * 🔧 Add sponsor Dropbase. PR [#4465](https://github.com/tiangolo/fastapi/pull/4465) by [@tiangolo](https://github.com/tiangolo). ## 0.72.0 From 291180bf2d8c39e84860c2426b1d58b6c80f6fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 23 Jan 2022 23:38:51 +0100 Subject: [PATCH 0081/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?73.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e75d46706..68b75e702 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.73.0 + ### Features * ✨ Add support for declaring `UploadFile` parameters without explicit `File()`. PR [#4469](https://github.com/tiangolo/fastapi/pull/4469) by [@tiangolo](https://github.com/tiangolo). New docs: [Request Files - File Parameters with UploadFile](https://fastapi.tiangolo.com/tutorial/request-files/#file-parameters-with-uploadfile). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index d83fe6fbd..8718788fa 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.72.0" +__version__ = "0.73.0" from starlette import status as status From 618c99d77444e383e7b95ebe32ededbd95155c43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 15:27:34 +0100 Subject: [PATCH 0082/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#4502)?= 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 | 192 ++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 86 deletions(-) diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index df088f39f..ebbe446ee 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1230 - prs: 269 + answers: 1237 + prs: 280 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 316 + count: 319 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: dmontagu @@ -14,7 +14,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: ycd - count: 219 + count: 221 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: Mause @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik - login: raphaelauv - count: 67 + count: 68 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: falkben @@ -57,18 +57,22 @@ experts: count: 38 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: STeveShary + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff -- login: STeveShary - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 - url: https://github.com/STeveShary - login: krishnardt count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: adriangb + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -77,10 +81,10 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 -- login: adriangb - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb +- login: chbndrhnns + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: ghandic count: 25 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -89,18 +93,14 @@ experts: count: 25 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: panla + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: SirTelemak count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: panla - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -129,22 +129,34 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 url: https://github.com/nkhitrov +- login: acidjunk + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: waynerv count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv -- login: acidjunk - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: dstlny - count: 14 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jgould22 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: harunyasar + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 + url: https://github.com/harunyasar - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar +- login: hellocoldworld + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 + url: https://github.com/hellocoldworld - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 @@ -173,39 +185,47 @@ experts: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/20441825?u=ee1e59446b98f8ec2363caeda4c17164d0d9cc7d&v=4 url: https://github.com/stefanondisponibile -- login: hellocoldworld - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 - url: https://github.com/hellocoldworld - login: oligond count: 10 avatarUrl: https://avatars.githubusercontent.com/u/2858306?u=1bb1182a5944e93624b7fb26585f22c8f7a9d76e&v=4 url: https://github.com/oligond last_month_active: -- login: insomnes - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes -- login: raphaelauv - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv -- login: jgould22 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: harunyasar - count: 4 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar +- login: jgould22 + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: rafsaf + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=be9f06b8ced2d2b677297decc781fa8ce4f7ddbd&v=4 + url: https://github.com/rafsaf +- login: STeveShary + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 + url: https://github.com/STeveShary +- login: ahnaf-zamil + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/57180217?u=849128b146771ace47beca5b5ff68eb82905dd6d&v=4 + url: https://github.com/ahnaf-zamil +- login: lucastosetto + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/89307132?u=56326696423df7126c9e7c702ee58f294db69a2a&v=4 + url: https://github.com/lucastosetto +- login: blokje + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/851418?v=4 + url: https://github.com/blokje +- login: MatthijsKok + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/7658129?u=1243e32d57e13abc45e3f5235ed5b9197e0d2b41&v=4 + url: https://github.com/MatthijsKok - login: Kludex count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex -- login: panla - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla top_contributors: - login: waynerv count: 25 @@ -219,14 +239,14 @@ top_contributors: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu +- login: jaystone776 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: jaystone776 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -285,7 +305,7 @@ top_contributors: url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 91 + count: 93 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=3682d9b9b93bef272f379ab623dc031c8d71432e&v=4 url: https://github.com/Kludex - login: waynerv @@ -301,7 +321,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: ycd - count: 44 + count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 url: https://github.com/ycd - login: AdrianDeAnda @@ -312,12 +332,16 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=81a84af39c89b898b0fbc5a04e8834f60f23e55a&v=4 url: https://github.com/ArcLightSlavik +- login: cikay + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 + url: https://github.com/cikay - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: cassiobotaro - count: 22 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro - login: komtaki @@ -336,6 +360,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 url: https://github.com/yanever +- login: lsglucas + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas - login: SwftAlpc count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 @@ -356,26 +384,22 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: lsglucas - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas - login: rjNemo - count: 13 + count: 14 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 url: https://github.com/RunningIkkyu +- login: yezz123 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: sh0nk count: 12 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk -- login: yezz123 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -400,6 +424,10 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 url: https://github.com/kty4119 +- login: zy7y + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 + url: https://github.com/zy7y - login: bezaca count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 @@ -444,39 +472,31 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 url: https://github.com/krocdort +- login: dimaqq + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 + url: https://github.com/dimaqq - login: jovicon count: 6 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 url: https://github.com/jovicon +- login: NinaHwang + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 + url: https://github.com/NinaHwang - login: diogoduartec count: 5 avatarUrl: https://avatars.githubusercontent.com/u/31852339?u=b50fc11c531e9b77922e19edfc9e7233d4d7b92e&v=4 url: https://github.com/diogoduartec -- login: nimctl +- login: n25a count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=e39b11d47188744ee07b2a1c7ce1a1bdf3c80760&v=4 - url: https://github.com/nimctl + avatarUrl: https://avatars.githubusercontent.com/u/49960770?u=eb3c95338741c78fff7d9d5d7ace9617e53eee4a&v=4 + url: https://github.com/n25a +- login: izaguerreiro + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 + url: https://github.com/izaguerreiro - login: israteneda count: 5 avatarUrl: https://avatars.githubusercontent.com/u/20668624?u=d7b2961d330aca65fbce5bdb26a0800a3d23ed2d&v=4 url: https://github.com/israteneda -- login: juntatalor - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 - url: https://github.com/juntatalor -- login: SnkSynthesis - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/63564282?u=0078826509dbecb2fdb543f4e881c9cd06157893&v=4 - url: https://github.com/SnkSynthesis -- login: anthonycepeda - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4 - url: https://github.com/anthonycepeda -- login: oandersonmagalhaes - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 - url: https://github.com/oandersonmagalhaes -- login: qysfblog - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/52229895?v=4 - url: https://github.com/qysfblog From b93f8a709ab3923d1268dbc845f41985c0302b33 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 1 Feb 2022 14:28:16 +0000 Subject: [PATCH 0083/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 68b75e702..6d5ee8ea1 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 [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.73.0 ### Features From 6034f80687302fa4f7da73cc3142750c3e239401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Feb 2022 18:18:38 +0100 Subject: [PATCH 0084/1881] =?UTF-8?q?=F0=9F=92=9A=20Only=20build=20docs=20?= =?UTF-8?q?on=20push=20when=20on=20master=20to=20avoid=20duplicate=20runs?= =?UTF-8?q?=20from=20PRs=20(#4564)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index ccf964486..2482660f3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -1,6 +1,8 @@ name: Build Docs on: push: + branches: + - master pull_request: types: [opened, synchronize] jobs: From 78b07cb809e97f400e196ff3d89862b9d5bd5dc2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Feb 2022 17:19:09 +0000 Subject: [PATCH 0085/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6d5ee8ea1..8c368e676 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Only build docs on push when on master to avoid duplicate runs from PRs. PR [#4564](https://github.com/tiangolo/fastapi/pull/4564) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.73.0 From 9d56a3cb59d59896bc38293b9fa54ae69b7cd36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 17 Feb 2022 13:40:12 +0100 Subject: [PATCH 0086/1881] =?UTF-8?q?=E2=9C=A8=20Update=20internal=20`Asyn?= =?UTF-8?q?cExitStack`=20to=20fix=20context=20for=20dependencies=20with=20?= =?UTF-8?q?`yield`=20(#4575)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-with-yield.md | 10 +-- fastapi/applications.py | 61 +++++++++++++--- fastapi/middleware/asyncexitstack.py | 28 ++++++++ tests/test_dependency_contextmanager.py | 44 ++++++++++-- tests/test_dependency_contextvars.py | 51 +++++++++++++ tests/test_dependency_normal_exceptions.py | 71 +++++++++++++++++++ tests/test_exception_handlers.py | 23 ++++++ 7 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 fastapi/middleware/asyncexitstack.py create mode 100644 tests/test_dependency_contextvars.py create mode 100644 tests/test_dependency_normal_exceptions.py diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 82553afae..ac2e9cb8c 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -99,7 +99,7 @@ You saw that you can use dependencies with `yield` and have `try` blocks that ca It might be tempting to raise an `HTTPException` or similar in the exit code, after the `yield`. But **it won't work**. -The exit code in dependencies with `yield` is executed *after* [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`). +The exit code in dependencies with `yield` is executed *after* the response is sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} will have already run. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`). So, if you raise an `HTTPException` after the `yield`, the default (or any custom) exception handler that catches `HTTPException`s and returns an HTTP 400 response won't be there to catch that exception anymore. @@ -138,9 +138,11 @@ participant tasks as Background tasks end dep ->> operation: Run dependency, e.g. DB session opt raise - operation -->> handler: Raise HTTPException + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception handler -->> client: HTTP error response operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception end operation ->> client: Return response to client Note over client,operation: Response is already sent, can't change it anymore @@ -162,9 +164,9 @@ participant tasks as Background tasks After one of those responses is sent, no other response can be sent. !!! tip - This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. And that exception would be handled by that custom exception handler instead of the dependency exit code. + This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. - But if you raise an exception that is not handled by the exception handlers, it will be handled by the exit code of the dependency. + If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`, and then **again** to the exception handlers. If there's no exception handler for that exception, it will then be handled by the default internal `ServerErrorMiddleware`, returning a 500 HTTP status code, to let the client know that there was an error in the server. ## Context Managers diff --git a/fastapi/applications.py b/fastapi/applications.py index dbfd76fb9..9fb78719c 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -2,7 +2,6 @@ from enum import Enum from typing import Any, Callable, Coroutine, Dict, List, Optional, Sequence, Type, Union from fastapi import routing -from fastapi.concurrency import AsyncExitStack from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.encoders import DictIntStrAny, SetIntStr from fastapi.exception_handlers import ( @@ -11,6 +10,7 @@ from fastapi.exception_handlers import ( ) from fastapi.exceptions import RequestValidationError from fastapi.logger import logger +from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( get_redoc_html, get_swagger_ui_html, @@ -21,8 +21,9 @@ from fastapi.params import Depends from fastapi.types import DecoratedCallable from starlette.applications import Starlette from starlette.datastructures import State -from starlette.exceptions import HTTPException +from starlette.exceptions import ExceptionMiddleware, HTTPException from starlette.middleware import Middleware +from starlette.middleware.errors import ServerErrorMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute @@ -134,6 +135,55 @@ class FastAPI(Starlette): self.openapi_schema: Optional[Dict[str, Any]] = None self.setup() + def build_middleware_stack(self) -> ASGIApp: + # Duplicate/override from Starlette to add AsyncExitStackMiddleware + # inside of ExceptionMiddleware, inside of custom user middlewares + debug = self.debug + error_handler = None + exception_handlers = {} + + for key, value in self.exception_handlers.items(): + if key in (500, Exception): + error_handler = value + else: + exception_handlers[key] = value + + middleware = ( + [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] + + self.user_middleware + + [ + Middleware( + ExceptionMiddleware, handlers=exception_handlers, debug=debug + ), + # Add FastAPI-specific AsyncExitStackMiddleware for dependencies with + # contextvars. + # This needs to happen after user middlewares because those create a + # new contextvars context copy by using a new AnyIO task group. + # The initial part of dependencies with yield is executed in the + # FastAPI code, inside all the middlewares, but the teardown part + # (after yield) is executed in the AsyncExitStack in this middleware, + # if the AsyncExitStack lived outside of the custom middlewares and + # contextvars were set in a dependency with yield in that internal + # contextvars context, the values would not be available in the + # outside context of the AsyncExitStack. + # By putting the middleware and the AsyncExitStack here, inside all + # user middlewares, the code before and after yield in dependencies + # with yield is executed in the same contextvars context, so all values + # set in contextvars before yield is still available after yield as + # would be expected. + # Additionally, by having this AsyncExitStack here, after the + # ExceptionMiddleware, now dependencies can catch handled exceptions, + # e.g. HTTPException, to customize the teardown code (e.g. DB session + # rollback). + Middleware(AsyncExitStackMiddleware), + ] + ) + + app = self.router + for cls, options in reversed(middleware): + app = cls(app=app, **options) + return app + def openapi(self) -> Dict[str, Any]: if not self.openapi_schema: self.openapi_schema = get_openapi( @@ -206,12 +256,7 @@ class FastAPI(Starlette): async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if self.root_path: scope["root_path"] = self.root_path - if AsyncExitStack: - async with AsyncExitStack() as stack: - scope["fastapi_astack"] = stack - await super().__call__(scope, receive, send) - else: - await super().__call__(scope, receive, send) # pragma: no cover + await super().__call__(scope, receive, send) def add_api_route( self, diff --git a/fastapi/middleware/asyncexitstack.py b/fastapi/middleware/asyncexitstack.py new file mode 100644 index 000000000..503a68ac7 --- /dev/null +++ b/fastapi/middleware/asyncexitstack.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi.concurrency import AsyncExitStack +from starlette.types import ASGIApp, Receive, Scope, Send + + +class AsyncExitStackMiddleware: + def __init__(self, app: ASGIApp, context_name: str = "fastapi_astack") -> None: + self.app = app + self.context_name = context_name + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if AsyncExitStack: + dependency_exception: Optional[Exception] = None + async with AsyncExitStack() as stack: + scope[self.context_name] = stack + try: + await self.app(scope, receive, send) + except Exception as e: + dependency_exception = e + raise e + if dependency_exception: + # This exception was possibly handled by the dependency but it should + # still bubble up so that the ServerErrorMiddleware can return a 500 + # or the ExceptionMiddleware can catch and handle any other exceptions + raise dependency_exception + else: + await self.app(scope, receive, send) # pragma: no cover diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 3e42b47f7..03ef56c4d 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -235,7 +235,16 @@ def test_sync_raise_other(): assert "/sync_raise" not in errors -def test_async_raise(): +def test_async_raise_raises(): + with pytest.raises(AsyncDependencyError): + client.get("/async_raise") + assert state["/async_raise"] == "asyncgen raise finalized" + assert "/async_raise" in errors + errors.clear() + + +def test_async_raise_server_error(): + client = TestClient(app, raise_server_exceptions=False) response = client.get("/async_raise") assert response.status_code == 500, response.text assert state["/async_raise"] == "asyncgen raise finalized" @@ -270,7 +279,16 @@ def test_background_tasks(): assert state["bg"] == "bg set - b: started b - a: started a" -def test_sync_raise(): +def test_sync_raise_raises(): + with pytest.raises(SyncDependencyError): + client.get("/sync_raise") + assert state["/sync_raise"] == "generator raise finalized" + assert "/sync_raise" in errors + errors.clear() + + +def test_sync_raise_server_error(): + client = TestClient(app, raise_server_exceptions=False) response = client.get("/sync_raise") assert response.status_code == 500, response.text assert state["/sync_raise"] == "generator raise finalized" @@ -306,7 +324,16 @@ def test_sync_sync_raise_other(): assert "/sync_raise" not in errors -def test_sync_async_raise(): +def test_sync_async_raise_raises(): + with pytest.raises(AsyncDependencyError): + client.get("/sync_async_raise") + assert state["/async_raise"] == "asyncgen raise finalized" + assert "/async_raise" in errors + errors.clear() + + +def test_sync_async_raise_server_error(): + client = TestClient(app, raise_server_exceptions=False) response = client.get("/sync_async_raise") assert response.status_code == 500, response.text assert state["/async_raise"] == "asyncgen raise finalized" @@ -314,7 +341,16 @@ def test_sync_async_raise(): errors.clear() -def test_sync_sync_raise(): +def test_sync_sync_raise_raises(): + with pytest.raises(SyncDependencyError): + client.get("/sync_sync_raise") + assert state["/sync_raise"] == "generator raise finalized" + assert "/sync_raise" in errors + errors.clear() + + +def test_sync_sync_raise_server_error(): + client = TestClient(app, raise_server_exceptions=False) response = client.get("/sync_sync_raise") assert response.status_code == 500, response.text assert state["/sync_raise"] == "generator raise finalized" diff --git a/tests/test_dependency_contextvars.py b/tests/test_dependency_contextvars.py new file mode 100644 index 000000000..076802df8 --- /dev/null +++ b/tests/test_dependency_contextvars.py @@ -0,0 +1,51 @@ +from contextvars import ContextVar +from typing import Any, Awaitable, Callable, Dict, Optional + +from fastapi import Depends, FastAPI, Request, Response +from fastapi.testclient import TestClient + +legacy_request_state_context_var: ContextVar[Optional[Dict[str, Any]]] = ContextVar( + "legacy_request_state_context_var", default=None +) + +app = FastAPI() + + +async def set_up_request_state_dependency(): + request_state = {"user": "deadpond"} + contextvar_token = legacy_request_state_context_var.set(request_state) + yield request_state + legacy_request_state_context_var.reset(contextvar_token) + + +@app.middleware("http") +async def custom_middleware( + request: Request, call_next: Callable[[Request], Awaitable[Response]] +): + response = await call_next(request) + response.headers["custom"] = "foo" + return response + + +@app.get("/user", dependencies=[Depends(set_up_request_state_dependency)]) +def get_user(): + request_state = legacy_request_state_context_var.get() + assert request_state + return request_state["user"] + + +client = TestClient(app) + + +def test_dependency_contextvars(): + """ + Check that custom middlewares don't affect the contextvar context for dependencies. + + The code before yield and the code after yield should be run in the same contextvar + context, so that request_state_context_var.reset(contextvar_token). + + If they are run in a different context, that raises an error. + """ + response = client.get("/user") + assert response.json() == "deadpond" + assert response.headers["custom"] == "foo" diff --git a/tests/test_dependency_normal_exceptions.py b/tests/test_dependency_normal_exceptions.py new file mode 100644 index 000000000..49a19f460 --- /dev/null +++ b/tests/test_dependency_normal_exceptions.py @@ -0,0 +1,71 @@ +import pytest +from fastapi import Body, Depends, FastAPI, HTTPException +from fastapi.testclient import TestClient + +initial_fake_database = {"rick": "Rick Sanchez"} + +fake_database = initial_fake_database.copy() + +initial_state = {"except": False, "finally": False} + +state = initial_state.copy() + +app = FastAPI() + + +async def get_database(): + temp_database = fake_database.copy() + try: + yield temp_database + fake_database.update(temp_database) + except HTTPException: + state["except"] = True + finally: + state["finally"] = True + + +@app.put("/invalid-user/{user_id}") +def put_invalid_user( + user_id: str, name: str = Body(...), db: dict = Depends(get_database) +): + db[user_id] = name + raise HTTPException(status_code=400, detail="Invalid user") + + +@app.put("/user/{user_id}") +def put_user(user_id: str, name: str = Body(...), db: dict = Depends(get_database)): + db[user_id] = name + return {"message": "OK"} + + +@pytest.fixture(autouse=True) +def reset_state_and_db(): + global fake_database + global state + fake_database = initial_fake_database.copy() + state = initial_state.copy() + + +client = TestClient(app) + + +def test_dependency_gets_exception(): + assert state["except"] is False + assert state["finally"] is False + response = client.put("/invalid-user/rick", json="Morty") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Invalid user"} + assert state["except"] is True + assert state["finally"] is True + assert fake_database["rick"] == "Rick Sanchez" + + +def test_dependency_no_exception(): + assert state["except"] is False + assert state["finally"] is False + response = client.put("/user/rick", json="Morty") + assert response.status_code == 200, response.text + assert response.json() == {"message": "OK"} + assert state["except"] is False + assert state["finally"] is True + assert fake_database["rick"] == "Morty" diff --git a/tests/test_exception_handlers.py b/tests/test_exception_handlers.py index 6153f7ab9..67a4becec 100644 --- a/tests/test_exception_handlers.py +++ b/tests/test_exception_handlers.py @@ -1,3 +1,4 @@ +import pytest from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.testclient import TestClient @@ -12,10 +13,15 @@ def request_validation_exception_handler(request, exception): return JSONResponse({"exception": "request-validation"}) +def server_error_exception_handler(request, exception): + return JSONResponse(status_code=500, content={"exception": "server-error"}) + + app = FastAPI( exception_handlers={ HTTPException: http_exception_handler, RequestValidationError: request_validation_exception_handler, + Exception: server_error_exception_handler, } ) @@ -32,6 +38,11 @@ def route_with_request_validation_exception(param: int): pass # pragma: no cover +@app.get("/server-error") +def route_with_server_error(): + raise RuntimeError("Oops!") + + def test_override_http_exception(): response = client.get("/http-exception") assert response.status_code == 200 @@ -42,3 +53,15 @@ def test_override_request_validation_exception(): response = client.get("/request-validation/invalid") assert response.status_code == 200 assert response.json() == {"exception": "request-validation"} + + +def test_override_server_error_exception_raises(): + with pytest.raises(RuntimeError): + client.get("/server-error") + + +def test_override_server_error_exception_response(): + client = TestClient(app, raise_server_exceptions=False) + response = client.get("/server-error") + assert response.status_code == 500 + assert response.json() == {"exception": "server-error"} From 4fcb00328c2b6c2ab5167359b7f62b1c481e1faf Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 17 Feb 2022 12:40:46 +0000 Subject: [PATCH 0087/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8c368e676..b23dc1b98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Update internal `AsyncExitStack` to fix context for dependencies with `yield`. PR [#4575](https://github.com/tiangolo/fastapi/pull/4575) by [@tiangolo](https://github.com/tiangolo). * 💚 Only build docs on push when on master to avoid duplicate runs from PRs. PR [#4564](https://github.com/tiangolo/fastapi/pull/4564) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#4502](https://github.com/tiangolo/fastapi/pull/4502) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.73.0 From 59e36481dc86673968c21cf9ea636eb3f379dc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 17 Feb 2022 15:18:01 +0100 Subject: [PATCH 0088/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20Striveworks=20sp?= =?UTF-8?q?onsor=20(#4596)?= 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/striveworks-banner.png | Bin 0 -> 9681 bytes docs/en/docs/img/sponsors/striveworks.png | Bin 0 -> 22639 bytes docs/en/overrides/main.html | 6 ++++++ 6 files changed, 11 insertions(+) create mode 100644 docs/en/docs/img/sponsors/striveworks-banner.png create mode 100644 docs/en/docs/img/sponsors/striveworks.png diff --git a/README.md b/README.md index c9c69d3e8..bec58aad1 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 b98e68b65..4d63a7288 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -8,6 +8,9 @@ gold: - url: https://www.dropbase.io/careers title: Dropbase - seamlessly collect, clean, and centralize data. img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg + - url: https://striveworks.us/careers?utm_source=fastapi&utm_medium=sponsor_banner&utm_campaign=feb_march#openings + title: https://striveworks.us/careers + img: https://fastapi.tiangolo.com/img/sponsors/striveworks.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 0c4e716d7..dbf69c1b3 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -8,3 +8,4 @@ logins: - deepset-ai - cryptapi - DropbaseHQ + - Striveworks diff --git a/docs/en/docs/img/sponsors/striveworks-banner.png b/docs/en/docs/img/sponsors/striveworks-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..5206744b7a6e45b8c541ea55941f47f84a63eaec GIT binary patch literal 9681 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/striveworks.png b/docs/en/docs/img/sponsors/striveworks.png new file mode 100644 index 0000000000000000000000000000000000000000..435ac536c14eacdd5c4f578d56495281c53261d6 GIT binary patch literal 22639 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
Viewer does not support full SVG 1.1 \ No newline at end of file +
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 diff --git a/docs/en/docs/img/deployment/https/https01.drawio b/docs/en/docs/img/deployment/https/https01.drawio index 9bc5340ce..181582f9b 100644 --- a/docs/en/docs/img/deployment/https/https01.drawio +++ b/docs/en/docs/img/deployment/https/https01.drawio @@ -75,4 +75,4 @@ - \ 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 index 4fee0adfc..2edbd0623 100644 --- a/docs/en/docs/img/deployment/https/https01.svg +++ b/docs/en/docs/img/deployment/https/https01.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
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 +
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 diff --git a/docs/en/docs/img/deployment/https/https02.drawio b/docs/en/docs/img/deployment/https/https02.drawio index 0f7578d3e..650c06d1e 100644 --- a/docs/en/docs/img/deployment/https/https02.drawio +++ b/docs/en/docs/img/deployment/https/https02.drawio @@ -107,4 +107,4 @@ - \ 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 index 1f37a7098..e16b7e94a 100644 --- a/docs/en/docs/img/deployment/https/https02.svg +++ b/docs/en/docs/img/deployment/https/https02.svg @@ -54,4 +54,4 @@ src: url("https://fonts.gstatic.com/s/roboto/v29/KFOmCnqEu92Fr1Mu4mxK.woff2") format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } -
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 +
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 diff --git a/docs/en/docs/img/deployment/https/https03.drawio b/docs/en/docs/img/deployment/https/https03.drawio index c5766086c..c178fd363 100644 --- a/docs/en/docs/img/deployment/https/https03.drawio +++ b/docs/en/docs/img/deployment/https/https03.drawio @@ -128,4 +128,4 @@ - \ 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 index e68e1c459..2badd1c7d 100644 --- a/docs/en/docs/img/deployment/https/https03.svg +++ b/docs/en/docs/img/deployment/https/https03.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/deployment/https/https04.drawio b/docs/en/docs/img/deployment/https/https04.drawio index ea357a6c1..78a6e919a 100644 --- a/docs/en/docs/img/deployment/https/https04.drawio +++ b/docs/en/docs/img/deployment/https/https04.drawio @@ -149,4 +149,4 @@ - \ 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 index 4c9b7999b..4513ac76b 100644 --- a/docs/en/docs/img/deployment/https/https04.svg +++ b/docs/en/docs/img/deployment/https/https04.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/deployment/https/https05.drawio b/docs/en/docs/img/deployment/https/https05.drawio index 9b8b7c6f7..236ecd841 100644 --- a/docs/en/docs/img/deployment/https/https05.drawio +++ b/docs/en/docs/img/deployment/https/https05.drawio @@ -163,4 +163,4 @@ - \ 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 index d11647b9b..ddcd2760a 100644 --- a/docs/en/docs/img/deployment/https/https05.svg +++ b/docs/en/docs/img/deployment/https/https05.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/deployment/https/https06.drawio b/docs/en/docs/img/deployment/https/https06.drawio index 5bb85813f..9dec13184 100644 --- a/docs/en/docs/img/deployment/https/https06.drawio +++ b/docs/en/docs/img/deployment/https/https06.drawio @@ -180,4 +180,4 @@ - \ 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 index 10e03b7c5..3695de40c 100644 --- a/docs/en/docs/img/deployment/https/https06.svg +++ b/docs/en/docs/img/deployment/https/https06.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/deployment/https/https07.drawio b/docs/en/docs/img/deployment/https/https07.drawio index 1ca994b22..aa8f4d6be 100644 --- a/docs/en/docs/img/deployment/https/https07.drawio +++ b/docs/en/docs/img/deployment/https/https07.drawio @@ -200,4 +200,4 @@ - \ 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 index e409d8871..551354cef 100644 --- a/docs/en/docs/img/deployment/https/https07.svg +++ b/docs/en/docs/img/deployment/https/https07.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/deployment/https/https08.drawio b/docs/en/docs/img/deployment/https/https08.drawio index 8a4f41056..794b192df 100644 --- a/docs/en/docs/img/deployment/https/https08.drawio +++ b/docs/en/docs/img/deployment/https/https08.drawio @@ -214,4 +214,4 @@ - \ 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 index 3047dd821..2d4680dcc 100644 --- a/docs/en/docs/img/deployment/https/https08.svg +++ b/docs/en/docs/img/deployment/https/https08.svg @@ -59,4 +59,4 @@
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 +
IP:
123.124.125.126
IP:...
Viewer does not support full SVG 1.1 diff --git a/docs/en/docs/img/tutorial/bigger-applications/package.drawio b/docs/en/docs/img/tutorial/bigger-applications/package.drawio index 48f6e76fe..cab3de2ca 100644 --- a/docs/en/docs/img/tutorial/bigger-applications/package.drawio +++ b/docs/en/docs/img/tutorial/bigger-applications/package.drawio @@ -40,4 +40,4 @@ - \ No newline at end of file + diff --git a/docs/en/docs/img/tutorial/bigger-applications/package.svg b/docs/en/docs/img/tutorial/bigger-applications/package.svg index a9cec926a..44da1dc30 100644 --- a/docs/en/docs/img/tutorial/bigger-applications/package.svg +++ b/docs/en/docs/img/tutorial/bigger-applications/package.svg @@ -1 +1 @@ -
Package app
app/__init__.py
Package app...
Module app.main
app/main.py
Module app.main...
Module app.dependencies
app/dependencies.py
Module app.dependencies...
Subpackage app.internal
app/internal/__init__.py
Subpackage app.internal...
Module app.internal.admin
app/internal/admin.py
Module app.internal.admin...
Subpackage app.routers
app/routers/__init__.py
Subpackage app.routers...
Module app.routers.items
app/routers/items.py
Module app.routers.items...
Module app.routers.users
app/routers/users.py
Module app.routers.users...
Viewer does not support full SVG 1.1
\ No newline at end of file +
Package app
app/__init__.py
Package app...
Module app.main
app/main.py
Module app.main...
Module app.dependencies
app/dependencies.py
Module app.dependencies...
Subpackage app.internal
app/internal/__init__.py
Subpackage app.internal...
Module app.internal.admin
app/internal/admin.py
Module app.internal.admin...
Subpackage app.routers
app/routers/__init__.py
Subpackage app.routers...
Module app.routers.items
app/routers/items.py
Module app.routers.items...
Module app.routers.users
app/routers/users.py
Module app.routers.users...
Viewer does not support full SVG 1.1
diff --git a/docs/en/docs/js/termynal.js b/docs/en/docs/js/termynal.js index 8b0e9339e..4ac32708a 100644 --- a/docs/en/docs/js/termynal.js +++ b/docs/en/docs/js/termynal.js @@ -72,14 +72,14 @@ class Termynal { * Initialise the widget, get lines, clear container and start animation. */ init() { - /** + /** * Calculates width and height of Termynal container. * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. - */ + */ const containerStyle = getComputedStyle(this.container); - this.container.style.width = containerStyle.width !== '0px' ? + this.container.style.width = containerStyle.width !== '0px' ? containerStyle.width : undefined; - this.container.style.minHeight = containerStyle.height !== '0px' ? + this.container.style.minHeight = containerStyle.height !== '0px' ? containerStyle.height : undefined; this.container.setAttribute('data-termynal', ''); @@ -138,7 +138,7 @@ class Termynal { restart.innerHTML = "restart ↻" return restart } - + generateFinish() { const finish = document.createElement('a') finish.onclick = (e) => { @@ -215,7 +215,7 @@ class Termynal { /** * Converts line data objects into line elements. - * + * * @param {Object[]} lineData - Dynamically loaded lines. * @param {Object} line - Line data object. * @returns {Element[]} - Array of line elements. @@ -231,7 +231,7 @@ class Termynal { /** * Helper function for generating attributes string. - * + * * @param {Object} line - Line data object. * @returns {string} - String of attributes. */ diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index fe56dadec..8486ed849 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -29,7 +29,7 @@ Calling this program outputs: John Doe ``` -The function does the following: +The function does the following: * Takes a `first_name` and `last_name`. * Converts the first letter of each one to upper case with `title()`. @@ -334,14 +334,14 @@ These types that take type parameters in square brackets are called **Generic ty === "Python 3.9 and above" You can use the same builtin types as generics (with square brakets and types inside): - + * `list` * `tuple` * `set` * `dict` And the same as with Python 3.6, from the `typing` module: - + * `Union` * `Optional` * ...and others. @@ -354,7 +354,7 @@ These types that take type parameters in square brackets are called **Generic ty * `tuple` * `set` * `dict` - + And the same as with Python 3.6, from the `typing` module: * `Union` diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 2a2e764b5..d201953df 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -334,7 +334,7 @@ from app.routers import items, users ```Python from .routers import items, users ``` - + The second version is an "absolute import": ```Python diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index fa38cfc48..bfc948f4f 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -366,7 +366,7 @@ In this case, you would accept any `dict` as long as it has `int` keys with `flo But Pydantic has automatic data conversion. This means that, even though your API clients can only send strings as keys, as long as those strings contain pure integers, Pydantic will convert them and validate them. - + And the `dict` you receive as `weights` will actually have `int` keys and `float` values. ## Recap diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 81441b41e..eb21f29a8 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -138,7 +138,7 @@ But you would get the same editor support with PyCharm as your editor, you can use the Pydantic PyCharm Plugin. It improves editor support for Pydantic models, with: - + * auto-completion * type checks * refactoring diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index ac2e9cb8c..a7300f0b7 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -10,7 +10,7 @@ To do this, use `yield` instead of `return`, and write the extra steps after. !!! note "Technical Details" Any function that is valid to use with: - * `@contextlib.contextmanager` or + * `@contextlib.contextmanager` or * `@contextlib.asynccontextmanager` would be valid to use as a **FastAPI** dependency. @@ -207,7 +207,7 @@ You can also use them inside of **FastAPI** dependencies with `yield` by using !!! tip Another way to create a context manager is with: - * `@contextlib.contextmanager` or + * `@contextlib.contextmanager` or * `@contextlib.asynccontextmanager` using them to decorate a function with a single `yield`. diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 3ca471a91..664a1102f 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -106,7 +106,7 @@ The way HTML forms (`
`) sends the data to the server normally uses Data from forms is normally encoded using the "media type" `application/x-www-form-urlencoded` when it doesn't include files. But when the form includes files, it is encoded as `multipart/form-data`. If you use `File`, **FastAPI** will know it has to get the files from the correct part of the body. - + If you want to read more about these encodings and form fields, head to the MDN web docs for POST. !!! warning diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index b5495a400..2021a098f 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -45,7 +45,7 @@ The way HTML forms (`
`) sends the data to the server normally uses Data from forms is normally encoded using the "media type" `application/x-www-form-urlencoded`. But when the form includes files, it is encoded as `multipart/form-data`. You'll read about handling files in the next chapter. - + If you want to read more about these encodings and form fields, head to the MDN web docs for POST. !!! warning diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index cac02ca7c..eb1cb5c82 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -69,9 +69,9 @@ }); }); -Marshmallow -L'une des principales fonctionnalités nécessaires aux systèmes API est la "sérialisation" des données, qui consiste à prendre les données du code (Python) et à les convertir en quelque chose qui peut être envoyé sur le réseau. Par exemple, convertir un objet contenant des données provenant d'une base de données en un objet JSON. Convertir des objets `datetime` en strings, etc. @@ -147,7 +147,7 @@ Sans un système de validation des données, vous devriez effectuer toutes les v Ces fonctionnalités sont ce pourquoi Marshmallow a été construit. C'est une excellente bibliothèque, et je l'ai déjà beaucoup utilisée. -Mais elle a été créée avant que les type hints n'existent en Python. Ainsi, pour définir chaque schéma, vous devez utiliser des utilitaires et des classes spécifiques fournies par Marshmallow. !!! check "A inspiré **FastAPI** à" @@ -155,7 +155,7 @@ Utilisez du code pour définir des "schémas" qui fournissent automatiquement le ### Webargs -Une autre grande fonctionnalité requise par les API est le parsing des données provenant des requêtes entrantes. Webargs est un outil qui a été créé pour fournir cela par-dessus plusieurs frameworks, dont Flask. diff --git a/docs/fr/docs/async.md b/docs/fr/docs/async.md index 20f4ee101..71c28b703 100644 --- a/docs/fr/docs/async.md +++ b/docs/fr/docs/async.md @@ -220,7 +220,7 @@ Et comme on peut avoir du parallélisme et de l'asynchronicité en même temps, Nope ! C'est ça la morale de l'histoire. La concurrence est différente du parallélisme. C'est mieux sur des scénarios **spécifiques** qui impliquent beaucoup d'attente. À cause de ça, c'est généralement bien meilleur que le parallélisme pour le développement d'applications web. Mais pas pour tout. - + Donc pour équilibrer tout ça, imaginez l'histoire suivante : > Vous devez nettoyer une grande et sale maison. @@ -293,7 +293,7 @@ def get_sequential_burgers(number: int): Avec `async def`, Python sait que dans cette fonction il doit prendre en compte les expressions `await`, et qu'il peut mettre en pause ⏸ l'exécution de la fonction pour aller faire autre chose 🔀 avant de revenir. -Pour appeler une fonction définie avec `async def`, vous devez utiliser `await`. Donc ceci ne marche pas : +Pour appeler une fonction définie avec `async def`, vous devez utiliser `await`. Donc ceci ne marche pas : ```Python # Ceci ne fonctionne pas, car get_burgers a été défini avec async def @@ -375,7 +375,7 @@ Au final, dans les deux situations, il est fort probable que **FastAPI** soit to La même chose s'applique aux dépendances. Si une dépendance est définie avec `def` plutôt que `async def`, elle est exécutée dans la threadpool externe. -### Sous-dépendances +### Sous-dépendances Vous pouvez avoir de multiples dépendances et sous-dépendances dépendant les unes des autres (en tant que paramètres de la définition de la *fonction de chemin*), certaines créées avec `async def` et d'autres avec `def`. Cela fonctionnerait aussi, et celles définies avec un simple `def` seraient exécutées sur un thread externe (venant de la threadpool) plutôt que d'être "attendues". diff --git a/docs/fr/docs/deployment/docker.md b/docs/fr/docs/deployment/docker.md index e4b59afbf..d2dcae722 100644 --- a/docs/fr/docs/deployment/docker.md +++ b/docs/fr/docs/deployment/docker.md @@ -118,7 +118,7 @@ $ docker run -d --name mycontainer -p 80:80 myimage -Vous disposez maintenant d'un serveur FastAPI optimisé dans un conteneur Docker. Configuré automatiquement pour votre +Vous disposez maintenant d'un serveur FastAPI optimisé dans un conteneur Docker. Configuré automatiquement pour votre serveur actuel (et le nombre de cœurs du CPU). ## Vérifier @@ -139,7 +139,7 @@ Vous verrez la documentation interactive automatique de l'API (fournie par http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou équivalent, en utilisant votre hôte Docker). @@ -149,7 +149,7 @@ Vous verrez la documentation automatique alternative (fournie par Traefik est un reverse proxy/load balancer +Traefik est un reverse proxy/load balancer haute performance. Il peut faire office de "Proxy de terminaison TLS" (entre autres fonctionnalités). Il est intégré à Let's Encrypt. Ainsi, il peut gérer toutes les parties HTTPS, y compris l'acquisition et le renouvellement des certificats. @@ -164,7 +164,7 @@ Avec ces informations et ces outils, passez à la section suivante pour tout com Vous pouvez avoir un cluster en mode Docker Swarm configuré en quelques minutes (environ 20 min) avec un processus Traefik principal gérant HTTPS (y compris l'acquisition et le renouvellement des certificats). -En utilisant le mode Docker Swarm, vous pouvez commencer par un "cluster" d'une seule machine (il peut même s'agir +En utilisant le mode Docker Swarm, vous pouvez commencer par un "cluster" d'une seule machine (il peut même s'agir d'un serveur à 5 USD/mois) et ensuite vous pouvez vous développer autant que vous le souhaitez en ajoutant d'autres serveurs. Pour configurer un cluster en mode Docker Swarm avec Traefik et la gestion de HTTPS, suivez ce guide : diff --git a/docs/fr/docs/features.md b/docs/fr/docs/features.md index 4b00ecb6f..dcc0e39ed 100644 --- a/docs/fr/docs/features.md +++ b/docs/fr/docs/features.md @@ -27,7 +27,7 @@ Documentation d'API interactive et interface web d'exploration. Comme le framewo Tout est basé sur la déclaration de type standard de **Python 3.6** (grâce à Pydantic). Pas de nouvelles syntaxes à apprendre. Juste du Python standard et moderne. -Si vous souhaitez un rappel de 2 minutes sur l'utilisation des types en Python (même si vous ne comptez pas utiliser FastAPI), jetez un oeil au tutoriel suivant: [Python Types](python-types.md){.internal-link target=_blank}. +Si vous souhaitez un rappel de 2 minutes sur l'utilisation des types en Python (même si vous ne comptez pas utiliser FastAPI), jetez un oeil au tutoriel suivant: [Python Types](python-types.md){.internal-link target=_blank}. Vous écrivez du python standard avec des annotations de types: diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 3922d9c77..0b537054e 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -321,7 +321,7 @@ And now, go to http://127.0.0.1:8000/items/foo, vous verrez comme réponse : @@ -44,7 +44,7 @@ Si vous exécutez cet exemple et allez sur "parsing" automatique. ## Validation de données @@ -91,7 +91,7 @@ documentation générée automatiquement et interactive : On voit bien dans la documentation que `item_id` est déclaré comme entier. -## Les avantages d'avoir une documentation basée sur une norme, et la documentation alternative. +## Les avantages d'avoir une documentation basée sur une norme, et la documentation alternative. Le schéma généré suivant la norme OpenAPI, il existe de nombreux outils compatibles. @@ -102,7 +102,7 @@ sur De la même façon, il existe bien d'autres outils compatibles, y compris des outils de génération de code -pour de nombreux langages. +pour de nombreux langages. ## Pydantic diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md index f1f2a605d..7bf3b9e79 100644 --- a/docs/fr/docs/tutorial/query-params.md +++ b/docs/fr/docs/tutorial/query-params.md @@ -6,7 +6,7 @@ Quand vous déclarez des paramètres dans votre fonction de chemin qui ne font p {!../../../docs_src/query_params/tutorial001.py!} ``` -La partie appelée requête (ou **query**) dans une URL est l'ensemble des paires clés-valeurs placées après le `?` , séparées par des `&`. +La partie appelée requête (ou **query**) dans une URL est l'ensemble des paires clés-valeurs placées après le `?` , séparées par des `&`. Par exemple, dans l'URL : @@ -120,7 +120,7 @@ ou n'importe quelle autre variation de casse (tout en majuscules, uniquement la ## Multiples paramètres de chemin et de requête -Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête dans la même fonction, **FastAPI** saura comment les gérer. +Vous pouvez déclarer plusieurs paramètres de chemin et paramètres de requête dans la même fonction, **FastAPI** saura comment les gérer. Et vous n'avez pas besoin de les déclarer dans un ordre spécifique. diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md index a7af14781..0bb7b55e3 100644 --- a/docs/id/docs/index.md +++ b/docs/id/docs/index.md @@ -321,7 +321,7 @@ And now, go to https://github.com/tiangolo/full-stack * https://github.com/tiangolo/full-stack-flask-couchbase -* https://github.com/tiangolo/full-stack-flask-couchdb +* https://github.com/tiangolo/full-stack-flask-couchdb そして、これらのフルスタックジェネレーターは、[**FastAPI** Project Generators](project-generation.md){.internal-link target=_blank}の元となっていました。 @@ -295,7 +295,7 @@ OpenAPIやJSON Schemaのような標準に基づいたものではありませ HugはAPIStarに部分的なインスピレーションを与えており、私が発見した中ではAPIStarと同様に最も期待の持てるツールの一つでした。 Hugは、**FastAPI**がPythonの型ヒントを用いてパラメータを宣言し自動的にAPIを定義するスキーマを生成することを触発しました。 - + Hugは、**FastAPI**がヘッダーやクッキーを設定するために関数に `response`引数を宣言することにインスピレーションを与えました。 ### APIStar (<= 0.5) diff --git a/docs/ja/docs/async.md b/docs/ja/docs/async.md index eff4f2f43..8fac2cb38 100644 --- a/docs/ja/docs/async.md +++ b/docs/ja/docs/async.md @@ -361,7 +361,7 @@ async def read_burgers(): この部分は**FastAPI**の仕組みに関する非常に技術的な詳細です。 かなりの技術知識 (コルーチン、スレッド、ブロッキングなど) があり、FastAPIが `async def` と通常の `def` をどのように処理するか知りたい場合は、先に進んでください。 - + ### Path operation 関数 *path operation 関数*を `async def` の代わりに通常の `def` で宣言すると、(サーバーをブロックするので) 直接呼び出す代わりに外部スレッドプール (awaitされる) で実行されます。 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 2ce81b551..40710a93a 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -4,4 +4,4 @@ ユースケースや使用しているツールによっていくつかの方法に分かれます。 -次のセクションでより詳しくそれらの方法について説明します。 \ No newline at end of file +次のセクションでより詳しくそれらの方法について説明します。 diff --git a/docs/ja/docs/deployment/manually.md b/docs/ja/docs/deployment/manually.md index 3296ba76f..dd4b568bd 100644 --- a/docs/ja/docs/deployment/manually.md +++ b/docs/ja/docs/deployment/manually.md @@ -20,7 +20,7 @@ !!! tip "豆知識" `standard` を加えることで、Uvicornがインストールされ、いくつかの推奨される依存関係を利用するようになります。 - + これには、`asyncio` の高性能な完全互換品である `uvloop` が含まれ、並行処理のパフォーマンスが大幅に向上します。 === "Hypercorn" diff --git a/docs/ja/docs/history-design-future.md b/docs/ja/docs/history-design-future.md index 778252d4e..d0d1230c4 100644 --- a/docs/ja/docs/history-design-future.md +++ b/docs/ja/docs/history-design-future.md @@ -77,4 +77,4 @@ **FastAPI**には大きな未来が待っています。 -そして、[あなたの助け](help-fastapi.md){.internal-link target=_blank}を大いに歓迎します。 \ No newline at end of file +そして、[あなたの助け](help-fastapi.md){.internal-link target=_blank}を大いに歓迎します。 diff --git a/docs/ja/docs/tutorial/body.md b/docs/ja/docs/tutorial/body.md index 59388d904..d2559205b 100644 --- a/docs/ja/docs/tutorial/body.md +++ b/docs/ja/docs/tutorial/body.md @@ -114,7 +114,7 @@ APIはほとんどの場合 **レスポンス** ボディを送らなければ PyCharmエディタを使用している場合は、Pydantic PyCharm Pluginが使用可能です。 以下のエディターサポートが強化されます: - + * 自動補完 * 型チェック * リファクタリング @@ -157,7 +157,7 @@ APIはほとんどの場合 **レスポンス** ボディを送らなければ !!! note "備考" FastAPIは、`= None`があるおかげで、`q`がオプショナルだとわかります。 - + `Optional[str]` の`Optional` はFastAPIでは使用されていません(FastAPIは`str`の部分のみ使用します)。しかし、`Optional[str]` はエディタがコードのエラーを見つけるのを助けてくれます。 ## Pydanticを使わない方法 diff --git a/docs/ja/docs/tutorial/middleware.md b/docs/ja/docs/tutorial/middleware.md index f2a22119b..973eb2b1a 100644 --- a/docs/ja/docs/tutorial/middleware.md +++ b/docs/ja/docs/tutorial/middleware.md @@ -35,7 +35,7 @@ !!! tip "豆知識" 'X-'プレフィックスを使用してカスタムの独自ヘッダーを追加できます。 - ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) + ただし、ブラウザのクライアントに表示させたいカスタムヘッダーがある場合は、StarletteのCORSドキュメントに記載されているパラメータ `expose_headers` を使用して、それらをCORS設定に追加する必要があります ([CORS (オリジン間リソース共有)](cors.md){.internal-link target=_blank}) !!! note "技術詳細" `from starlette.requests import Request` を使用することもできます。 diff --git a/docs/ja/docs/tutorial/path-params.md b/docs/ja/docs/tutorial/path-params.md index 452ca0c98..66de05afb 100644 --- a/docs/ja/docs/tutorial/path-params.md +++ b/docs/ja/docs/tutorial/path-params.md @@ -139,7 +139,7 @@ Pythonのformat文字列と同様のシンタックスで「パスパラメー ### *パスパラメータ*の宣言 -次に、作成したenumクラスである`ModelName`を使用した型アノテーションをもつ*パスパラメータ*を作成します: +次に、作成したenumクラスである`ModelName`を使用した型アノテーションをもつ*パスパラメータ*を作成します: ```Python hl_lines="16" {!../../../docs_src/path_params/tutorial005.py!} diff --git a/docs/ja/docs/tutorial/query-params.md b/docs/ja/docs/tutorial/query-params.md index 91783a53a..9f8c6ab9f 100644 --- a/docs/ja/docs/tutorial/query-params.md +++ b/docs/ja/docs/tutorial/query-params.md @@ -18,7 +18,7 @@ http://127.0.0.1:8000/items/?skip=0&limit=10 ...クエリパラメータは: * `skip`: 値は `0` -* `limit`: 値は `10` +* `limit`: 値は `10` これらはURLの一部なので、「自然に」文字列になります。 @@ -75,7 +75,7 @@ http://127.0.0.1:8000/items/?skip=20 !!! note "備考" FastAPIは、`= None`があるおかげで、`q`がオプショナルだとわかります。 - + `Optional[str]` の`Optional` はFastAPIでは使用されていません(FastAPIは`str`の部分のみ使用します)。しかし、`Optional[str]` はエディタがコードのエラーを見つけるのを助けてくれます。 ## クエリパラメータの型変換 @@ -116,7 +116,7 @@ http://127.0.0.1:8000/items/foo?short=on http://127.0.0.1:8000/items/foo?short=yes ``` -もしくは、他の大文字小文字のバリエーション (アッパーケース、最初の文字だけアッパーケース、など)で、関数は `short` パラメータを `True` な `bool` 値として扱います。それ以外は `False` になります。 +もしくは、他の大文字小文字のバリエーション (アッパーケース、最初の文字だけアッパーケース、など)で、関数は `short` パラメータを `True` な `bool` 値として扱います。それ以外は `False` になります。 ## 複数のパスパラメータとクエリパラメータ diff --git a/docs/ja/docs/tutorial/request-forms.md b/docs/ja/docs/tutorial/request-forms.md index 06105c9ef..bce6e8d9a 100644 --- a/docs/ja/docs/tutorial/request-forms.md +++ b/docs/ja/docs/tutorial/request-forms.md @@ -45,7 +45,7 @@ HTMLフォーム(`
`)がサーバにデータを送信する方 フォームからのデータは通常、`application/x-www-form-urlencoded`の「media type」を使用してエンコードされます。 しかし、フォームがファイルを含む場合は、`multipart/form-data`としてエンコードされます。ファイルの扱いについては次の章で説明します。 - + これらのエンコーディングやフォームフィールドの詳細については、MDNPOSTのウェブドキュメントを参照してください。 !!! warning "注意" diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 3db493294..03b0e1dee 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -36,7 +36,7 @@ !!! tip "豆知識" FastAPIアプリケーションへのリクエストの送信とは別に、テストで `async` 関数 (非同期データベース関数など) を呼び出したい場合は、高度なチュートリアルの[Async Tests](../advanced/async-tests.md){.internal-link target=_blank} を参照してください。 - + ## テストの分離 実際のアプリケーションでは、おそらくテストを別のファイルに保存します。 @@ -112,7 +112,7 @@ `TestClient` は、Pydanticモデルではなく、JSONに変換できるデータを受け取ることに注意してください。 テストにPydanticモデルがあり、テスト中にそのデータをアプリケーションに送信したい場合は、[JSON互換エンコーダ](encoder.md){.internal-link target=_blank} で説明されている `jsonable_encoder` が利用できます。 - + ## 実行 後は、`pytest` をインストールするだけです: diff --git a/docs/ko/docs/deployment/versions.md b/docs/ko/docs/deployment/versions.md index 4c1bcdc2e..074c15158 100644 --- a/docs/ko/docs/deployment/versions.md +++ b/docs/ko/docs/deployment/versions.md @@ -6,7 +6,7 @@ 이것이 아직도 최신 버전이 `0.x.x`인 이유입니다. 이것은 각각의 버전들이 잠재적으로 변할 수 있다는 것을 보여줍니다. 이는 유의적 버전 관습을 따릅니다. -지금 바로 **FastAPI**로 응용 프로그램을 만들 수 있습니다. 이때 (아마 지금까지 그래 왔던 것처럼), 사용하는 버전이 코드와 잘 맞는지 확인해야합니다. +지금 바로 **FastAPI**로 응용 프로그램을 만들 수 있습니다. 이때 (아마 지금까지 그래 왔던 것처럼), 사용하는 버전이 코드와 잘 맞는지 확인해야합니다. ## `fastapi` 버전을 표시 @@ -46,7 +46,7 @@ FastAPI는 오류를 수정하고, 일반적인 변경사항을 위해 "패치" !!! tip "팁" 여기서 말하는 "패치"란 버전의 마지막 숫자로, 예를 들어 `0.2.3` 버전에서 "패치"는 `3`을 의미합니다. -따라서 다음과 같이 버전을 표시할 수 있습니다: +따라서 다음과 같이 버전을 표시할 수 있습니다: ```txt fastapi>=0.45.0,<0.46.0 @@ -71,7 +71,7 @@ fastapi>=0.45.0,<0.46.0 `starlette`의 버전은 표시할 수 없습니다. -서로다른 버전의 **FastAPI**가 구체적이고 새로운 버전의 Starlette을 사용할 것입니다. +서로다른 버전의 **FastAPI**가 구체적이고 새로운 버전의 Starlette을 사용할 것입니다. 그러므로 **FastAPI**가 알맞은 Starlette 버전을 사용하도록 하십시오. diff --git a/docs/ko/docs/tutorial/header-params.md b/docs/ko/docs/tutorial/header-params.md index 1c46b32ba..484554e97 100644 --- a/docs/ko/docs/tutorial/header-params.md +++ b/docs/ko/docs/tutorial/header-params.md @@ -57,7 +57,7 @@ 타입 정의에서 리스트를 사용하여 이러한 케이스를 정의할 수 있습니다. -중복 헤더의 모든 값을 파이썬 `list`로 수신합니다. +중복 헤더의 모든 값을 파이썬 `list`로 수신합니다. 예를 들어, 두 번 이상 나타날 수 있는 `X-Token`헤더를 선언하려면, 다음과 같이 작성합니다: diff --git a/docs/ko/docs/tutorial/path-params.md b/docs/ko/docs/tutorial/path-params.md index ede63f69d..5cf397e7a 100644 --- a/docs/ko/docs/tutorial/path-params.md +++ b/docs/ko/docs/tutorial/path-params.md @@ -241,4 +241,4 @@ Starlette에서 직접 옵션을 사용하면 다음과 같은 URL을 사용하 위 사항들을 그저 한번에 선언하면 됩니다. -이는 (원래 성능과는 별개로) 대체 프레임워크와 비교했을 때 **FastAPI**의 주요 가시적 장점일 것입니다. \ No newline at end of file +이는 (원래 성능과는 별개로) 대체 프레임워크와 비교했을 때 **FastAPI**의 주요 가시적 장점일 것입니다. diff --git a/docs/ko/docs/tutorial/request-files.md b/docs/ko/docs/tutorial/request-files.md index 769a676cd..decefe981 100644 --- a/docs/ko/docs/tutorial/request-files.md +++ b/docs/ko/docs/tutorial/request-files.md @@ -13,7 +13,7 @@ `fastapi` 에서 `File` 과 `UploadFile` 을 임포트 합니다: -```Python hl_lines="1" +```Python hl_lines="1" {!../../../docs_src/request_files/tutorial001.py!} ``` @@ -21,7 +21,7 @@ `Body` 및 `Form` 과 동일한 방식으로 파일의 매개변수를 생성합니다: -```Python hl_lines="7" +```Python hl_lines="7" {!../../../docs_src/request_files/tutorial001.py!} ``` @@ -45,7 +45,7 @@ `File` 매개변수를 `UploadFile` 타입으로 정의합니다: -```Python hl_lines="12" +```Python hl_lines="12" {!../../../docs_src/request_files/tutorial001.py!} ``` @@ -97,7 +97,7 @@ contents = myfile.file.read() ## "폼 데이터"란 -HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용합니다. +HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용합니다. **FastAPI**는 JSON 대신 올바른 위치에서 데이터를 읽을 수 있도록 합니다. @@ -121,7 +121,7 @@ HTML의 폼들(`
`)이 서버에 데이터를 전송하는 방식은 이 기능을 사용하기 위해 , `bytes` 의 `List` 또는 `UploadFile` 를 선언하기 바랍니다: -```Python hl_lines="10 15" +```Python hl_lines="10 15" {!../../../docs_src/request_files/tutorial002.py!} ``` diff --git a/docs/ko/docs/tutorial/request-forms-and-files.md b/docs/ko/docs/tutorial/request-forms-and-files.md index 6750c7b23..ddf232e7f 100644 --- a/docs/ko/docs/tutorial/request-forms-and-files.md +++ b/docs/ko/docs/tutorial/request-forms-and-files.md @@ -9,7 +9,7 @@ ## `File` 및 `Form` 업로드 -```Python hl_lines="1" +```Python hl_lines="1" {!../../../docs_src/request_forms_and_files/tutorial001.py!} ``` @@ -17,7 +17,7 @@ `Body` 및 `Query`와 동일한 방식으로 파일과 폼의 매개변수를 생성합니다: -```Python hl_lines="8" +```Python hl_lines="8" {!../../../docs_src/request_forms_and_files/tutorial001.py!} ``` diff --git a/docs/ko/docs/tutorial/response-status-code.md b/docs/ko/docs/tutorial/response-status-code.md index d201867a1..f92c057be 100644 --- a/docs/ko/docs/tutorial/response-status-code.md +++ b/docs/ko/docs/tutorial/response-status-code.md @@ -8,11 +8,11 @@ * `@app.delete()` * 기타 -```Python hl_lines="6" +```Python hl_lines="6" {!../../../docs_src/response_status_code/tutorial001.py!} ``` -!!! note "참고" +!!! note "참고" `status_code` 는 "데코레이터" 메소드(`get`, `post` 등)의 매개변수입니다. 모든 매개변수들과 본문처럼 *경로 작동 함수*가 아닙니다. `status_code` 매개변수는 HTTP 상태 코드를 숫자로 입력받습니다. @@ -27,7 +27,7 @@ -!!! note "참고" +!!! note "참고" 어떤 응답 코드들은 해당 응답에 본문이 없다는 것을 의미하기도 합니다 (다음 항목 참고). 이에 따라 FastAPI는 응답 본문이 없음을 명시하는 OpenAPI를 생성합니다. @@ -61,7 +61,7 @@ HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다 상기 예시 참고: -```Python hl_lines="6" +```Python hl_lines="6" {!../../../docs_src/response_status_code/tutorial001.py!} ``` @@ -71,7 +71,7 @@ HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송합니다 `fastapi.status` 의 편의 변수를 사용할 수 있습니다. -```Python hl_lines="1 6" +```Python hl_lines="1 6" {!../../../docs_src/response_status_code/tutorial002.py!} ``` diff --git a/docs/pl/docs/tutorial/index.md b/docs/pl/docs/tutorial/index.md index 1a97214af..ed8752a95 100644 --- a/docs/pl/docs/tutorial/index.md +++ b/docs/pl/docs/tutorial/index.md @@ -78,4 +78,3 @@ Jest też **Zaawansowany poradnik**, który możesz przeczytać po lekturze tego Najpierw jednak powinieneś przeczytać **Samouczek** (czytasz go teraz). Ten rozdział jest zaprojektowany tak, że możesz stworzyć kompletną aplikację używając tylko informacji tutaj zawartych, a następnie rozszerzać ją na różne sposoby, w zależności od potrzeb, używając kilku dodatkowych pomysłów z **Zaawansowanego poradnika**. - diff --git a/docs/pt/docs/alternatives.md b/docs/pt/docs/alternatives.md index 6559b7398..61ee4f900 100644 --- a/docs/pt/docs/alternatives.md +++ b/docs/pt/docs/alternatives.md @@ -331,7 +331,7 @@ Agora APIStar é um conjunto de ferramentas para validar especificações OpenAP Existir. A idéia de declarar múltiplas coisas (validação de dados, serialização e documentação) com os mesmos tipos Python, que ao mesmo tempo fornecesse grande suporte ao editor, era algo que eu considerava uma brilhante idéia. - + E após procurar por um logo tempo por um framework similar e testar muitas alternativas diferentes, APIStar foi a melhor opção disponível. Então APIStar parou de existir como um servidor e Starlette foi criado, e foi uma nova melhor fundação para tal sistema. Essa foi a inspiração final para construir **FastAPI**. @@ -390,7 +390,7 @@ Essa é uma das principais coisas que **FastAPI** adiciona no topo, tudo baseado Controlar todas as partes web centrais. Adiciona recursos no topo. A classe `FastAPI` em si herda `Starlette`. - + Então, qualquer coisa que você faz com Starlette, você pode fazer diretamente com **FastAPI**, pois ele é basicamente um Starlette com esteróides. ### Uvicorn diff --git a/docs/pt/docs/async.md b/docs/pt/docs/async.md index 44f4b5148..be1278a1b 100644 --- a/docs/pt/docs/async.md +++ b/docs/pt/docs/async.md @@ -94,7 +94,7 @@ Para "síncrono" (contrário de "assíncrono") também é utilizado o termo "seq Essa idéia de código **assíncrono** descrito acima é algo às vezes chamado de **"concorrência"**. E é diferente de **"paralelismo"**. -**Concorrência** e **paralelismo** ambos são relacionados a "diferentes coisas acontecendo mais ou menos ao mesmo tempo". +**Concorrência** e **paralelismo** ambos são relacionados a "diferentes coisas acontecendo mais ou menos ao mesmo tempo". Mas os detalhes entre *concorrência* e *paralelismo* são bem diferentes. @@ -134,7 +134,7 @@ Mas então, embora você ainda não tenha os hambúrgueres, seu trabalho no caix Mas enquanto você se afasta do balcão e senta na mesa com o número da sua chamada, você pode trocar sua atenção para seu _crush_ :heart_eyes:, e "trabalhar" nisso. Então você está novamente fazendo algo muito "produtivo", como flertar com seu _crush_ :heart_eyes:. -Então o caixa diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porquê você tem o número de chamada, e os outros tem os números deles. +Então o caixa diz que "seus hambúrgueres estão prontos" colocando seu número no balcão, mas você não corre que nem um maluco imediatamente quando o número exibido é o seu. Você sabe que ninguém irá roubar seus hambúrgueres porquê você tem o número de chamada, e os outros tem os números deles. Então você espera que seu _crush_ :heart_eyes: termine a história que estava contando (terminar o trabalho atual / tarefa sendo processada), sorri gentilmente e diz que você está indo buscar os hambúrgueres. @@ -358,9 +358,9 @@ Tudo isso é o que deixa o FastAPI poderoso (através do Starlette) e que o faz !!! warning Você pode provavelmente pular isso. - + Esses são detalhes muito técnicos de como **FastAPI** funciona por baixo do capô. - + Se você tem algum conhecimento técnico (corrotinas, threads, blocking etc) e está curioso sobre como o FastAPI controla o `async def` vs normal `def`, vá em frente. ### Funções de operação de rota diff --git a/docs/pt/docs/benchmarks.md b/docs/pt/docs/benchmarks.md index 7f7c95ba1..07461ce46 100644 --- a/docs/pt/docs/benchmarks.md +++ b/docs/pt/docs/benchmarks.md @@ -2,7 +2,7 @@ As comparações independentes da TechEmpower mostram as aplicações **FastAPI** rodando com Uvicorn como um dos _frameworks_ Python mais rápidos disponíveis, somente atrás dos próprios Starlette e Uvicorn (utilizados internamente pelo FastAPI). (*) -Mas quando se checa _benchmarks_ e comparações você deveria ter o seguinte em mente. +Mas quando se checa _benchmarks_ e comparações você deveria ter o seguinte em mente. ## Comparações e velocidade diff --git a/docs/pt/docs/help-fastapi.md b/docs/pt/docs/help-fastapi.md index 086273a1d..d82ce3414 100644 --- a/docs/pt/docs/help-fastapi.md +++ b/docs/pt/docs/help-fastapi.md @@ -36,7 +36,7 @@ Você pode "acompanhar" (watch) o FastAPI no GitHub (clicando no botão com um " Podendo selecionar apenas "Novos Updates". -Fazendo isto, serão enviadas notificações (em seu email) sempre que tiver novos updates (uma nova versão) com correções de bugs e novos recursos no **FastAPI** +Fazendo isto, serão enviadas notificações (em seu email) sempre que tiver novos updates (uma nova versão) com correções de bugs e novos recursos no **FastAPI** ## Conect-se com o autor diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 97044dd90..c1a0dbf0d 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -365,7 +365,7 @@ Voltando ao código do exemplo anterior, **FastAPI** irá: * Como o parâmetro `q` é declarado com `= None`, ele é opcional. * Sem o `None` ele poderia ser obrigatório (como o corpo no caso de `PUT`). * Para requisições `PUT` para `/items/{item_id}`, lerá o corpo como JSON e: - * Verifica que tem um atributo obrigatório `name` que deve ser `str`. + * Verifica que tem um atributo obrigatório `name` que deve ser `str`. * Verifica que tem um atributo obrigatório `price` que deve ser `float`. * Verifica que tem an atributo opcional `is_offer`, que deve ser `bool`, se presente. * Tudo isso também funciona para objetos JSON profundamente aninhados. diff --git a/docs/pt/docs/python-types.md b/docs/pt/docs/python-types.md index df70afd40..9f12211c7 100644 --- a/docs/pt/docs/python-types.md +++ b/docs/pt/docs/python-types.md @@ -313,4 +313,3 @@ O importante é que, usando tipos padrão de Python, em um único local (em vez !!! info "Informação" Se você já passou por todo o tutorial e voltou para ver mais sobre os tipos, um bom recurso é a "cheat sheet" do `mypy` . - diff --git a/docs/pt/docs/tutorial/body.md b/docs/pt/docs/tutorial/body.md index 5891185f3..5abc91177 100644 --- a/docs/pt/docs/tutorial/body.md +++ b/docs/pt/docs/tutorial/body.md @@ -114,7 +114,7 @@ Mas você terá o mesmo suporte do editor no PyCharm como editor, você pode utilizar o Plugin do Pydantic para o PyCharm . Melhora o suporte do editor para seus modelos Pydantic com:: - + * completação automática * verificação de tipos * refatoração diff --git a/docs/ru/docs/async.md b/docs/ru/docs/async.md index fc5e44471..4c44fc22d 100644 --- a/docs/ru/docs/async.md +++ b/docs/ru/docs/async.md @@ -228,7 +228,7 @@ def results(): И то же самое с большинством веб-приложений. -Пользователей очень много, но ваш сервер всё равно вынужден ждать 🕙 запросы по их слабому интернет-соединению. +Пользователей очень много, но ваш сервер всё равно вынужден ждать 🕙 запросы по их слабому интернет-соединению. Потом снова ждать 🕙, пока вернётся ответ. @@ -379,7 +379,7 @@ async def read_burgers(): burgers = await get_burgers(2) return burgers ``` - + ### Технические подробности Как вы могли заметить, `await` может применяться только в функциях, объявленных с использованием `async def`. diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index c0a958c3d..a1d302276 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -321,7 +321,7 @@ And now, go to тип переменной. diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md index a7af14781..0bb7b55e3 100644 --- a/docs/sq/docs/index.md +++ b/docs/sq/docs/index.md @@ -321,7 +321,7 @@ And now, go to OpenAPI buna path operasyonları parametreleri, body talebi, güvenlik gibi şeyler dahil olmak üzere deklare bunların deklare edilmesi. +* API oluşturma işlemlerinde OpenAPI buna path operasyonları parametreleri, body talebi, güvenlik gibi şeyler dahil olmak üzere deklare bunların deklare edilmesi. * Otomatik olarak data modelinin JSON Schema ile beraber dokümante edilmesi (OpenAPI'n kendisi zaten JSON Schema'ya dayanıyor). * Titiz bir çalışmanın sonucunda yukarıdaki standartlara uygun bir framework oluşturduk. Standartları pastanın üzerine sonradan eklenmiş bir çilek olarak görmedik. * Ayrıca bu bir çok dilde kullanılabilecek **client code generator** kullanımına da izin veriyor. @@ -74,7 +74,7 @@ my_second_user: User = User(**second_user_data) ### Editor desteği -Bütün framework kullanılması kolay ve sezgileri güçlü olması için tasarlandı, verilen bütün kararlar geliştiricilere en iyi geliştirme deneyimini yaşatmak üzere, bir çok editör üzerinde test edildi. +Bütün framework kullanılması kolay ve sezgileri güçlü olması için tasarlandı, verilen bütün kararlar geliştiricilere en iyi geliştirme deneyimini yaşatmak üzere, bir çok editör üzerinde test edildi. Son yapılan Python geliştiricileri anketinde, açık ara en çok kullanılan özellik "oto-tamamlama" idi.. @@ -135,7 +135,7 @@ Bütün güvenlik şemaları OpenAPI'da tanımlanmış durumda, kapsadıkları: Bütün güvenlik özellikleri Starlette'den geliyor (**session cookies'de** dahil olmak üzere). -Bütün hepsi tekrardan kullanılabilir aletler ve bileşenler olarak, kolayca sistemlerinize, data depolarınıza, ilişkisel ve NoSQL databaselerinize entegre edebileceğiniz şekilde yapıldı. +Bütün hepsi tekrardan kullanılabilir aletler ve bileşenler olarak, kolayca sistemlerinize, data depolarınıza, ilişkisel ve NoSQL databaselerinize entegre edebileceğiniz şekilde yapıldı. ### Dependency injection @@ -206,4 +206,3 @@ Aynı şekilde, databaseden gelen objeyi de **direkt olarak isteğe** de tamamiy * **Genişletilebilir**: * Pydantic özelleştirilmiş data tiplerinin tanımlanmasının yapılmasına izin veriyor ayrıca validator decoratorü ile senin doğrulamaları genişletip, kendi doğrulayıcılarını yazmana izin veriyor. * 100% test kapsayıcılığı. - diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 19f46fb4c..3195cd440 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -28,7 +28,7 @@ --- -FastAPI, Python 3.6+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. +FastAPI, Python 3.6+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. Ana özellikleri: @@ -315,7 +315,7 @@ Server otomatik olarak yeniden başlamalı (çünkü yukarıda `uvicorn`'u çal ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Şimdi "Execute" butonuna tıkla, kullanıcı arayüzü otomatik olarak API'ın ile bağlantı kurarak ona bu parametreleri gönderecek ve sonucu karşına getirecek. +* Şimdi "Execute" butonuna tıkla, kullanıcı arayüzü otomatik olarak API'ın ile bağlantı kurarak ona bu parametreleri gönderecek ve sonucu karşına getirecek. ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) @@ -329,7 +329,7 @@ Server otomatik olarak yeniden başlamalı (çünkü yukarıda `uvicorn`'u çal ### Özet -Özetleyecek olursak, URL, sorgu veya request body'deki parametrelerini fonksiyon parametresi olarak kullanıyorsun. Bu parametrelerin veri tiplerini bir kere belirtmen yeterli. +Özetleyecek olursak, URL, sorgu veya request body'deki parametrelerini fonksiyon parametresi olarak kullanıyorsun. Bu parametrelerin veri tiplerini bir kere belirtmen yeterli. Type-hinting işlemini Python dilindeki standart veri tipleri ile yapabilirsin @@ -381,14 +381,14 @@ Az önceki kod örneğine geri dönelim, **FastAPI**'ın yapacaklarına bir bak * `item_id`'nin `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. * `item_id`'nin tipinin `int` olduğunu `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. - * Eğer `GET` ve `PUT` içinde yok ise ve `int` değil ise, sebebini belirten bir hata mesajı gösterecek + * Eğer `GET` ve `PUT` içinde yok ise ve `int` değil ise, sebebini belirten bir hata mesajı gösterecek * Opsiyonel bir `q` parametresinin `GET` talebi için (`http://127.0.0.1:8000/items/foo?q=somequery` içinde) olup olmadığını kontrol edecek * `q` parametresini `= None` ile oluşturduğumuz için, opsiyonel bir parametre olacak. * Eğer `None` olmasa zorunlu bir parametre olacak idi (bu yüzden body'de `PUT` parametresi var). * `PUT` talebi için `/items/{item_id}`'nin body'sini, JSON olarak okuyor: - * `name` adında bir parametetre olup olmadığını ve var ise onun `str` olup olmadığını kontol ediyor. - * `price` adında bir parametetre olup olmadığını ve var ise onun `float` olup olmadığını kontol ediyor. - * `is_offer` adında bir parametetre olup olmadığını ve var ise onun `bool` olup olmadığını kontol ediyor. + * `name` adında bir parametetre olup olmadığını ve var ise onun `str` olup olmadığını kontol ediyor. + * `price` adında bir parametetre olup olmadığını ve var ise onun `float` olup olmadığını kontol ediyor. + * `is_offer` adında bir parametetre olup olmadığını ve var ise onun `bool` olup olmadığını kontol ediyor. * Bunların hepsini en derin JSON modellerinde bile yapacaktır. * Bütün veri tiplerini otomatik olarak JSON'a çeviriyor veya tam tersi. * Her şeyi dokümanlayıp, çeşitli yerlerde: diff --git a/docs/tr/docs/python-types.md b/docs/tr/docs/python-types.md index 7e46bd031..3b9ab9050 100644 --- a/docs/tr/docs/python-types.md +++ b/docs/tr/docs/python-types.md @@ -29,7 +29,7 @@ Programın çıktısı: John Doe ``` -Fonksiyon sırayla şunları yapar: +Fonksiyon sırayla şunları yapar: * `first_name` ve `last_name` değerlerini alır. * `title()` ile değişkenlerin ilk karakterlerini büyütür. diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index a7af14781..0bb7b55e3 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -321,7 +321,7 @@ And now, go to -该命令生成了一个 `./htmlcov/` 目录,如果你在浏览器中打开 `./htmlcov/index.html` 文件,你可以交互式地浏览被测试所覆盖的代码区块,并注意是否缺少了任何区块。 +该命令生成了一个 `./htmlcov/` 目录,如果你在浏览器中打开 `./htmlcov/index.html` 文件,你可以交互式地浏览被测试所覆盖的代码区块,并注意是否缺少了任何区块。 diff --git a/docs/zh/docs/features.md b/docs/zh/docs/features.md index 2d5ba2982..fefe4b197 100644 --- a/docs/zh/docs/features.md +++ b/docs/zh/docs/features.md @@ -193,7 +193,7 @@ FastAPI 有一个使用非常简单,但是非常强大的`Enum`. diff --git a/docs_src/path_params/tutorial003b.py b/docs_src/path_params/tutorial003b.py new file mode 100644 index 000000000..822d37369 --- /dev/null +++ b/docs_src/path_params/tutorial003b.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/users") +async def read_users(): + return ["Rick", "Morty"] + + +@app.get("/users") +async def read_users2(): + return ["Bean", "Elfo"] From 16f1d073db0a8672f85769227f47b47c22d156e0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 16:16:56 +0000 Subject: [PATCH 0246/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b438112e2..2e596e03b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). * 🔥 Remove un-used old pending tests, already covered in other places. PR [#4891](https://github.com/tiangolo/fastapi/pull/4891) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Python formatting hooks to pre-commit. PR [#4890](https://github.com/tiangolo/fastapi/pull/4890) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit with first config and first formatting pass. PR [#4888](https://github.com/tiangolo/fastapi/pull/4888) by [@tiangolo](https://github.com/tiangolo). From 29df6b3e830ba3f8f1e57f85e26860445534db68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 May 2022 11:42:47 -0500 Subject: [PATCH 0247/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20Git?= =?UTF-8?q?Hub=20Action=20workflow=20(#4895)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit --- .github/workflows/autoformat.yml | 43 ++++++++++++++++++++++++ docs/en/docs/img/sponsors/testdriven.svg | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/autoformat.yml diff --git a/.github/workflows/autoformat.yml b/.github/workflows/autoformat.yml new file mode 100644 index 000000000..2bd58008d --- /dev/null +++ b/.github/workflows/autoformat.yml @@ -0,0 +1,43 @@ +name: Auto Format + +on: + pull_request: + types: [opened, synchronize] + +jobs: + autoformat: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + - uses: actions/cache@v2 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01 + - name: Install Flit + if: steps.cache.outputs.cache-hit != 'true' + run: pip install flit + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: flit install --symlink + - uses: actions/cache@v2 + id: pre-commit-hooks-cache + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-hooks-${{ hashFiles('.pre-commit-config.yaml') }}-v01 + - name: Run pre-commit + run: pre-commit run --all-files + - name: Commit pre-commit changes + if: failure() + run: | + git config --global user.name "pre-commit" + git config --global user.email github-actions@github.com + git add --update + git commit -m "🎨 Format code with pre-commit" + git push diff --git a/docs/en/docs/img/sponsors/testdriven.svg b/docs/en/docs/img/sponsors/testdriven.svg index 97741b9e0..6ba2daa3b 100644 --- a/docs/en/docs/img/sponsors/testdriven.svg +++ b/docs/en/docs/img/sponsors/testdriven.svg @@ -1 +1 @@ - \ No newline at end of file + From bcabbf8b37db3fbc020560e94ad2f90e64d1510a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 16:43:22 +0000 Subject: [PATCH 0248/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2e596e03b..681d7f183 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add pre-commit GitHub Action workflow. PR [#4895](https://github.com/tiangolo/fastapi/pull/4895) by [@tiangolo](https://github.com/tiangolo). * 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). * 🔥 Remove un-used old pending tests, already covered in other places. PR [#4891](https://github.com/tiangolo/fastapi/pull/4891) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Python formatting hooks to pre-commit. PR [#4890](https://github.com/tiangolo/fastapi/pull/4890) by [@tiangolo](https://github.com/tiangolo). From f204e8010a190ac28149c969f496acfc548895ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 May 2022 12:15:13 -0500 Subject: [PATCH 0249/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20pre-commit=20CI?= =?UTF-8?q?=20instead=20of=20custom=20GitHub=20Action=20(#4896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/autoformat.yml | 43 -------------------------------- .pre-commit-config.yaml | 3 +++ 2 files changed, 3 insertions(+), 43 deletions(-) delete mode 100644 .github/workflows/autoformat.yml diff --git a/.github/workflows/autoformat.yml b/.github/workflows/autoformat.yml deleted file mode 100644 index 2bd58008d..000000000 --- a/.github/workflows/autoformat.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Auto Format - -on: - pull_request: - types: [opened, synchronize] - -jobs: - autoformat: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - uses: actions/cache@v2 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v01 - - name: Install Flit - if: steps.cache.outputs.cache-hit != 'true' - run: pip install flit - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: flit install --symlink - - uses: actions/cache@v2 - id: pre-commit-hooks-cache - with: - path: ~/.cache/pre-commit - key: ${{ runner.os }}-pre-commit-hooks-${{ hashFiles('.pre-commit-config.yaml') }}-v01 - - name: Run pre-commit - run: pre-commit run --all-files - - name: Commit pre-commit changes - if: failure() - run: | - git config --global user.name "pre-commit" - git config --global user.email github-actions@github.com - git add --update - git commit -m "🎨 Format code with pre-commit" - git push diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6a0b251c..5c278571e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,3 +46,6 @@ repos: rev: 22.3.0 hooks: - id: black +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate From d75c69e01f010642b45b723177dd7c5ebf973f46 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 17:15:56 +0000 Subject: [PATCH 0250/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 681d7f183..5efa5b818 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add pre-commit CI instead of custom GitHub Action. PR [#4896](https://github.com/tiangolo/fastapi/pull/4896) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit GitHub Action workflow. PR [#4895](https://github.com/tiangolo/fastapi/pull/4895) by [@tiangolo](https://github.com/tiangolo). * 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). * 🔥 Remove un-used old pending tests, already covered in other places. PR [#4891](https://github.com/tiangolo/fastapi/pull/4891) by [@tiangolo](https://github.com/tiangolo). From f31ad41dda3e01c9f65b5454c17fc30918d2079b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 May 2022 13:10:57 -0500 Subject: [PATCH 0251/1881] =?UTF-8?q?=F0=9F=91=B7=20Fix=20installing=20Mat?= =?UTF-8?q?erial=20for=20MkDocs=20Insiders=20in=20CI=20(#4897)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index bdef6ea8a..505d66f9f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -22,7 +22,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs-v2 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03 - name: Install Flit if: steps.cache.outputs.cache-hit != 'true' run: python3.7 -m pip install flit @@ -30,7 +30,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python3.7 -m flit install --deps production --extras doc - name: Install Material for MkDocs Insiders - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && steps.cache.outputs.cache-hit != 'true' + if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Build Docs run: python3.7 ./scripts/docs.py build-all From 497e5a2422f672daec7e8f4b4ee42b6e8bfd2df0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 18:11:39 +0000 Subject: [PATCH 0252/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5efa5b818..71c0be532 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Fix installing Material for MkDocs Insiders in CI. PR [#4897](https://github.com/tiangolo/fastapi/pull/4897) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit CI instead of custom GitHub Action. PR [#4896](https://github.com/tiangolo/fastapi/pull/4896) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit GitHub Action workflow. PR [#4895](https://github.com/tiangolo/fastapi/pull/4895) by [@tiangolo](https://github.com/tiangolo). * 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). From 82775f7cd01f93ca10ed7dcf0081d5c746e62068 Mon Sep 17 00:00:00 2001 From: Shahriyar Rzayev Date: Fri, 13 May 2022 00:38:30 +0400 Subject: [PATCH 0253/1881] =?UTF-8?q?=E2=99=BB=20Refactor=20dict=20value?= =?UTF-8?q?=20extraction=20to=20minimize=20key=20lookups=20`fastapi/utils.?= =?UTF-8?q?py`=20(#3139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index b9301499a..9d720feb3 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -147,15 +147,15 @@ def generate_unique_id(route: "APIRoute") -> str: def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None: - for key in update_dict: + for key, value in update_dict.items(): if ( key in main_dict and isinstance(main_dict[key], dict) - and isinstance(update_dict[key], dict) + and isinstance(value, dict) ): - deep_dict_update(main_dict[key], update_dict[key]) + deep_dict_update(main_dict[key], value) else: - main_dict[key] = update_dict[key] + main_dict[key] = value def get_value_or_default( From 975d859ac49e72b7346c52e3660f08de55c20e60 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 20:39:14 +0000 Subject: [PATCH 0254/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 71c0be532..4ad301c6a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Refactor dict value extraction to minimize key lookups `fastapi/utils.py`. PR [#3139](https://github.com/tiangolo/fastapi/pull/3139) by [@ShahriyarR](https://github.com/ShahriyarR). * 👷 Fix installing Material for MkDocs Insiders in CI. PR [#4897](https://github.com/tiangolo/fastapi/pull/4897) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit CI instead of custom GitHub Action. PR [#4896](https://github.com/tiangolo/fastapi/pull/4896) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit GitHub Action workflow. PR [#4895](https://github.com/tiangolo/fastapi/pull/4895) by [@tiangolo](https://github.com/tiangolo). From 8b66b9ca3ef8b1cdb0ca2089781a04fe0e021c65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 12 May 2022 15:47:31 -0500 Subject: [PATCH 0255/1881] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20default=20value?= =?UTF-8?q?=20as=20set=20in=20tutorial=20for=20Path=20Operations=20Advance?= =?UTF-8?q?d=20Configurations=20(#4899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/path_operation_advanced_configuration/tutorial004.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/path_operation_advanced_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004.py index fa867e794..da678aed3 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004.py @@ -11,7 +11,7 @@ class Item(BaseModel): description: Optional[str] = None price: float tax: Optional[float] = None - tags: Set[str] = [] + tags: Set[str] = set() @app.post("/items/", response_model=Item, summary="Create an item") From 31690dda2c1e8557fe96bc30b7d8a170ff4a90a5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 May 2022 20:48:12 +0000 Subject: [PATCH 0256/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4ad301c6a..5c73818fe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Fix default value as set in tutorial for Path Operations Advanced Configurations. PR [#4899](https://github.com/tiangolo/fastapi/pull/4899) by [@tiangolo](https://github.com/tiangolo). * ♻ Refactor dict value extraction to minimize key lookups `fastapi/utils.py`. PR [#3139](https://github.com/tiangolo/fastapi/pull/3139) by [@ShahriyarR](https://github.com/ShahriyarR). * 👷 Fix installing Material for MkDocs Insiders in CI. PR [#4897](https://github.com/tiangolo/fastapi/pull/4897) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit CI instead of custom GitHub Action. PR [#4896](https://github.com/tiangolo/fastapi/pull/4896) by [@tiangolo](https://github.com/tiangolo). From 9262fa836283a3fcd05ba7f7fb6f3aa14b377f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 13 May 2022 18:38:22 -0500 Subject: [PATCH 0257/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20not?= =?UTF-8?q?=20needing=20`...`=20as=20default=20value=20in=20required=20Que?= =?UTF-8?q?ry(),=20Path(),=20Header(),=20etc.=20(#4906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Do not require default value in Query(), Path(), Header(), etc * 📝 Update source examples for docs with default and required values * ✅ Update tests with new default values and not required Ellipsis * 📝 Update docs for Query params and update info about default value, required, Ellipsis --- .../tutorial/query-params-str-validations.md | 78 ++++++++++++++----- .../additional_status_codes/tutorial001.py | 6 +- docs_src/app_testing/app_b/main.py | 8 +- docs_src/app_testing/app_b_py310/main.py | 4 +- .../bigger_applications/app/dependencies.py | 2 +- docs_src/body_fields/tutorial001.py | 12 +-- docs_src/body_fields/tutorial001_py310.py | 6 +- docs_src/body_multiple_params/tutorial001.py | 12 +-- .../body_multiple_params/tutorial001_py310.py | 2 +- docs_src/body_multiple_params/tutorial003.py | 12 ++- .../body_multiple_params/tutorial003_py310.py | 4 +- docs_src/body_multiple_params/tutorial004.py | 12 +-- .../body_multiple_params/tutorial004_py310.py | 2 +- docs_src/body_multiple_params/tutorial005.py | 8 +- .../body_multiple_params/tutorial005_py310.py | 2 +- docs_src/cookie_params/tutorial001.py | 4 +- docs_src/cookie_params/tutorial001_py310.py | 2 +- .../custom_request_and_route/tutorial001.py | 2 +- .../custom_request_and_route/tutorial002.py | 2 +- docs_src/dependencies/tutorial005.py | 2 +- docs_src/dependencies/tutorial005_py310.py | 2 +- docs_src/dependencies/tutorial006.py | 4 +- docs_src/dependencies/tutorial012.py | 4 +- docs_src/extra_data_types/tutorial001.py | 8 +- .../extra_data_types/tutorial001_py310.py | 8 +- docs_src/header_params/tutorial001.py | 2 +- docs_src/header_params/tutorial001_py310.py | 2 +- docs_src/header_params/tutorial002.py | 2 +- docs_src/header_params/tutorial002_py310.py | 2 +- docs_src/header_params/tutorial003.py | 2 +- docs_src/header_params/tutorial003_py310.py | 2 +- docs_src/header_params/tutorial003_py39.py | 2 +- .../tutorial001.py | 6 +- .../tutorial001_py310.py | 4 +- .../tutorial002.py | 4 +- .../tutorial003.py | 4 +- .../tutorial004.py | 2 +- .../tutorial005.py | 2 +- .../tutorial006.py | 4 +- .../tutorial002.py | 4 +- .../tutorial002_py310.py | 2 +- .../tutorial003.py | 6 +- .../tutorial003_py310.py | 2 +- .../tutorial004.py | 6 +- .../tutorial004_py310.py | 3 +- .../tutorial005.py | 2 +- .../tutorial006.py | 2 +- .../tutorial006b.py | 11 +++ .../tutorial006c.py | 13 ++++ .../tutorial006c_py310.py | 11 +++ .../tutorial006d.py | 12 +++ .../tutorial007.py | 4 +- .../tutorial007_py310.py | 4 +- .../tutorial008.py | 6 +- .../tutorial008_py310.py | 2 +- .../tutorial009.py | 4 +- .../tutorial009_py310.py | 2 +- .../tutorial010.py | 6 +- .../tutorial010_py310.py | 2 +- .../tutorial011.py | 4 +- .../tutorial011_py310.py | 2 +- .../tutorial011_py39.py | 4 +- .../tutorial012.py | 2 +- .../tutorial012_py39.py | 2 +- .../tutorial013.py | 2 +- .../tutorial014.py | 4 +- .../tutorial014_py310.py | 4 +- docs_src/request_files/tutorial001.py | 2 +- docs_src/request_files/tutorial001_02.py | 2 +- .../request_files/tutorial001_02_py310.py | 2 +- docs_src/request_files/tutorial001_03.py | 4 +- docs_src/request_files/tutorial002.py | 2 +- docs_src/request_files/tutorial002_py39.py | 2 +- docs_src/request_files/tutorial003.py | 4 +- docs_src/request_files/tutorial003_py39.py | 4 +- docs_src/request_forms/tutorial001.py | 2 +- .../request_forms_and_files/tutorial001.py | 2 +- docs_src/schema_extra_example/tutorial002.py | 8 +- .../schema_extra_example/tutorial002_py310.py | 8 +- docs_src/schema_extra_example/tutorial003.py | 1 - .../schema_extra_example/tutorial003_py310.py | 1 - docs_src/schema_extra_example/tutorial004.py | 1 - .../schema_extra_example/tutorial004_py310.py | 1 - docs_src/websockets/tutorial002.py | 4 +- fastapi/dependencies/utils.py | 17 ++-- fastapi/openapi/models.py | 30 +++---- fastapi/param_functions.py | 26 +++---- fastapi/params.py | 28 +++---- fastapi/security/oauth2.py | 24 +++--- fastapi/utils.py | 2 +- tests/main.py | 44 +++++------ tests/test_dependency_normal_exceptions.py | 4 +- tests/test_forms_from_non_typing_sequences.py | 6 +- tests/test_invalid_sequence_param.py | 8 +- tests/test_jsonable_encoder.py | 2 +- tests/test_modules_same_name_body/app/a.py | 2 +- tests/test_modules_same_name_body/app/b.py | 2 +- tests/test_multi_query_errors.py | 2 +- tests/test_multipart_installation.py | 20 ++--- tests/test_param_class.py | 2 +- tests/test_param_include_in_schema.py | 8 +- tests/test_repeated_dependency_schema.py | 2 +- ...test_request_body_parameters_media_type.py | 6 +- tests/test_schema_extra_examples.py | 47 ++++++----- tests/test_serialize_response_model.py | 2 +- tests/test_starlette_urlconvertors.py | 6 +- tests/test_tuples.py | 2 +- 107 files changed, 404 insertions(+), 314 deletions(-) create mode 100644 docs_src/query_params_str_validations/tutorial006b.py create mode 100644 docs_src/query_params_str_validations/tutorial006c.py create mode 100644 docs_src/query_params_str_validations/tutorial006c_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial006d.py diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index ee62b9718..c5fc35b88 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -16,12 +16,12 @@ Let's take this application as example: {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` -The query parameter `q` is of type `Optional[str]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. +The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. !!! note FastAPI will know that the value of `q` is not required because of the default value `= None`. - The `Optional` in `Optional[str]` is not used by FastAPI, but will allow your editor to give you better support and detect errors. + The `Union` in `Union[str, None]` will allow your editor to give you better support and detect errors. ## Additional validation @@ -59,24 +59,24 @@ And now use it as the default value of your parameter, setting the parameter `ma {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -As we have to replace the default value `None` with `Query(None)`, the first parameter to `Query` serves the same purpose of defining that default value. +As we have to replace the default value `None` in the function with `Query()`, we can now set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value. So: ```Python -q: Optional[str] = Query(None) +q: Union[str, None] = Query(default=None) ``` ...makes the parameter optional, the same as: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` And in Python 3.10 and above: ```Python -q: str | None = Query(None) +q: str | None = Query(default=None) ``` ...makes the parameter optional, the same as: @@ -97,17 +97,17 @@ But it declares it explicitly as being a query parameter. or the: ```Python - = Query(None) + = Query(default=None) ``` as it will use that `None` as the default value, and that way make the parameter **not required**. - The `Optional` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required. + The `Union[str, None]` part allows your editor to provide better support, but it is not what tells FastAPI that this parameter is not required. Then, we can pass more parameters to `Query`. In this case, the `max_length` parameter that applies to strings: ```Python -q: str = Query(None, max_length=50) +q: Union[str, None] = Query(default=None, max_length=50) ``` This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. @@ -118,7 +118,7 @@ You can also add a parameter `min_length`: === "Python 3.6 and above" - ```Python hl_lines="9" + ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} ``` @@ -134,13 +134,13 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="8" + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} ``` @@ -156,7 +156,7 @@ But whenever you need them and go and learn them, know that you can already use ## Default values -The same way that you can pass `None` as the first argument to be used as the default value, you can pass other values. +The same way that you can pass `None` as the value for the `default` parameter, you can pass other values. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: @@ -178,26 +178,68 @@ q: str instead of: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` But we are now declaring it with `Query`, for example like: ```Python -q: Optional[str] = Query(None, min_length=3) +q: Union[str, None] = Query(default=None, min_length=3) ``` -So, when you need to declare a value as required while using `Query`, you can use `...` as the first argument: +So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: ```Python hl_lines="7" {!../../../docs_src/query_params_str_validations/tutorial006.py!} ``` +### Required with Ellipsis (`...`) + +There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006b.py!} +``` + !!! info If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". + It is used by Pydantic and FastAPI to explicitly declare that a value is required. + This will let **FastAPI** know that this parameter is required. +### Required with `None` + +You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. + +To do that, you can declare that `None` is a valid type but still use `default=...`: + +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} + ``` + +!!! tip + Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. + +### Use Pydantic's `Required` instead of Ellipsis (`...`) + +If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: + +```Python hl_lines="2 8" +{!../../../docs_src/query_params_str_validations/tutorial006d.py!} +``` + +!!! tip + Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`. + ## Query parameter list / multiple values When you define a query parameter explicitly with `Query` you can also declare it to receive a list of values, or said in other way, to receive multiple values. @@ -315,7 +357,7 @@ You can add a `title`: === "Python 3.10 and above" - ```Python hl_lines="7" + ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` @@ -399,7 +441,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t === "Python 3.10 and above" - ```Python hl_lines="7" + ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` diff --git a/docs_src/additional_status_codes/tutorial001.py b/docs_src/additional_status_codes/tutorial001.py index ae101e0a0..74a986a6a 100644 --- a/docs_src/additional_status_codes/tutorial001.py +++ b/docs_src/additional_status_codes/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI, status from fastapi.responses import JSONResponse @@ -10,7 +10,9 @@ items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "siz @app.put("/items/{item_id}") async def upsert_item( - item_id: str, name: Optional[str] = Body(None), size: Optional[int] = Body(None) + item_id: str, + name: Union[str, None] = Body(default=None), + size: Union[int, None] = Body(default=None), ): if item_id in items: item = items[item_id] diff --git a/docs_src/app_testing/app_b/main.py b/docs_src/app_testing/app_b/main.py index df43db806..11558b8e8 100644 --- a/docs_src/app_testing/app_b/main.py +++ b/docs_src/app_testing/app_b/main.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel @@ -16,11 +16,11 @@ app = FastAPI() class Item(BaseModel): id: str title: str - description: Optional[str] = None + description: Union[str, None] = None @app.get("/items/{item_id}", response_model=Item) -async def read_main(item_id: str, x_token: str = Header(...)): +async def read_main(item_id: str, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item_id not in fake_db: @@ -29,7 +29,7 @@ async def read_main(item_id: str, x_token: str = Header(...)): @app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: str = Header(...)): +async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py index d44ab9e7c..b4c72de5c 100644 --- a/docs_src/app_testing/app_b_py310/main.py +++ b/docs_src/app_testing/app_b_py310/main.py @@ -18,7 +18,7 @@ class Item(BaseModel): @app.get("/items/{item_id}", response_model=Item) -async def read_main(item_id: str, x_token: str = Header(...)): +async def read_main(item_id: str, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item_id not in fake_db: @@ -27,7 +27,7 @@ async def read_main(item_id: str, x_token: str = Header(...)): @app.post("/items/", response_model=Item) -async def create_item(item: Item, x_token: str = Header(...)): +async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: diff --git a/docs_src/bigger_applications/app/dependencies.py b/docs_src/bigger_applications/app/dependencies.py index 267b0d3a8..8e45f004b 100644 --- a/docs_src/bigger_applications/app/dependencies.py +++ b/docs_src/bigger_applications/app/dependencies.py @@ -1,7 +1,7 @@ from fastapi import Header, HTTPException -async def get_token_header(x_token: str = Header(...)): +async def get_token_header(x_token: str = Header()): if x_token != "fake-super-secret-token": raise HTTPException(status_code=400, detail="X-Token header invalid") diff --git a/docs_src/body_fields/tutorial001.py b/docs_src/body_fields/tutorial001.py index dabc48a0f..cbeebd614 100644 --- a/docs_src/body_fields/tutorial001.py +++ b/docs_src/body_fields/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel, Field @@ -8,14 +8,14 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = Field( - None, title="The description of the item", max_length=300 + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 ) - price: float = Field(..., gt=0, description="The price must be greater than zero") - tax: Optional[float] = None + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None @app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_fields/tutorial001_py310.py b/docs_src/body_fields/tutorial001_py310.py index 01e02a050..4437327f3 100644 --- a/docs_src/body_fields/tutorial001_py310.py +++ b/docs_src/body_fields/tutorial001_py310.py @@ -7,13 +7,13 @@ app = FastAPI() class Item(BaseModel): name: str description: str | None = Field( - None, title="The description of the item", max_length=300 + default=None, title="The description of the item", max_length=300 ) - price: float = Field(..., gt=0, description="The price must be greater than zero") + price: float = Field(gt=0, description="The price must be greater than zero") tax: float | None = None @app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_multiple_params/tutorial001.py b/docs_src/body_multiple_params/tutorial001.py index 7ce0ae6f2..a73975b3a 100644 --- a/docs_src/body_multiple_params/tutorial001.py +++ b/docs_src/body_multiple_params/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Path from pydantic import BaseModel @@ -8,17 +8,17 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None @app.put("/items/{item_id}") async def update_item( *, - item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), - q: Optional[str] = None, - item: Optional[Item] = None, + item_id: int = Path(title="The ID of the item to get", ge=0, le=1000), + q: Union[str, None] = None, + item: Union[Item, None] = None, ): results = {"item_id": item_id} if q: diff --git a/docs_src/body_multiple_params/tutorial001_py310.py b/docs_src/body_multiple_params/tutorial001_py310.py index b08d397b3..be0eba2ae 100644 --- a/docs_src/body_multiple_params/tutorial001_py310.py +++ b/docs_src/body_multiple_params/tutorial001_py310.py @@ -14,7 +14,7 @@ class Item(BaseModel): @app.put("/items/{item_id}") async def update_item( *, - item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), + item_id: int = Path(title="The ID of the item to get", ge=0, le=1000), q: str | None = None, item: Item | None = None, ): diff --git a/docs_src/body_multiple_params/tutorial003.py b/docs_src/body_multiple_params/tutorial003.py index 7e9e24374..cf344e6c5 100644 --- a/docs_src/body_multiple_params/tutorial003.py +++ b/docs_src/body_multiple_params/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel @@ -8,19 +8,17 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None class User(BaseModel): username: str - full_name: Optional[str] = None + full_name: Union[str, None] = None @app.put("/items/{item_id}") -async def update_item( - item_id: int, item: Item, user: User, importance: int = Body(...) -): +async def update_item(item_id: int, item: Item, user: User, importance: int = Body()): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} return results diff --git a/docs_src/body_multiple_params/tutorial003_py310.py b/docs_src/body_multiple_params/tutorial003_py310.py index 9ddbda3f7..a1a75fe8e 100644 --- a/docs_src/body_multiple_params/tutorial003_py310.py +++ b/docs_src/body_multiple_params/tutorial003_py310.py @@ -17,8 +17,6 @@ class User(BaseModel): @app.put("/items/{item_id}") -async def update_item( - item_id: int, item: Item, user: User, importance: int = Body(...) -): +async def update_item(item_id: int, item: Item, user: User, importance: int = Body()): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} return results diff --git a/docs_src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004.py index 8dc0d374d..beea7d1e3 100644 --- a/docs_src/body_multiple_params/tutorial004.py +++ b/docs_src/body_multiple_params/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel @@ -8,14 +8,14 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None class User(BaseModel): username: str - full_name: Optional[str] = None + full_name: Union[str, None] = None @app.put("/items/{item_id}") @@ -24,8 +24,8 @@ async def update_item( item_id: int, item: Item, user: User, - importance: int = Body(..., gt=0), - q: Optional[str] = None + importance: int = Body(gt=0), + q: Union[str, None] = None ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py index 77321300e..6d495d408 100644 --- a/docs_src/body_multiple_params/tutorial004_py310.py +++ b/docs_src/body_multiple_params/tutorial004_py310.py @@ -22,7 +22,7 @@ async def update_item( item_id: int, item: Item, user: User, - importance: int = Body(..., gt=0), + importance: int = Body(gt=0), q: str | None = None ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} diff --git a/docs_src/body_multiple_params/tutorial005.py b/docs_src/body_multiple_params/tutorial005.py index 4657b4144..29e6e14b7 100644 --- a/docs_src/body_multiple_params/tutorial005.py +++ b/docs_src/body_multiple_params/tutorial005.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel @@ -8,12 +8,12 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None @app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/body_multiple_params/tutorial005_py310.py b/docs_src/body_multiple_params/tutorial005_py310.py index 97b213b16..06744507b 100644 --- a/docs_src/body_multiple_params/tutorial005_py310.py +++ b/docs_src/body_multiple_params/tutorial005_py310.py @@ -12,6 +12,6 @@ class Item(BaseModel): @app.put("/items/{item_id}") -async def update_item(item_id: int, item: Item = Body(..., embed=True)): +async def update_item(item_id: int, item: Item = Body(embed=True)): results = {"item_id": item_id, "item": item} return results diff --git a/docs_src/cookie_params/tutorial001.py b/docs_src/cookie_params/tutorial001.py index 67d03b133..c4a497fda 100644 --- a/docs_src/cookie_params/tutorial001.py +++ b/docs_src/cookie_params/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Cookie, FastAPI @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(ads_id: Optional[str] = Cookie(None)): +async def read_items(ads_id: Union[str, None] = Cookie(default=None)): return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_py310.py b/docs_src/cookie_params/tutorial001_py310.py index d0b004631..6c9d5f9a1 100644 --- a/docs_src/cookie_params/tutorial001_py310.py +++ b/docs_src/cookie_params/tutorial001_py310.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(ads_id: str | None = Cookie(None)): +async def read_items(ads_id: str | None = Cookie(default=None)): return {"ads_id": ads_id} diff --git a/docs_src/custom_request_and_route/tutorial001.py b/docs_src/custom_request_and_route/tutorial001.py index 2e64ad45d..268ce9019 100644 --- a/docs_src/custom_request_and_route/tutorial001.py +++ b/docs_src/custom_request_and_route/tutorial001.py @@ -31,5 +31,5 @@ app.router.route_class = GzipRoute @app.post("/sum") -async def sum_numbers(numbers: List[int] = Body(...)): +async def sum_numbers(numbers: List[int] = Body()): return {"sum": sum(numbers)} diff --git a/docs_src/custom_request_and_route/tutorial002.py b/docs_src/custom_request_and_route/tutorial002.py index f4c093ac9..cee4a95f0 100644 --- a/docs_src/custom_request_and_route/tutorial002.py +++ b/docs_src/custom_request_and_route/tutorial002.py @@ -25,5 +25,5 @@ app.router.route_class = ValidationErrorLoggingRoute @app.post("/") -async def sum_numbers(numbers: List[int] = Body(...)): +async def sum_numbers(numbers: List[int] = Body()): return sum(numbers) diff --git a/docs_src/dependencies/tutorial005.py b/docs_src/dependencies/tutorial005.py index c8923d143..24f73c617 100644 --- a/docs_src/dependencies/tutorial005.py +++ b/docs_src/dependencies/tutorial005.py @@ -10,7 +10,7 @@ def query_extractor(q: Optional[str] = None): def query_or_cookie_extractor( - q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(None) + q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(default=None) ): if not q: return last_query diff --git a/docs_src/dependencies/tutorial005_py310.py b/docs_src/dependencies/tutorial005_py310.py index 5e1d7e0ef..247cdabe2 100644 --- a/docs_src/dependencies/tutorial005_py310.py +++ b/docs_src/dependencies/tutorial005_py310.py @@ -8,7 +8,7 @@ def query_extractor(q: str | None = None): def query_or_cookie_extractor( - q: str = Depends(query_extractor), last_query: str | None = Cookie(None) + q: str = Depends(query_extractor), last_query: str | None = Cookie(default=None) ): if not q: return last_query diff --git a/docs_src/dependencies/tutorial006.py b/docs_src/dependencies/tutorial006.py index a71d7cce6..9aff4154f 100644 --- a/docs_src/dependencies/tutorial006.py +++ b/docs_src/dependencies/tutorial006.py @@ -3,12 +3,12 @@ from fastapi import Depends, FastAPI, Header, HTTPException app = FastAPI() -async def verify_token(x_token: str = Header(...)): +async def verify_token(x_token: str = Header()): if x_token != "fake-super-secret-token": raise HTTPException(status_code=400, detail="X-Token header invalid") -async def verify_key(x_key: str = Header(...)): +async def verify_key(x_key: str = Header()): if x_key != "fake-super-secret-key": raise HTTPException(status_code=400, detail="X-Key header invalid") return x_key diff --git a/docs_src/dependencies/tutorial012.py b/docs_src/dependencies/tutorial012.py index 8f8868a55..36ce6c711 100644 --- a/docs_src/dependencies/tutorial012.py +++ b/docs_src/dependencies/tutorial012.py @@ -1,12 +1,12 @@ from fastapi import Depends, FastAPI, Header, HTTPException -async def verify_token(x_token: str = Header(...)): +async def verify_token(x_token: str = Header()): if x_token != "fake-super-secret-token": raise HTTPException(status_code=400, detail="X-Token header invalid") -async def verify_key(x_key: str = Header(...)): +async def verify_key(x_key: str = Header()): if x_key != "fake-super-secret-key": raise HTTPException(status_code=400, detail="X-Key header invalid") return x_key diff --git a/docs_src/extra_data_types/tutorial001.py b/docs_src/extra_data_types/tutorial001.py index e8d7e1ea3..9f5e911bf 100644 --- a/docs_src/extra_data_types/tutorial001.py +++ b/docs_src/extra_data_types/tutorial001.py @@ -10,10 +10,10 @@ app = FastAPI() @app.put("/items/{item_id}") async def read_items( item_id: UUID, - start_datetime: Optional[datetime] = Body(None), - end_datetime: Optional[datetime] = Body(None), - repeat_at: Optional[time] = Body(None), - process_after: Optional[timedelta] = Body(None), + start_datetime: Optional[datetime] = Body(default=None), + end_datetime: Optional[datetime] = Body(default=None), + repeat_at: Optional[time] = Body(default=None), + process_after: Optional[timedelta] = Body(default=None), ): start_process = start_datetime + process_after duration = end_datetime - start_process diff --git a/docs_src/extra_data_types/tutorial001_py310.py b/docs_src/extra_data_types/tutorial001_py310.py index 4a33481b7..d22f81888 100644 --- a/docs_src/extra_data_types/tutorial001_py310.py +++ b/docs_src/extra_data_types/tutorial001_py310.py @@ -9,10 +9,10 @@ app = FastAPI() @app.put("/items/{item_id}") async def read_items( item_id: UUID, - start_datetime: datetime | None = Body(None), - end_datetime: datetime | None = Body(None), - repeat_at: time | None = Body(None), - process_after: timedelta | None = Body(None), + start_datetime: datetime | None = Body(default=None), + end_datetime: datetime | None = Body(default=None), + repeat_at: time | None = Body(default=None), + process_after: timedelta | None = Body(default=None), ): start_process = start_datetime + process_after duration = end_datetime - start_process diff --git a/docs_src/header_params/tutorial001.py b/docs_src/header_params/tutorial001.py index 7d69b027e..1df561a12 100644 --- a/docs_src/header_params/tutorial001.py +++ b/docs_src/header_params/tutorial001.py @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(user_agent: Optional[str] = Header(None)): +async def read_items(user_agent: Optional[str] = Header(default=None)): return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_py310.py b/docs_src/header_params/tutorial001_py310.py index b28463346..2203ed1b8 100644 --- a/docs_src/header_params/tutorial001_py310.py +++ b/docs_src/header_params/tutorial001_py310.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(user_agent: str | None = Header(None)): +async def read_items(user_agent: str | None = Header(default=None)): return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002.py b/docs_src/header_params/tutorial002.py index 2de3dddd7..2250727f6 100644 --- a/docs_src/header_params/tutorial002.py +++ b/docs_src/header_params/tutorial002.py @@ -7,6 +7,6 @@ app = FastAPI() @app.get("/items/") async def read_items( - strange_header: Optional[str] = Header(None, convert_underscores=False) + strange_header: Optional[str] = Header(default=None, convert_underscores=False) ): return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_py310.py b/docs_src/header_params/tutorial002_py310.py index 98ab5a807..b7979b542 100644 --- a/docs_src/header_params/tutorial002_py310.py +++ b/docs_src/header_params/tutorial002_py310.py @@ -5,6 +5,6 @@ app = FastAPI() @app.get("/items/") async def read_items( - strange_header: str | None = Header(None, convert_underscores=False) + strange_header: str | None = Header(default=None, convert_underscores=False) ): return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003.py b/docs_src/header_params/tutorial003.py index 6d0eefdd2..1ef131cee 100644 --- a/docs_src/header_params/tutorial003.py +++ b/docs_src/header_params/tutorial003.py @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: Optional[List[str]] = Header(None)): +async def read_items(x_token: Optional[List[str]] = Header(default=None)): return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_py310.py b/docs_src/header_params/tutorial003_py310.py index 2dac2c13c..435c67574 100644 --- a/docs_src/header_params/tutorial003_py310.py +++ b/docs_src/header_params/tutorial003_py310.py @@ -4,5 +4,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: list[str] | None = Header(None)): +async def read_items(x_token: list[str] | None = Header(default=None)): return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_py39.py b/docs_src/header_params/tutorial003_py39.py index 359766527..78dda58da 100644 --- a/docs_src/header_params/tutorial003_py39.py +++ b/docs_src/header_params/tutorial003_py39.py @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: Optional[list[str]] = Header(None)): +async def read_items(x_token: Optional[list[str]] = Header(default=None)): return {"X-Token values": x_token} diff --git a/docs_src/path_params_numeric_validations/tutorial001.py b/docs_src/path_params_numeric_validations/tutorial001.py index 11777bba7..530147028 100644 --- a/docs_src/path_params_numeric_validations/tutorial001.py +++ b/docs_src/path_params_numeric_validations/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Path, Query @@ -7,8 +7,8 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_items( - item_id: int = Path(..., title="The ID of the item to get"), - q: Optional[str] = Query(None, alias="item-query"), + item_id: int = Path(title="The ID of the item to get"), + q: Union[str, None] = Query(default=None, alias="item-query"), ): results = {"item_id": item_id} if q: diff --git a/docs_src/path_params_numeric_validations/tutorial001_py310.py b/docs_src/path_params_numeric_validations/tutorial001_py310.py index b940a0949..b1a77cc9d 100644 --- a/docs_src/path_params_numeric_validations/tutorial001_py310.py +++ b/docs_src/path_params_numeric_validations/tutorial001_py310.py @@ -5,8 +5,8 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_items( - item_id: int = Path(..., title="The ID of the item to get"), - q: str | None = Query(None, alias="item-query"), + item_id: int = Path(title="The ID of the item to get"), + q: str | None = Query(default=None, alias="item-query"), ): results = {"item_id": item_id} if q: diff --git a/docs_src/path_params_numeric_validations/tutorial002.py b/docs_src/path_params_numeric_validations/tutorial002.py index 57ca50ece..63ac691a8 100644 --- a/docs_src/path_params_numeric_validations/tutorial002.py +++ b/docs_src/path_params_numeric_validations/tutorial002.py @@ -4,9 +4,7 @@ app = FastAPI() @app.get("/items/{item_id}") -async def read_items( - q: str, item_id: int = Path(..., title="The ID of the item to get") -): +async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")): results = {"item_id": item_id} if q: results.update({"q": q}) diff --git a/docs_src/path_params_numeric_validations/tutorial003.py b/docs_src/path_params_numeric_validations/tutorial003.py index b6b5a1986..8df0ffc62 100644 --- a/docs_src/path_params_numeric_validations/tutorial003.py +++ b/docs_src/path_params_numeric_validations/tutorial003.py @@ -4,9 +4,7 @@ app = FastAPI() @app.get("/items/{item_id}") -async def read_items( - *, item_id: int = Path(..., title="The ID of the item to get"), q: str -): +async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str): results = {"item_id": item_id} if q: results.update({"q": q}) diff --git a/docs_src/path_params_numeric_validations/tutorial004.py b/docs_src/path_params_numeric_validations/tutorial004.py index 2ec708280..86651d47c 100644 --- a/docs_src/path_params_numeric_validations/tutorial004.py +++ b/docs_src/path_params_numeric_validations/tutorial004.py @@ -5,7 +5,7 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_items( - *, item_id: int = Path(..., title="The ID of the item to get", ge=1), q: str + *, item_id: int = Path(title="The ID of the item to get", ge=1), q: str ): results = {"item_id": item_id} if q: diff --git a/docs_src/path_params_numeric_validations/tutorial005.py b/docs_src/path_params_numeric_validations/tutorial005.py index 2809f37b2..8f12f2da0 100644 --- a/docs_src/path_params_numeric_validations/tutorial005.py +++ b/docs_src/path_params_numeric_validations/tutorial005.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_items( *, - item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000), + item_id: int = Path(title="The ID of the item to get", gt=0, le=1000), q: str, ): results = {"item_id": item_id} diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py index 0c19579f5..85bd6e8b4 100644 --- a/docs_src/path_params_numeric_validations/tutorial006.py +++ b/docs_src/path_params_numeric_validations/tutorial006.py @@ -6,9 +6,9 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_items( *, - item_id: int = Path(..., title="The ID of the item to get", ge=0, le=1000), + item_id: int = Path(title="The ID of the item to get", ge=0, le=1000), q: str, - size: float = Query(..., gt=0, lt=10.5) + size: float = Query(gt=0, lt=10.5) ): results = {"item_id": item_id} if q: diff --git a/docs_src/query_params_str_validations/tutorial002.py b/docs_src/query_params_str_validations/tutorial002.py index 68ea58206..17e017b7e 100644 --- a/docs_src/query_params_str_validations/tutorial002.py +++ b/docs_src/query_params_str_validations/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[str] = Query(None, max_length=50)): +async def read_items(q: Union[str, None] = Query(default=None, max_length=50)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial002_py310.py b/docs_src/query_params_str_validations/tutorial002_py310.py index fa3139d5a..f15351d29 100644 --- a/docs_src/query_params_str_validations/tutorial002_py310.py +++ b/docs_src/query_params_str_validations/tutorial002_py310.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str | None = Query(None, max_length=50)): +async def read_items(q: str | None = Query(default=None, max_length=50)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial003.py b/docs_src/query_params_str_validations/tutorial003.py index e52acc72f..73d2e08c8 100644 --- a/docs_src/query_params_str_validations/tutorial003.py +++ b/docs_src/query_params_str_validations/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -6,7 +6,9 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[str] = Query(None, min_length=3, max_length=50)): +async def read_items( + q: Union[str, None] = Query(default=None, min_length=3, max_length=50) +): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial003_py310.py b/docs_src/query_params_str_validations/tutorial003_py310.py index 335858a40..dc60ecb39 100644 --- a/docs_src/query_params_str_validations/tutorial003_py310.py +++ b/docs_src/query_params_str_validations/tutorial003_py310.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str | None = Query(None, min_length=3, max_length=50)): +async def read_items(q: str | None = Query(default=None, min_length=3, max_length=50)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py index d2c30331f..5a7129816 100644 --- a/docs_src/query_params_str_validations/tutorial004.py +++ b/docs_src/query_params_str_validations/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -7,7 +7,9 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: Optional[str] = Query(None, min_length=3, max_length=50, regex="^fixedquery$") + q: Union[str, None] = Query( + default=None, min_length=3, max_length=50, regex="^fixedquery$" + ) ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py index 518b779f7..180a2e511 100644 --- a/docs_src/query_params_str_validations/tutorial004_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -5,7 +5,8 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str | None = Query(None, min_length=3, max_length=50, regex="^fixedquery$") + q: str + | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$") ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial005.py b/docs_src/query_params_str_validations/tutorial005.py index 22eb3acba..8ab42869e 100644 --- a/docs_src/query_params_str_validations/tutorial005.py +++ b/docs_src/query_params_str_validations/tutorial005.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str = Query("fixedquery", min_length=3)): +async def read_items(q: str = Query(default="fixedquery", min_length=3)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006.py b/docs_src/query_params_str_validations/tutorial006.py index 720bf07f1..9a90eb64e 100644 --- a/docs_src/query_params_str_validations/tutorial006.py +++ b/docs_src/query_params_str_validations/tutorial006.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str = Query(..., min_length=3)): +async def read_items(q: str = Query(min_length=3)): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial006b.py b/docs_src/query_params_str_validations/tutorial006b.py new file mode 100644 index 000000000..a8d69c889 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str = Query(default=..., min_length=3)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c.py b/docs_src/query_params_str_validations/tutorial006c.py new file mode 100644 index 000000000..2ac148c94 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Union[str, None] = Query(default=..., min_length=3)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_py310.py b/docs_src/query_params_str_validations/tutorial006c_py310.py new file mode 100644 index 000000000..82dd9e5d7 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str | None = Query(default=..., min_length=3)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d.py b/docs_src/query_params_str_validations/tutorial006d.py new file mode 100644 index 000000000..42c5bf4eb --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from pydantic import Required + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: str = Query(default=Required, min_length=3)): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007.py b/docs_src/query_params_str_validations/tutorial007.py index e360feda9..cb836569e 100644 --- a/docs_src/query_params_str_validations/tutorial007.py +++ b/docs_src/query_params_str_validations/tutorial007.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: Optional[str] = Query(None, title="Query string", min_length=3) + q: Union[str, None] = Query(default=None, title="Query string", min_length=3) ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial007_py310.py b/docs_src/query_params_str_validations/tutorial007_py310.py index 14ef4cb69..e3e1ef2e0 100644 --- a/docs_src/query_params_str_validations/tutorial007_py310.py +++ b/docs_src/query_params_str_validations/tutorial007_py310.py @@ -4,7 +4,9 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str | None = Query(None, title="Query string", min_length=3)): +async def read_items( + q: str | None = Query(default=None, title="Query string", min_length=3) +): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial008.py b/docs_src/query_params_str_validations/tutorial008.py index 238add471..d112a9ab8 100644 --- a/docs_src/query_params_str_validations/tutorial008.py +++ b/docs_src/query_params_str_validations/tutorial008.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -7,8 +7,8 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: Optional[str] = Query( - None, + q: Union[str, None] = Query( + default=None, title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, diff --git a/docs_src/query_params_str_validations/tutorial008_py310.py b/docs_src/query_params_str_validations/tutorial008_py310.py index 06bb02442..489f631d5 100644 --- a/docs_src/query_params_str_validations/tutorial008_py310.py +++ b/docs_src/query_params_str_validations/tutorial008_py310.py @@ -7,7 +7,7 @@ app = FastAPI() async def read_items( q: str | None = Query( - None, + default=None, title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, diff --git a/docs_src/query_params_str_validations/tutorial009.py b/docs_src/query_params_str_validations/tutorial009.py index 7e5c0b81a..8a6bfe2d9 100644 --- a/docs_src/query_params_str_validations/tutorial009.py +++ b/docs_src/query_params_str_validations/tutorial009.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[str] = Query(None, alias="item-query")): +async def read_items(q: Union[str, None] = Query(default=None, alias="item-query")): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial009_py310.py b/docs_src/query_params_str_validations/tutorial009_py310.py index e84c116f1..a38d32cbb 100644 --- a/docs_src/query_params_str_validations/tutorial009_py310.py +++ b/docs_src/query_params_str_validations/tutorial009_py310.py @@ -4,7 +4,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: str | None = Query(None, alias="item-query")): +async def read_items(q: str | None = Query(default=None, alias="item-query")): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py index 7921506b6..35443d194 100644 --- a/docs_src/query_params_str_validations/tutorial010.py +++ b/docs_src/query_params_str_validations/tutorial010.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -7,8 +7,8 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: Optional[str] = Query( - None, + q: Union[str, None] = Query( + default=None, alias="item-query", title="Query string", description="Query string for the items to search in the database that have a good match", diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py index c35800858..f2839516e 100644 --- a/docs_src/query_params_str_validations/tutorial010_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -7,7 +7,7 @@ app = FastAPI() async def read_items( q: str | None = Query( - None, + default=None, alias="item-query", title="Query string", description="Query string for the items to search in the database that have a good match", diff --git a/docs_src/query_params_str_validations/tutorial011.py b/docs_src/query_params_str_validations/tutorial011.py index 7fda267ed..65bbce781 100644 --- a/docs_src/query_params_str_validations/tutorial011.py +++ b/docs_src/query_params_str_validations/tutorial011.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI, Query @@ -6,6 +6,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[List[str]] = Query(None)): +async def read_items(q: Union[List[str], None] = Query(default=None)): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_py310.py b/docs_src/query_params_str_validations/tutorial011_py310.py index c3d992e62..70155de7c 100644 --- a/docs_src/query_params_str_validations/tutorial011_py310.py +++ b/docs_src/query_params_str_validations/tutorial011_py310.py @@ -4,6 +4,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: list[str] | None = Query(None)): +async def read_items(q: list[str] | None = Query(default=None)): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_py39.py b/docs_src/query_params_str_validations/tutorial011_py39.py index 38ba764d6..878f95c79 100644 --- a/docs_src/query_params_str_validations/tutorial011_py39.py +++ b/docs_src/query_params_str_validations/tutorial011_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -6,6 +6,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[list[str]] = Query(None)): +async def read_items(q: Union[list[str], None] = Query(default=None)): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial012.py b/docs_src/query_params_str_validations/tutorial012.py index 7ea9f017d..e77d56974 100644 --- a/docs_src/query_params_str_validations/tutorial012.py +++ b/docs_src/query_params_str_validations/tutorial012.py @@ -6,6 +6,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: List[str] = Query(["foo", "bar"])): +async def read_items(q: List[str] = Query(default=["foo", "bar"])): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_py39.py b/docs_src/query_params_str_validations/tutorial012_py39.py index 1900133d9..070d0b04b 100644 --- a/docs_src/query_params_str_validations/tutorial012_py39.py +++ b/docs_src/query_params_str_validations/tutorial012_py39.py @@ -4,6 +4,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: list[str] = Query(["foo", "bar"])): +async def read_items(q: list[str] = Query(default=["foo", "bar"])): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial013.py b/docs_src/query_params_str_validations/tutorial013.py index 95dd6999d..0b0f44869 100644 --- a/docs_src/query_params_str_validations/tutorial013.py +++ b/docs_src/query_params_str_validations/tutorial013.py @@ -4,6 +4,6 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: list = Query([])): +async def read_items(q: list = Query(default=[])): query_items = {"q": q} return query_items diff --git a/docs_src/query_params_str_validations/tutorial014.py b/docs_src/query_params_str_validations/tutorial014.py index fb50bc27b..50e0a6c2b 100644 --- a/docs_src/query_params_str_validations/tutorial014.py +++ b/docs_src/query_params_str_validations/tutorial014.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Query @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/items/") async def read_items( - hidden_query: Optional[str] = Query(None, include_in_schema=False) + hidden_query: Union[str, None] = Query(default=None, include_in_schema=False) ): if hidden_query: return {"hidden_query": hidden_query} diff --git a/docs_src/query_params_str_validations/tutorial014_py310.py b/docs_src/query_params_str_validations/tutorial014_py310.py index 7ae39c7f9..1b617efdd 100644 --- a/docs_src/query_params_str_validations/tutorial014_py310.py +++ b/docs_src/query_params_str_validations/tutorial014_py310.py @@ -4,7 +4,9 @@ app = FastAPI() @app.get("/items/") -async def read_items(hidden_query: str | None = Query(None, include_in_schema=False)): +async def read_items( + hidden_query: str | None = Query(default=None, include_in_schema=False) +): if hidden_query: return {"hidden_query": hidden_query} else: diff --git a/docs_src/request_files/tutorial001.py b/docs_src/request_files/tutorial001.py index 0fb1dd571..2e0ea6391 100644 --- a/docs_src/request_files/tutorial001.py +++ b/docs_src/request_files/tutorial001.py @@ -4,7 +4,7 @@ app = FastAPI() @app.post("/files/") -async def create_file(file: bytes = File(...)): +async def create_file(file: bytes = File()): return {"file_size": len(file)} diff --git a/docs_src/request_files/tutorial001_02.py b/docs_src/request_files/tutorial001_02.py index 26a4c9cbf..3f311c4b8 100644 --- a/docs_src/request_files/tutorial001_02.py +++ b/docs_src/request_files/tutorial001_02.py @@ -6,7 +6,7 @@ app = FastAPI() @app.post("/files/") -async def create_file(file: Optional[bytes] = File(None)): +async def create_file(file: Optional[bytes] = File(default=None)): if not file: return {"message": "No file sent"} else: diff --git a/docs_src/request_files/tutorial001_02_py310.py b/docs_src/request_files/tutorial001_02_py310.py index 0e576251b..298c9974f 100644 --- a/docs_src/request_files/tutorial001_02_py310.py +++ b/docs_src/request_files/tutorial001_02_py310.py @@ -4,7 +4,7 @@ app = FastAPI() @app.post("/files/") -async def create_file(file: bytes | None = File(None)): +async def create_file(file: bytes | None = File(default=None)): if not file: return {"message": "No file sent"} else: diff --git a/docs_src/request_files/tutorial001_03.py b/docs_src/request_files/tutorial001_03.py index abcac9e4c..d8005cc7d 100644 --- a/docs_src/request_files/tutorial001_03.py +++ b/docs_src/request_files/tutorial001_03.py @@ -4,12 +4,12 @@ app = FastAPI() @app.post("/files/") -async def create_file(file: bytes = File(..., description="A file read as bytes")): +async def create_file(file: bytes = File(description="A file read as bytes")): return {"file_size": len(file)} @app.post("/uploadfile/") async def create_upload_file( - file: UploadFile = File(..., description="A file read as UploadFile") + file: UploadFile = File(description="A file read as UploadFile"), ): return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002.py b/docs_src/request_files/tutorial002.py index 94abb7c6c..b4d0acc68 100644 --- a/docs_src/request_files/tutorial002.py +++ b/docs_src/request_files/tutorial002.py @@ -7,7 +7,7 @@ app = FastAPI() @app.post("/files/") -async def create_files(files: List[bytes] = File(...)): +async def create_files(files: List[bytes] = File()): return {"file_sizes": [len(file) for file in files]} diff --git a/docs_src/request_files/tutorial002_py39.py b/docs_src/request_files/tutorial002_py39.py index 2779618bd..b64cf5598 100644 --- a/docs_src/request_files/tutorial002_py39.py +++ b/docs_src/request_files/tutorial002_py39.py @@ -5,7 +5,7 @@ app = FastAPI() @app.post("/files/") -async def create_files(files: list[bytes] = File(...)): +async def create_files(files: list[bytes] = File()): return {"file_sizes": [len(file) for file in files]} diff --git a/docs_src/request_files/tutorial003.py b/docs_src/request_files/tutorial003.py index 4a91b7a8b..e3f805f60 100644 --- a/docs_src/request_files/tutorial003.py +++ b/docs_src/request_files/tutorial003.py @@ -8,14 +8,14 @@ app = FastAPI() @app.post("/files/") async def create_files( - files: List[bytes] = File(..., description="Multiple files as bytes") + files: List[bytes] = File(description="Multiple files as bytes"), ): return {"file_sizes": [len(file) for file in files]} @app.post("/uploadfiles/") async def create_upload_files( - files: List[UploadFile] = File(..., description="Multiple files as UploadFile") + files: List[UploadFile] = File(description="Multiple files as UploadFile"), ): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_files/tutorial003_py39.py b/docs_src/request_files/tutorial003_py39.py index d853f48d1..96f5e8742 100644 --- a/docs_src/request_files/tutorial003_py39.py +++ b/docs_src/request_files/tutorial003_py39.py @@ -6,14 +6,14 @@ app = FastAPI() @app.post("/files/") async def create_files( - files: list[bytes] = File(..., description="Multiple files as bytes") + files: list[bytes] = File(description="Multiple files as bytes"), ): return {"file_sizes": [len(file) for file in files]} @app.post("/uploadfiles/") async def create_upload_files( - files: list[UploadFile] = File(..., description="Multiple files as UploadFile") + files: list[UploadFile] = File(description="Multiple files as UploadFile"), ): return {"filenames": [file.filename for file in files]} diff --git a/docs_src/request_forms/tutorial001.py b/docs_src/request_forms/tutorial001.py index c07e22945..a53770001 100644 --- a/docs_src/request_forms/tutorial001.py +++ b/docs_src/request_forms/tutorial001.py @@ -4,5 +4,5 @@ app = FastAPI() @app.post("/login/") -async def login(username: str = Form(...), password: str = Form(...)): +async def login(username: str = Form(), password: str = Form()): return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001.py b/docs_src/request_forms_and_files/tutorial001.py index 5bf3a5bc0..7b5224ce5 100644 --- a/docs_src/request_forms_and_files/tutorial001.py +++ b/docs_src/request_forms_and_files/tutorial001.py @@ -5,7 +5,7 @@ app = FastAPI() @app.post("/files/") async def create_file( - file: bytes = File(...), fileb: UploadFile = File(...), token: str = Form(...) + file: bytes = File(), fileb: UploadFile = File(), token: str = Form() ): return { "file_size": len(file), diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002.py index df3df8854..a2aec46f5 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -7,10 +7,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(..., example="Foo") - description: Optional[str] = Field(None, example="A very nice Item") - price: float = Field(..., example=35.4) - tax: Optional[float] = Field(None, example=3.2) + name: str = Field(example="Foo") + description: Optional[str] = Field(default=None, example="A very nice Item") + price: float = Field(example=35.4) + tax: Optional[float] = Field(default=None, example=3.2) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py index 4f8f8304e..e84928bb1 100644 --- a/docs_src/schema_extra_example/tutorial002_py310.py +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -5,10 +5,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(..., example="Foo") - description: str | None = Field(None, example="A very nice Item") - price: float = Field(..., example=35.4) - tax: float | None = Field(None, example=3.2) + name: str = Field(example="Foo") + description: str | None = Field(default=None, example="A very nice Item") + price: float = Field(example=35.4) + tax: float | None = Field(default=None, example=3.2) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index 58c79f554..43d46b81b 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -17,7 +17,6 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - ..., example={ "name": "Foo", "description": "A very nice Item", diff --git a/docs_src/schema_extra_example/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py index cf4c99dc0..1e137101d 100644 --- a/docs_src/schema_extra_example/tutorial003_py310.py +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -15,7 +15,6 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - ..., example={ "name": "Foo", "description": "A very nice Item", diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index 9f0e8b437..42d7a04a3 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -18,7 +18,6 @@ async def update_item( *, item_id: int, item: Item = Body( - ..., examples={ "normal": { "summary": "A normal example", diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index 6f29c1a5c..100a30860 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -16,7 +16,6 @@ async def update_item( *, item_id: int, item: Item = Body( - ..., examples={ "normal": { "summary": "A normal example", diff --git a/docs_src/websockets/tutorial002.py b/docs_src/websockets/tutorial002.py index 53cdb41ff..b01008530 100644 --- a/docs_src/websockets/tutorial002.py +++ b/docs_src/websockets/tutorial002.py @@ -57,8 +57,8 @@ async def get(): async def get_cookie_or_token( websocket: WebSocket, - session: Optional[str] = Cookie(None), - token: Optional[str] = Query(None), + session: Optional[str] = Cookie(default=None), + token: Optional[str] = Query(default=None), ): if session is None and token is None: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 9dccd354e..f397e333c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -43,6 +43,7 @@ from pydantic.fields import ( FieldInfo, ModelField, Required, + Undefined, ) from pydantic.schema import get_annotation_from_field_info from pydantic.typing import ForwardRef, evaluate_forwardref @@ -316,7 +317,7 @@ def get_dependant( field_info = param_field.field_info assert isinstance( field_info, params.Body - ), f"Param: {param_field.name} can only be a request body, using Body(...)" + ), f"Param: {param_field.name} can only be a request body, using Body()" dependant.body_params.append(param_field) return dependant @@ -353,7 +354,7 @@ def get_param_field( force_type: Optional[params.ParamTypes] = None, ignore_default: bool = False, ) -> ModelField: - default_value = Required + default_value: Any = Undefined had_schema = False if not param.default == param.empty and ignore_default is False: default_value = param.default @@ -369,8 +370,13 @@ def get_param_field( if force_type: field_info.in_ = force_type # type: ignore else: - field_info = default_field_info(default_value) - required = default_value == Required + field_info = default_field_info(default=default_value) + required = True + if default_value is Required or ignore_default: + required = True + default_value = None + elif default_value is not Undefined: + required = False annotation: Any = Any if not param.annotation == param.empty: annotation = param.annotation @@ -382,12 +388,11 @@ def get_param_field( field = create_response_field( name=param.name, type_=annotation, - default=None if required else default_value, + default=default_value, alias=alias, required=required, field_info=field_info, ) - field.required = required if not had_schema and not is_scalar_field(field=field): field.field_info = params.Body(field_info.default) if not had_schema and lenient_issubclass(field.type_, UploadFile): diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 9c6598d2d..35aa1672b 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -73,7 +73,7 @@ class Server(BaseModel): class Reference(BaseModel): - ref: str = Field(..., alias="$ref") + ref: str = Field(alias="$ref") class Discriminator(BaseModel): @@ -101,28 +101,28 @@ class ExternalDocumentation(BaseModel): class Schema(BaseModel): - ref: Optional[str] = Field(None, alias="$ref") + ref: Optional[str] = Field(default=None, alias="$ref") title: Optional[str] = None multipleOf: Optional[float] = None maximum: Optional[float] = None exclusiveMaximum: Optional[float] = None minimum: Optional[float] = None exclusiveMinimum: Optional[float] = None - maxLength: Optional[int] = Field(None, gte=0) - minLength: Optional[int] = Field(None, gte=0) + maxLength: Optional[int] = Field(default=None, gte=0) + minLength: Optional[int] = Field(default=None, gte=0) pattern: Optional[str] = None - maxItems: Optional[int] = Field(None, gte=0) - minItems: Optional[int] = Field(None, gte=0) + maxItems: Optional[int] = Field(default=None, gte=0) + minItems: Optional[int] = Field(default=None, gte=0) uniqueItems: Optional[bool] = None - maxProperties: Optional[int] = Field(None, gte=0) - minProperties: Optional[int] = Field(None, gte=0) + maxProperties: Optional[int] = Field(default=None, gte=0) + minProperties: Optional[int] = Field(default=None, gte=0) required: Optional[List[str]] = None enum: Optional[List[Any]] = None type: Optional[str] = None allOf: Optional[List["Schema"]] = None oneOf: Optional[List["Schema"]] = None anyOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(None, alias="not") + not_: Optional["Schema"] = Field(default=None, alias="not") items: Optional[Union["Schema", List["Schema"]]] = None properties: Optional[Dict[str, "Schema"]] = None additionalProperties: Optional[Union["Schema", Reference, bool]] = None @@ -171,7 +171,7 @@ class Encoding(BaseModel): class MediaType(BaseModel): - schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema") + schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") example: Optional[Any] = None examples: Optional[Dict[str, Union[Example, Reference]]] = None encoding: Optional[Dict[str, Encoding]] = None @@ -188,7 +188,7 @@ class ParameterBase(BaseModel): style: Optional[str] = None explode: Optional[bool] = None allowReserved: Optional[bool] = None - schema_: Optional[Union[Schema, Reference]] = Field(None, alias="schema") + schema_: Optional[Union[Schema, Reference]] = Field(default=None, alias="schema") example: Optional[Any] = None examples: Optional[Dict[str, Union[Example, Reference]]] = None # Serialization rules for more complex scenarios @@ -200,7 +200,7 @@ class ParameterBase(BaseModel): class Parameter(ParameterBase): name: str - in_: ParameterInType = Field(..., alias="in") + in_: ParameterInType = Field(alias="in") class Header(ParameterBase): @@ -258,7 +258,7 @@ class Operation(BaseModel): class PathItem(BaseModel): - ref: Optional[str] = Field(None, alias="$ref") + ref: Optional[str] = Field(default=None, alias="$ref") summary: Optional[str] = None description: Optional[str] = None get: Optional[Operation] = None @@ -284,7 +284,7 @@ class SecuritySchemeType(Enum): class SecurityBase(BaseModel): - type_: SecuritySchemeType = Field(..., alias="type") + type_: SecuritySchemeType = Field(alias="type") description: Optional[str] = None class Config: @@ -299,7 +299,7 @@ class APIKeyIn(Enum): class APIKey(SecurityBase): type_ = Field(SecuritySchemeType.apiKey, alias="type") - in_: APIKeyIn = Field(..., alias="in") + in_: APIKeyIn = Field(alias="in") name: str diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index a553a1461..1932ef065 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,7 +5,7 @@ from pydantic.fields import Undefined def Path( # noqa: N802 - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -44,7 +44,7 @@ def Path( # noqa: N802 def Query( # noqa: N802 - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -63,7 +63,7 @@ def Query( # noqa: N802 **extra: Any, ) -> Any: return params.Query( - default, + default=default, alias=alias, title=title, description=description, @@ -83,7 +83,7 @@ def Query( # noqa: N802 def Header( # noqa: N802 - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, convert_underscores: bool = True, @@ -103,7 +103,7 @@ def Header( # noqa: N802 **extra: Any, ) -> Any: return params.Header( - default, + default=default, alias=alias, convert_underscores=convert_underscores, title=title, @@ -124,7 +124,7 @@ def Header( # noqa: N802 def Cookie( # noqa: N802 - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -143,7 +143,7 @@ def Cookie( # noqa: N802 **extra: Any, ) -> Any: return params.Cookie( - default, + default=default, alias=alias, title=title, description=description, @@ -163,7 +163,7 @@ def Cookie( # noqa: N802 def Body( # noqa: N802 - default: Any, + default: Any = Undefined, *, embed: bool = False, media_type: str = "application/json", @@ -182,7 +182,7 @@ def Body( # noqa: N802 **extra: Any, ) -> Any: return params.Body( - default, + default=default, embed=embed, media_type=media_type, alias=alias, @@ -202,7 +202,7 @@ def Body( # noqa: N802 def Form( # noqa: N802 - default: Any, + default: Any = Undefined, *, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, @@ -220,7 +220,7 @@ def Form( # noqa: N802 **extra: Any, ) -> Any: return params.Form( - default, + default=default, media_type=media_type, alias=alias, title=title, @@ -239,7 +239,7 @@ def Form( # noqa: N802 def File( # noqa: N802 - default: Any, + default: Any = Undefined, *, media_type: str = "multipart/form-data", alias: Optional[str] = None, @@ -257,7 +257,7 @@ def File( # noqa: N802 **extra: Any, ) -> Any: return params.File( - default, + default=default, media_type=media_type, alias=alias, title=title, diff --git a/fastapi/params.py b/fastapi/params.py index 042bbd42f..5395b98a3 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -16,7 +16,7 @@ class Param(FieldInfo): def __init__( self, - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -39,7 +39,7 @@ class Param(FieldInfo): self.examples = examples self.include_in_schema = include_in_schema super().__init__( - default, + default=default, alias=alias, title=title, description=description, @@ -62,7 +62,7 @@ class Path(Param): def __init__( self, - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -82,7 +82,7 @@ class Path(Param): ): self.in_ = self.in_ super().__init__( - ..., + default=..., alias=alias, title=title, description=description, @@ -106,7 +106,7 @@ class Query(Param): def __init__( self, - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -125,7 +125,7 @@ class Query(Param): **extra: Any, ): super().__init__( - default, + default=default, alias=alias, title=title, description=description, @@ -149,7 +149,7 @@ class Header(Param): def __init__( self, - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, convert_underscores: bool = True, @@ -170,7 +170,7 @@ class Header(Param): ): self.convert_underscores = convert_underscores super().__init__( - default, + default=default, alias=alias, title=title, description=description, @@ -194,7 +194,7 @@ class Cookie(Param): def __init__( self, - default: Any, + default: Any = Undefined, *, alias: Optional[str] = None, title: Optional[str] = None, @@ -213,7 +213,7 @@ class Cookie(Param): **extra: Any, ): super().__init__( - default, + default=default, alias=alias, title=title, description=description, @@ -235,7 +235,7 @@ class Cookie(Param): class Body(FieldInfo): def __init__( self, - default: Any, + default: Any = Undefined, *, embed: bool = False, media_type: str = "application/json", @@ -258,7 +258,7 @@ class Body(FieldInfo): self.example = example self.examples = examples super().__init__( - default, + default=default, alias=alias, title=title, description=description, @@ -297,7 +297,7 @@ class Form(Body): **extra: Any, ): super().__init__( - default, + default=default, embed=True, media_type=media_type, alias=alias, @@ -337,7 +337,7 @@ class File(Form): **extra: Any, ): super().__init__( - default, + default=default, media_type=media_type, alias=alias, title=title, diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index bdc6e2ea9..888208c15 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -45,12 +45,12 @@ class OAuth2PasswordRequestForm: def __init__( self, - grant_type: str = Form(None, regex="password"), - username: str = Form(...), - password: str = Form(...), - scope: str = Form(""), - client_id: Optional[str] = Form(None), - client_secret: Optional[str] = Form(None), + grant_type: str = Form(default=None, regex="password"), + username: str = Form(), + password: str = Form(), + scope: str = Form(default=""), + client_id: Optional[str] = Form(default=None), + client_secret: Optional[str] = Form(default=None), ): self.grant_type = grant_type self.username = username @@ -95,12 +95,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): def __init__( self, - grant_type: str = Form(..., regex="password"), - username: str = Form(...), - password: str = Form(...), - scope: str = Form(""), - client_id: Optional[str] = Form(None), - client_secret: Optional[str] = Form(None), + grant_type: str = Form(regex="password"), + username: str = Form(), + password: str = Form(), + scope: str = Form(default=""), + client_id: Optional[str] = Form(default=None), + client_secret: Optional[str] = Form(default=None), ): super().__init__( grant_type=grant_type, diff --git a/fastapi/utils.py b/fastapi/utils.py index 9d720feb3..a7e135bca 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -52,7 +52,7 @@ def create_response_field( Create a new response field. Raises if type_ is invalid. """ class_validators = class_validators or {} - field_info = field_info or FieldInfo(None) + field_info = field_info or FieldInfo() response_field = functools.partial( ModelField, diff --git a/tests/main.py b/tests/main.py index d5603d0e6..f70496db8 100644 --- a/tests/main.py +++ b/tests/main.py @@ -49,97 +49,97 @@ def get_bool_id(item_id: bool): @app.get("/path/param/{item_id}") -def get_path_param_id(item_id: Optional[str] = Path(None)): +def get_path_param_id(item_id: str = Path()): return item_id @app.get("/path/param-required/{item_id}") -def get_path_param_required_id(item_id: str = Path(...)): +def get_path_param_required_id(item_id: str = Path()): return item_id @app.get("/path/param-minlength/{item_id}") -def get_path_param_min_length(item_id: str = Path(..., min_length=3)): +def get_path_param_min_length(item_id: str = Path(min_length=3)): return item_id @app.get("/path/param-maxlength/{item_id}") -def get_path_param_max_length(item_id: str = Path(..., max_length=3)): +def get_path_param_max_length(item_id: str = Path(max_length=3)): return item_id @app.get("/path/param-min_maxlength/{item_id}") -def get_path_param_min_max_length(item_id: str = Path(..., max_length=3, min_length=2)): +def get_path_param_min_max_length(item_id: str = Path(max_length=3, min_length=2)): return item_id @app.get("/path/param-gt/{item_id}") -def get_path_param_gt(item_id: float = Path(..., gt=3)): +def get_path_param_gt(item_id: float = Path(gt=3)): return item_id @app.get("/path/param-gt0/{item_id}") -def get_path_param_gt0(item_id: float = Path(..., gt=0)): +def get_path_param_gt0(item_id: float = Path(gt=0)): return item_id @app.get("/path/param-ge/{item_id}") -def get_path_param_ge(item_id: float = Path(..., ge=3)): +def get_path_param_ge(item_id: float = Path(ge=3)): return item_id @app.get("/path/param-lt/{item_id}") -def get_path_param_lt(item_id: float = Path(..., lt=3)): +def get_path_param_lt(item_id: float = Path(lt=3)): return item_id @app.get("/path/param-lt0/{item_id}") -def get_path_param_lt0(item_id: float = Path(..., lt=0)): +def get_path_param_lt0(item_id: float = Path(lt=0)): return item_id @app.get("/path/param-le/{item_id}") -def get_path_param_le(item_id: float = Path(..., le=3)): +def get_path_param_le(item_id: float = Path(le=3)): return item_id @app.get("/path/param-lt-gt/{item_id}") -def get_path_param_lt_gt(item_id: float = Path(..., lt=3, gt=1)): +def get_path_param_lt_gt(item_id: float = Path(lt=3, gt=1)): return item_id @app.get("/path/param-le-ge/{item_id}") -def get_path_param_le_ge(item_id: float = Path(..., le=3, ge=1)): +def get_path_param_le_ge(item_id: float = Path(le=3, ge=1)): return item_id @app.get("/path/param-lt-int/{item_id}") -def get_path_param_lt_int(item_id: int = Path(..., lt=3)): +def get_path_param_lt_int(item_id: int = Path(lt=3)): return item_id @app.get("/path/param-gt-int/{item_id}") -def get_path_param_gt_int(item_id: int = Path(..., gt=3)): +def get_path_param_gt_int(item_id: int = Path(gt=3)): return item_id @app.get("/path/param-le-int/{item_id}") -def get_path_param_le_int(item_id: int = Path(..., le=3)): +def get_path_param_le_int(item_id: int = Path(le=3)): return item_id @app.get("/path/param-ge-int/{item_id}") -def get_path_param_ge_int(item_id: int = Path(..., ge=3)): +def get_path_param_ge_int(item_id: int = Path(ge=3)): return item_id @app.get("/path/param-lt-gt-int/{item_id}") -def get_path_param_lt_gt_int(item_id: int = Path(..., lt=3, gt=1)): +def get_path_param_lt_gt_int(item_id: int = Path(lt=3, gt=1)): return item_id @app.get("/path/param-le-ge-int/{item_id}") -def get_path_param_le_ge_int(item_id: int = Path(..., le=3, ge=1)): +def get_path_param_le_ge_int(item_id: int = Path(le=3, ge=1)): return item_id @@ -173,19 +173,19 @@ def get_query_type_int_default(query: int = 10): @app.get("/query/param") -def get_query_param(query=Query(None)): +def get_query_param(query=Query(default=None)): if query is None: return "foo bar" return f"foo bar {query}" @app.get("/query/param-required") -def get_query_param_required(query=Query(...)): +def get_query_param_required(query=Query()): return f"foo bar {query}" @app.get("/query/param-required/int") -def get_query_param_required_type(query: int = Query(...)): +def get_query_param_required_type(query: int = Query()): return f"foo bar {query}" diff --git a/tests/test_dependency_normal_exceptions.py b/tests/test_dependency_normal_exceptions.py index 49a19f460..23c366d5d 100644 --- a/tests/test_dependency_normal_exceptions.py +++ b/tests/test_dependency_normal_exceptions.py @@ -26,14 +26,14 @@ async def get_database(): @app.put("/invalid-user/{user_id}") def put_invalid_user( - user_id: str, name: str = Body(...), db: dict = Depends(get_database) + user_id: str, name: str = Body(), db: dict = Depends(get_database) ): db[user_id] = name raise HTTPException(status_code=400, detail="Invalid user") @app.put("/user/{user_id}") -def put_user(user_id: str, name: str = Body(...), db: dict = Depends(get_database)): +def put_user(user_id: str, name: str = Body(), db: dict = Depends(get_database)): db[user_id] = name return {"message": "OK"} diff --git a/tests/test_forms_from_non_typing_sequences.py b/tests/test_forms_from_non_typing_sequences.py index be917eab7..52ce24753 100644 --- a/tests/test_forms_from_non_typing_sequences.py +++ b/tests/test_forms_from_non_typing_sequences.py @@ -5,17 +5,17 @@ app = FastAPI() @app.post("/form/python-list") -def post_form_param_list(items: list = Form(...)): +def post_form_param_list(items: list = Form()): return items @app.post("/form/python-set") -def post_form_param_set(items: set = Form(...)): +def post_form_param_set(items: set = Form()): return items @app.post("/form/python-tuple") -def post_form_param_tuple(items: tuple = Form(...)): +def post_form_param_tuple(items: tuple = Form()): return items diff --git a/tests/test_invalid_sequence_param.py b/tests/test_invalid_sequence_param.py index f00dd7b93..475786adb 100644 --- a/tests/test_invalid_sequence_param.py +++ b/tests/test_invalid_sequence_param.py @@ -13,7 +13,7 @@ def test_invalid_sequence(): title: str @app.get("/items/") - def read_items(q: List[Item] = Query(None)): + def read_items(q: List[Item] = Query(default=None)): pass # pragma: no cover @@ -25,7 +25,7 @@ def test_invalid_tuple(): title: str @app.get("/items/") - def read_items(q: Tuple[Item, Item] = Query(None)): + def read_items(q: Tuple[Item, Item] = Query(default=None)): pass # pragma: no cover @@ -37,7 +37,7 @@ def test_invalid_dict(): title: str @app.get("/items/") - def read_items(q: Dict[str, Item] = Query(None)): + def read_items(q: Dict[str, Item] = Query(default=None)): pass # pragma: no cover @@ -49,5 +49,5 @@ def test_invalid_simple_dict(): title: str @app.get("/items/") - def read_items(q: Optional[dict] = Query(None)): + def read_items(q: Optional[dict] = Query(default=None)): pass # pragma: no cover diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index fa82b5ea8..ed35fd32e 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -67,7 +67,7 @@ class ModelWithConfig(BaseModel): class ModelWithAlias(BaseModel): - foo: str = Field(..., alias="Foo") + foo: str = Field(alias="Foo") class ModelWithDefault(BaseModel): diff --git a/tests/test_modules_same_name_body/app/a.py b/tests/test_modules_same_name_body/app/a.py index 3c86c1865..377236890 100644 --- a/tests/test_modules_same_name_body/app/a.py +++ b/tests/test_modules_same_name_body/app/a.py @@ -4,5 +4,5 @@ router = APIRouter() @router.post("/compute") -def compute(a: int = Body(...), b: str = Body(...)): +def compute(a: int = Body(), b: str = Body()): return {"a": a, "b": b} diff --git a/tests/test_modules_same_name_body/app/b.py b/tests/test_modules_same_name_body/app/b.py index f7c7fdfc6..b62118f84 100644 --- a/tests/test_modules_same_name_body/app/b.py +++ b/tests/test_modules_same_name_body/app/b.py @@ -4,5 +4,5 @@ router = APIRouter() @router.post("/compute/") -def compute(a: int = Body(...), b: str = Body(...)): +def compute(a: int = Body(), b: str = Body()): return {"a": a, "b": b} diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 0a15833fa..3da461af5 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/items/") -def read_items(q: List[int] = Query(None)): +def read_items(q: List[int] = Query(default=None)): return {"q": q} diff --git a/tests/test_multipart_installation.py b/tests/test_multipart_installation.py index c8a6fd942..788d9ef5a 100644 --- a/tests/test_multipart_installation.py +++ b/tests/test_multipart_installation.py @@ -12,7 +12,7 @@ def test_incorrect_multipart_installed_form(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...)): + async def root(username: str = Form()): return username # pragma: nocover @@ -22,7 +22,7 @@ def test_incorrect_multipart_installed_file_upload(monkeypatch): app = FastAPI() @app.post("/") - async def root(f: UploadFile = File(...)): + async def root(f: UploadFile = File()): return f # pragma: nocover @@ -32,7 +32,7 @@ def test_incorrect_multipart_installed_file_bytes(monkeypatch): app = FastAPI() @app.post("/") - async def root(f: bytes = File(...)): + async def root(f: bytes = File()): return f # pragma: nocover @@ -42,7 +42,7 @@ def test_incorrect_multipart_installed_multi_form(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...), password: str = Form(...)): + async def root(username: str = Form(), password: str = Form()): return username # pragma: nocover @@ -52,7 +52,7 @@ def test_incorrect_multipart_installed_form_file(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...), f: UploadFile = File(...)): + async def root(username: str = Form(), f: UploadFile = File()): return username # pragma: nocover @@ -62,7 +62,7 @@ def test_no_multipart_installed(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...)): + async def root(username: str = Form()): return username # pragma: nocover @@ -72,7 +72,7 @@ def test_no_multipart_installed_file(monkeypatch): app = FastAPI() @app.post("/") - async def root(f: UploadFile = File(...)): + async def root(f: UploadFile = File()): return f # pragma: nocover @@ -82,7 +82,7 @@ def test_no_multipart_installed_file_bytes(monkeypatch): app = FastAPI() @app.post("/") - async def root(f: bytes = File(...)): + async def root(f: bytes = File()): return f # pragma: nocover @@ -92,7 +92,7 @@ def test_no_multipart_installed_multi_form(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...), password: str = Form(...)): + async def root(username: str = Form(), password: str = Form()): return username # pragma: nocover @@ -102,5 +102,5 @@ def test_no_multipart_installed_form_file(monkeypatch): app = FastAPI() @app.post("/") - async def root(username: str = Form(...), f: UploadFile = File(...)): + async def root(username: str = Form(), f: UploadFile = File()): return username # pragma: nocover diff --git a/tests/test_param_class.py b/tests/test_param_class.py index f5767ec96..1fd40dcd2 100644 --- a/tests/test_param_class.py +++ b/tests/test_param_class.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") -def read_items(q: Optional[str] = Param(None)): # type: ignore +def read_items(q: Optional[str] = Param(default=None)): # type: ignore return {"q": q} diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index 26aa63897..214f039b6 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -9,26 +9,26 @@ app = FastAPI() @app.get("/hidden_cookie") async def hidden_cookie( - hidden_cookie: Optional[str] = Cookie(None, include_in_schema=False) + hidden_cookie: Optional[str] = Cookie(default=None, include_in_schema=False) ): return {"hidden_cookie": hidden_cookie} @app.get("/hidden_header") async def hidden_header( - hidden_header: Optional[str] = Header(None, include_in_schema=False) + hidden_header: Optional[str] = Header(default=None, include_in_schema=False) ): return {"hidden_header": hidden_header} @app.get("/hidden_path/{hidden_path}") -async def hidden_path(hidden_path: str = Path(..., include_in_schema=False)): +async def hidden_path(hidden_path: str = Path(include_in_schema=False)): return {"hidden_path": hidden_path} @app.get("/hidden_query") async def hidden_query( - hidden_query: Optional[str] = Query(None, include_in_schema=False) + hidden_query: Optional[str] = Query(default=None, include_in_schema=False) ): return {"hidden_query": hidden_query} diff --git a/tests/test_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index 00441694e..ca0305184 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -4,7 +4,7 @@ from fastapi.testclient import TestClient app = FastAPI() -def get_header(*, someheader: str = Header(...)): +def get_header(*, someheader: str = Header()): return someheader diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index ace6bdef7..e9cf4006d 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -21,14 +21,14 @@ class Shop(BaseModel): @app.post("/products") -async def create_product(data: Product = Body(..., media_type=media_type, embed=True)): +async def create_product(data: Product = Body(media_type=media_type, embed=True)): pass # pragma: no cover @app.post("/shops") async def create_shop( - data: Shop = Body(..., media_type=media_type), - included: typing.List[Product] = Body([], media_type=media_type), + data: Shop = Body(media_type=media_type), + included: typing.List[Product] = Body(default=[], media_type=media_type), ): pass # pragma: no cover diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 444e350a8..5047aeaa4 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,3 +1,5 @@ +from typing import Union + from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel @@ -18,14 +20,13 @@ def schema_extra(item: Item): @app.post("/example/") -def example(item: Item = Body(..., example={"data": "Data in Body example"})): +def example(item: Item = Body(example={"data": "Data in Body example"})): return item @app.post("/examples/") def examples( item: Item = Body( - ..., examples={ "example1": { "summary": "example1 summary", @@ -41,7 +42,6 @@ def examples( @app.post("/example_examples/") def example_examples( item: Item = Body( - ..., example={"data": "Overriden example"}, examples={ "example1": {"value": {"data": "examples example_examples 1"}}, @@ -55,7 +55,7 @@ def example_examples( # TODO: enable these tests once/if Form(embed=False) is supported # TODO: In that case, define if File() should support example/examples too # @app.post("/form_example") -# def form_example(firstname: str = Form(..., example="John")): +# def form_example(firstname: str = Form(example="John")): # return firstname @@ -89,7 +89,6 @@ def example_examples( @app.get("/path_example/{item_id}") def path_example( item_id: str = Path( - ..., example="item_1", ), ): @@ -99,7 +98,6 @@ def path_example( @app.get("/path_examples/{item_id}") def path_examples( item_id: str = Path( - ..., examples={ "example1": {"summary": "item ID summary", "value": "item_1"}, "example2": {"value": "item_2"}, @@ -112,7 +110,6 @@ def path_examples( @app.get("/path_example_examples/{item_id}") def path_example_examples( item_id: str = Path( - ..., example="item_overriden", examples={ "example1": {"summary": "item ID summary", "value": "item_1"}, @@ -125,8 +122,8 @@ def path_example_examples( @app.get("/query_example/") def query_example( - data: str = Query( - None, + data: Union[str, None] = Query( + default=None, example="query1", ), ): @@ -135,8 +132,8 @@ def query_example( @app.get("/query_examples/") def query_examples( - data: str = Query( - None, + data: Union[str, None] = Query( + default=None, examples={ "example1": {"summary": "Query example 1", "value": "query1"}, "example2": {"value": "query2"}, @@ -148,8 +145,8 @@ def query_examples( @app.get("/query_example_examples/") def query_example_examples( - data: str = Query( - None, + data: Union[str, None] = Query( + default=None, example="query_overriden", examples={ "example1": {"summary": "Qeury example 1", "value": "query1"}, @@ -162,8 +159,8 @@ def query_example_examples( @app.get("/header_example/") def header_example( - data: str = Header( - None, + data: Union[str, None] = Header( + default=None, example="header1", ), ): @@ -172,8 +169,8 @@ def header_example( @app.get("/header_examples/") def header_examples( - data: str = Header( - None, + data: Union[str, None] = Header( + default=None, examples={ "example1": {"summary": "header example 1", "value": "header1"}, "example2": {"value": "header2"}, @@ -185,8 +182,8 @@ def header_examples( @app.get("/header_example_examples/") def header_example_examples( - data: str = Header( - None, + data: Union[str, None] = Header( + default=None, example="header_overriden", examples={ "example1": {"summary": "Qeury example 1", "value": "header1"}, @@ -199,8 +196,8 @@ def header_example_examples( @app.get("/cookie_example/") def cookie_example( - data: str = Cookie( - None, + data: Union[str, None] = Cookie( + default=None, example="cookie1", ), ): @@ -209,8 +206,8 @@ def cookie_example( @app.get("/cookie_examples/") def cookie_examples( - data: str = Cookie( - None, + data: Union[str, None] = Cookie( + default=None, examples={ "example1": {"summary": "cookie example 1", "value": "cookie1"}, "example2": {"value": "cookie2"}, @@ -222,8 +219,8 @@ def cookie_examples( @app.get("/cookie_example_examples/") def cookie_example_examples( - data: str = Cookie( - None, + data: Union[str, None] = Cookie( + default=None, example="cookie_overriden", examples={ "example1": {"summary": "Qeury example 1", "value": "cookie1"}, diff --git a/tests/test_serialize_response_model.py b/tests/test_serialize_response_model.py index 295667437..3bb46b2e9 100644 --- a/tests/test_serialize_response_model.py +++ b/tests/test_serialize_response_model.py @@ -8,7 +8,7 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(..., alias="aliased_name") + name: str = Field(alias="aliased_name") price: Optional[float] = None owner_ids: Optional[List[int]] = None diff --git a/tests/test_starlette_urlconvertors.py b/tests/test_starlette_urlconvertors.py index 2320c7005..5a980cbf6 100644 --- a/tests/test_starlette_urlconvertors.py +++ b/tests/test_starlette_urlconvertors.py @@ -5,17 +5,17 @@ app = FastAPI() @app.get("/int/{param:int}") -def int_convertor(param: int = Path(...)): +def int_convertor(param: int = Path()): return {"int": param} @app.get("/float/{param:float}") -def float_convertor(param: float = Path(...)): +def float_convertor(param: float = Path()): return {"float": param} @app.get("/path/{param:path}") -def path_convertor(param: str = Path(...)): +def path_convertor(param: str = Path()): return {"path": param} diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 2085dc367..18ec2d048 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -27,7 +27,7 @@ def post_tuple_of_models(square: Tuple[Coordinate, Coordinate]): @app.post("/tuple-form/") -def hello(values: Tuple[int, int] = Form(...)): +def hello(values: Tuple[int, int] = Form()): return values From c5be1b0550f17d827721a5be1dc4344e73b1993f Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 13 May 2022 23:39:00 +0000 Subject: [PATCH 0258/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5c73818fe..416983bc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for not needing `...` as default value in required Query(), Path(), Header(), etc.. PR [#4906](https://github.com/tiangolo/fastapi/pull/4906) by [@tiangolo](https://github.com/tiangolo). * 🎨 Fix default value as set in tutorial for Path Operations Advanced Configurations. PR [#4899](https://github.com/tiangolo/fastapi/pull/4899) by [@tiangolo](https://github.com/tiangolo). * ♻ Refactor dict value extraction to minimize key lookups `fastapi/utils.py`. PR [#3139](https://github.com/tiangolo/fastapi/pull/3139) by [@ShahriyarR](https://github.com/ShahriyarR). * 👷 Fix installing Material for MkDocs Insiders in CI. PR [#4897](https://github.com/tiangolo/fastapi/pull/4897) by [@tiangolo](https://github.com/tiangolo). From ca437cdfabe7673b838cf42889ba1c653acd2228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 14 May 2022 06:59:59 -0500 Subject: [PATCH 0259/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs=20recommend?= =?UTF-8?q?ing=20`Union`=20over=20`Optional`=20and=20migrate=20source=20ex?= =?UTF-8?q?amples=20(#4908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Add docs recommending Union over Optional * 📝 Update docs recommending Union over Optional * 📝 Update source examples for docs, recommend Union over Optional * 📝 Update highlighted lines with updated source examples * 📝 Update highlighted lines in Markdown with recent code changes * 📝 Update docs, use Union instead of Optional * ♻️ Update source examples to recommend Union over Optional * 🎨 Update highlighted code in Markdown after moving from Optional to Union --- README.md | 14 +++---- docs/de/docs/index.md | 14 +++---- .../docs/advanced/additional-status-codes.md | 2 +- docs/en/docs/advanced/testing-dependencies.md | 2 +- docs/en/docs/deployment/docker.md | 4 +- docs/en/docs/index.md | 14 +++---- docs/en/docs/python-types.md | 42 +++++++++++++++++++ docs/en/docs/tutorial/body-multiple-params.md | 11 +++-- docs/en/docs/tutorial/body.md | 2 +- .../dependencies/classes-as-dependencies.md | 2 +- docs/en/docs/tutorial/dependencies/index.md | 4 +- .../tutorial/dependencies/sub-dependencies.md | 2 +- .../path-params-numeric-validations.md | 4 +- docs/en/docs/tutorial/response-model.md | 2 +- docs/en/docs/tutorial/schema-extra-example.md | 8 ++-- .../docs/advanced/additional-status-codes.md | 2 +- docs/es/docs/index.md | 14 +++---- docs/es/docs/tutorial/query-params.md | 2 +- docs/fa/docs/index.md | 14 +++---- docs/fr/docs/index.md | 14 +++---- .../docs/advanced/additional-status-codes.md | 2 +- .../tutorial/query-params-str-validations.md | 16 +++---- docs/ko/docs/index.md | 14 +++---- .../path-params-numeric-validations.md | 4 +- docs/ko/docs/tutorial/query-params.md | 2 +- docs/nl/docs/index.md | 14 +++---- docs/pl/docs/index.md | 14 +++---- docs/pt/docs/index.md | 14 ++++--- docs/pt/docs/tutorial/body.md | 2 +- .../tutorial/query-params-str-validations.md | 24 +++++------ docs/ru/docs/index.md | 14 +++---- docs/sq/docs/index.md | 14 +++---- docs/tr/docs/index.md | 14 +++---- docs/uk/docs/index.md | 14 +++---- .../docs/advanced/additional-status-codes.md | 2 +- docs/zh/docs/index.md | 14 +++---- docs/zh/docs/tutorial/body-multiple-params.md | 4 +- docs/zh/docs/tutorial/dependencies/index.md | 4 +- .../tutorial/dependencies/sub-dependencies.md | 2 +- .../path-params-numeric-validations.md | 4 +- .../tutorial/query-params-str-validations.md | 12 +++--- docs/zh/docs/tutorial/response-model.md | 2 +- docs/zh/docs/tutorial/schema-extra-example.md | 2 +- docs_src/additional_responses/tutorial002.py | 4 +- docs_src/additional_responses/tutorial004.py | 4 +- docs_src/background_tasks/tutorial002.py | 4 +- docs_src/body/tutorial001.py | 6 +-- docs_src/body/tutorial002.py | 6 +-- docs_src/body/tutorial003.py | 6 +-- docs_src/body/tutorial004.py | 8 ++-- docs_src/body_multiple_params/tutorial002.py | 8 ++-- docs_src/body_nested_models/tutorial001.py | 6 +-- docs_src/body_nested_models/tutorial002.py | 6 +-- .../body_nested_models/tutorial002_py39.py | 6 +-- docs_src/body_nested_models/tutorial003.py | 6 +-- .../body_nested_models/tutorial003_py39.py | 6 +-- docs_src/body_nested_models/tutorial004.py | 8 ++-- .../body_nested_models/tutorial004_py39.py | 8 ++-- docs_src/body_nested_models/tutorial005.py | 8 ++-- .../body_nested_models/tutorial005_py39.py | 8 ++-- docs_src/body_nested_models/tutorial006.py | 8 ++-- .../body_nested_models/tutorial006_py39.py | 8 ++-- docs_src/body_nested_models/tutorial007.py | 10 ++--- .../body_nested_models/tutorial007_py39.py | 10 ++--- docs_src/body_updates/tutorial001.py | 8 ++-- docs_src/body_updates/tutorial001_py39.py | 8 ++-- docs_src/body_updates/tutorial002.py | 8 ++-- docs_src/body_updates/tutorial002_py39.py | 8 ++-- docs_src/dataclasses/tutorial001.py | 6 +-- docs_src/dataclasses/tutorial002.py | 6 +-- docs_src/dataclasses/tutorial003.py | 4 +- docs_src/dependencies/tutorial001.py | 6 ++- docs_src/dependencies/tutorial002.py | 4 +- docs_src/dependencies/tutorial003.py | 4 +- docs_src/dependencies/tutorial004.py | 4 +- docs_src/dependencies/tutorial005.py | 7 ++-- docs_src/dependency_testing/tutorial001.py | 8 ++-- docs_src/encoder/tutorial001.py | 4 +- docs_src/extra_data_types/tutorial001.py | 10 ++--- docs_src/extra_models/tutorial001.py | 8 ++-- docs_src/extra_models/tutorial002.py | 4 +- docs_src/header_params/tutorial001.py | 4 +- docs_src/header_params/tutorial002.py | 4 +- docs_src/header_params/tutorial003.py | 4 +- docs_src/header_params/tutorial003_py39.py | 4 +- docs_src/nosql_databases/tutorial001.py | 8 ++-- docs_src/openapi_callbacks/tutorial001.py | 6 +-- .../tutorial004.py | 6 +-- .../tutorial001.py | 6 +-- .../tutorial001_py39.py | 6 +-- .../tutorial002.py | 6 +-- .../tutorial002_py39.py | 6 +-- .../tutorial003.py | 6 +-- .../tutorial003_py39.py | 6 +-- .../tutorial004.py | 6 +-- .../tutorial004_py39.py | 6 +-- .../tutorial005.py | 6 +-- .../tutorial005_py39.py | 6 +-- docs_src/python_types/tutorial009c.py | 5 +++ docs_src/python_types/tutorial009c_py310.py | 2 + docs_src/python_types/tutorial011.py | 4 +- docs_src/python_types/tutorial011_py39.py | 4 +- docs_src/python_types/tutorial012.py | 8 ++++ docs_src/query_params/tutorial002.py | 4 +- docs_src/query_params/tutorial003.py | 4 +- docs_src/query_params/tutorial004.py | 4 +- docs_src/query_params/tutorial006.py | 4 +- .../tutorial001.py | 4 +- docs_src/request_files/tutorial001_02.py | 6 +-- docs_src/response_directly/tutorial001.py | 4 +- docs_src/response_model/tutorial001.py | 6 +-- docs_src/response_model/tutorial001_py39.py | 6 +-- docs_src/response_model/tutorial002.py | 4 +- docs_src/response_model/tutorial003.py | 6 +-- docs_src/response_model/tutorial004.py | 4 +- docs_src/response_model/tutorial004_py39.py | 4 +- docs_src/response_model/tutorial005.py | 4 +- docs_src/response_model/tutorial006.py | 4 +- docs_src/schema_extra_example/tutorial001.py | 6 +-- docs_src/schema_extra_example/tutorial002.py | 6 +-- docs_src/schema_extra_example/tutorial003.py | 6 +-- docs_src/schema_extra_example/tutorial004.py | 6 +-- docs_src/security/tutorial002.py | 8 ++-- docs_src/security/tutorial003.py | 8 ++-- docs_src/security/tutorial004.py | 12 +++--- docs_src/security/tutorial005.py | 12 +++--- docs_src/security/tutorial005_py39.py | 12 +++--- docs_src/sql_databases/sql_app/schemas.py | 4 +- .../sql_databases/sql_app_py39/schemas.py | 4 +- .../sql_databases_peewee/sql_app/schemas.py | 4 +- docs_src/websockets/tutorial002.py | 8 ++-- 131 files changed, 489 insertions(+), 426 deletions(-) create mode 100644 docs_src/python_types/tutorial009c.py create mode 100644 docs_src/python_types/tutorial009c_py310.py create mode 100644 docs_src/python_types/tutorial012.py diff --git a/README.md b/README.md index 9ad50f271..5e9e97a2a 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ $ pip install "uvicorn[standard]" * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -164,7 +164,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -174,7 +174,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -187,7 +187,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -266,7 +266,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -277,7 +277,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -286,7 +286,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index ce13bcc4a..929754462 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -149,7 +149,7 @@ $ pip install uvicorn[standard] * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -162,7 +162,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -172,7 +172,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -185,7 +185,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -264,7 +264,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -275,7 +275,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -284,7 +284,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index 37ec283ff..b61f88b93 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -14,7 +14,7 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: -```Python hl_lines="4 23" +```Python hl_lines="4 25" {!../../../docs_src/additional_status_codes/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 79208e8dc..7bba82fb7 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -28,7 +28,7 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. -```Python hl_lines="26-27 30" +```Python hl_lines="28-29 32" {!../../../docs_src/dependency_testing/tutorial001.py!} ``` diff --git a/docs/en/docs/deployment/docker.md b/docs/en/docs/deployment/docker.md index 651b0e840..8a542622e 100644 --- a/docs/en/docs/deployment/docker.md +++ b/docs/en/docs/deployment/docker.md @@ -142,7 +142,7 @@ Successfully installed fastapi pydantic uvicorn * Create a `main.py` file with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -155,7 +155,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 7de1e50df..17163ba01 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -148,7 +148,7 @@ $ pip install "uvicorn[standard]" * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -161,7 +161,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -171,7 +171,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -184,7 +184,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -263,7 +263,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -274,7 +274,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -283,7 +283,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 8486ed849..963fcaf1c 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -317,6 +317,45 @@ This also means that in Python 3.10, you can use `Something | None`: {!> ../../../docs_src/python_types/tutorial009_py310.py!} ``` +#### Using `Union` or `Optional` + +If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: + +* 🚨 Avoid using `Optional[SomeType]` +* Instead ✨ **use `Union[SomeType, None]`** ✨. + +Both are equivalent and underneath they are the same, but I would recommend `Union` instead of `Optional` because the word "**optional**" would seem to imply that the value is optional, and it actually means "it can be `None`", even if it's not optional and is still required. + +I think `Union[str, SomeType]` is more explicit about what it means. + +It's just about the words and names. But those words can affect how you and your teammates think about the code. + +As an example, let's take this function: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +The parameter `name` is defined as `Optional[str]`, but it is **not optional**, you cannot call the function without the parameter: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +The `name` parameter is **still required** (not *optional*) because it doesn't have a default value. Still, `name` accepts `None` as the value: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +The good news is, once you are on Python 3.10 you won't have to worry about that, as you will be able to simply use `|` to define unions of types: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +And then you won't have to worry about names like `Optional` and `Union`. 😎 + #### Generic types These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: @@ -422,6 +461,9 @@ An example from the official Pydantic docs: You will see a lot more of all this in practice in the [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. +!!! tip + Pydantic has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. + ## Type hints in **FastAPI** **FastAPI** takes advantage of these type hints to do several things. diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 13de4c8ea..31dd27fed 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -89,13 +89,13 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: === "Python 3.6 and above" - ```Python hl_lines="23" + ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="21" + ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` @@ -126,7 +126,7 @@ Of course, you can also declare additional query parameters whenever you need, a As, by default, singular values are interpreted as query parameters, you don't have to explicitly add a `Query`, you can just do: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` Or in Python 3.10 and above: @@ -139,7 +139,7 @@ For example: === "Python 3.6 and above" - ```Python hl_lines="28" + ```Python hl_lines="27" {!> ../../../docs_src/body_multiple_params/tutorial004.py!} ``` @@ -152,7 +152,6 @@ For example: !!! info `Body` also has all the same extra validation and metadata parameters as `Query`,`Path` and others you will see later. - ## Embed a single body parameter Let's say you only have a single `item` body parameter from a Pydantic model `Item`. @@ -162,7 +161,7 @@ By default, **FastAPI** will then expect its body directly. But if you want it to expect a JSON with a key `item` and inside of it the model contents, as it does when you declare extra body parameters, you can use the special `Body` parameter `embed`: ```Python -item: Item = Body(..., embed=True) +item: Item = Body(embed=True) ``` as in: diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index eb21f29a8..509005936 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -206,7 +206,7 @@ The function parameters will be recognized as follows: !!! note FastAPI will know that the value of `q` is not required because of the default value `= None`. - The `Optional` in `Optional[str]` is not used by FastAPI, but will allow your editor to give you better support and detect errors. + The `Union` in `Union[str, None]` is not used by FastAPI, but will allow your editor to give you better support and detect errors. ## Without Pydantic diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 663fff15b..fb41ba1f6 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -109,7 +109,7 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6 and above" - ```Python hl_lines="8" + ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index fe10facfb..5078c0096 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -33,7 +33,7 @@ It is just a function that can take all the same parameters that a *path operati === "Python 3.6 and above" - ```Python hl_lines="8-9" + ```Python hl_lines="8-11" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` @@ -81,7 +81,7 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p === "Python 3.6 and above" - ```Python hl_lines="13 18" + ```Python hl_lines="15 20" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 51531228d..a5b40c9ad 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -55,7 +55,7 @@ Then we can use the dependency with: === "Python 3.6 and above" - ```Python hl_lines="21" + ```Python hl_lines="22" {!> ../../../docs_src/dependencies/tutorial005.py!} ``` diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 31bf91a0e..29235c6e2 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -59,7 +59,7 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -```Python hl_lines="8" +```Python hl_lines="7" {!../../../docs_src/path_params_numeric_validations/tutorial002.py!} ``` @@ -71,7 +71,7 @@ Pass `*`, as the first parameter of the function. Python won't do anything with that `*`, but it will know that all the following parameters should be called as keyword arguments (key-value pairs), also known as kwargs. Even if they don't have a default value. -```Python hl_lines="8" +```Python hl_lines="7" {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index c751a9256..e371e86e4 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -162,7 +162,7 @@ Your response model could have default values, like: {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -* `description: Optional[str] = None` has a default of `None`. +* `description: Union[str, None] = None` has a default of `None`. * `tax: float = 10.5` has a default of `10.5`. * `tags: List[str] = []` as a default of an empty list: `[]`. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index c69df51dc..94347018d 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -68,13 +68,13 @@ Here we pass an `example` of the data expected in `Body()`: === "Python 3.6 and above" - ```Python hl_lines="21-26" + ```Python hl_lines="20-25" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="19-24" + ```Python hl_lines="18-23" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` @@ -99,13 +99,13 @@ Each specific example `dict` in the `examples` can contain: === "Python 3.6 and above" - ```Python hl_lines="22-48" + ```Python hl_lines="21-47" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="20-46" + ```Python hl_lines="19-45" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` diff --git a/docs/es/docs/advanced/additional-status-codes.md b/docs/es/docs/advanced/additional-status-codes.md index 67224fb36..1f28ea85b 100644 --- a/docs/es/docs/advanced/additional-status-codes.md +++ b/docs/es/docs/advanced/additional-status-codes.md @@ -14,7 +14,7 @@ Pero también quieres que acepte nuevos ítems. Cuando los ítems no existan ant Para conseguir esto importa `JSONResponse` y devuelve ahí directamente tu contenido, asignando el `status_code` que quieras: -```Python hl_lines="2 19" +```Python hl_lines="4 25" {!../../../docs_src/additional_status_codes/tutorial001.py!} ``` diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 1fa79fdde..ef4850b56 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -145,7 +145,7 @@ $ pip install uvicorn[standard] ```Python from fastapi import FastAPI -from typing import Optional +from typing import Union app = FastAPI() @@ -156,7 +156,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -167,7 +167,7 @@ Si tu código usa `async` / `await`, usa `async def`: ```Python hl_lines="7 12" from fastapi import FastAPI -from typing import Optional +from typing import Union app = FastAPI() @@ -178,7 +178,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -259,7 +259,7 @@ Declara el body usando las declaraciones de tipo estándares de Python gracias a ```Python hl_lines="2 7-10 23-25" from fastapi import FastAPI from pydantic import BaseModel -from typing import Optional +from typing import Union app = FastAPI() @@ -267,7 +267,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -276,7 +276,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/es/docs/tutorial/query-params.md b/docs/es/docs/tutorial/query-params.md index 69caee6e8..482af8dc0 100644 --- a/docs/es/docs/tutorial/query-params.md +++ b/docs/es/docs/tutorial/query-params.md @@ -75,7 +75,7 @@ En este caso el parámetro de la función `q` será opcional y será `None` por !!! note "Nota" FastAPI sabrá que `q` es opcional por el `= None`. - El `Optional` en `Optional[str]` no es usado por FastAPI (FastAPI solo usará la parte `str`), pero el `Optional[str]` le permitirá a tu editor ayudarte a encontrar errores en tu código. + El `Union` en `Union[str, None]` no es usado por FastAPI (FastAPI solo usará la parte `str`), pero el `Union[str, None]` le permitirá a tu editor ayudarte a encontrar errores en tu código. ## Conversión de tipos de parámetros de query diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index 0070de179..fd52f994c 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -152,7 +152,7 @@ $ pip install "uvicorn[standard]" * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -165,7 +165,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -175,7 +175,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -188,7 +188,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -267,7 +267,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -278,7 +278,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -287,7 +287,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 0b537054e..f713ee96b 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -149,7 +149,7 @@ $ pip install uvicorn[standard] * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -162,7 +162,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -172,7 +172,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -185,7 +185,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -264,7 +264,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9 10 11 12 25 26 27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -275,7 +275,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -284,7 +284,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/ja/docs/advanced/additional-status-codes.md b/docs/ja/docs/advanced/additional-status-codes.md index 6c03cd92b..d1f8e6451 100644 --- a/docs/ja/docs/advanced/additional-status-codes.md +++ b/docs/ja/docs/advanced/additional-status-codes.md @@ -14,7 +14,7 @@ これを達成するには、 `JSONResponse` をインポートし、 `status_code` を設定して直接内容を返します。 -```Python hl_lines="4 23" +```Python hl_lines="4 25" {!../../../docs_src/additional_status_codes/tutorial001.py!} ``` diff --git a/docs/ja/docs/tutorial/query-params-str-validations.md b/docs/ja/docs/tutorial/query-params-str-validations.md index ff0af725f..8d375d7ce 100644 --- a/docs/ja/docs/tutorial/query-params-str-validations.md +++ b/docs/ja/docs/tutorial/query-params-str-validations.md @@ -34,12 +34,12 @@ {!../../../docs_src/query_params_str_validations/tutorial002.py!} ``` -デフォルト値`None`を`Query(None)`に置き換える必要があるので、`Query`の最初の引数はデフォルト値を定義するのと同じです。 +デフォルト値`None`を`Query(default=None)`に置き換える必要があるので、`Query`の最初の引数はデフォルト値を定義するのと同じです。 なので: ```Python -q: Optional[str] = Query(None) +q: Optional[str] = Query(default=None) ``` ...を以下と同じようにパラメータをオプションにします: @@ -60,7 +60,7 @@ q: Optional[str] = None もしくは: ```Python - = Query(None) + = Query(default=None) ``` そして、 `None` を利用することでクエリパラメータが必須ではないと検知します。 @@ -70,7 +70,7 @@ q: Optional[str] = None そして、さらに多くのパラメータを`Query`に渡すことができます。この場合、文字列に適用される、`max_length`パラメータを指定します。 ```Python -q: str = Query(None, max_length=50) +q: Union[str, None] = Query(default=None, max_length=50) ``` これにより、データを検証し、データが有効でない場合は明確なエラーを表示し、OpenAPIスキーマの *path operation* にパラメータを記載します。 @@ -79,7 +79,7 @@ q: str = Query(None, max_length=50) パラメータ`min_length`も追加することができます: -```Python hl_lines="9" +```Python hl_lines="10" {!../../../docs_src/query_params_str_validations/tutorial003.py!} ``` @@ -87,7 +87,7 @@ q: str = Query(None, max_length=50) パラメータが一致するべき正規表現を定義することができます: -```Python hl_lines="10" +```Python hl_lines="11" {!../../../docs_src/query_params_str_validations/tutorial004.py!} ``` @@ -125,13 +125,13 @@ q: str 以下の代わりに: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` 現在は以下の例のように`Query`で宣言しています: ```Python -q: Optional[str] = Query(None, min_length=3) +q: Union[str, None] = Query(default=None, min_length=3) ``` そのため、`Query`を使用して必須の値を宣言する必要がある場合は、第一引数に`...`を使用することができます: diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index 284628955..ec4422994 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -145,7 +145,7 @@ $ pip install uvicorn[standard] * `main.py` 파일을 만드십시오: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -158,7 +158,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -168,7 +168,7 @@ def read_item(item_id: int, q: Optional[str] = None): 여러분의 코드가 `async` / `await`을 사용한다면, `async def`를 사용하십시오. ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -181,7 +181,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -260,7 +260,7 @@ INFO: Application startup complete. Pydantic을 이용해 파이썬 표준 타입으로 본문을 선언합니다. ```Python hl_lines="4 9 10 11 12 25 26 27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -271,7 +271,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -280,7 +280,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/ko/docs/tutorial/path-params-numeric-validations.md b/docs/ko/docs/tutorial/path-params-numeric-validations.md index abb9d03db..cadf543fc 100644 --- a/docs/ko/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ko/docs/tutorial/path-params-numeric-validations.md @@ -43,7 +43,7 @@ 따라서 함수를 다음과 같이 선언 할 수 있습니다: -```Python hl_lines="8" +```Python hl_lines="7" {!../../../docs_src/path_params_numeric_validations/tutorial002.py!} ``` @@ -55,7 +55,7 @@ 파이썬은 `*`으로 아무런 행동도 하지 않지만, 따르는 매개변수들은 kwargs로도 알려진 키워드 인자(키-값 쌍)여야 함을 인지합니다. 기본값을 가지고 있지 않더라도 그렇습니다. -```Python hl_lines="8" +```Python hl_lines="7" {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` diff --git a/docs/ko/docs/tutorial/query-params.md b/docs/ko/docs/tutorial/query-params.md index 05f2ff9c9..bb631e6ff 100644 --- a/docs/ko/docs/tutorial/query-params.md +++ b/docs/ko/docs/tutorial/query-params.md @@ -75,7 +75,7 @@ http://127.0.0.1:8000/items/?skip=20 !!! note "참고" FastAPI는 `q`가 `= None`이므로 선택적이라는 것을 인지합니다. - `Optional[str]`에 있는 `Optional`은 FastAPI(FastAPI는 `str` 부분만 사용합니다)가 사용하는게 아니지만, `Optional[str]`은 편집기에게 코드에서 오류를 찾아낼 수 있게 도와줍니다. + `Union[str, None]`에 있는 `Union`은 FastAPI(FastAPI는 `str` 부분만 사용합니다)가 사용하는게 아니지만, `Union[str, None]`은 편집기에게 코드에서 오류를 찾아낼 수 있게 도와줍니다. ## 쿼리 매개변수 형변환 diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md index 0070de179..fd52f994c 100644 --- a/docs/nl/docs/index.md +++ b/docs/nl/docs/index.md @@ -152,7 +152,7 @@ $ pip install "uvicorn[standard]" * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -165,7 +165,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -175,7 +175,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -188,7 +188,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -267,7 +267,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -278,7 +278,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -287,7 +287,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 4a300ae63..bbe1b1ad1 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -144,7 +144,7 @@ $ pip install uvicorn[standard] * Utwórz plik o nazwie `main.py` z: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -157,7 +157,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -167,7 +167,7 @@ def read_item(item_id: int, q: Optional[str] = None): Jeżeli twój kod korzysta z `async` / `await`, użyj `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -180,7 +180,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -258,7 +258,7 @@ Zmodyfikuj teraz plik `main.py`, aby otrzmywał treść (body) żądania `PUT`. Zadeklaruj treść żądania, używając standardowych typów w Pythonie dzięki Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -269,7 +269,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -278,7 +278,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index c1a0dbf0d..b1d0c89f2 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -138,7 +138,7 @@ $ pip install uvicorn[standard] * Crie um arquivo `main.py` com: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -151,7 +151,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -161,7 +161,7 @@ def read_item(item_id: int, q: Optional[str] = None): Se seu código utiliza `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -174,7 +174,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -253,6 +253,8 @@ Agora modifique o arquivo `main.py` para receber um corpo para uma requisição Declare o corpo utilizando tipos padrão Python, graças ao Pydantic. ```Python hl_lines="4 9-12 25-27" +from typing import Union + from fastapi import FastAPI from pydantic import BaseModel @@ -262,7 +264,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool] = None @app.get("/") @@ -271,7 +273,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/pt/docs/tutorial/body.md b/docs/pt/docs/tutorial/body.md index 5abc91177..99e05ab77 100644 --- a/docs/pt/docs/tutorial/body.md +++ b/docs/pt/docs/tutorial/body.md @@ -158,7 +158,7 @@ Os parâmetros da função serão reconhecidos conforme abaixo: !!! note "Observação" O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. - O `Optional` em `Optional[str]` não é utilizado pelo FastAPI, mas permite ao seu editor de texto lhe dar um suporte melhor e detectar erros. + O `Union` em `Union[str, None]` não é utilizado pelo FastAPI, mas permite ao seu editor de texto lhe dar um suporte melhor e detectar erros. ## Sem o Pydantic diff --git a/docs/pt/docs/tutorial/query-params-str-validations.md b/docs/pt/docs/tutorial/query-params-str-validations.md index baac5f493..9a9e071db 100644 --- a/docs/pt/docs/tutorial/query-params-str-validations.md +++ b/docs/pt/docs/tutorial/query-params-str-validations.md @@ -8,12 +8,12 @@ Vamos utilizar essa aplicação como exemplo: {!../../../docs_src/query_params_str_validations/tutorial001.py!} ``` -O parâmetro de consulta `q` é do tipo `Optional[str]`, o que significa que é do tipo `str` mas que também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. +O parâmetro de consulta `q` é do tipo `Union[str, None]`, o que significa que é do tipo `str` mas que também pode ser `None`, e de fato, o valor padrão é `None`, então o FastAPI saberá que não é obrigatório. !!! note "Observação" O FastAPI saberá que o valor de `q` não é obrigatório por causa do valor padrão `= None`. - O `Optional` em `Optional[str]` não é usado pelo FastAPI, mas permitirá que seu editor lhe dê um melhor suporte e detecte erros. + O `Union` em `Union[str, None]` não é usado pelo FastAPI, mas permitirá que seu editor lhe dê um melhor suporte e detecte erros. ## Validação adicional @@ -35,18 +35,18 @@ Agora utilize-o como valor padrão do seu parâmetro, definindo o parâmetro `ma {!../../../docs_src/query_params_str_validations/tutorial002.py!} ``` -Note que substituímos o valor padrão de `None` para `Query(None)`, o primeiro parâmetro de `Query` serve para o mesmo propósito: definir o valor padrão do parâmetro. +Note que substituímos o valor padrão de `None` para `Query(default=None)`, o primeiro parâmetro de `Query` serve para o mesmo propósito: definir o valor padrão do parâmetro. Então: ```Python -q: Optional[str] = Query(None) +q: Union[str, None] = Query(default=None) ``` ...Torna o parâmetro opcional, da mesma maneira que: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` Mas o declara explicitamente como um parâmetro de consulta. @@ -61,17 +61,17 @@ Mas o declara explicitamente como um parâmetro de consulta. Ou com: ```Python - = Query(None) + = Query(default=None) ``` E irá utilizar o `None` para detectar que o parâmetro de consulta não é obrigatório. - O `Optional` é apenas para permitir que seu editor de texto lhe dê um melhor suporte. + O `Union` é apenas para permitir que seu editor de texto lhe dê um melhor suporte. Então, podemos passar mais parâmetros para `Query`. Neste caso, o parâmetro `max_length` que se aplica a textos: ```Python -q: str = Query(None, max_length=50) +q: str = Query(default=None, max_length=50) ``` Isso irá validar os dados, mostrar um erro claro quando os dados forem inválidos, e documentar o parâmetro na *operação de rota* do esquema OpenAPI.. @@ -80,7 +80,7 @@ Isso irá validar os dados, mostrar um erro claro quando os dados forem inválid Você também pode incluir um parâmetro `min_length`: -```Python hl_lines="9" +```Python hl_lines="10" {!../../../docs_src/query_params_str_validations/tutorial003.py!} ``` @@ -88,7 +88,7 @@ Você também pode incluir um parâmetro `min_length`: Você pode definir uma expressão regular que combine com um padrão esperado pelo parâmetro: -```Python hl_lines="10" +```Python hl_lines="11" {!../../../docs_src/query_params_str_validations/tutorial004.py!} ``` @@ -126,13 +126,13 @@ q: str em vez desta: ```Python -q: Optional[str] = None +q: Union[str, None] = None ``` Mas agora nós o estamos declarando como `Query`, conforme abaixo: ```Python -q: Optional[str] = Query(None, min_length=3) +q: Union[str, None] = Query(default=None, min_length=3) ``` Então, quando você precisa declarar um parâmetro obrigatório utilizando o `Query`, você pode utilizar `...` como o primeiro argumento: diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index a1d302276..9a3957d5f 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -149,7 +149,7 @@ $ pip install uvicorn[standard] * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -162,7 +162,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -172,7 +172,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -185,7 +185,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -264,7 +264,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -275,7 +275,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -284,7 +284,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md index 0bb7b55e3..29f92e020 100644 --- a/docs/sq/docs/index.md +++ b/docs/sq/docs/index.md @@ -149,7 +149,7 @@ $ pip install uvicorn[standard] * Create a file `main.py` with: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -162,7 +162,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -172,7 +172,7 @@ def read_item(item_id: int, q: Optional[str] = None): If your code uses `async` / `await`, use `async def`: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -185,7 +185,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -264,7 +264,7 @@ Now modify the file `main.py` to receive a body from a `PUT` request. Declare the body using standard Python types, thanks to Pydantic. ```Python hl_lines="4 9-12 25-27" -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -275,7 +275,7 @@ app = FastAPI() class Item(BaseModel): name: str price: float - is_offer: Optional[bool] = None + is_offer: Union[bool, None] = None @app.get("/") @@ -284,7 +284,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 3195cd440..5693029b5 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -157,7 +157,7 @@ $ pip install uvicorn[standard] * `main.py` adında bir dosya oluştur : ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -170,7 +170,7 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -180,7 +180,7 @@ def read_item(item_id: int, q: Optional[str] = None): Eğer kodunda `async` / `await` var ise, `async def` kullan: ```Python hl_lines="9 14" -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -193,7 +193,7 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): +async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` @@ -272,7 +272,7 @@ Senin için alternatif olarak (kwargs,来调用。即使它们没有默认值。 -```Python hl_lines="8" +```Python hl_lines="7" {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 1d1d383d4..0b2b9446a 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -30,12 +30,12 @@ {!../../../docs_src/query_params_str_validations/tutorial002.py!} ``` -由于我们必须用 `Query(None)` 替换默认值 `None`,`Query` 的第一个参数同样也是用于定义默认值。 +由于我们必须用 `Query(default=None)` 替换默认值 `None`,`Query` 的第一个参数同样也是用于定义默认值。 所以: ```Python -q: str = Query(None) +q: Union[str, None] = Query(default=None) ``` ...使得参数可选,等同于: @@ -49,7 +49,7 @@ q: str = None 然后,我们可以将更多的参数传递给 `Query`。在本例中,适用于字符串的 `max_length` 参数: ```Python -q: str = Query(None, max_length=50) +q: Union[str, None] = Query(default=None, max_length=50) ``` 将会校验数据,在数据无效时展示清晰的错误信息,并在 OpenAPI 模式的*路径操作*中记录该参​​数。 @@ -58,7 +58,7 @@ q: str = Query(None, max_length=50) 你还可以添加 `min_length` 参数: -```Python hl_lines="9" +```Python hl_lines="10" {!../../../docs_src/query_params_str_validations/tutorial003.py!} ``` @@ -66,7 +66,7 @@ q: str = Query(None, max_length=50) 你可以定义一个参数值必须匹配的正则表达式: -```Python hl_lines="10" +```Python hl_lines="11" {!../../../docs_src/query_params_str_validations/tutorial004.py!} ``` @@ -110,7 +110,7 @@ q: str = None 但是现在我们正在用 `Query` 声明它,例如: ```Python -q: str = Query(None, min_length=3) +q: Union[str, None] = Query(default=None, min_length=3) ``` 因此,当你在使用 `Query` 且需要声明一个值是必需的时,可以将 `...` 用作第一个参数值: diff --git a/docs/zh/docs/tutorial/response-model.md b/docs/zh/docs/tutorial/response-model.md index 59a7c17d5..ea3d0666d 100644 --- a/docs/zh/docs/tutorial/response-model.md +++ b/docs/zh/docs/tutorial/response-model.md @@ -94,7 +94,7 @@ FastAPI 将使用此 `response_model` 来: {!../../../docs_src/response_model/tutorial004.py!} ``` -* `description: Optional[str] = None` 具有默认值 `None`。 +* `description: Union[str, None] = None` 具有默认值 `None`。 * `tax: float = 10.5` 具有默认值 `10.5`. * `tags: List[str] = []` 具有一个空列表作为默认值: `[]`. diff --git a/docs/zh/docs/tutorial/schema-extra-example.md b/docs/zh/docs/tutorial/schema-extra-example.md index 6482366b0..8f5fbfe70 100644 --- a/docs/zh/docs/tutorial/schema-extra-example.md +++ b/docs/zh/docs/tutorial/schema-extra-example.md @@ -33,7 +33,7 @@ 比如,你可以将请求体的一个 `example` 传递给 `Body`: -```Python hl_lines="21-26" +```Python hl_lines="20-25" {!../../../docs_src/schema_extra_example/tutorial003.py!} ``` diff --git a/docs_src/additional_responses/tutorial002.py b/docs_src/additional_responses/tutorial002.py index a46e95959..bd0c95704 100644 --- a/docs_src/additional_responses/tutorial002.py +++ b/docs_src/additional_responses/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.responses import FileResponse @@ -23,7 +23,7 @@ app = FastAPI() } }, ) -async def read_item(item_id: str, img: Optional[bool] = None): +async def read_item(item_id: str, img: Union[bool, None] = None): if img: return FileResponse("image.png", media_type="image/png") else: diff --git a/docs_src/additional_responses/tutorial004.py b/docs_src/additional_responses/tutorial004.py index 361aecb8e..978bc18c1 100644 --- a/docs_src/additional_responses/tutorial004.py +++ b/docs_src/additional_responses/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.responses import FileResponse @@ -25,7 +25,7 @@ app = FastAPI() response_model=Item, responses={**responses, 200: {"content": {"image/png": {}}}}, ) -async def read_item(item_id: str, img: Optional[bool] = None): +async def read_item(item_id: str, img: Union[bool, None] = None): if img: return FileResponse("image.png", media_type="image/png") else: diff --git a/docs_src/background_tasks/tutorial002.py b/docs_src/background_tasks/tutorial002.py index e7517e8cd..2e1b2f6c6 100644 --- a/docs_src/background_tasks/tutorial002.py +++ b/docs_src/background_tasks/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import BackgroundTasks, Depends, FastAPI @@ -10,7 +10,7 @@ def write_log(message: str): log.write(message) -def get_query(background_tasks: BackgroundTasks, q: Optional[str] = None): +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): if q: message = f"found query: {q}\n" background_tasks.add_task(write_log, message) diff --git a/docs_src/body/tutorial001.py b/docs_src/body/tutorial001.py index 52144bd2b..f93317274 100644 --- a/docs_src/body/tutorial001.py +++ b/docs_src/body/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -6,9 +6,9 @@ from pydantic import BaseModel class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None app = FastAPI() diff --git a/docs_src/body/tutorial002.py b/docs_src/body/tutorial002.py index 644fabae9..7f5183908 100644 --- a/docs_src/body/tutorial002.py +++ b/docs_src/body/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -6,9 +6,9 @@ from pydantic import BaseModel class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None app = FastAPI() diff --git a/docs_src/body/tutorial003.py b/docs_src/body/tutorial003.py index c99ea694b..89a6b833c 100644 --- a/docs_src/body/tutorial003.py +++ b/docs_src/body/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -6,9 +6,9 @@ from pydantic import BaseModel class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None app = FastAPI() diff --git a/docs_src/body/tutorial004.py b/docs_src/body/tutorial004.py index 7a222a390..e2df0df2b 100644 --- a/docs_src/body/tutorial004.py +++ b/docs_src/body/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -6,16 +6,16 @@ from pydantic import BaseModel class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item, q: Optional[str] = None): +async def create_item(item_id: int, item: Item, q: Union[str, None] = None): result = {"item_id": item_id, **item.dict()} if q: result.update({"q": q}) diff --git a/docs_src/body_multiple_params/tutorial002.py b/docs_src/body_multiple_params/tutorial002.py index 6b8748420..2d7160ae8 100644 --- a/docs_src/body_multiple_params/tutorial002.py +++ b/docs_src/body_multiple_params/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,14 +8,14 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None class User(BaseModel): username: str - full_name: Optional[str] = None + full_name: Union[str, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial001.py b/docs_src/body_nested_models/tutorial001.py index fe14fdf93..37ef6dda5 100644 --- a/docs_src/body_nested_models/tutorial001.py +++ b/docs_src/body_nested_models/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: list = [] diff --git a/docs_src/body_nested_models/tutorial002.py b/docs_src/body_nested_models/tutorial002.py index 1770516a4..155cff788 100644 --- a/docs_src/body_nested_models/tutorial002.py +++ b/docs_src/body_nested_models/tutorial002.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: List[str] = [] diff --git a/docs_src/body_nested_models/tutorial002_py39.py b/docs_src/body_nested_models/tutorial002_py39.py index af523a74e..8a93a7233 100644 --- a/docs_src/body_nested_models/tutorial002_py39.py +++ b/docs_src/body_nested_models/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: list[str] = [] diff --git a/docs_src/body_nested_models/tutorial003.py b/docs_src/body_nested_models/tutorial003.py index 33dbbe3a9..84ed18bf4 100644 --- a/docs_src/body_nested_models/tutorial003.py +++ b/docs_src/body_nested_models/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/body_nested_models/tutorial003_py39.py b/docs_src/body_nested_models/tutorial003_py39.py index 931d92f88..b590ece36 100644 --- a/docs_src/body_nested_models/tutorial003_py39.py +++ b/docs_src/body_nested_models/tutorial003_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/body_nested_models/tutorial004.py b/docs_src/body_nested_models/tutorial004.py index 311a4e73f..a07bfacac 100644 --- a/docs_src/body_nested_models/tutorial004.py +++ b/docs_src/body_nested_models/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() - image: Optional[Image] = None + image: Union[Image, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial004_py39.py b/docs_src/body_nested_models/tutorial004_py39.py index ab05da023..dc2b175fb 100644 --- a/docs_src/body_nested_models/tutorial004_py39.py +++ b/docs_src/body_nested_models/tutorial004_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() - image: Optional[Image] = None + image: Union[Image, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial005.py b/docs_src/body_nested_models/tutorial005.py index e76498c3b..5a01264ed 100644 --- a/docs_src/body_nested_models/tutorial005.py +++ b/docs_src/body_nested_models/tutorial005.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() - image: Optional[Image] = None + image: Union[Image, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial005_py39.py b/docs_src/body_nested_models/tutorial005_py39.py index 504551883..47db90008 100644 --- a/docs_src/body_nested_models/tutorial005_py39.py +++ b/docs_src/body_nested_models/tutorial005_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() - image: Optional[Image] = None + image: Union[Image, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial006.py b/docs_src/body_nested_models/tutorial006.py index da7836715..75f1f30e3 100644 --- a/docs_src/body_nested_models/tutorial006.py +++ b/docs_src/body_nested_models/tutorial006.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Set +from typing import List, Set, Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() - images: Optional[List[Image]] = None + images: Union[List[Image], None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial006_py39.py b/docs_src/body_nested_models/tutorial006_py39.py index 61898178e..b14409703 100644 --- a/docs_src/body_nested_models/tutorial006_py39.py +++ b/docs_src/body_nested_models/tutorial006_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,11 +13,11 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() - images: Optional[list[Image]] = None + images: Union[list[Image], None] = None @app.put("/items/{item_id}") diff --git a/docs_src/body_nested_models/tutorial007.py b/docs_src/body_nested_models/tutorial007.py index dfbbeaab1..641f09dce 100644 --- a/docs_src/body_nested_models/tutorial007.py +++ b/docs_src/body_nested_models/tutorial007.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Set +from typing import List, Set, Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,16 +13,16 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() - images: Optional[List[Image]] = None + images: Union[List[Image], None] = None class Offer(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float items: List[Item] diff --git a/docs_src/body_nested_models/tutorial007_py39.py b/docs_src/body_nested_models/tutorial007_py39.py index 0c7d32fbb..59cf01e23 100644 --- a/docs_src/body_nested_models/tutorial007_py39.py +++ b/docs_src/body_nested_models/tutorial007_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, HttpUrl @@ -13,16 +13,16 @@ class Image(BaseModel): class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() - images: Optional[list[Image]] = None + images: Union[list[Image], None] = None class Offer(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float items: list[Item] diff --git a/docs_src/body_updates/tutorial001.py b/docs_src/body_updates/tutorial001.py index 9b8f3ccf1..4e65d77e2 100644 --- a/docs_src/body_updates/tutorial001.py +++ b/docs_src/body_updates/tutorial001.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - price: Optional[float] = None + name: Union[str, None] = None + description: Union[str, None] = None + price: Union[float, None] = None tax: float = 10.5 tags: List[str] = [] diff --git a/docs_src/body_updates/tutorial001_py39.py b/docs_src/body_updates/tutorial001_py39.py index 5d5388b56..999bcdb82 100644 --- a/docs_src/body_updates/tutorial001_py39.py +++ b/docs_src/body_updates/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - price: Optional[float] = None + name: Union[str, None] = None + description: Union[str, None] = None + price: Union[float, None] = None tax: float = 10.5 tags: list[str] = [] diff --git a/docs_src/body_updates/tutorial002.py b/docs_src/body_updates/tutorial002.py index 46d27e67e..c3a0fe79e 100644 --- a/docs_src/body_updates/tutorial002.py +++ b/docs_src/body_updates/tutorial002.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - price: Optional[float] = None + name: Union[str, None] = None + description: Union[str, None] = None + price: Union[float, None] = None tax: float = 10.5 tags: List[str] = [] diff --git a/docs_src/body_updates/tutorial002_py39.py b/docs_src/body_updates/tutorial002_py39.py index ab85bd5ae..eb35b3521 100644 --- a/docs_src/body_updates/tutorial002_py39.py +++ b/docs_src/body_updates/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): - name: Optional[str] = None - description: Optional[str] = None - price: Optional[float] = None + name: Union[str, None] = None + description: Union[str, None] = None + price: Union[float, None] = None tax: float = 10.5 tags: list[str] = [] diff --git a/docs_src/dataclasses/tutorial001.py b/docs_src/dataclasses/tutorial001.py index 43015eb27..2954c391f 100644 --- a/docs_src/dataclasses/tutorial001.py +++ b/docs_src/dataclasses/tutorial001.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -8,8 +8,8 @@ from fastapi import FastAPI class Item: name: str price: float - description: Optional[str] = None - tax: Optional[float] = None + description: Union[str, None] = None + tax: Union[float, None] = None app = FastAPI() diff --git a/docs_src/dataclasses/tutorial002.py b/docs_src/dataclasses/tutorial002.py index aaa7b8799..08a238080 100644 --- a/docs_src/dataclasses/tutorial002.py +++ b/docs_src/dataclasses/tutorial002.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI @@ -9,8 +9,8 @@ class Item: name: str price: float tags: List[str] = field(default_factory=list) - description: Optional[str] = None - tax: Optional[float] = None + description: Union[str, None] = None + tax: Union[float, None] = None app = FastAPI() diff --git a/docs_src/dataclasses/tutorial003.py b/docs_src/dataclasses/tutorial003.py index 2c1fccdd7..34ce1199e 100644 --- a/docs_src/dataclasses/tutorial003.py +++ b/docs_src/dataclasses/tutorial003.py @@ -1,5 +1,5 @@ from dataclasses import field # (1) -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from pydantic.dataclasses import dataclass # (2) @@ -8,7 +8,7 @@ from pydantic.dataclasses import dataclass # (2) @dataclass class Item: name: str - description: Optional[str] = None + description: Union[str, None] = None @dataclass diff --git a/docs_src/dependencies/tutorial001.py b/docs_src/dependencies/tutorial001.py index a9da971dc..b1275103a 100644 --- a/docs_src/dependencies/tutorial001.py +++ b/docs_src/dependencies/tutorial001.py @@ -1,11 +1,13 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI app = FastAPI() -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): return {"q": q, "skip": skip, "limit": limit} diff --git a/docs_src/dependencies/tutorial002.py b/docs_src/dependencies/tutorial002.py index 458f6b5bb..8e863e4fa 100644 --- a/docs_src/dependencies/tutorial002.py +++ b/docs_src/dependencies/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI @@ -9,7 +9,7 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz" class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit diff --git a/docs_src/dependencies/tutorial003.py b/docs_src/dependencies/tutorial003.py index 3f3e940f8..34614e539 100644 --- a/docs_src/dependencies/tutorial003.py +++ b/docs_src/dependencies/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI @@ -9,7 +9,7 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz" class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit diff --git a/docs_src/dependencies/tutorial004.py b/docs_src/dependencies/tutorial004.py index daa7b4670..d9fe88148 100644 --- a/docs_src/dependencies/tutorial004.py +++ b/docs_src/dependencies/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI @@ -9,7 +9,7 @@ fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz" class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): self.q = q self.skip = skip self.limit = limit diff --git a/docs_src/dependencies/tutorial005.py b/docs_src/dependencies/tutorial005.py index 24f73c617..697332b5b 100644 --- a/docs_src/dependencies/tutorial005.py +++ b/docs_src/dependencies/tutorial005.py @@ -1,16 +1,17 @@ -from typing import Optional +from typing import Union from fastapi import Cookie, Depends, FastAPI app = FastAPI() -def query_extractor(q: Optional[str] = None): +def query_extractor(q: Union[str, None] = None): return q def query_or_cookie_extractor( - q: str = Depends(query_extractor), last_query: Optional[str] = Cookie(default=None) + q: str = Depends(query_extractor), + last_query: Union[str, None] = Cookie(default=None), ): if not q: return last_query diff --git a/docs_src/dependency_testing/tutorial001.py b/docs_src/dependency_testing/tutorial001.py index 237d3b231..a5fe1d9bf 100644 --- a/docs_src/dependency_testing/tutorial001.py +++ b/docs_src/dependency_testing/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI from fastapi.testclient import TestClient @@ -6,7 +6,9 @@ from fastapi.testclient import TestClient app = FastAPI() -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): return {"q": q, "skip": skip, "limit": limit} @@ -23,7 +25,7 @@ async def read_users(commons: dict = Depends(common_parameters)): client = TestClient(app) -async def override_dependency(q: Optional[str] = None): +async def override_dependency(q: Union[str, None] = None): return {"q": q, "skip": 5, "limit": 10} diff --git a/docs_src/encoder/tutorial001.py b/docs_src/encoder/tutorial001.py index a918fdd64..5f7e7061e 100644 --- a/docs_src/encoder/tutorial001.py +++ b/docs_src/encoder/tutorial001.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -11,7 +11,7 @@ fake_db = {} class Item(BaseModel): title: str timestamp: datetime - description: Optional[str] = None + description: Union[str, None] = None app = FastAPI() diff --git a/docs_src/extra_data_types/tutorial001.py b/docs_src/extra_data_types/tutorial001.py index 9f5e911bf..8ae8472a7 100644 --- a/docs_src/extra_data_types/tutorial001.py +++ b/docs_src/extra_data_types/tutorial001.py @@ -1,5 +1,5 @@ from datetime import datetime, time, timedelta -from typing import Optional +from typing import Union from uuid import UUID from fastapi import Body, FastAPI @@ -10,10 +10,10 @@ app = FastAPI() @app.put("/items/{item_id}") async def read_items( item_id: UUID, - start_datetime: Optional[datetime] = Body(default=None), - end_datetime: Optional[datetime] = Body(default=None), - repeat_at: Optional[time] = Body(default=None), - process_after: Optional[timedelta] = Body(default=None), + start_datetime: Union[datetime, None] = Body(default=None), + end_datetime: Union[datetime, None] = Body(default=None), + repeat_at: Union[time, None] = Body(default=None), + process_after: Union[timedelta, None] = Body(default=None), ): start_process = start_datetime + process_after duration = end_datetime - start_process diff --git a/docs_src/extra_models/tutorial001.py b/docs_src/extra_models/tutorial001.py index e95844f60..4be56cd2a 100644 --- a/docs_src/extra_models/tutorial001.py +++ b/docs_src/extra_models/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -10,20 +10,20 @@ class UserIn(BaseModel): username: str password: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None class UserOut(BaseModel): username: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None class UserInDB(BaseModel): username: str hashed_password: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None def fake_password_hasher(raw_password: str): diff --git a/docs_src/extra_models/tutorial002.py b/docs_src/extra_models/tutorial002.py index 5bc6e707f..70fa16441 100644 --- a/docs_src/extra_models/tutorial002.py +++ b/docs_src/extra_models/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -9,7 +9,7 @@ app = FastAPI() class UserBase(BaseModel): username: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None class UserIn(UserBase): diff --git a/docs_src/header_params/tutorial001.py b/docs_src/header_params/tutorial001.py index 1df561a12..74429c8e2 100644 --- a/docs_src/header_params/tutorial001.py +++ b/docs_src/header_params/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Header @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(user_agent: Optional[str] = Header(default=None)): +async def read_items(user_agent: Union[str, None] = Header(default=None)): return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002.py b/docs_src/header_params/tutorial002.py index 2250727f6..639ab1735 100644 --- a/docs_src/header_params/tutorial002.py +++ b/docs_src/header_params/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Header @@ -7,6 +7,6 @@ app = FastAPI() @app.get("/items/") async def read_items( - strange_header: Optional[str] = Header(default=None, convert_underscores=False) + strange_header: Union[str, None] = Header(default=None, convert_underscores=False) ): return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003.py b/docs_src/header_params/tutorial003.py index 1ef131cee..a61314aed 100644 --- a/docs_src/header_params/tutorial003.py +++ b/docs_src/header_params/tutorial003.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI, Header @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: Optional[List[str]] = Header(default=None)): +async def read_items(x_token: Union[List[str], None] = Header(default=None)): return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_py39.py b/docs_src/header_params/tutorial003_py39.py index 78dda58da..34437db16 100644 --- a/docs_src/header_params/tutorial003_py39.py +++ b/docs_src/header_params/tutorial003_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, Header @@ -6,5 +6,5 @@ app = FastAPI() @app.get("/items/") -async def read_items(x_token: Optional[list[str]] = Header(default=None)): +async def read_items(x_token: Union[list[str], None] = Header(default=None)): return {"X-Token values": x_token} diff --git a/docs_src/nosql_databases/tutorial001.py b/docs_src/nosql_databases/tutorial001.py index 39548d862..91893e528 100644 --- a/docs_src/nosql_databases/tutorial001.py +++ b/docs_src/nosql_databases/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from couchbase import LOCKMODE_WAIT from couchbase.bucket import Bucket @@ -23,9 +23,9 @@ def get_bucket(): class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None class UserInDB(User): diff --git a/docs_src/openapi_callbacks/tutorial001.py b/docs_src/openapi_callbacks/tutorial001.py index 2fb836751..3f1bac6e2 100644 --- a/docs_src/openapi_callbacks/tutorial001.py +++ b/docs_src/openapi_callbacks/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import APIRouter, FastAPI from pydantic import BaseModel, HttpUrl @@ -8,7 +8,7 @@ app = FastAPI() class Invoice(BaseModel): id: str - title: Optional[str] = None + title: Union[str, None] = None customer: str total: float @@ -33,7 +33,7 @@ def invoice_notification(body: InvoiceEvent): @app.post("/invoices/", callbacks=invoices_callback_router.routes) -def create_invoice(invoice: Invoice, callback_url: Optional[HttpUrl] = None): +def create_invoice(invoice: Invoice, callback_url: Union[HttpUrl, None] = None): """ Create an invoice. diff --git a/docs_src/path_operation_advanced_configuration/tutorial004.py b/docs_src/path_operation_advanced_configuration/tutorial004.py index da678aed3..a3aad4ac4 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial004.py +++ b/docs_src/path_operation_advanced_configuration/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial001.py b/docs_src/path_operation_configuration/tutorial001.py index 1316d9237..83fd8377a 100644 --- a/docs_src/path_operation_configuration/tutorial001.py +++ b/docs_src/path_operation_configuration/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI, status from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial001_py39.py b/docs_src/path_operation_configuration/tutorial001_py39.py index 5c04d8bac..a9dcbf389 100644 --- a/docs_src/path_operation_configuration/tutorial001_py39.py +++ b/docs_src/path_operation_configuration/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, status from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial002.py b/docs_src/path_operation_configuration/tutorial002.py index 2df537d86..798b0c231 100644 --- a/docs_src/path_operation_configuration/tutorial002.py +++ b/docs_src/path_operation_configuration/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial002_py39.py b/docs_src/path_operation_configuration/tutorial002_py39.py index 766d9fb0b..e7ced7de7 100644 --- a/docs_src/path_operation_configuration/tutorial002_py39.py +++ b/docs_src/path_operation_configuration/tutorial002_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial003.py b/docs_src/path_operation_configuration/tutorial003.py index 269a1a253..26bf7daba 100644 --- a/docs_src/path_operation_configuration/tutorial003.py +++ b/docs_src/path_operation_configuration/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial003_py39.py b/docs_src/path_operation_configuration/tutorial003_py39.py index 446198b5c..607c5707e 100644 --- a/docs_src/path_operation_configuration/tutorial003_py39.py +++ b/docs_src/path_operation_configuration/tutorial003_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial004.py b/docs_src/path_operation_configuration/tutorial004.py index de83be836..8f865c58a 100644 --- a/docs_src/path_operation_configuration/tutorial004.py +++ b/docs_src/path_operation_configuration/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial004_py39.py b/docs_src/path_operation_configuration/tutorial004_py39.py index bf6005b95..fc25680c5 100644 --- a/docs_src/path_operation_configuration/tutorial004_py39.py +++ b/docs_src/path_operation_configuration/tutorial004_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial005.py b/docs_src/path_operation_configuration/tutorial005.py index 0f62c3814..2c1be4a34 100644 --- a/docs_src/path_operation_configuration/tutorial005.py +++ b/docs_src/path_operation_configuration/tutorial005.py @@ -1,4 +1,4 @@ -from typing import Optional, Set +from typing import Set, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: Set[str] = set() diff --git a/docs_src/path_operation_configuration/tutorial005_py39.py b/docs_src/path_operation_configuration/tutorial005_py39.py index 5ef320405..ddf29b733 100644 --- a/docs_src/path_operation_configuration/tutorial005_py39.py +++ b/docs_src/path_operation_configuration/tutorial005_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: set[str] = set() diff --git a/docs_src/python_types/tutorial009c.py b/docs_src/python_types/tutorial009c.py new file mode 100644 index 000000000..2f539a34b --- /dev/null +++ b/docs_src/python_types/tutorial009c.py @@ -0,0 +1,5 @@ +from typing import Optional + + +def say_hi(name: Optional[str]): + print(f"Hey {name}!") diff --git a/docs_src/python_types/tutorial009c_py310.py b/docs_src/python_types/tutorial009c_py310.py new file mode 100644 index 000000000..96b1220fc --- /dev/null +++ b/docs_src/python_types/tutorial009c_py310.py @@ -0,0 +1,2 @@ +def say_hi(name: str | None): + print(f"Hey {name}!") diff --git a/docs_src/python_types/tutorial011.py b/docs_src/python_types/tutorial011.py index 047b633b5..c8634cbff 100644 --- a/docs_src/python_types/tutorial011.py +++ b/docs_src/python_types/tutorial011.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import List, Optional +from typing import List, Union from pydantic import BaseModel @@ -7,7 +7,7 @@ from pydantic import BaseModel class User(BaseModel): id: int name = "John Doe" - signup_ts: Optional[datetime] = None + signup_ts: Union[datetime, None] = None friends: List[int] = [] diff --git a/docs_src/python_types/tutorial011_py39.py b/docs_src/python_types/tutorial011_py39.py index af79e2df0..468496f51 100644 --- a/docs_src/python_types/tutorial011_py39.py +++ b/docs_src/python_types/tutorial011_py39.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional +from typing import Union from pydantic import BaseModel @@ -7,7 +7,7 @@ from pydantic import BaseModel class User(BaseModel): id: int name = "John Doe" - signup_ts: Optional[datetime] = None + signup_ts: Union[datetime, None] = None friends: list[int] = [] diff --git a/docs_src/python_types/tutorial012.py b/docs_src/python_types/tutorial012.py new file mode 100644 index 000000000..74fa94c43 --- /dev/null +++ b/docs_src/python_types/tutorial012.py @@ -0,0 +1,8 @@ +from typing import Optional + +from pydantic import BaseModel + + +class User(BaseModel): + name: str + age: Optional[int] diff --git a/docs_src/query_params/tutorial002.py b/docs_src/query_params/tutorial002.py index 32918465e..8465f45ee 100644 --- a/docs_src/query_params/tutorial002.py +++ b/docs_src/query_params/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/{item_id}") -async def read_item(item_id: str, q: Optional[str] = None): +async def read_item(item_id: str, q: Union[str, None] = None): if q: return {"item_id": item_id, "q": q} return {"item_id": item_id} diff --git a/docs_src/query_params/tutorial003.py b/docs_src/query_params/tutorial003.py index c81a96785..3362715b3 100644 --- a/docs_src/query_params/tutorial003.py +++ b/docs_src/query_params/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/{item_id}") -async def read_item(item_id: str, q: Optional[str] = None, short: bool = False): +async def read_item(item_id: str, q: Union[str, None] = None, short: bool = False): item = {"item_id": item_id} if q: item.update({"q": q}) diff --git a/docs_src/query_params/tutorial004.py b/docs_src/query_params/tutorial004.py index 37f97fa2a..049c3ae93 100644 --- a/docs_src/query_params/tutorial004.py +++ b/docs_src/query_params/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/users/{user_id}/items/{item_id}") async def read_user_item( - user_id: int, item_id: str, q: Optional[str] = None, short: bool = False + user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False ): item = {"item_id": item_id, "owner_id": user_id} if q: diff --git a/docs_src/query_params/tutorial006.py b/docs_src/query_params/tutorial006.py index ffe328340..f0dbfe08f 100644 --- a/docs_src/query_params/tutorial006.py +++ b/docs_src/query_params/tutorial006.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -7,7 +7,7 @@ app = FastAPI() @app.get("/items/{item_id}") async def read_user_item( - item_id: str, needy: str, skip: int = 0, limit: Optional[int] = None + item_id: str, needy: str, skip: int = 0, limit: Union[int, None] = None ): item = {"item_id": item_id, "needy": needy, "skip": skip, "limit": limit} return item diff --git a/docs_src/query_params_str_validations/tutorial001.py b/docs_src/query_params_str_validations/tutorial001.py index 5d7bfb0ee..e38326b18 100644 --- a/docs_src/query_params_str_validations/tutorial001.py +++ b/docs_src/query_params_str_validations/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") -async def read_items(q: Optional[str] = None): +async def read_items(q: Union[str, None] = None): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: results.update({"q": q}) diff --git a/docs_src/request_files/tutorial001_02.py b/docs_src/request_files/tutorial001_02.py index 3f311c4b8..ac30be2d3 100644 --- a/docs_src/request_files/tutorial001_02.py +++ b/docs_src/request_files/tutorial001_02.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI, File, UploadFile @@ -6,7 +6,7 @@ app = FastAPI() @app.post("/files/") -async def create_file(file: Optional[bytes] = File(default=None)): +async def create_file(file: Union[bytes, None] = File(default=None)): if not file: return {"message": "No file sent"} else: @@ -14,7 +14,7 @@ async def create_file(file: Optional[bytes] = File(default=None)): @app.post("/uploadfile/") -async def create_upload_file(file: Optional[UploadFile] = None): +async def create_upload_file(file: Union[UploadFile, None] = None): if not file: return {"message": "No upload file sent"} else: diff --git a/docs_src/response_directly/tutorial001.py b/docs_src/response_directly/tutorial001.py index 6acdc0fc8..5ab655a8a 100644 --- a/docs_src/response_directly/tutorial001.py +++ b/docs_src/response_directly/tutorial001.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional +from typing import Union from fastapi import FastAPI from fastapi.encoders import jsonable_encoder @@ -10,7 +10,7 @@ from pydantic import BaseModel class Item(BaseModel): title: str timestamp: datetime - description: Optional[str] = None + description: Union[str, None] = None app = FastAPI() diff --git a/docs_src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py index 57992ecfc..0f6e03e5b 100644 --- a/docs_src/response_model/tutorial001.py +++ b/docs_src/response_model/tutorial001.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: List[str] = [] diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py index 37b866864..cdcca39d2 100644 --- a/docs_src/response_model/tutorial001_py39.py +++ b/docs_src/response_model/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None tags: list[str] = [] diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py index 373317eb9..c68e8b138 100644 --- a/docs_src/response_model/tutorial002.py +++ b/docs_src/response_model/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -10,7 +10,7 @@ class UserIn(BaseModel): username: str password: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None # Don't do this in production! diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py index e14026dd8..37e493dcb 100644 --- a/docs_src/response_model/tutorial003.py +++ b/docs_src/response_model/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -10,13 +10,13 @@ class UserIn(BaseModel): username: str password: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None class UserOut(BaseModel): username: str email: EmailStr - full_name: Optional[str] = None + full_name: Union[str, None] = None @app.post("/user/", response_model=UserOut) diff --git a/docs_src/response_model/tutorial004.py b/docs_src/response_model/tutorial004.py index 1e18f989d..10b48039a 100644 --- a/docs_src/response_model/tutorial004.py +++ b/docs_src/response_model/tutorial004.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,7 +8,7 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float tax: float = 10.5 tags: List[str] = [] diff --git a/docs_src/response_model/tutorial004_py39.py b/docs_src/response_model/tutorial004_py39.py index 07ccbbf41..9463b45ec 100644 --- a/docs_src/response_model/tutorial004_py39.py +++ b/docs_src/response_model/tutorial004_py39.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,7 +8,7 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float tax: float = 10.5 tags: list[str] = [] diff --git a/docs_src/response_model/tutorial005.py b/docs_src/response_model/tutorial005.py index 03933d1f7..30eb9f8e3 100644 --- a/docs_src/response_model/tutorial005.py +++ b/docs_src/response_model/tutorial005.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,7 +8,7 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float tax: float = 10.5 diff --git a/docs_src/response_model/tutorial006.py b/docs_src/response_model/tutorial006.py index 629ab8a3a..3ffdb512b 100644 --- a/docs_src/response_model/tutorial006.py +++ b/docs_src/response_model/tutorial006.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,7 +8,7 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float tax: float = 10.5 diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index fab4d7a44..a5ae28127 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None class Config: schema_extra = { diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002.py index a2aec46f5..6de434f81 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import FastAPI from pydantic import BaseModel, Field @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str = Field(example="Foo") - description: Optional[str] = Field(default=None, example="A very nice Item") + description: Union[str, None] = Field(default=None, example="A very nice Item") price: float = Field(example=35.4) - tax: Optional[float] = Field(default=None, example=3.2) + tax: Union[float, None] = Field(default=None, example=3.2) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index 43d46b81b..ce1736bba 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index 42d7a04a3..b67edf30c 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Body, FastAPI from pydantic import BaseModel @@ -8,9 +8,9 @@ app = FastAPI() class Item(BaseModel): name: str - description: Optional[str] = None + description: Union[str, None] = None price: float - tax: Optional[float] = None + tax: Union[float, None] = None @app.put("/items/{item_id}") diff --git a/docs_src/security/tutorial002.py b/docs_src/security/tutorial002.py index 03e0cd5fc..bfd035221 100644 --- a/docs_src/security/tutorial002.py +++ b/docs_src/security/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI from fastapi.security import OAuth2PasswordBearer @@ -11,9 +11,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None def fake_decode_token(token): diff --git a/docs_src/security/tutorial003.py b/docs_src/security/tutorial003.py index a6bb176e4..4b324866f 100644 --- a/docs_src/security/tutorial003.py +++ b/docs_src/security/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -33,9 +33,9 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None class UserInDB(User): diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 18e2c428f..64099abe9 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -31,14 +31,14 @@ class Token(BaseModel): class TokenData(BaseModel): - username: Optional[str] = None + username: Union[str, None] = None class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None class UserInDB(User): @@ -75,7 +75,7 @@ def authenticate_user(fake_db, username: str, password: str): return user -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index 5b34a09f1..ab3af9a6a 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import List, Optional +from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status from fastapi.security import ( @@ -42,15 +42,15 @@ class Token(BaseModel): class TokenData(BaseModel): - username: Optional[str] = None + username: Union[str, None] = None scopes: List[str] = [] class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None class UserInDB(User): @@ -90,7 +90,7 @@ def authenticate_user(fake_db, username: str, password: str): return user -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index d45c08ce6..38391308a 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import Optional +from typing import Union from fastapi import Depends, FastAPI, HTTPException, Security, status from fastapi.security import ( @@ -42,15 +42,15 @@ class Token(BaseModel): class TokenData(BaseModel): - username: Optional[str] = None + username: Union[str, None] = None scopes: list[str] = [] class User(BaseModel): username: str - email: Optional[str] = None - full_name: Optional[str] = None - disabled: Optional[bool] = None + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None class UserInDB(User): @@ -90,7 +90,7 @@ def authenticate_user(fake_db, username: str, password: str): return user -def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta diff --git a/docs_src/sql_databases/sql_app/schemas.py b/docs_src/sql_databases/sql_app/schemas.py index 51655663a..c49beba88 100644 --- a/docs_src/sql_databases/sql_app/schemas.py +++ b/docs_src/sql_databases/sql_app/schemas.py @@ -1,11 +1,11 @@ -from typing import List, Optional +from typing import List, Union from pydantic import BaseModel class ItemBase(BaseModel): title: str - description: Optional[str] = None + description: Union[str, None] = None class ItemCreate(ItemBase): diff --git a/docs_src/sql_databases/sql_app_py39/schemas.py b/docs_src/sql_databases/sql_app_py39/schemas.py index a19f1cdfe..dadc403d9 100644 --- a/docs_src/sql_databases/sql_app_py39/schemas.py +++ b/docs_src/sql_databases/sql_app_py39/schemas.py @@ -1,11 +1,11 @@ -from typing import Optional +from typing import Union from pydantic import BaseModel class ItemBase(BaseModel): title: str - description: Optional[str] = None + description: Union[str, None] = None class ItemCreate(ItemBase): diff --git a/docs_src/sql_databases_peewee/sql_app/schemas.py b/docs_src/sql_databases_peewee/sql_app/schemas.py index b715604ee..d8775cb30 100644 --- a/docs_src/sql_databases_peewee/sql_app/schemas.py +++ b/docs_src/sql_databases_peewee/sql_app/schemas.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any, List, Union import peewee from pydantic import BaseModel @@ -15,7 +15,7 @@ class PeeweeGetterDict(GetterDict): class ItemBase(BaseModel): title: str - description: Optional[str] = None + description: Union[str, None] = None class ItemCreate(ItemBase): diff --git a/docs_src/websockets/tutorial002.py b/docs_src/websockets/tutorial002.py index b01008530..cf5c7e805 100644 --- a/docs_src/websockets/tutorial002.py +++ b/docs_src/websockets/tutorial002.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Union from fastapi import Cookie, Depends, FastAPI, Query, WebSocket, status from fastapi.responses import HTMLResponse @@ -57,8 +57,8 @@ async def get(): async def get_cookie_or_token( websocket: WebSocket, - session: Optional[str] = Cookie(default=None), - token: Optional[str] = Query(default=None), + session: Union[str, None] = Cookie(default=None), + token: Union[str, None] = Query(default=None), ): if session is None and token is None: await websocket.close(code=status.WS_1008_POLICY_VIOLATION) @@ -69,7 +69,7 @@ async def get_cookie_or_token( async def websocket_endpoint( websocket: WebSocket, item_id: str, - q: Optional[int] = None, + q: Union[int, None] = None, cookie_or_token: str = Depends(get_cookie_or_token), ): await websocket.accept() From 0a8d6871fb860a1dcedd169035c9df847825439a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 14 May 2022 12:00:32 +0000 Subject: [PATCH 0260/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 416983bc1..2a43718a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add docs recommending `Union` over `Optional` and migrate source examples. PR [#4908](https://github.com/tiangolo/fastapi/pull/4908) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for not needing `...` as default value in required Query(), Path(), Header(), etc.. PR [#4906](https://github.com/tiangolo/fastapi/pull/4906) by [@tiangolo](https://github.com/tiangolo). * 🎨 Fix default value as set in tutorial for Path Operations Advanced Configurations. PR [#4899](https://github.com/tiangolo/fastapi/pull/4899) by [@tiangolo](https://github.com/tiangolo). * ♻ Refactor dict value extraction to minimize key lookups `fastapi/utils.py`. PR [#3139](https://github.com/tiangolo/fastapi/pull/3139) by [@ShahriyarR](https://github.com/ShahriyarR). From acab64b3c355a2aeb13c7739a639f9fe2a03bc1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 14 May 2022 14:08:31 -0500 Subject: [PATCH 0261/1881] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20requi?= =?UTF-8?q?red=20nonable=20parameters=20and=20body=20fields=20(#4907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_required_noneable.py | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_required_noneable.py diff --git a/tests/test_required_noneable.py b/tests/test_required_noneable.py new file mode 100644 index 000000000..5da8cd4d0 --- /dev/null +++ b/tests/test_required_noneable.py @@ -0,0 +1,62 @@ +from typing import Union + +from fastapi import Body, FastAPI, Query +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/query") +def read_query(q: Union[str, None]): + return q + + +@app.get("/explicit-query") +def read_explicit_query(q: Union[str, None] = Query()): + return q + + +@app.post("/body-embed") +def send_body_embed(b: Union[str, None] = Body(embed=True)): + return b + + +client = TestClient(app) + + +def test_required_nonable_query_invalid(): + response = client.get("/query") + assert response.status_code == 422 + + +def test_required_noneable_query_value(): + response = client.get("/query", params={"q": "foo"}) + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_required_nonable_explicit_query_invalid(): + response = client.get("/explicit-query") + assert response.status_code == 422 + + +def test_required_nonable_explicit_query_value(): + response = client.get("/explicit-query", params={"q": "foo"}) + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_required_nonable_body_embed_no_content(): + response = client.post("/body-embed") + assert response.status_code == 422 + + +def test_required_nonable_body_embed_invalid(): + response = client.post("/body-embed", json={"invalid": "invalid"}) + assert response.status_code == 422 + + +def test_required_noneable_body_embed_value(): + response = client.post("/body-embed", json={"b": "foo"}) + assert response.status_code == 200 + assert response.json() == "foo" From 1711403732cf092ac69cfca0783862631489ad21 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 14 May 2022 19:09:00 +0000 Subject: [PATCH 0262/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2a43718a1..81bc21645 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Add tests for required nonable parameters and body fields. PR [#4907](https://github.com/tiangolo/fastapi/pull/4907) by [@tiangolo](https://github.com/tiangolo). * 📝 Add docs recommending `Union` over `Optional` and migrate source examples. PR [#4908](https://github.com/tiangolo/fastapi/pull/4908) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for not needing `...` as default value in required Query(), Path(), Header(), etc.. PR [#4906](https://github.com/tiangolo/fastapi/pull/4906) by [@tiangolo](https://github.com/tiangolo). * 🎨 Fix default value as set in tutorial for Path Operations Advanced Configurations. PR [#4899](https://github.com/tiangolo/fastapi/pull/4899) by [@tiangolo](https://github.com/tiangolo). From 1673b3ec118d5b7efe4865d1d752ff2a878282c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 14 May 2022 14:53:50 -0500 Subject: [PATCH 0263/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 106 ++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 12 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 81bc21645..a671fe276 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,27 +2,109 @@ ## Latest Changes -* ✅ Add tests for required nonable parameters and body fields. PR [#4907](https://github.com/tiangolo/fastapi/pull/4907) by [@tiangolo](https://github.com/tiangolo). -* 📝 Add docs recommending `Union` over `Optional` and migrate source examples. PR [#4908](https://github.com/tiangolo/fastapi/pull/4908) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for not needing `...` as default value in required Query(), Path(), Header(), etc.. PR [#4906](https://github.com/tiangolo/fastapi/pull/4906) by [@tiangolo](https://github.com/tiangolo). +### Features + +* ✨ Add support for omitting `...` as default value when declaring required parameters with: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +New docs at [Tutorial - Query Parameters and String Validations - Make it required](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#make-it-required). PR [#4906](https://github.com/tiangolo/fastapi/pull/4906) by [@tiangolo](https://github.com/tiangolo). + +Up to now, declaring a required parameter while adding additional validation or metadata needed using `...` (Ellipsis). + +For example: + +```Python +from fastapi import Cookie, FastAPI, Header, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +def main( + item_id: int = Path(default=..., gt=0), + query: str = Query(default=..., max_length=10), + session: str = Cookie(default=..., min_length=3), + x_trace: str = Header(default=..., title="Tracing header"), +): + return {"message": "Hello World"} +``` + +...all these parameters are required because the default value is `...` (Ellipsis). + +But now it's possible and supported to just omit the default value, as would be done with Pydantic fields, and the parameters would still be required. + +✨ For example, this is now supported: + +```Python +from fastapi import Cookie, FastAPI, Header, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +def main( + item_id: int = Path(gt=0), + query: str = Query(max_length=10), + session: str = Cookie(min_length=3), + x_trace: str = Header(title="Tracing header"), +): + return {"message": "Hello World"} +``` + +To declare parameters as optional (not required), you can set a default value as always, for example using `None`: + +```Python +from typing import Union +from fastapi import Cookie, FastAPI, Header, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +def main( + item_id: int = Path(gt=0), + query: Union[str, None] = Query(default=None, max_length=10), + session: Union[str, None] = Cookie(default=None, min_length=3), + x_trace: Union[str, None] = Header(default=None, title="Tracing header"), +): + return {"message": "Hello World"} +``` + +### Docs + +* 📝 Add docs recommending `Union` over `Optional` and migrate source examples. New docs at [Python Types Intro - Using `Union` or `Optional`](https://fastapi.tiangolo.com/python-types/#using-union-or-optional). PR [#4908](https://github.com/tiangolo/fastapi/pull/4908) by [@tiangolo](https://github.com/tiangolo). * 🎨 Fix default value as set in tutorial for Path Operations Advanced Configurations. PR [#4899](https://github.com/tiangolo/fastapi/pull/4899) by [@tiangolo](https://github.com/tiangolo). +* 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). +* 📝 Updates links for Celery documentation. PR [#4736](https://github.com/tiangolo/fastapi/pull/4736) by [@sammyzord](https://github.com/sammyzord). +* ✏ Fix example code with sets in tutorial for body nested models. PR [#3030](https://github.com/tiangolo/fastapi/pull/3030) by [@hitrust](https://github.com/hitrust). +* ✏ Fix links to Pydantic docs. PR [#4670](https://github.com/tiangolo/fastapi/pull/4670) by [@kinuax](https://github.com/kinuax). +* 📝 Update docs about Swagger UI self-hosting with newer source links. PR [#4813](https://github.com/tiangolo/fastapi/pull/4813) by [@Kastakin](https://github.com/Kastakin). +* 📝 Add link to external article: Building the Poll App From Django Tutorial With FastAPI And React. PR [#4778](https://github.com/tiangolo/fastapi/pull/4778) by [@jbrocher](https://github.com/jbrocher). +* 📝 Add OpenAPI warning to "Body - Fields" docs with extra schema extensions. PR [#4846](https://github.com/tiangolo/fastapi/pull/4846) by [@ml-evs](https://github.com/ml-evs). + +### Translations + +* 🌐 Fix code examples in Japanese translation for `docs/ja/docs/tutorial/testing.md`. PR [#4623](https://github.com/tiangolo/fastapi/pull/4623) by [@hirotoKirimaru](https://github.com/hirotoKirimaru). + +### Internal + * ♻ Refactor dict value extraction to minimize key lookups `fastapi/utils.py`. PR [#3139](https://github.com/tiangolo/fastapi/pull/3139) by [@ShahriyarR](https://github.com/ShahriyarR). +* ✅ Add tests for required nonable parameters and body fields. PR [#4907](https://github.com/tiangolo/fastapi/pull/4907) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix installing Material for MkDocs Insiders in CI. PR [#4897](https://github.com/tiangolo/fastapi/pull/4897) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit CI instead of custom GitHub Action. PR [#4896](https://github.com/tiangolo/fastapi/pull/4896) by [@tiangolo](https://github.com/tiangolo). * 👷 Add pre-commit GitHub Action workflow. PR [#4895](https://github.com/tiangolo/fastapi/pull/4895) by [@tiangolo](https://github.com/tiangolo). -* 📝 Add documentation for redefined path operations. PR [#4864](https://github.com/tiangolo/fastapi/pull/4864) by [@madkinsz](https://github.com/madkinsz). +* 📝 Add dark mode auto switch to docs based on OS preference. PR [#4869](https://github.com/tiangolo/fastapi/pull/4869) by [@ComicShrimp](https://github.com/ComicShrimp). * 🔥 Remove un-used old pending tests, already covered in other places. PR [#4891](https://github.com/tiangolo/fastapi/pull/4891) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Python formatting hooks to pre-commit. PR [#4890](https://github.com/tiangolo/fastapi/pull/4890) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add pre-commit with first config and first formatting pass. PR [#4888](https://github.com/tiangolo/fastapi/pull/4888) by [@tiangolo](https://github.com/tiangolo). * 👷 Disable CI installing Material for MkDocs in forks. PR [#4410](https://github.com/tiangolo/fastapi/pull/4410) by [@dolfinus](https://github.com/dolfinus). -* 📝 Add OpenAPI warning to "Body - Fields" docs with extra schema extensions. PR [#4846](https://github.com/tiangolo/fastapi/pull/4846) by [@ml-evs](https://github.com/ml-evs). -* 📝 Add dark mode auto switch to docs based on OS preference. PR [#4869](https://github.com/tiangolo/fastapi/pull/4869) by [@ComicShrimp](https://github.com/ComicShrimp). -* 📝 Update docs about Swagger UI self-hosting with newer source links. PR [#4813](https://github.com/tiangolo/fastapi/pull/4813) by [@Kastakin](https://github.com/Kastakin). -* 📝 Add link to external article: Building the Poll App From Django Tutorial With FastAPI And React. PR [#4778](https://github.com/tiangolo/fastapi/pull/4778) by [@jbrocher](https://github.com/jbrocher). -* 🌐 Fix code examples in Japanese translation for `docs/ja/docs/tutorial/testing.md`. PR [#4623](https://github.com/tiangolo/fastapi/pull/4623) by [@hirotoKirimaru](https://github.com/hirotoKirimaru). -* 📝 Updates links for Celery documentation. PR [#4736](https://github.com/tiangolo/fastapi/pull/4736) by [@sammyzord](https://github.com/sammyzord). -* ✏ Fix example code with sets in tutorial for body nested models. PR [#3030](https://github.com/tiangolo/fastapi/pull/3030) by [@hitrust](https://github.com/hitrust). -* ✏ Fix links to Pydantic docs. PR [#4670](https://github.com/tiangolo/fastapi/pull/4670) by [@kinuax](https://github.com/kinuax). ## 0.77.1 From 1876ebc77949a9a254909ec61ea0c09365169ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 14 May 2022 14:58:04 -0500 Subject: [PATCH 0264/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?78.0?= 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 a671fe276..edc58171b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.78.0 + ### Features * ✨ Add support for omitting `...` as default value when declaring required parameters with: diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 1a4d00162..2465e2c27 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.77.1" +__version__ = "0.78.0" from starlette import status as status From d8f3e8f20de8ebca5293f5b3ce71ee93c9124328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 25 Jun 2022 21:49:44 +0200 Subject: [PATCH 0265/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Classiq,=20add=20ImgWhale=20(#5079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Update sponsors, remove Classiq, add ImgWhale * 🔧 Update README --- README.md | 2 +- docs/en/data/sponsors.yml | 6 ++-- docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/imgwhale-banner.svg | 14 ++++++++++ docs/en/docs/img/sponsors/imgwhale.svg | 28 +++++++++++++++++++ docs/en/overrides/main.html | 12 ++++---- 6 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 docs/en/docs/img/sponsors/imgwhale-banner.svg create mode 100644 docs/en/docs/img/sponsors/imgwhale.svg diff --git a/README.md b/README.md index 5e9e97a2a..d605affac 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index ac825193b..2fc08b3a5 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,12 +5,12 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg - - url: https://classiq.link/n4s - title: Join the team building a new SaaS platform that will change the computing world - img: https://fastapi.tiangolo.com/img/sponsors/classiq.png - url: https://www.dropbase.io/careers title: Dropbase - seamlessly collect, clean, and centralize data. img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg + - url: https://app.imgwhale.xyz/ + title: The ultimate solution to unlimited and forever cloud storage. + img: https://fastapi.tiangolo.com/img/sponsors/imgwhale.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 1c8b0cde7..10a31b86a 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -10,3 +10,4 @@ logins: - InesIvanova - DropbaseHQ - VincentParedes + - BLUE-DEVIL1134 diff --git a/docs/en/docs/img/sponsors/imgwhale-banner.svg b/docs/en/docs/img/sponsors/imgwhale-banner.svg new file mode 100644 index 000000000..db87cc4c9 --- /dev/null +++ b/docs/en/docs/img/sponsors/imgwhale-banner.svg @@ -0,0 +1,14 @@ + + + + + + + + + + ImgWhale + The ultimate solution to unlimited and forevercloud storage. + + diff --git a/docs/en/docs/img/sponsors/imgwhale.svg b/docs/en/docs/img/sponsors/imgwhale.svg new file mode 100644 index 000000000..46aefd930 --- /dev/null +++ b/docs/en/docs/img/sponsors/imgwhale.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + ImgWhale + + The ultimate solution to unlimited and forever cloud storage. + + The ultimate solution to unlimited and forever cloud storage. + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index eb1cb5c82..62063da47 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -40,18 +40,18 @@ - + {% endblock %} From f8c875bb3fd2f01112df666c4ab4ff4d1fe7d49b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 25 Jun 2022 19:50:20 +0000 Subject: [PATCH 0266/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 edc58171b..0e60b0abf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, remove Classiq, add ImgWhale. PR [#5079](https://github.com/tiangolo/fastapi/pull/5079) by [@tiangolo](https://github.com/tiangolo). ## 0.78.0 From 6c6382df4d2d2c9f003658dae42f84fd2fbad36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 1 Jul 2022 10:58:40 +0200 Subject: [PATCH 0267/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Dropbase,=20add=20Doist=20(#5096)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 6 +-- docs/en/docs/img/sponsors/doist-banner.svg | 46 ++++++++++++++++++ docs/en/docs/img/sponsors/doist.svg | 54 ++++++++++++++++++++++ docs/en/overrides/main.html | 12 ++--- 5 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 docs/en/docs/img/sponsors/doist-banner.svg create mode 100644 docs/en/docs/img/sponsors/doist.svg diff --git a/README.md b/README.md index d605affac..505005ae9 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,8 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 2fc08b3a5..c99c4b57a 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,12 +5,12 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg - - url: https://www.dropbase.io/careers - title: Dropbase - seamlessly collect, clean, and centralize data. - img: https://fastapi.tiangolo.com/img/sponsors/dropbase.svg - url: https://app.imgwhale.xyz/ title: The ultimate solution to unlimited and forever cloud storage. img: https://fastapi.tiangolo.com/img/sponsors/imgwhale.svg + - url: https://doist.com/careers/9B437B1615-wa-senior-backend-engineer-python + title: Help us migrate doist to FastAPI + img: https://fastapi.tiangolo.com/img/sponsors/doist.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/doist-banner.svg b/docs/en/docs/img/sponsors/doist-banner.svg new file mode 100644 index 000000000..3a4d9a295 --- /dev/null +++ b/docs/en/docs/img/sponsors/doist-banner.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/doist.svg b/docs/en/docs/img/sponsors/doist.svg new file mode 100644 index 000000000..b55855f06 --- /dev/null +++ b/docs/en/docs/img/sponsors/doist.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 62063da47..9bed0253f 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -40,18 +40,18 @@ - + {% endblock %} From 80472301817d3581f56163874b42d06c26d99942 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 1 Jul 2022 08:59:26 +0000 Subject: [PATCH 0268/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0e60b0abf..2ae0a2ed0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, remove Dropbase, add Doist. PR [#5096](https://github.com/tiangolo/fastapi/pull/5096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Classiq, add ImgWhale. PR [#5079](https://github.com/tiangolo/fastapi/pull/5079) by [@tiangolo](https://github.com/tiangolo). ## 0.78.0 From b7686435777d1d4f79adad8f29191039bbc558b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 11 Jul 2022 21:30:41 +0200 Subject: [PATCH 0269/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Move=20from=20`O?= =?UTF-8?q?ptional[X]`=20to=20`Union[X,=20None]`=20for=20internal=20utils?= =?UTF-8?q?=20(#5124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment-docs-preview-in-pr/app/main.py | 6 +++--- .../actions/notify-translations/app/main.py | 6 +++--- .github/actions/people/app/main.py | 18 +++++++++--------- .github/actions/watch-previews/app/main.py | 6 +++--- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py index c9fb7cbbe..68914fdb9 100644 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ b/.github/actions/comment-docs-preview-in-pr/app/main.py @@ -1,7 +1,7 @@ import logging import sys from pathlib import Path -from typing import Optional +from typing import Union import httpx from github import Github @@ -14,7 +14,7 @@ github_api = "https://api.github.com" class Settings(BaseSettings): github_repository: str github_event_path: Path - github_event_name: Optional[str] = None + github_event_name: Union[str, None] = None input_token: SecretStr input_deploy_url: str @@ -42,7 +42,7 @@ if __name__ == "__main__": except ValidationError as e: logging.error(f"Error parsing event file: {e.errors()}") sys.exit(0) - use_pr: Optional[PullRequest] = None + use_pr: Union[PullRequest, None] = None for pr in repo.get_pulls(): if pr.head.sha == event.workflow_run.head_commit.id: use_pr = pr diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index 823685e00..d4ba0ecfc 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -2,7 +2,7 @@ import logging import random import time from pathlib import Path -from typing import Dict, Optional +from typing import Dict, Union import yaml from github import Github @@ -18,8 +18,8 @@ class Settings(BaseSettings): github_repository: str input_token: SecretStr github_event_path: Path - github_event_name: Optional[str] = None - input_debug: Optional[bool] = False + github_event_name: Union[str, None] = None + input_debug: Union[bool, None] = False class PartialGitHubEventIssue(BaseModel): diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 9de6fc250..1455d01ca 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -4,7 +4,7 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Container, DefaultDict, Dict, List, Optional, Set +from typing import Container, DefaultDict, Dict, List, Set, Union import httpx import yaml @@ -133,7 +133,7 @@ class Author(BaseModel): class CommentsNode(BaseModel): createdAt: datetime - author: Optional[Author] = None + author: Union[Author, None] = None class Comments(BaseModel): @@ -142,7 +142,7 @@ class Comments(BaseModel): class IssuesNode(BaseModel): number: int - author: Optional[Author] = None + author: Union[Author, None] = None title: str createdAt: datetime state: str @@ -179,7 +179,7 @@ class Labels(BaseModel): class ReviewNode(BaseModel): - author: Optional[Author] = None + author: Union[Author, None] = None state: str @@ -190,7 +190,7 @@ class Reviews(BaseModel): class PullRequestNode(BaseModel): number: int labels: Labels - author: Optional[Author] = None + author: Union[Author, None] = None title: str createdAt: datetime state: str @@ -263,7 +263,7 @@ class Settings(BaseSettings): def get_graphql_response( - *, settings: Settings, query: str, after: Optional[str] = None + *, settings: Settings, query: str, after: Union[str, None] = None ): headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} variables = {"after": after} @@ -280,19 +280,19 @@ def get_graphql_response( return data -def get_graphql_issue_edges(*, settings: Settings, after: Optional[str] = None): +def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=issues_query, after=after) graphql_response = IssuesResponse.parse_obj(data) return graphql_response.data.repository.issues.edges -def get_graphql_pr_edges(*, settings: Settings, after: Optional[str] = None): +def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=prs_query, after=after) graphql_response = PRsResponse.parse_obj(data) return graphql_response.data.repository.pullRequests.edges -def get_graphql_sponsor_edges(*, settings: Settings, after: Optional[str] = None): +def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=sponsors_query, after=after) graphql_response = SponsorsResponse.parse_obj(data) return graphql_response.data.user.sponsorshipsAsMaintainer.edges diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py index 3b3520599..51285d02b 100644 --- a/.github/actions/watch-previews/app/main.py +++ b/.github/actions/watch-previews/app/main.py @@ -1,7 +1,7 @@ import logging from datetime import datetime from pathlib import Path -from typing import List, Optional +from typing import List, Union import httpx from github import Github @@ -16,7 +16,7 @@ class Settings(BaseSettings): input_token: SecretStr github_repository: str github_event_path: Path - github_event_name: Optional[str] = None + github_event_name: Union[str, None] = None class Artifact(BaseModel): @@ -74,7 +74,7 @@ if __name__ == "__main__": logging.info(f"Docs preview was notified: {notified}") if not notified: artifact_name = f"docs-zip-{commit}" - use_artifact: Optional[Artifact] = None + use_artifact: Union[Artifact, None] = None for artifact in artifacts_response.artifacts: if artifact.name == artifact_name: use_artifact = artifact From bea5194ffcbc943f7666a8ac46950936a0a43811 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 11 Jul 2022 19:31:26 +0000 Subject: [PATCH 0270/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2ae0a2ed0..9e5ed6371 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Move from `Optional[X]` to `Union[X, None]` for internal utils. PR [#5124](https://github.com/tiangolo/fastapi/pull/5124) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Dropbase, add Doist. PR [#5096](https://github.com/tiangolo/fastapi/pull/5096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Classiq, add ImgWhale. PR [#5079](https://github.com/tiangolo/fastapi/pull/5079) by [@tiangolo](https://github.com/tiangolo). From 1d5bbe5552a7eb9f7cefd3ae265c12c18dd67974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 12 Jul 2022 18:53:38 +0200 Subject: [PATCH 0271/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20Dependabot=20(#5?= =?UTF-8?q?128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..946f2358c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + # Python + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" From a0fd613527d4bb3e035b5c00051c654777dcbca1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 12 Jul 2022 16:54:11 +0000 Subject: [PATCH 0272/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9e5ed6371..e7326d0f1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add Dependabot. PR [#5128](https://github.com/tiangolo/fastapi/pull/5128) by [@tiangolo](https://github.com/tiangolo). * ♻️ Move from `Optional[X]` to `Union[X, None]` for internal utils. PR [#5124](https://github.com/tiangolo/fastapi/pull/5124) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Dropbase, add Doist. PR [#5096](https://github.com/tiangolo/fastapi/pull/5096) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Classiq, add ImgWhale. PR [#5079](https://github.com/tiangolo/fastapi/pull/5079) by [@tiangolo](https://github.com/tiangolo). From c43120258fa89bc20d6f8ee671b6ead9ab223fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 13:19:42 +0200 Subject: [PATCH 0273/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20removing=20body?= =?UTF-8?q?=20from=20status=20codes=20that=20do=20not=20support=20it=20(#5?= =?UTF-8?q?145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/openapi/constants.py | 1 - fastapi/openapi/utils.py | 12 ++++------- fastapi/routing.py | 33 +++++++++++++++++------------ fastapi/utils.py | 7 ++++++ tests/test_response_code_no_body.py | 9 +++++++- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index 3e69e5524..1897ad750 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,3 +1,2 @@ METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} -STATUS_CODES_WITH_NO_BODY = {100, 101, 102, 103, 204, 304} REF_PREFIX = "#/components/schemas/" diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 4eb727bd4..5d3d95c24 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -9,11 +9,7 @@ from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import get_flat_dependant, get_flat_params from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import ( - METHODS_WITH_BODY, - REF_PREFIX, - STATUS_CODES_WITH_NO_BODY, -) +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX from fastapi.openapi.models import OpenAPI from fastapi.params import Body, Param from fastapi.responses import Response @@ -21,6 +17,7 @@ from fastapi.utils import ( deep_dict_update, generate_operation_id_for_path, get_model_definitions, + is_body_allowed_for_status_code, ) from pydantic import BaseModel from pydantic.fields import ModelField, Undefined @@ -265,9 +262,8 @@ def get_openapi_path( operation.setdefault("responses", {}).setdefault(status_code, {})[ "description" ] = route.response_description - if ( - route_response_media_type - and route.status_code not in STATUS_CODES_WITH_NO_BODY + if route_response_media_type and is_body_allowed_for_status_code( + route.status_code ): response_schema = {"type": "string"} if lenient_issubclass(current_response_class, JSONResponse): diff --git a/fastapi/routing.py b/fastapi/routing.py index a6542c15a..6f1a8e900 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -29,13 +29,13 @@ from fastapi.dependencies.utils import ( ) from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError -from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY from fastapi.types import DecoratedCallable from fastapi.utils import ( create_cloned_field, create_response_field, generate_unique_id, get_value_or_default, + is_body_allowed_for_status_code, ) from pydantic import BaseModel from pydantic.error_wrappers import ErrorWrapper, ValidationError @@ -232,7 +232,17 @@ def get_request_handler( if raw_response.background is None: raw_response.background = background_tasks return raw_response - response_data = await serialize_response( + response_args: Dict[str, Any] = {"background": background_tasks} + # If status_code was set, use it, otherwise use the default from the + # response class, in the case of redirect it's 307 + current_status_code = ( + status_code if status_code else sub_response.status_code + ) + if current_status_code is not None: + response_args["status_code"] = current_status_code + if sub_response.status_code: + response_args["status_code"] = sub_response.status_code + content = await serialize_response( field=response_field, response_content=raw_response, include=response_model_include, @@ -243,15 +253,10 @@ def get_request_handler( exclude_none=response_model_exclude_none, is_coroutine=is_coroutine, ) - response_args: Dict[str, Any] = {"background": background_tasks} - # If status_code was set, use it, otherwise use the default from the - # response class, in the case of redirect it's 307 - if status_code is not None: - response_args["status_code"] = status_code - response = actual_response_class(response_data, **response_args) + response = actual_response_class(content, **response_args) + if not is_body_allowed_for_status_code(status_code): + response.body = b"" response.headers.raw.extend(sub_response.headers.raw) - if sub_response.status_code: - response.status_code = sub_response.status_code return response return app @@ -377,8 +382,8 @@ class APIRoute(routing.Route): status_code = int(status_code) self.status_code = status_code if self.response_model: - assert ( - status_code not in STATUS_CODES_WITH_NO_BODY + assert is_body_allowed_for_status_code( + status_code ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id self.response_field = create_response_field( @@ -410,8 +415,8 @@ class APIRoute(routing.Route): assert isinstance(response, dict), "An additional response must be a dict" model = response.get("model") if model: - assert ( - additional_status_code not in STATUS_CODES_WITH_NO_BODY + assert is_body_allowed_for_status_code( + additional_status_code ), f"Status code {additional_status_code} must not have a response body" response_name = f"Response_{additional_status_code}_{self.unique_id}" response_field = create_response_field(name=response_name, type_=model) diff --git a/fastapi/utils.py b/fastapi/utils.py index a7e135bca..887d57c90 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -18,6 +18,13 @@ if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute +def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: + if status_code is None: + return True + current_status_code = int(status_code) + return not (current_status_code < 200 or current_status_code in {204, 304}) + + def get_model_definitions( *, flat_models: Set[Union[Type[BaseModel], Type[Enum]]], diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 45e2fabc7..6d9b5c333 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -28,7 +28,7 @@ class JsonApiError(BaseModel): responses={500: {"description": "Error", "model": JsonApiError}}, ) async def a(): - pass # pragma: no cover + pass @app.get("/b", responses={204: {"description": "No Content"}}) @@ -106,3 +106,10 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema + + +def test_get_response(): + response = client.get("/a") + assert response.status_code == 204, response.text + assert "content-length" not in response.headers + assert response.content == b"" From 43d8ebfb4d118b5c904b40f66a5faaedff298611 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:20:21 +0000 Subject: [PATCH 0274/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e7326d0f1..042d4a921 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). * 👷 Add Dependabot. PR [#5128](https://github.com/tiangolo/fastapi/pull/5128) by [@tiangolo](https://github.com/tiangolo). * ♻️ Move from `Optional[X]` to `Union[X, None]` for internal utils. PR [#5124](https://github.com/tiangolo/fastapi/pull/5124) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Dropbase, add Doist. PR [#5096](https://github.com/tiangolo/fastapi/pull/5096) by [@tiangolo](https://github.com/tiangolo). From ff47d50a9b59bbb96d64fb12bd6d584c9aed6595 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:20:51 +0200 Subject: [PATCH 0275/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/setup-pyth?= =?UTF-8?q?on=20from=202=20to=204=20(#5129)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 505d66f9f..7f0c4fa66 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -15,7 +15,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.7" - uses: actions/cache@v2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9dde4e066..ad61ff7a8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.6" - uses: actions/cache@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0a82344e..dbc452048 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - uses: actions/cache@v2 From 2fcf044a9009e299ed22b4ae2c376ea3163cd256 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:21:27 +0000 Subject: [PATCH 0276/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 042d4a921..d522ed333 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/setup-python from 2 to 4. PR [#5129](https://github.com/tiangolo/fastapi/pull/5129) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). * 👷 Add Dependabot. PR [#5128](https://github.com/tiangolo/fastapi/pull/5128) by [@tiangolo](https://github.com/tiangolo). * ♻️ Move from `Optional[X]` to `Union[X, None]` for internal utils. PR [#5124](https://github.com/tiangolo/fastapi/pull/5124) by [@tiangolo](https://github.com/tiangolo). From 397a2e3484e043cfce857bc8a709691dad690ac4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:21:37 +0200 Subject: [PATCH 0277/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.9.0=20to=202.21.1=20(#5130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1 Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.9.0 to 2.21.1. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.9.0...v2.21.1) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 104c2677f..49a9a50c1 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.9.0 + uses: dawidd6/action-download-artifact@v2.21.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml From 7c3137301b2b5051c78ae4b42c9c36fc7bbc2769 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:22:02 +0200 Subject: [PATCH 0278/1881] =?UTF-8?q?=E2=AC=86=20Bump=20codecov/codecov-ac?= =?UTF-8?q?tion=20from=202=20to=203=20(#5131)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v2...v3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dbc452048..dfa3ed2df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,4 +38,4 @@ jobs: - name: Test run: bash scripts/test.sh - name: Upload coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 From 6497cb08f5314e78f3bad842fbfc17f43be79add Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:22:16 +0200 Subject: [PATCH 0279/1881] =?UTF-8?q?=E2=AC=86=20Bump=20nwtgck/actions-net?= =?UTF-8?q?lify=20from=201.1.5=20to=201.2.3=20(#5132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/preview-docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 7f0c4fa66..bdc664429 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -41,7 +41,7 @@ jobs: name: docs-zip path: ./docs.zip - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.1.5 + uses: nwtgck/actions-netlify@v1.2.3 with: publish-dir: './site' production-branch: master diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 49a9a50c1..a05b367d4 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -25,7 +25,7 @@ jobs: rm -f docs.zip - name: Deploy to Netlify id: netlify - uses: nwtgck/actions-netlify@v1.1.5 + uses: nwtgck/actions-netlify@v1.2.3 with: publish-dir: './site' production-deploy: false From 3ccb9604781cdc0d5cdfe7b112b96b0bea4044b8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:22:19 +0000 Subject: [PATCH 0280/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d522ed333..86e26372d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1. PR [#5130](https://github.com/tiangolo/fastapi/pull/5130) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 2 to 4. PR [#5129](https://github.com/tiangolo/fastapi/pull/5129) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). * 👷 Add Dependabot. PR [#5128](https://github.com/tiangolo/fastapi/pull/5128) by [@tiangolo](https://github.com/tiangolo). From b7bd3c1d55b98b475c9d1e11edf5b1a2ab96fedc Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:23:15 +0000 Subject: [PATCH 0281/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 86e26372d..44d08b86b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump codecov/codecov-action from 2 to 3. PR [#5131](https://github.com/tiangolo/fastapi/pull/5131) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1. PR [#5130](https://github.com/tiangolo/fastapi/pull/5130) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 2 to 4. PR [#5129](https://github.com/tiangolo/fastapi/pull/5129) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). From c15fce3248f83444106409e82d31fa0381ce7600 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:24:11 +0000 Subject: [PATCH 0282/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 44d08b86b..8ff9f8fcc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.3. PR [#5132](https://github.com/tiangolo/fastapi/pull/5132) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#5131](https://github.com/tiangolo/fastapi/pull/5131) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1. PR [#5130](https://github.com/tiangolo/fastapi/pull/5130) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 2 to 4. PR [#5129](https://github.com/tiangolo/fastapi/pull/5129) by [@dependabot[bot]](https://github.com/apps/dependabot). From 100799cde2c7a9ea86c9addcf6b9a8766c9a8415 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:25:32 +0200 Subject: [PATCH 0283/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.2.0 → v4.3.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.2.0...v4.3.0) - [github.com/asottile/pyupgrade: v2.32.1 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.32.1...v2.37.1) - [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5c278571e..6944e4a25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.2.0 + rev: v4.3.0 hooks: - id: check-added-large-files - id: check-toml @@ -12,7 +12,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.37.1 hooks: - id: pyupgrade args: @@ -43,7 +43,7 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black ci: From 606028dc8c395d1f93ee9ec59047834634f5ac9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:26:14 +0000 Subject: [PATCH 0284/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout?= =?UTF-8?q?=20from=202=20to=203=20(#5133)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 2 +- .github/workflows/preview-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index bdc664429..512d70078 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -13,7 +13,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 5783c993a..4aa8475b6 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,7 +20,7 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: # To allow latest-changes to commit to master token: ${{ secrets.ACTIONS_TOKEN }} diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 7e414ab95..2fcb7595e 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -9,7 +9,7 @@ jobs: notify-translations: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 2004ee7b3..4b47b4072 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -14,7 +14,7 @@ jobs: fastapi-people: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index a05b367d4..9e71a461a 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -10,7 +10,7 @@ jobs: preview-docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download Artifact Docs uses: dawidd6/action-download-artifact@v2.21.1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ad61ff7a8..4686fd0d4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfa3ed2df..763dbc44b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: From 19701d12fb9078c81072d1c4907d255554f286fd Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:26:52 +0000 Subject: [PATCH 0285/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8ff9f8fcc..fcd57540b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5030](https://github.com/tiangolo/fastapi/pull/5030) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.3. PR [#5132](https://github.com/tiangolo/fastapi/pull/5132) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#5131](https://github.com/tiangolo/fastapi/pull/5131) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1. PR [#5130](https://github.com/tiangolo/fastapi/pull/5130) by [@dependabot[bot]](https://github.com/apps/dependabot). From 48b7804a791579648359353ed06400d9cc595dc7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:27:49 +0000 Subject: [PATCH 0286/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fcd57540b..3fe585e72 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/checkout from 2 to 3. PR [#5133](https://github.com/tiangolo/fastapi/pull/5133) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5030](https://github.com/tiangolo/fastapi/pull/5030) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.3. PR [#5132](https://github.com/tiangolo/fastapi/pull/5132) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#5131](https://github.com/tiangolo/fastapi/pull/5131) by [@dependabot[bot]](https://github.com/apps/dependabot). From 120cf49089f2ce0e1d34534a3295157e29026636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wo=C5=BAniak?= <41345824+Valaraucoo@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:30:09 +0200 Subject: [PATCH 0287/1881] =?UTF-8?q?=F0=9F=8C=90=F0=9F=87=B5=F0=9F=87=B1?= =?UTF-8?q?=20Add=20Polish=20translation=20for=20`docs/pl/docs/tutorial/fi?= =?UTF-8?q?rst-steps.md`=20(#5024)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: Polish translation for tutorial/first-steps.md * docs: Polish translation / fixes after cr --- docs/pl/docs/tutorial/first-steps.md | 334 +++++++++++++++++++++++++++ docs/pl/mkdocs.yml | 1 + 2 files changed, 335 insertions(+) create mode 100644 docs/pl/docs/tutorial/first-steps.md diff --git a/docs/pl/docs/tutorial/first-steps.md b/docs/pl/docs/tutorial/first-steps.md new file mode 100644 index 000000000..9406d703d --- /dev/null +++ b/docs/pl/docs/tutorial/first-steps.md @@ -0,0 +1,334 @@ +# Pierwsze kroki + +Najprostszy plik FastAPI może wyglądać tak: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Skopiuj to do pliku `main.py`. + +Uruchom serwer: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + Polecenie `uvicorn main:app` odnosi się do: + + * `main`: plik `main.py` ("moduł" Python). + * `app`: obiekt utworzony w pliku `main.py` w lini `app = FastAPI()`. + * `--reload`: sprawia, że serwer uruchamia się ponownie po zmianie kodu. Używany tylko w trakcie tworzenia oprogramowania. + +Na wyjściu znajduje się linia z czymś w rodzaju: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Ta linia pokazuje adres URL, pod którym Twoja aplikacja jest obsługiwana, na Twoim lokalnym komputerze. + +### Sprawdź to + +Otwórz w swojej przeglądarce http://127.0.0.1:8000. + +Zobaczysz odpowiedź w formacie JSON: + +```JSON +{"message": "Hello World"} +``` + +### Interaktywna dokumentacja API + +Przejdź teraz do http://127.0.0.1:8000/docs. + +Zobaczysz automatyczną i interaktywną dokumentację API (dostarczoną przez Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatywna dokumentacja API + +Teraz przejdź do http://127.0.0.1:8000/redoc. + +Zobaczysz alternatywną automatycznie wygenerowaną dokumentację API (dostarczoną przez ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** generuje "schemat" z całym Twoim API przy użyciu standardu **OpenAPI** służącego do definiowania API. + +#### Schema + +"Schema" jest definicją lub opisem czegoś. Nie jest to kod, który go implementuje, ale po prostu abstrakcyjny opis. + +#### API "Schema" + +W typ przypadku, OpenAPI to specyfikacja, która dyktuje sposób definiowania schematu interfejsu API. + +Definicja schematu zawiera ścieżki API, możliwe parametry, które są przyjmowane przez endpointy, itp. + +#### "Schemat" danych + +Termin "schemat" może również odnosić się do wyglądu niektórych danych, takich jak zawartość JSON. + +W takim przypadku będzie to oznaczać atrybuty JSON, ich typy danych itp. + +#### OpenAPI i JSON Schema + +OpenAPI definiuje API Schema dla Twojego API, który zawiera definicje (lub "schematy") danych wysyłanych i odbieranych przez Twój interfejs API przy użyciu **JSON Schema**, standardu dla schematów danych w formacie JSON. + +#### Sprawdź `openapi.json` + +Jeśli jesteś ciekawy, jak wygląda surowy schemat OpenAPI, FastAPI automatycznie generuje JSON Schema z opisami wszystkich Twoich API. + +Możesz to zobaczyć bezpośrednio pod adresem: http://127.0.0.1:8000/openapi.json. + +Zobaczysz JSON zaczynający się od czegoś takiego: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### Do czego służy OpenAPI + +Schemat OpenAPI jest tym, co zasila dwa dołączone interaktywne systemy dokumentacji. + +Istnieją dziesiątki alternatyw, wszystkie oparte na OpenAPI. Możesz łatwo dodać dowolną z nich do swojej aplikacji zbudowanej za pomocą **FastAPI**. + +Możesz go również użyć do automatycznego generowania kodu dla klientów, którzy komunikują się z Twoim API. Na przykład aplikacje frontendowe, mobilne lub IoT. + +## Przypomnijmy, krok po kroku + +### Krok 1: zaimportuj `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` jest klasą, która zapewnia wszystkie funkcjonalności Twojego API. + +!!! note "Szczegóły techniczne" + `FastAPI` jest klasą, która dziedziczy bezpośrednio z `Starlette`. + + Oznacza to, że możesz korzystać ze wszystkich funkcjonalności Starlette również w `FastAPI`. + + +### Krok 2: utwórz instancję `FastAPI` + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Zmienna `app` będzie tutaj "instancją" klasy `FastAPI`. + +Będzie to główny punkt interakcji przy tworzeniu całego interfejsu API. + +Ta zmienna `app` jest tą samą zmienną, do której odnosi się `uvicorn` w poleceniu: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Jeśli stworzysz swoją aplikację, np.: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +I umieścisz to w pliku `main.py`, to będziesz mógł tak wywołać `uvicorn`: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Krok 3: wykonaj *operację na ścieżce* + +#### Ścieżka + +"Ścieżka" tutaj odnosi się do ostatniej części adresu URL, zaczynając od pierwszego `/`. + +Więc, w adresie URL takim jak: + +``` +https://example.com/items/foo +``` + +...ścieżką będzie: + +``` +/items/foo +``` + +!!! info + "Ścieżka" jest zazwyczaj nazywana "path", "endpoint" lub "route'. + +Podczas budowania API, "ścieżka" jest głównym sposobem na oddzielenie "odpowiedzialności" i „zasobów”. + +#### Operacje + +"Operacje" tutaj odnoszą się do jednej z "metod" HTTP. + +Jedna z: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...i te bardziej egzotyczne: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +W protokole HTTP można komunikować się z każdą ścieżką za pomocą jednej (lub więcej) "metod". + +--- + +Podczas tworzenia API zwykle używasz tych metod HTTP do wykonania określonej akcji. + +Zazwyczaj używasz: + +* `POST`: do tworzenia danych. +* `GET`: do odczytywania danych. +* `PUT`: do aktualizacji danych. +* `DELETE`: do usuwania danych. + +Tak więc w OpenAPI każda z metod HTTP nazywana jest "operacją". + +Będziemy je również nazywali "**operacjami**". + +#### Zdefiniuj *dekorator operacji na ścieżce* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` mówi **FastAPI** że funkcja poniżej odpowiada za obsługę żądań, które trafiają do: + +* ścieżki `/` +* używając operacji get + +!!! info "`@decorator` Info" + Składnia `@something` jest w Pythonie nazywana "dekoratorem". + + Umieszczasz to na szczycie funkcji. Jak ładną ozdobną czapkę (chyba stąd wzięła się nazwa). + + "Dekorator" przyjmuje funkcję znajdującą się poniżej jego i coś z nią robi. + + W naszym przypadku dekorator mówi **FastAPI**, że poniższa funkcja odpowiada **ścieżce** `/` z **operacją** `get`. + + Jest to "**dekorator operacji na ścieżce**". + +Możesz również użyć innej operacji: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Oraz tych bardziej egzotycznych: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + Możesz dowolnie używać każdej operacji (metody HTTP). + + **FastAPI** nie narzuca żadnego konkretnego znaczenia. + + Informacje tutaj są przedstawione jako wskazówka, a nie wymóg. + + Na przykład, używając GraphQL, normalnie wykonujesz wszystkie akcje używając tylko operacji `POST`. + +### Krok 4: zdefiniuj **funkcję obsługującą ścieżkę** + +To jest nasza "**funkcja obsługująca ścieżkę**": + +* **ścieżka**: to `/`. +* **operacja**: to `get`. +* **funkcja**: to funkcja poniżej "dekoratora" (poniżej `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Jest to funkcja Python. + +Zostanie ona wywołana przez **FastAPI** za każdym razem, gdy otrzyma żądanie do adresu URL "`/`" przy użyciu operacji `GET`. + +W tym przypadku jest to funkcja "asynchroniczna". + +--- + +Możesz również zdefiniować to jako normalną funkcję zamiast `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + Jeśli nie znasz różnicy, sprawdź [Async: *"In a hurry?"*](/async/#in-a-hurry){.internal-link target=_blank}. + +### Krok 5: zwróć zawartość + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Możesz zwrócić `dict`, `list`, pojedynczą wartość jako `str`, `int`, itp. + +Możesz również zwrócić modele Pydantic (więcej o tym później). + +Istnieje wiele innych obiektów i modeli, które zostaną automatycznie skonwertowane do formatu JSON (w tym ORM itp.). Spróbuj użyć swoich ulubionych, jest bardzo prawdopodobne, że są już obsługiwane. + +## Podsumowanie + +* Zaimportuj `FastAPI`. +* Stwórz instancję `app`. +* Dodaj **dekorator operacji na ścieżce** (taki jak `@app.get("/")`). +* Napisz **funkcję obsługującą ścieżkę** (taką jak `def root(): ...` powyżej). +* Uruchom serwer deweloperski (`uvicorn main:app --reload`). diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 0c3d100e7..86383d985 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -58,6 +58,7 @@ nav: - zh: /zh/ - Samouczek: - tutorial/index.md + - tutorial/first-steps.md markdown_extensions: - toc: permalink: true From fb26c1ee70c271edb0796c20ccf4c187d1d536e9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:30:45 +0000 Subject: [PATCH 0288/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3fe585e72..b43845e06 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). * ⬆ Bump actions/checkout from 2 to 3. PR [#5133](https://github.com/tiangolo/fastapi/pull/5133) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5030](https://github.com/tiangolo/fastapi/pull/5030) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.3. PR [#5132](https://github.com/tiangolo/fastapi/pull/5132) by [@dependabot[bot]](https://github.com/apps/dependabot). From 80cb57e4b22ca81708129da3ee79beea5b574f23 Mon Sep 17 00:00:00 2001 From: wakabame <35513518+wakabame@users.noreply.github.com> Date: Thu, 14 Jul 2022 20:34:38 +0900 Subject: [PATCH 0289/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/advanced/index.md`=20(#5043)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amazyra Co-authored-by: tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ja/docs/advanced/index.md | 24 ++++++++++++++++++++++++ docs/ja/mkdocs.yml | 1 + 2 files changed, 25 insertions(+) create mode 100644 docs/ja/docs/advanced/index.md diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md new file mode 100644 index 000000000..676f60359 --- /dev/null +++ b/docs/ja/docs/advanced/index.md @@ -0,0 +1,24 @@ +# ユーザーガイド 応用編 + +## さらなる機能 + +[チュートリアル - ユーザーガイド](../tutorial/){.internal-link target=_blank}により、**FastAPI**の主要な機能は十分に理解できたことでしょう。 + +以降のセクションでは、チュートリアルでは説明しきれなかったオプションや設定、および機能について説明します。 + +!!! tip "豆知識" + 以降のセクションは、 **必ずしも"応用編"ではありません**。 + + ユースケースによっては、その中から解決策を見つけられるかもしれません。 + +## 先にチュートリアルを読む + +[チュートリアル - ユーザーガイド](../tutorial/){.internal-link target=_blank}の知識があれば、**FastAPI**の主要な機能を利用することができます。 + +以降のセクションは、すでにチュートリアルを読んで、その主要なアイデアを理解できていることを前提としています。 + +## テスト駆動開発のコース + +このセクションの内容を補完するために脱初心者用コースを受けたい場合は、**TestDriven.io**による、Test-Driven Development with FastAPI and Dockerを確認するのがよいかもしれません。 + +現在、このコースで得られた利益の10%が**FastAPI**の開発のために寄付されています。🎉 😄 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 055404fea..1548b1905 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -78,6 +78,7 @@ nav: - tutorial/testing.md - tutorial/debugging.md - 高度なユーザーガイド: + - advanced/index.md - advanced/path-operation-advanced-configuration.md - advanced/additional-status-codes.md - advanced/response-directly.md From c5954d3bc0852a0b0450a54dea014733c63413f5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:35:16 +0000 Subject: [PATCH 0290/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b43845e06..eb078f165 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). * 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). * ⬆ Bump actions/checkout from 2 to 3. PR [#5133](https://github.com/tiangolo/fastapi/pull/5133) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5030](https://github.com/tiangolo/fastapi/pull/5030) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From b4a98a7224b642e9a728366137cc5675269a731d Mon Sep 17 00:00:00 2001 From: Robin <51365552+MrRawbin@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:45:01 +0200 Subject: [PATCH 0291/1881] =?UTF-8?q?=F0=9F=8C=90=20Start=20of=20Swedish?= =?UTF-8?q?=20translation=20(#5062)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 7 +- docs/de/mkdocs.yml | 7 +- docs/en/mkdocs.yml | 7 +- docs/es/mkdocs.yml | 7 +- docs/fa/mkdocs.yml | 7 +- docs/fr/mkdocs.yml | 7 +- docs/id/mkdocs.yml | 7 +- docs/it/mkdocs.yml | 7 +- docs/ja/mkdocs.yml | 7 +- docs/ko/mkdocs.yml | 7 +- docs/nl/mkdocs.yml | 7 +- docs/pl/mkdocs.yml | 7 +- docs/pt/mkdocs.yml | 7 +- docs/ru/mkdocs.yml | 7 +- docs/sq/mkdocs.yml | 7 +- docs/sv/docs/index.md | 468 +++++++++++++++++++++++++++++++++++ docs/sv/mkdocs.yml | 140 +++++++++++ docs/sv/overrides/.gitignore | 0 docs/tr/mkdocs.yml | 7 +- docs/uk/mkdocs.yml | 7 +- docs/zh/mkdocs.yml | 7 +- 21 files changed, 698 insertions(+), 36 deletions(-) create mode 100644 docs/sv/docs/index.md create mode 100644 docs/sv/mkdocs.yml create mode 100644 docs/sv/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 60bd8eaad..7ebf94384 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index c72f325f6..c617e55af 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -124,6 +125,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 322de0f2f..04639200d 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -230,6 +231,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index b544f9b38..cd1c04ed3 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -133,6 +134,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 3966a6026..79975288a 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index bf0d2b21c..69a323cec 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -138,6 +139,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 769547d11..6c9f88c90 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index ebec9a642..5f0b7c73b 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 1548b1905..66694ef36 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -165,6 +166,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 60cf7d30a..ddadebe7b 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -133,6 +134,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 9cd1e0401..620a4b25f 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 86383d985..c04f3c1c6 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -126,6 +127,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 2bb0b568d..51d448c95 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -148,6 +149,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index bb0702489..816a0d3a0 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -124,6 +125,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 8914395fe..4df6d5b1f 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md new file mode 100644 index 000000000..fd52f994c --- /dev/null +++ b/docs/sv/docs/index.md @@ -0,0 +1,468 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

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

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Other sponsors + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.6+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* **GraphQL** integration with Strawberry and other libraries. +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install "fastapi[all]"`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml new file mode 100644 index 000000000..fa0296d5c --- /dev/null +++ b/docs/sv/mkdocs.yml @@ -0,0 +1,140 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/sv/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: sv +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +extra: + analytics: + provider: google + property: UA-133183413-1 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/sv/overrides/.gitignore b/docs/sv/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 74186033c..5371cb71f 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -126,6 +127,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index ddf299d8b..fd371765a 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -123,6 +124,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a72ecb657..c1c35c6ed 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -5,14 +5,14 @@ theme: name: material custom_dir: overrides palette: - - media: "(prefers-color-scheme: light)" + - media: '(prefers-color-scheme: light)' scheme: default primary: teal accent: amber toggle: icon: material/lightbulb name: Switch to light mode - - media: "(prefers-color-scheme: dark)" + - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber @@ -53,6 +53,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -174,6 +175,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ From 2226f962ffffeae09d959ab96f65d6692c1ec87a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 13:45:26 +0200 Subject: [PATCH 0292/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20config=20for=20S?= =?UTF-8?q?wedish=20translations=20notification=20(#5147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/translations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml index 0e5093f3a..a68167ff8 100644 --- a/.github/actions/notify-translations/app/translations.yml +++ b/.github/actions/notify-translations/app/translations.yml @@ -15,3 +15,4 @@ id: 3717 az: 3994 nl: 4701 uz: 4883 +sv: 5146 From 86fb017aadf973200e7a0f79281d73fb5bc4ccc8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:45:51 +0000 Subject: [PATCH 0293/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 eb078f165..0c97db5ca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). * 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). * ⬆ Bump actions/checkout from 2 to 3. PR [#5133](https://github.com/tiangolo/fastapi/pull/5133) by [@dependabot[bot]](https://github.com/apps/dependabot). From 801e90863a51108ea4e363d3836b095a48c264df Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 11:46:23 +0000 Subject: [PATCH 0294/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0c97db5ca..714401fde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). * 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). * 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). From 85dc173d192b5514cd81bc1e6f62dc3b867930fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 14:37:37 +0200 Subject: [PATCH 0295/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20Jina=20sponso?= =?UTF-8?q?r=20badges=20(#5151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🍱 Update Jina badges * 💄 Tweak sponsors badge CSS * 🔧 Update Jina sponsor * 📝 Re-generate README --- README.md | 2 +- docs/en/data/sponsors.yml | 6 +++--- docs/en/docs/css/custom.css | 2 +- docs/en/docs/img/sponsors/jina-ai-banner.png | Bin 0 -> 14153 bytes docs/en/docs/img/sponsors/jina-ai.png | Bin 0 -> 26112 bytes docs/en/overrides/main.html | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 docs/en/docs/img/sponsors/jina-ai-banner.png create mode 100644 docs/en/docs/img/sponsors/jina-ai.png diff --git a/README.md b/README.md index 505005ae9..bcea9fe73 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index c99c4b57a..efd0f00f8 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,7 +1,7 @@ gold: - - url: https://bit.ly/2QSouzH - title: "Jina: build neural search-as-a-service for any kind of data in just minutes." - img: https://fastapi.tiangolo.com/img/sponsors/jina.svg + - url: https://bit.ly/3PjOZqc + title: "DiscoArt: Create compelling Disco Diffusion artworks in just one line" + img: https://fastapi.tiangolo.com/img/sponsors/jina-ai.png - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 7d3503e49..42b752bcf 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -94,7 +94,7 @@ a.announce-link:hover { .announce-wrapper .sponsor-badge { display: block; position: absolute; - top: -5px; + top: -10px; right: 0; font-size: 0.5rem; color: #999; diff --git a/docs/en/docs/img/sponsors/jina-ai-banner.png b/docs/en/docs/img/sponsors/jina-ai-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac6b44adcf4b0c28df130ca0e16c674ea43e229 GIT binary patch literal 14153 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 T7SUJA00000NkvXXu0mjfJ5Bsy literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/jina-ai.png b/docs/en/docs/img/sponsors/jina-ai.png new file mode 100644 index 0000000000000000000000000000000000000000..d6b0bfb4ec24acce9ab9cb4ca46740bb16c702ae GIT binary patch literal 26112 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 X@XdfIPq1E700000NkvXXu0mjfyQ^GY literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 9bed0253f..5c7029b85 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -29,9 +29,9 @@
From e3339f6770eaaa35e89ec6c1df917a5fd02d6f39 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 12:38:22 +0000 Subject: [PATCH 0296/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 714401fde..84b511c0a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Jina sponsor badges. PR [#5151](https://github.com/tiangolo/fastapi/pull/5151) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). * 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). From ec2ec482930b0683b5dd6aa4f580c18f1bdd2f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 16:56:40 +0200 Subject: [PATCH 0297/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 197 +++++++++++++++++-------- docs/en/data/people.yml | 246 +++++++++++++++++-------------- 2 files changed, 269 insertions(+), 174 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index db4a9acc6..6c1efcbbd 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,16 +1,25 @@ sponsors: -- - login: cryptapi +- - login: github + avatarUrl: https://avatars.githubusercontent.com/u/9919?v=4 + url: https://github.com/github +- - login: Doist + avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 + url: https://github.com/Doist + - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: BLUE-DEVIL1134 + avatarUrl: https://avatars.githubusercontent.com/u/55914808?u=f283d674fce31be7fb3ed2665b0f20d89958e541&v=4 + url: https://github.com/BLUE-DEVIL1134 - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai - login: DropbaseHQ avatarUrl: https://avatars.githubusercontent.com/u/85367855?v=4 url: https://github.com/DropbaseHQ -- - login: sushi2all - avatarUrl: https://avatars.githubusercontent.com/u/1043732?v=4 - url: https://github.com/sushi2all +- - login: ObliviousAI + avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 + url: https://github.com/ObliviousAI - login: chaserowbotham avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4 url: https://github.com/chaserowbotham @@ -32,9 +41,6 @@ sponsors: - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes -- - login: plocher - avatarUrl: https://avatars.githubusercontent.com/u/1082871?v=4 - url: https://github.com/plocher - - login: InesIvanova avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 url: https://github.com/InesIvanova @@ -53,7 +59,10 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP -- - login: johnadjei +- - login: nnfuzzy + avatarUrl: https://avatars.githubusercontent.com/u/687670?v=4 + url: https://github.com/nnfuzzy + - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei - login: HiredScore @@ -68,9 +77,6 @@ sponsors: - login: RodneyU215 avatarUrl: https://avatars.githubusercontent.com/u/3329665?u=ec6a9adf8e7e8e306eed7d49687c398608d1604f&v=4 url: https://github.com/RodneyU215 - - login: grillazz - avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=0b32b7073ae1ab8b7f6d2db0188c2e1e357ff451&v=4 - url: https://github.com/grillazz - login: tizz98 avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 url: https://github.com/tizz98 @@ -80,30 +86,36 @@ sponsors: - login: marutoraman avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/marutoraman + - login: leynier + avatarUrl: https://avatars.githubusercontent.com/u/36774373?u=2284831c821307de562ebde5b59014d5416c7e0d&v=4 + url: https://github.com/leynier - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge + - login: DelfinaCare + avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 + url: https://github.com/DelfinaCare +- - login: povilasb + avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 + url: https://github.com/povilasb - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 url: https://github.com/samuelcolvin - - login: jokull - avatarUrl: https://avatars.githubusercontent.com/u/701?u=0532b62166893d5160ef795c4c8b7512d971af05&v=4 - url: https://github.com/jokull - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 url: https://github.com/jefftriplett + - login: medecau + avatarUrl: https://avatars.githubusercontent.com/u/59870?u=f9341c95adaba780828162fd4c7442357ecfcefa&v=4 + url: https://github.com/medecau - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill - - login: jsutton - avatarUrl: https://avatars.githubusercontent.com/u/280777?v=4 - url: https://github.com/jsutton - login: deserat avatarUrl: https://avatars.githubusercontent.com/u/299332?v=4 url: https://github.com/deserat @@ -117,14 +129,17 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 url: https://github.com/koxudaxi - login: jqueguiner - avatarUrl: https://avatars.githubusercontent.com/u/690878?u=e4835b2a985a0f2d52018e4926cb5a58c26a62e8&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner + - login: alexsantos + avatarUrl: https://avatars.githubusercontent.com/u/932219?v=4 + url: https://github.com/alexsantos + - login: tcsmith + avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 + url: https://github.com/tcsmith - login: ltieman avatarUrl: https://avatars.githubusercontent.com/u/1084689?u=e69b17de17cb3ca141a17daa7ccbe173ceb1eb17&v=4 url: https://github.com/ltieman - - login: westonsteimel - avatarUrl: https://avatars.githubusercontent.com/u/1593939?u=0f2c0e3647f916fe295d62fa70da7a4c177115e3&v=4 - url: https://github.com/westonsteimel - login: corleyma avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 url: https://github.com/corleyma @@ -140,6 +155,9 @@ sponsors: - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 + - login: grillazz + avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=0b32b7073ae1ab8b7f6d2db0188c2e1e357ff451&v=4 + url: https://github.com/grillazz - login: dblackrun avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 url: https://github.com/dblackrun @@ -152,6 +170,9 @@ sponsors: - login: peterHoburg avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 url: https://github.com/peterHoburg + - login: gorhack + avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 + url: https://github.com/gorhack - login: jaredtrog avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 url: https://github.com/jaredtrog @@ -182,6 +203,9 @@ sponsors: - login: pkucmus avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4 url: https://github.com/pkucmus + - login: ioalloc + avatarUrl: https://avatars.githubusercontent.com/u/6737824?u=6c3a31449f1c92064287171aa9ebe6363a0c9b7b&v=4 + url: https://github.com/ioalloc - login: s3ich4n avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=ba3025d698e1c986655e776ae383a3d60d9d578e&v=4 url: https://github.com/s3ich4n @@ -200,6 +224,9 @@ sponsors: - login: Ge0f3 avatarUrl: https://avatars.githubusercontent.com/u/11887760?u=ccd80f1ac36dcb8517ef5c4e702e8cc5a80cad2f&v=4 url: https://github.com/Ge0f3 + - login: svats2k + avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 + url: https://github.com/svats2k - login: gokulyc avatarUrl: https://avatars.githubusercontent.com/u/13468848?u=269f269d3e70407b5fb80138c52daba7af783997&v=4 url: https://github.com/gokulyc @@ -215,9 +242,6 @@ sponsors: - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck - - login: linusg - avatarUrl: https://avatars.githubusercontent.com/u/19366641?u=125e390abef8fff3b3b0d370c369cba5d7fd4c67&v=4 - url: https://github.com/linusg - login: stradivari96 avatarUrl: https://avatars.githubusercontent.com/u/19752586?u=255f5f06a768f518b20cebd6963e840ac49294fd&v=4 url: https://github.com/stradivari96 @@ -230,6 +254,9 @@ sponsors: - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu + - login: Joeriksson + avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4 + url: https://github.com/Joeriksson - login: cometa-haley avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 url: https://github.com/cometa-haley @@ -258,23 +285,32 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/35070513?u=b48c05f669d1ea1d329f90dc70e45f10b569ef55&v=4 url: https://github.com/guligon90 - login: ybressler - avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=6621dc9ab53b697912ab2a32211bb29ae90a9112&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler - - login: iamkarshe - avatarUrl: https://avatars.githubusercontent.com/u/43641892?u=d08c901b359c931784501740610d416558ff3e24&v=4 - url: https://github.com/iamkarshe + - login: ddilidili + avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 + url: https://github.com/ddilidili - login: dbanty avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty + - login: VictorCalderon + avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4 + url: https://github.com/VictorCalderon + - login: arthuRHD + avatarUrl: https://avatars.githubusercontent.com/u/48015496?u=05a0d5b8b9320eeb7990d35c9337b823f269d2ff&v=4 + url: https://github.com/arthuRHD - login: rafsaf - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=be9f06b8ced2d2b677297decc781fa8ce4f7ddbd&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 url: https://github.com/dudikbender - login: daisuke8000 - avatarUrl: https://avatars.githubusercontent.com/u/55035595?u=5025e379cd3655ae1a96039efc85223a873d2e38&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/55035595?u=23a3f2f2925ad3efc27c7420041622b7f5fd2b79&v=4 url: https://github.com/daisuke8000 + - login: dazeddd + avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 + url: https://github.com/dazeddd - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -291,11 +327,8 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 url: https://github.com/daverin - login: anthonycepeda - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=892f700c79f9732211bd5221bf16eec32356a732&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda - - login: NinaHwang - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang - login: dotlas avatarUrl: https://avatars.githubusercontent.com/u/88832003?v=4 url: https://github.com/dotlas @@ -347,27 +380,27 @@ sponsors: - login: hardbyte avatarUrl: https://avatars.githubusercontent.com/u/855189?u=aa29e92f34708814d6b67fcd47ca4cf2ce1c04ed&v=4 url: https://github.com/hardbyte + - login: browniebroke + avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 + url: https://github.com/browniebroke - login: janfilips avatarUrl: https://avatars.githubusercontent.com/u/870699?u=6034d81731ecb41ae5c717e56a901ed46fc039a8&v=4 url: https://github.com/janfilips - - login: scari - avatarUrl: https://avatars.githubusercontent.com/u/964251?v=4 - url: https://github.com/scari + - login: woodrad + avatarUrl: https://avatars.githubusercontent.com/u/1410765?u=86707076bb03d143b3b11afc1743d2aa496bd8bf&v=4 + url: https://github.com/woodrad - login: Pytlicek avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=169dba3bfbc04ed214a914640ff435969f19ddb3&v=4 url: https://github.com/Pytlicek - - login: Celeborn2BeAlive - avatarUrl: https://avatars.githubusercontent.com/u/1659465?u=944517e4db0f6df65070074e81cabdad9c8a434b&v=4 - url: https://github.com/Celeborn2BeAlive + - login: allen0125 + avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=d4feb3d06a61baa4a69857ce371cc53fb4dffd2c&v=4 + url: https://github.com/allen0125 - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: Abbe98 - avatarUrl: https://avatars.githubusercontent.com/u/2631719?u=8a064aba9a710229ad28c616549d81a24191a5df&v=4 - url: https://github.com/Abbe98 - login: rglsk avatarUrl: https://avatars.githubusercontent.com/u/2768101?u=e349c88673f2155fe021331377c656a9d74bcc25&v=4 url: https://github.com/rglsk @@ -386,6 +419,9 @@ sponsors: - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood - login: unredundant avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=57dd0023365bec03f4fc566df6b81bc0a264a47d&v=4 url: https://github.com/unredundant @@ -401,9 +437,6 @@ sponsors: - login: yenchenLiu avatarUrl: https://avatars.githubusercontent.com/u/9199638?u=8cdf5ae507448430d90f6f3518d1665a23afe99b&v=4 url: https://github.com/yenchenLiu - - login: VivianSolide - avatarUrl: https://avatars.githubusercontent.com/u/9358572?u=4a38ef72dd39e8b262bd5ab819992128b55c52b4&v=4 - url: https://github.com/VivianSolide - login: xncbf avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 url: https://github.com/xncbf @@ -425,12 +458,24 @@ sponsors: - login: logan-connolly avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly + - login: sanghunka + avatarUrl: https://avatars.githubusercontent.com/u/16280020?u=960f5426ae08303229f045b9cc2ed463dcd41c15&v=4 + url: https://github.com/sanghunka + - login: stevenayers + avatarUrl: https://avatars.githubusercontent.com/u/16361214?u=098b797d8d48afb8cd964b717847943b61d24a6d&v=4 + url: https://github.com/stevenayers - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre + - login: aprilcoskun + avatarUrl: https://avatars.githubusercontent.com/u/17393603?u=29145243b4c7fadc80c7099471309cc2c04b6bcc&v=4 + url: https://github.com/aprilcoskun - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia + - login: yannicschroeer + avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=4df05a7296c207b91c5d7c7a11c29df5ab313e2b&v=4 + url: https://github.com/yannicschroeer - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic @@ -440,9 +485,12 @@ sponsors: - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli - - login: dwreeves - avatarUrl: https://avatars.githubusercontent.com/u/31971762?u=69732aba05aa5cf0780866349ebe109cf632b047&v=4 - url: https://github.com/dwreeves + - login: elisoncrum + avatarUrl: https://avatars.githubusercontent.com/u/30413278?u=531190845bb0935dbc1e4f017cda3cb7b4dd0e54&v=4 + url: https://github.com/elisoncrum + - login: HosamAlmoghraby + avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 + url: https://github.com/HosamAlmoghraby - login: kitaramu0401 avatarUrl: https://avatars.githubusercontent.com/u/33246506?u=929e6efa2c518033b8097ba524eb5347a069bb3b&v=4 url: https://github.com/kitaramu0401 @@ -452,45 +500,72 @@ sponsors: - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon + - login: alvarobartt + avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=ac9ccb8b9164eb5fe7d5276142591aa1b8080daf&v=4 + url: https://github.com/alvarobartt - login: d-e-h-i-o avatarUrl: https://avatars.githubusercontent.com/u/36816716?v=4 url: https://github.com/d-e-h-i-o + - login: ww-daniel-mora + avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 + url: https://github.com/ww-daniel-mora + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=9ddf8023ca3326381ba8fb77285ae36598a15de3&v=4 + url: https://github.com/rwxd - login: ilias-ant avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4 url: https://github.com/ilias-ant - login: arrrrrmin avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=fee5739394fea074cb0b66929d070114a5067aae&v=4 url: https://github.com/arrrrrmin - - login: Nephilim-Jack - avatarUrl: https://avatars.githubusercontent.com/u/48372168?u=6f2bb405238d7efc467536fe01f58df6779c58a9&v=4 - url: https://github.com/Nephilim-Jack + - login: BomGard + avatarUrl: https://avatars.githubusercontent.com/u/47395385?u=8e9052f54e0b8dc7285099c438fa29c55a7d6407&v=4 + url: https://github.com/BomGard - login: akanz1 avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4 url: https://github.com/akanz1 - - login: rooflexx - avatarUrl: https://avatars.githubusercontent.com/u/58993673?u=f8ba450460f1aea18430ed1e4a3889049a3b4dfa&v=4 - url: https://github.com/rooflexx - - login: denisyao1 - avatarUrl: https://avatars.githubusercontent.com/u/60019356?v=4 - url: https://github.com/denisyao1 + - login: shidenko97 + avatarUrl: https://avatars.githubusercontent.com/u/54946990?u=3fdc0caea36af9217dacf1cc7760c7ed9d67dcfe&v=4 + url: https://github.com/shidenko97 + - login: data-djinn + avatarUrl: https://avatars.githubusercontent.com/u/56449985?u=42146e140806908d49bd59ccc96f222abf587886&v=4 + url: https://github.com/data-djinn + - login: leo-jp-edwards + avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 + url: https://github.com/leo-jp-edwards - login: apar-tiwari avatarUrl: https://avatars.githubusercontent.com/u/61064197?v=4 url: https://github.com/apar-tiwari + - login: Vyvy-vi + avatarUrl: https://avatars.githubusercontent.com/u/62864373?u=1a9b0b28779abc2bc9b62cb4d2e44d453973c9c3&v=4 + url: https://github.com/Vyvy-vi - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun + - login: realabja + avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 + url: https://github.com/realabja - login: alessio-proietti avatarUrl: https://avatars.githubusercontent.com/u/67370599?u=8ac73db1e18e946a7681f173abdb640516f88515&v=4 url: https://github.com/alessio-proietti + - login: Mr-Sunglasses + avatarUrl: https://avatars.githubusercontent.com/u/81439109?u=a5d0762fdcec26e18a028aef05323de3c6fb195c&v=4 + url: https://github.com/Mr-Sunglasses - - login: backbord avatarUrl: https://avatars.githubusercontent.com/u/6814946?v=4 url: https://github.com/backbord - - login: sadikkuzu - avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=765ed469c44c004560079210ccdad5b29938eaa9&v=4 - url: https://github.com/sadikkuzu - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline + - login: zachspar + avatarUrl: https://avatars.githubusercontent.com/u/41600414?u=edf29c197137f51bace3f19a2ba759662640771f&v=4 + url: https://github.com/zachspar + - login: sownt + avatarUrl: https://avatars.githubusercontent.com/u/44340502?u=c06e3c45fb00a403075172770805fe57ff17b1cf&v=4 + url: https://github.com/sownt + - login: aahouzi + avatarUrl: https://avatars.githubusercontent.com/u/75032370?u=82677ee9cd86b3ccf4e13d9cb6765d8de5713e1e&v=4 + url: https://github.com/aahouzi diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 92aab109f..031c1ca4d 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1243 - prs: 300 - avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=5cad72c846b7aba2e960546af490edc7375dafc4&v=4 + answers: 1248 + prs: 318 + avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 335 + count: 352 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -30,19 +30,23 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: raphaelauv - count: 71 + count: 77 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: ArcLightSlavik count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: JarroVGIT + count: 68 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: falkben count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen - count: 48 + count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen - login: insomnes @@ -53,22 +57,22 @@ experts: count: 43 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa +- login: adriangb + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 + url: https://github.com/adriangb +- login: jgould22 + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: includeamin count: 39 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: jgould22 - count: 38 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: adriangb - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -81,18 +85,22 @@ experts: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: chbndrhnns + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: chbndrhnns - count: 28 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: panla - count: 26 + count: 27 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla +- login: acidjunk + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: ghandic count: 25 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -121,14 +129,18 @@ experts: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: acidjunk - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk +- login: odiseo0 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 + url: https://github.com/odiseo0 - login: Hultner count: 18 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner +- login: rafsaf + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 + url: https://github.com/rafsaf - login: jorgerpo count: 17 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -141,10 +153,6 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: rafsaf - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=be9f06b8ced2d2b677297decc781fa8ce4f7ddbd&v=4 - url: https://github.com/rafsaf - login: waynerv count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 @@ -153,6 +161,14 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jonatasoli + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli +- login: hellocoldworld + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 + url: https://github.com/hellocoldworld - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 @@ -161,10 +177,6 @@ experts: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 url: https://github.com/valentin994 -- login: hellocoldworld - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?v=4 - url: https://github.com/hellocoldworld - login: David-Lor count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 @@ -173,10 +185,10 @@ experts: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 url: https://github.com/yinziyan1206 -- login: jonatasoli +- login: n8sty count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: lowercase00 count: 11 avatarUrl: https://avatars.githubusercontent.com/u/21188280?v=4 @@ -185,39 +197,31 @@ experts: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4 url: https://github.com/zamiramir -- login: juntatalor - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/8134632?v=4 - url: https://github.com/juntatalor -- login: n8sty - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: aalifadv - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/78442260?v=4 - url: https://github.com/aalifadv last_month_active: -- login: jgould22 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: accelleon +- login: JarroVGIT + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT +- login: zoliknemet + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet +- login: iudeen count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/5001614?v=4 - url: https://github.com/accelleon -- login: jonatasoli - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: Kludex - count: 4 + count: 5 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: yinziyan1206 +- login: odiseo0 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 + url: https://github.com/odiseo0 +- login: jonatasoli count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 - url: https://github.com/yinziyan1206 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli top_contributors: - login: waynerv count: 25 @@ -243,21 +247,21 @@ top_contributors: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl +- login: Kludex + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: Smlep - count: 9 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 url: https://github.com/Serrones -- login: Kludex - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex - login: RunningIkkyu count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=706e1ee3f248245f2d68b976d149d06fd5a2010d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 url: https://github.com/RunningIkkyu - login: hard-coders count: 7 @@ -275,6 +279,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 +- login: dependabot + count: 5 + avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 + url: https://github.com/apps/dependabot - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -291,15 +299,31 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki +- login: hitrust + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 + url: https://github.com/hitrust +- login: lsglucas + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas +- login: ComicShrimp + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: NinaHwang count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 93 + count: 95 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: tokusumi + count: 49 + avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 + url: https://github.com/tokusumi - login: waynerv count: 47 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 @@ -308,10 +332,10 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 -- login: tokusumi - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 - url: https://github.com/tokusumi +- login: BilalAlpaslan + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 @@ -320,13 +344,13 @@ top_reviewers: count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay -- login: BilalAlpaslan - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan +- login: yezz123 + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: AdrianDeAnda count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=bb7f8a0d6c9de4e9d0320a9f271210206e202250&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda - login: ArcLightSlavik count: 31 @@ -344,10 +368,6 @@ top_reviewers: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: yezz123 - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: hard-coders count: 19 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 @@ -356,6 +376,14 @@ top_reviewers: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun +- login: lsglucas + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas +- login: JarroVGIT + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: zy7y count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 @@ -364,14 +392,14 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 url: https://github.com/yanever -- login: lsglucas - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas - login: SwftAlpc count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +- login: rjNemo + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: Smlep count: 16 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -388,18 +416,18 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: rjNemo - count: 14 - 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 - url: https://github.com/RunningIkkyu - login: sh0nk - count: 12 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: RunningIkkyu + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 + url: https://github.com/RunningIkkyu +- login: solomein-sv + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + url: https://github.com/solomein-sv - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -412,10 +440,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: solomein-sv +- login: odiseo0 count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 - url: https://github.com/solomein-sv + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 + url: https://github.com/odiseo0 - login: graingert count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 @@ -432,6 +460,10 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca +- login: raphaelauv + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv - login: blt232018 count: 8 avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 @@ -460,10 +492,6 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/36391432?u=094eec0cfddd5013f76f31e55e56147d78b19553&v=4 url: https://github.com/ryuckel -- login: raphaelauv - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv - login: NastasiaSaby count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 @@ -472,6 +500,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause +- login: wakabame + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/35513518?v=4 + url: https://github.com/wakabame - login: AlexandreBiguet count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1483079?u=ff926455cd4cab03c6c49441aa5dc2b21df3e266&v=4 @@ -480,15 +512,3 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 url: https://github.com/krocdort -- login: jovicon - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 - url: https://github.com/jovicon -- login: LorhanSohaky - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky -- login: peidrao - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=88c2cb42a99e0f50cdeae3606992568184783ee5&v=4 - url: https://github.com/peidrao From 21fd708c6e9d9be470e287760e324e970fda7b35 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 14:57:21 +0000 Subject: [PATCH 0298/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 84b511c0a..230d501c2 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 [#5154](https://github.com/tiangolo/fastapi/pull/5154) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsor badges. PR [#5151](https://github.com/tiangolo/fastapi/pull/5151) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). * 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). From 169af0217ed771698f518713b12a0f5a74ca3a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 17:04:37 +0200 Subject: [PATCH 0299/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors=20ba?= =?UTF-8?q?dge=20configs=20(#5155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 10a31b86a..a8bfb0b1b 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -11,3 +11,5 @@ logins: - DropbaseHQ - VincentParedes - BLUE-DEVIL1134 + - ObliviousAI + - Doist From 0be244ad5273b074109b980cae7ba8e83b223fad Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 15:05:19 +0000 Subject: [PATCH 0300/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 230d501c2..160518644 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors badge configs. PR [#5155](https://github.com/tiangolo/fastapi/pull/5155) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#5154](https://github.com/tiangolo/fastapi/pull/5154) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsor badges. PR [#5151](https://github.com/tiangolo/fastapi/pull/5151) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). From 35244ee83bb94b4f663ffa957f87ef432c137761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:14:54 +0200 Subject: [PATCH 0301/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/cache=20fr?= =?UTF-8?q?om=202=20to=203=20(#5149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 2 to 3. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 512d70078..a0d895911 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.7" - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4686fd0d4..02846cefd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.6" - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 763dbc44b..14dc141d9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v2 + - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} From ec3b6e975c33530343bbbcacaebc6288bdb9466a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:15:08 +0200 Subject: [PATCH 0302/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/upload-art?= =?UTF-8?q?ifact=20from=202=20to=203=20(#5148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a0d895911..0d666b82a 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -36,7 +36,7 @@ jobs: run: python3.7 ./scripts/docs.py build-all - name: Zip docs run: bash ./scripts/zip-docs.sh - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: docs-zip path: ./docs.zip From 84f6a030114bccdcfd49546b12d73423b4db1782 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 16:15:31 +0000 Subject: [PATCH 0303/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 160518644..802079cd4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/cache from 2 to 3. PR [#5149](https://github.com/tiangolo/fastapi/pull/5149) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors badge configs. PR [#5155](https://github.com/tiangolo/fastapi/pull/5155) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#5154](https://github.com/tiangolo/fastapi/pull/5154) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsor badges. PR [#5151](https://github.com/tiangolo/fastapi/pull/5151) by [@tiangolo](https://github.com/tiangolo). From 49619c0180c40dfb5039874dfbd279e4fed19479 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 16:15:55 +0000 Subject: [PATCH 0304/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 802079cd4..e3265c704 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/upload-artifact from 2 to 3. PR [#5148](https://github.com/tiangolo/fastapi/pull/5148) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 2 to 3. PR [#5149](https://github.com/tiangolo/fastapi/pull/5149) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors badge configs. PR [#5155](https://github.com/tiangolo/fastapi/pull/5155) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#5154](https://github.com/tiangolo/fastapi/pull/5154) by [@tiangolo](https://github.com/tiangolo). From 71b10e58904aa1543a8761bcf4028eee155c1af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 19:04:20 +0200 Subject: [PATCH 0305/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20Dependabot=20?= =?UTF-8?q?commit=20message=20(#5156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 946f2358c..cd972a0ba 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,12 @@ updates: directory: "/" schedule: interval: "daily" + commit-message: + prefix: ⬆ # Python - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" + commit-message: + prefix: ⬆ From f30b6c6001186794950eca0553d0466c0b59eaa6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 17:05:01 +0000 Subject: [PATCH 0306/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e3265c704..4dce21b64 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Dependabot commit message. PR [#5156](https://github.com/tiangolo/fastapi/pull/5156) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#5148](https://github.com/tiangolo/fastapi/pull/5148) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 2 to 3. PR [#5149](https://github.com/tiangolo/fastapi/pull/5149) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Update sponsors badge configs. PR [#5155](https://github.com/tiangolo/fastapi/pull/5155) by [@tiangolo](https://github.com/tiangolo). From 9197cbd36cc2a275ecffb231ade6200dbea62326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 19:14:53 +0200 Subject: [PATCH 0307/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20translations?= =?UTF-8?q?=20notification=20for=20Hebrew=20(#5158)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/translations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml index a68167ff8..ac15978a9 100644 --- a/.github/actions/notify-translations/app/translations.yml +++ b/.github/actions/notify-translations/app/translations.yml @@ -16,3 +16,4 @@ az: 3994 nl: 4701 uz: 4883 sv: 5146 +he: 5157 From ee182c6a0ad86083a2855321a51690cf597efd7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 17:15:39 +0000 Subject: [PATCH 0308/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4dce21b64..714696529 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update translations notification for Hebrew. PR [#5158](https://github.com/tiangolo/fastapi/pull/5158) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Dependabot commit message. PR [#5156](https://github.com/tiangolo/fastapi/pull/5156) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#5148](https://github.com/tiangolo/fastapi/pull/5148) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/cache from 2 to 3. PR [#5149](https://github.com/tiangolo/fastapi/pull/5149) by [@dependabot[bot]](https://github.com/apps/dependabot). From 59d154fa6f7b7345c4a8e2650954f1cc67230974 Mon Sep 17 00:00:00 2001 From: Itay Raveh <83612679+itay-raveh@users.noreply.github.com> Date: Thu, 14 Jul 2022 17:16:28 +0000 Subject: [PATCH 0309/1881] =?UTF-8?q?=F0=9F=8C=90=20Start=20of=20Hebrew=20?= =?UTF-8?q?translation=20(#5050)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 3 + docs/de/mkdocs.yml | 3 + docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/docs/index.md | 464 +++++++++++++++++++++++++++++++++++ docs/he/mkdocs.yml | 140 +++++++++++ docs/he/overrides/.gitignore | 0 docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 21 files changed, 658 insertions(+) create mode 100644 docs/he/docs/index.md create mode 100644 docs/he/mkdocs.yml create mode 100644 docs/he/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 7ebf94384..90ee0bb82 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index c617e55af..6009dd2fe 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -107,6 +108,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 04639200d..7adfae0f9 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -213,6 +214,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index cd1c04ed3..511ea0255 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -116,6 +117,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 79975288a..7d74e0407 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 69a323cec..f790c0299 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -121,6 +122,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/he/docs/index.md b/docs/he/docs/index.md new file mode 100644 index 000000000..fa63d8cb7 --- /dev/null +++ b/docs/he/docs/index.md @@ -0,0 +1,464 @@ +

+ FastAPI +

+

+ תשתית FastAPI, ביצועים גבוהים, קלה ללמידה, מהירה לתכנות, מוכנה לסביבת ייצור +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**תיעוד**: https://fastapi.tiangolo.com + +**קוד**: https://github.com/tiangolo/fastapi + +--- + +FastAPI היא תשתית רשת מודרנית ומהירה (ביצועים גבוהים) לבניית ממשקי תכנות יישומים (API) עם פייתון 3.6+ בהתבסס על רמזי טיפוסים סטנדרטיים. + +תכונות המפתח הן: + +- **מהירה**: ביצועים גבוהים מאוד, בקנה אחד עם NodeJS ו - Go (תודות ל - Starlette ו - Pydantic). [אחת מתשתיות הפייתון המהירות ביותר](#performance). + +- **מהירה לתכנות**: הגבירו את מהירות פיתוח התכונות החדשות בכ - %200 עד %300. \* +- **פחות שגיאות**: מנעו כ - %40 משגיאות אנוש (מפתחים). \* +- **אינטואיטיבית**: תמיכת עורך מעולה. השלמה בכל מקום. פחות זמן ניפוי שגיאות. +- **קלה**: מתוכננת להיות קלה לשימוש וללמידה. פחות זמן קריאת תיעוד. +- **קצרה**: מזערו שכפול קוד. מספר תכונות מכל הכרזת פרמטר. פחות שגיאות. +- **חסונה**: קבלו קוד מוכן לסביבת ייצור. עם תיעוד אינטרקטיבי אוטומטי. +- **מבוססת סטנדרטים**: מבוססת על (ותואמת לחלוטין ל -) הסטדנרטים הפתוחים לממשקי תכנות יישומים: OpenAPI (ידועים לשעבר כ - Swagger) ו - JSON Schema. + +\* הערכה מבוססת על בדיקות של צוות פיתוח פנימי שבונה אפליקציות בסביבת ייצור. + +## נותני חסות + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +נותני חסות אחרים + +## דעות + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, ה - FastAPI של ממשקי שורת פקודה (CLI). + + + +אם אתם בונים אפליקציית CLI לשימוש במסוף במקום ממשק רשת, העיפו מבט על **Typer**. + +**Typer** היא אחותה הקטנה של FastAPI. ומטרתה היא להיות ה - **FastAPI של ממשקי שורת פקודה**. ⌨️ 🚀 + +## תלויות + +פייתון 3.6+ + +FastAPI עומדת על כתפי ענקיות: + +- Starlette לחלקי הרשת. +- Pydantic לחלקי המידע. + +## התקנה + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +תצטרכו גם שרת ASGI כגון Uvicorn או Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## דוגמא + +### צרו אותה + +- צרו קובץ בשם `main.py` עם: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+או השתמשו ב - async def... + +אם הקוד שלכם משתמש ב - `async` / `await`, השתמשו ב - `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**שימו לב**: + +אם אינכם יודעים, בדקו את פרק "ממהרים?" על `async` ו - `await` בתיעוד. + +
+ +### הריצו אותה + +התחילו את השרת עם: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+על הפקודה uvicorn main:app --reload... + +הפקודה `uvicorn main:app` מתייחסת ל: + +- `main`: הקובץ `main.py` (מודול פייתון). +- `app`: האובייקט שנוצר בתוך `main.py` עם השורה app = FastAPI(). +- --reload: גרמו לשרת להתאתחל לאחר שינויים בקוד. עשו זאת רק בסביבת פיתוח. + +
+ +### בדקו אותה + +פתחו את הדפדפן שלכם בכתובת http://127.0.0.1:8000/items/5?q=somequery. + +אתם תראו תגובת JSON: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +כבר יצרתם API ש: + +- מקבל בקשות HTTP בנתיבים `/` ו - /items/{item_id}. +- שני ה _נתיבים_ מקבלים _בקשות_ `GET` (ידועות גם כ*מתודות* HTTP). +- ה _נתיב_ /items/{item_id} כולל \*פרמטר נתיב\_ `item_id` שאמור להיות `int`. +- ה _נתיב_ /items/{item_id} \*פרמטר שאילתא\_ אופציונלי `q`. + +### תיעוד API אינטרקטיבי + +כעת פנו לכתובת http://127.0.0.1:8000/docs. + +אתם תראו את התיעוד האוטומטי (מסופק על ידי Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### תיעוד אלטרנטיבי + +כעת פנו לכתובת http://127.0.0.1:8000/redoc. + +אתם תראו תיעוד אלטרנטיבי (מסופק על ידי ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## שדרוג לדוגמא + +כעת ערכו את הקובץ `main.py` כך שיוכל לקבל גוף מבקשת `PUT`. + +הגדירו את הגוף בעזרת רמזי טיפוסים סטנדרטיים, הודות ל - `Pydantic`. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +השרת אמול להתאתחל אוטומטית (מאחר והוספתם --reload לפקודת `uvicorn` שלמעלה). + +### שדרוג התיעוד האינטרקטיבי + +כעת פנו לכתובת http://127.0.0.1:8000/docs. + +- התיעוד האוטומטי יתעדכן, כולל הגוף החדש: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +- לחצו על הכפתור "Try it out", הוא יאפשר לכם למלא את הפרמטרים ולעבוד ישירות מול ה - API. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +- אחר כך לחצו על הכפתור "Execute", האתר יתקשר עם ה - API שלכם, ישלח את הפרמטרים, ישיג את התוצאות ואז יראה אותן על המסך: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### שדרוג התיעוד האלטרנטיבי + +כעת פנו לכתובת http://127.0.0.1:8000/redoc. + +- התיעוד האלטרנטיבי גם יראה את פרמטר השאילתא והגוף החדשים. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### סיכום + +לסיכום, אתם מכריזים ** פעם אחת** על טיפוסי הפרמטרים, גוף וכו' כפרמטרים לפונקציה. + +אתם עושים את זה עם טיפוסי פייתון מודרניים. + +אתם לא צריכים ללמוד תחביר חדש, מתודות או מחלקות של ספרייה ספיציפית, וכו' + +רק **פייתון 3.6+** סטנדרטי. + +לדוגמא, ל - `int`: + +```Python +item_id: int +``` + +או למודל `Item` מורכב יותר: + +```Python +item: Item +``` + +...ועם הכרזת הטיפוס האחת הזו אתם מקבלים: + +- תמיכת עורך, כולל: + - השלמות. + - בדיקת טיפוסים. +- אימות מידע: + - שגיאות ברורות ואטומטיות כאשר מוכנס מידע לא חוקי . + - אימות אפילו לאובייקטי JSON מקוננים. +- המרה של מידע קלט: המרה של מידע שמגיע מהרשת למידע וטיפוסים של פייתון. קורא מ: + - JSON. + - פרמטרי נתיב. + - פרמטרי שאילתא. + - עוגיות. + - כותרות. + - טפסים. + - קבצים. +- המרה של מידע פלט: המרה של מידע וטיפוסים מפייתון למידע רשת (כ - JSON): + - המירו טיפוסי פייתון (`str`, `int`, `float`, `bool`, `list`, etc). + - עצמי `datetime`. + - עצמי `UUID`. + - מודלי בסיסי נתונים. + - ...ורבים אחרים. +- תיעוד API אוטומטי ואינטרקטיבית כולל שתי אלטרנטיבות לממשק המשתמש: + - Swagger UI. + - ReDoc. + +--- + +בחזרה לדוגמאת הקוד הקודמת, **FastAPI** ידאג: + +- לאמת שיש `item_id` בנתיב בבקשות `GET` ו - `PUT`. +- לאמת שה - `item_id` הוא מטיפוס `int` בבקשות `GET` ו - `PUT`. + - אם הוא לא, הלקוח יראה שגיאה ברורה ושימושית. +- לבדוק האם קיים פרמטר שאילתא בשם `q` (קרי `http://127.0.0.1:8000/items/foo?q=somequery`) לבקשות `GET`. + - מאחר והפרמטר `q` מוגדר עם = None, הוא אופציונלי. + - לולא ה - `None` הוא היה חובה (כמו הגוף במקרה של `PUT`). +- לבקשות `PUT` לנתיב /items/{item_id}, לקרוא את גוף הבקשה כ - JSON: + - לאמת שהוא כולל את מאפיין החובה `name` שאמור להיות מטיפוס `str`. + - לאמת שהוא כולל את מאפיין החובה `price` שחייב להיות מטיפוס `float`. + - לבדוק האם הוא כולל את מאפיין הרשות `is_offer` שאמור להיות מטיפוס `bool`, אם הוא נמצא. + - כל זה יעבוד גם לאובייקט JSON מקונן. +- להמיר מ - JSON ול- JSON אוטומטית. +- לתעד הכל באמצעות OpenAPI, תיעוד שבו יוכלו להשתמש: + - מערכות תיעוד אינטרקטיביות. + - מערכות ייצור קוד אוטומטיות, להרבה שפות. +- לספק ישירות שתי מערכות תיעוד רשתיות. + +--- + +רק גרדנו את קצה הקרחון, אבל כבר יש לכם רעיון של איך הכל עובד. + +נסו לשנות את השורה: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...מ: + +```Python + ... "item_name": item.name ... +``` + +...ל: + +```Python + ... "item_price": item.price ... +``` + +...וראו איך העורך שלכם משלים את המאפיינים ויודע את הטיפוסים שלהם: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +לדוגמא יותר שלמה שכוללת עוד תכונות, ראו את המדריך - למשתמש. + +**התראת ספוילרים**: המדריך - למשתמש כולל: + +- הכרזה על **פרמטרים** ממקורות אחרים ושונים כגון: **כותרות**, **עוגיות**, **טפסים** ו - **קבצים**. +- איך לקבוע **מגבלות אימות** בעזרת `maximum_length` או `regex`. +- דרך חזקה וקלה להשתמש ב**הזרקת תלויות**. +- אבטחה והתאמתות, כולל תמיכה ב - **OAuth2** עם **JWT** והתאמתות **HTTP Basic**. +- טכניקות מתקדמות (אבל קלות באותה מידה) להכרזת אובייקטי JSON מקוננים (תודות ל - Pydantic). +- אינטרקציה עם **GraphQL** דרך Strawberry וספריות אחרות. +- תכונות נוספות רבות (תודות ל - Starlette) כגון: + - **WebSockets** + - בדיקות קלות במיוחד מבוססות על `requests` ו - `pytest` + - **CORS** + - **Cookie Sessions** + - ...ועוד. + +## ביצועים + +בדיקות עצמאיות של TechEmpower הראו שאפליקציות **FastAPI** שרצות תחת Uvicorn הן מתשתיות הפייתון המהירות ביותר, רק מתחת ל - Starlette ו - Uvicorn עצמן (ש - FastAPI מבוססת עליהן). (\*) + +כדי להבין עוד על הנושא, ראו את הפרק Benchmarks. + +## תלויות אופציונליות + +בשימוש Pydantic: + +- ujson - "פרסור" JSON. +- email_validator - לאימות כתובות אימייל. + +בשימוש Starlette: + +- requests - דרוש אם ברצונכם להשתמש ב - `TestClient`. +- jinja2 - דרוש אם ברצונכם להשתמש בברירת המחדל של תצורת הטמפלייטים. +- python-multipart - דרוש אם ברצונכם לתמוך ב "פרסור" טפסים, באצמעות request.form(). +- itsdangerous - דרוש אם ברצונכם להשתמש ב - `SessionMiddleware`. +- pyyaml - דרוש אם ברצונכם להשתמש ב - `SchemaGenerator` של Starlette (כנראה שאתם לא צריכים את זה עם FastAPI). +- ujson - דרוש אם ברצונכם להשתמש ב - `UJSONResponse`. + +בשימוש FastAPI / Starlette: + +- uvicorn - לשרת שטוען ומגיש את האפליקציה שלכם. +- orjson - דרוש אם ברצונכם להשתמש ב - `ORJSONResponse`. + +תוכלו להתקין את כל אלו באמצעות pip install "fastapi[all]". + +## רשיון + +הפרויקט הזה הוא תחת התנאים של רשיון MIT. diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml new file mode 100644 index 000000000..34a3b0e6a --- /dev/null +++ b/docs/he/mkdocs.yml @@ -0,0 +1,140 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/he/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: he +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +extra: + analytics: + provider: google + property: UA-133183413-1 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/he/overrides/.gitignore b/docs/he/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 6c9f88c90..697ecd4cb 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 5f0b7c73b..1f1d0016d 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 66694ef36..d96074ef1 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -148,6 +149,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index ddadebe7b..4a576baf2 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -116,6 +117,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 620a4b25f..8831571dd 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index c04f3c1c6..982b1a060 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -109,6 +110,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 51d448c95..fb95bfe29 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -131,6 +132,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 816a0d3a0..2eb8eb935 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -107,6 +108,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 4df6d5b1f..1d8d9d04e 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5371cb71f..bf66edd68 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -109,6 +110,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index fd371765a..3b8475907 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index c1c35c6ed..f9bfd6875 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -157,6 +158,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ From e35df688d55c93f92e31019a5652247082ae51b3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 14 Jul 2022 17:17:09 +0000 Subject: [PATCH 0310/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 714696529..542637032 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Start of Hebrew translation. PR [#5050](https://github.com/tiangolo/fastapi/pull/5050) by [@itay-raveh](https://github.com/itay-raveh). * 🔧 Update translations notification for Hebrew. PR [#5158](https://github.com/tiangolo/fastapi/pull/5158) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Dependabot commit message. PR [#5156](https://github.com/tiangolo/fastapi/pull/5156) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#5148](https://github.com/tiangolo/fastapi/pull/5148) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2f21bef91b72dbd723fceb12a23ca5cf58540684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 19:34:25 +0200 Subject: [PATCH 0311/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 542637032..5dece6163 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,23 @@ ## Latest Changes +### Fixes - Breaking Changes + +* 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). + * Setting `status_code` to `204`, `304`, or any code below `200` (1xx) will remove the body from the response. + * This fixes an error in Uvicorn that otherwise would be thrown: `RuntimeError: Response content longer than Content-Length`. + * This removes `fastapi.openapi.constants.STATUS_CODES_WITH_NO_BODY`, it is replaced by a function in utils. + +### Translations + * 🌐 Start of Hebrew translation. PR [#5050](https://github.com/tiangolo/fastapi/pull/5050) by [@itay-raveh](https://github.com/itay-raveh). +* 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). +* 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). +* 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). + +### Internal + * 🔧 Update translations notification for Hebrew. PR [#5158](https://github.com/tiangolo/fastapi/pull/5158) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Dependabot commit message. PR [#5156](https://github.com/tiangolo/fastapi/pull/5156) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#5148](https://github.com/tiangolo/fastapi/pull/5148) by [@dependabot[bot]](https://github.com/apps/dependabot). @@ -10,17 +26,12 @@ * 🔧 Update sponsors badge configs. PR [#5155](https://github.com/tiangolo/fastapi/pull/5155) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#5154](https://github.com/tiangolo/fastapi/pull/5154) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsor badges. PR [#5151](https://github.com/tiangolo/fastapi/pull/5151) by [@tiangolo](https://github.com/tiangolo). -* 🔧 Add config for Swedish translations notification. PR [#5147](https://github.com/tiangolo/fastapi/pull/5147) by [@tiangolo](https://github.com/tiangolo). -* 🌐 Start of Swedish translation. PR [#5062](https://github.com/tiangolo/fastapi/pull/5062) by [@MrRawbin](https://github.com/MrRawbin). -* 🌐 Add Japanese translation for `docs/ja/docs/advanced/index.md`. PR [#5043](https://github.com/tiangolo/fastapi/pull/5043) by [@wakabame](https://github.com/wakabame). -* 🌐🇵🇱 Add Polish translation for `docs/pl/docs/tutorial/first-steps.md`. PR [#5024](https://github.com/tiangolo/fastapi/pull/5024) by [@Valaraucoo](https://github.com/Valaraucoo). * ⬆ Bump actions/checkout from 2 to 3. PR [#5133](https://github.com/tiangolo/fastapi/pull/5133) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5030](https://github.com/tiangolo/fastapi/pull/5030) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.3. PR [#5132](https://github.com/tiangolo/fastapi/pull/5132) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump codecov/codecov-action from 2 to 3. PR [#5131](https://github.com/tiangolo/fastapi/pull/5131) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.21.1. PR [#5130](https://github.com/tiangolo/fastapi/pull/5130) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/setup-python from 2 to 4. PR [#5129](https://github.com/tiangolo/fastapi/pull/5129) by [@dependabot[bot]](https://github.com/apps/dependabot). -* 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). * 👷 Add Dependabot. PR [#5128](https://github.com/tiangolo/fastapi/pull/5128) by [@tiangolo](https://github.com/tiangolo). * ♻️ Move from `Optional[X]` to `Union[X, None]` for internal utils. PR [#5124](https://github.com/tiangolo/fastapi/pull/5124) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Dropbase, add Doist. PR [#5096](https://github.com/tiangolo/fastapi/pull/5096) by [@tiangolo](https://github.com/tiangolo). From 50fb34bf55c1711e3753363ab6427d77be25dbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 14 Jul 2022 19:35:13 +0200 Subject: [PATCH 0312/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?79.0?= 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 5dece6163..55df22c06 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.79.0 + ### Fixes - Breaking Changes * 🐛 Fix removing body from status codes that do not support it. PR [#5145](https://github.com/tiangolo/fastapi/pull/5145) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2465e2c27..e5cdbeb09 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.78.0" +__version__ = "0.79.0" from starlette import status as status From 3b5839260f916ac416585ef0452f101105884834 Mon Sep 17 00:00:00 2001 From: Mohsen Mahmoodi Date: Wed, 20 Jul 2022 16:55:23 +0430 Subject: [PATCH 0313/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20transl?= =?UTF-8?q?ation=20for=20`docs/fa/docs/index.md`=20and=20tweak=20right-to-?= =?UTF-8?q?left=20CSS=20(#2395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../notify-translations/app/translations.yml | 2 +- docs/en/docs/css/custom.css | 21 ++ docs/fa/docs/index.md | 338 +++++++++--------- docs/he/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + 5 files changed, 194 insertions(+), 173 deletions(-) diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml index ac15978a9..d283ef9f7 100644 --- a/.github/actions/notify-translations/app/translations.yml +++ b/.github/actions/notify-translations/app/translations.yml @@ -8,7 +8,7 @@ uk: 1748 tr: 1892 fr: 1972 ko: 2017 -sq: 2041 +fa: 2041 pl: 3169 de: 3716 id: 3717 diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 42b752bcf..226ca2b11 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -4,10 +4,21 @@ display: block; } +.termy { + /* For right to left languages */ + direction: ltr; +} + .termy [data-termynal] { white-space: pre-wrap; } +a.external-link { + /* For right to left languages */ + direction: ltr; + display: inline-block; +} + a.external-link::after { /* \00A0 is a non-breaking space to make the mark be on the same line as the link @@ -118,3 +129,13 @@ a.announce-link:hover { .twitter { color: #00acee; } + +/* Right to left languages */ +code { + direction: ltr; + display: inline-block; +} + +.md-content__inner h1 { + direction: ltr !important; +} diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index fd52f994c..0f7cd569a 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -1,16 +1,12 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + فریم‌ورک FastAPI، کارایی بالا، یادگیری آسان، کدنویسی سریع، آماده برای استفاده در محیط پروداکشن

- - Test + + Test Coverage @@ -25,103 +21,99 @@ --- -**Documentation**: https://fastapi.tiangolo.com +**مستندات**: https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**کد منبع**: https://github.com/tiangolo/fastapi --- +FastAPI یک وب فریم‌ورک مدرن و سریع (با کارایی بالا) برای ایجاد APIهای متنوع (وب، وب‌سوکت و غبره) با زبان پایتون نسخه +۳.۶ است. این فریم‌ورک با رعایت کامل راهنمای نوع داده (Type Hint) ایجاد شده است. -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +ویژگی‌های کلیدی این فریم‌ورک عبارتند از: -The key features are: +* **سرعت**: کارایی بسیار بالا و قابل مقایسه با **NodeJS** و **Go** (با تشکر از Starlette و Pydantic). [یکی از سریع‌ترین فریم‌ورک‌های پایتونی موجود](#performance). -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **کدنویسی سریع**: افزایش ۲۰۰ تا ۳۰۰ درصدی سرعت توسعه فابلیت‌های جدید. * +* **باگ کمتر**: کاهش ۴۰ درصدی خطاهای انسانی (برنامه‌نویسی). * +* **غریزی**: پشتیبانی فوق‌العاده در محیط‌های توسعه یکپارچه (IDE). تکمیل در همه بخش‌های کد. کاهش زمان رفع باگ. +* **آسان**: طراحی شده برای یادگیری و استفاده آسان. کاهش زمان مورد نیاز برای مراجعه به مستندات. +* **کوچک**: کاهش تکرار در کد. چندین قابلیت برای هر پارامتر (منظور پارامترهای ورودی تابع هندلر می‌باشد، به بخش خلاصه در همین صفحه مراجعه شود). باگ کمتر. +* **استوار**: ایجاد کدی آماده برای استفاده در محیط پروداکشن و تولید خودکار مستندات تعاملی +* **مبتنی بر استانداردها**: مبتنی بر (و منطبق با) استانداردهای متن باز مربوط به API: OpenAPI (سوگر سابق) و JSON Schema. -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. +* تخمین‌ها بر اساس تست‌های انجام شده در یک تیم توسعه داخلی که مشغول ایجاد برنامه‌های کاربردی واقعی بودند صورت گرفته است. -* estimation based on tests on an internal development team, building production applications. - -## Sponsors +## اسپانسرهای طلایی {% if sponsors %} {% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - + {% endfor %} {% endif %} -Other sponsors +دیگر اسپانسرها -## Opinions +## نظر دیگران در مورد FastAPI -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" +

[...] I'm using FastAPI a ton these days. [...] I'm actually planning to use it for all of my team's ML services at Microsoft. Some of them are getting integrated into the core Windows product and some Office products."
Kabir Khan - Microsoft (ref)
--- -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" +
"We adopted the FastAPI library to spawn a RESTserver that can be queried to obtain predictions. [for Ludwig]"
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" +
"Netflix is pleased to announce the open-source release of our crisis management orchestration framework: Dispatch! [built with FastAPI]"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" +
"I’m over the moon excited about FastAPI. It’s so fun!"
Brian Okken - Python Bytes podcast host (ref)
--- -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" +
"Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted Hug to be - it's really inspiring to see someone build that."
Timothy Crosley - Hug creator (ref)
--- -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" +
"If you're looking to learn one modern framework for building REST APIs, check out FastAPI [...] It's fast, easy to use and easy to learn [...]"
-"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" +
"We've switched over to FastAPI for our APIs [...] I think you'll like it [...]"
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
--- -## **Typer**, the FastAPI of CLIs +## **Typer**, فریم‌ورکی معادل FastAPI برای کار با واسط خط فرمان -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +اگر در حال ساختن برنامه‌ای برای استفاده در CLI (به جای استفاده در وب) هستید، می‌توانید از **Typer**. استفاده کنید. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** دوقلوی کوچکتر FastAPI است و قرار است معادلی برای FastAPI در برنامه‌های CLI باشد.️ 🚀 -## Requirements +## نیازمندی‌ها -Python 3.6+ +پایتون +۳.۶ -FastAPI stands on the shoulders of giants: +FastAPI مبتنی بر ابزارهای قدرتمند زیر است: -* Starlette for the web parts. -* Pydantic for the data parts. +* فریم‌ورک Starlette برای بخش وب. +* کتابخانه Pydantic برای بخش داده‌. -## Installation +## نصب
@@ -133,7 +125,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +نصب یک سرور پروداکشن نظیر Uvicorn یا Hypercorn نیز جزء نیازمندی‌هاست.
@@ -145,14 +137,13 @@ $ pip install "uvicorn[standard]"
-## Example +## مثال -### Create it - -* Create a file `main.py` with: +### ایجاد کنید +* فایلی به نام `main.py` با محتوای زیر ایجاد کنید : ```Python -from typing import Union +from typing import Optional from fastapi import FastAPI @@ -170,12 +161,12 @@ def read_item(item_id: int, q: Union[str, None] = None): ```
-Or use async def... +همچنین می‌توانید از async def... نیز استفاده کنید -If your code uses `async` / `await`, use `async def`: +اگر در کدتان از `async` / `await` استفاده می‌کنید, از `async def` برای تعریف تابع خود استفاده کنید: ```Python hl_lines="9 14" -from typing import Union +from typing import Optional from fastapi import FastAPI @@ -188,19 +179,20 @@ async def read_root(): @app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): +async def read_item(item_id: int, q: Optional[str] = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**توجه**: + +اگر با `async / await` آشنا نیستید، به بخش _"عجله‌ دارید?"_ در صفحه درباره `async` و `await` در مستندات مراجعه کنید. -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs.
-### Run it +### اجرا کنید -Run the server with: +با استفاده از دستور زیر سرور را اجرا کنید:
@@ -217,57 +209,57 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +درباره دستور uvicorn main:app --reload... -The command `uvicorn main:app` refers to: +دستور `uvicorn main:app` شامل موارد زیر است: -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main`: فایل `main.py` (ماژول پایتون ایجاد شده). +* `app`: شیء ایجاد شده در فایل `main.py` در خط `app = FastAPI()`. +* `--reload`: ریستارت کردن سرور با تغییر کد. تنها در هنگام توسعه از این گزینه استفاده شود..
-### Check it +### بررسی کنید -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +آدرس http://127.0.0.1:8000/items/5?q=somequery را در مرورگر خود باز کنید. -You will see the JSON response as: +پاسخ JSON زیر را مشاهده خواهید کرد: ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +تا اینجا شما APIای ساختید که: -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* درخواست‌های HTTP به _مسیرهای_ `/` و `/items/{item_id}` را دریافت می‌کند. +* هردو _مسیر_ عملیات (یا HTTP _متد_) `GET` را پشتیبانی می‌کنند. +* _مسیر_ `/items/{item_id}` شامل _پارامتر مسیر_ `item_id` از نوع `int` است. +* _مسیر_ `/items/{item_id}` شامل _پارامتر پرسمان_ اختیاری `q` از نوع `str` است. -### Interactive API docs +### مستندات API تعاملی -Now go to http://127.0.0.1:8000/docs. +حال به آدرس http://127.0.0.1:8000/docs بروید. -You will see the automatic interactive API documentation (provided by Swagger UI): +مستندات API تعاملی (ایجاد شده به کمک Swagger UI) را مشاهده خواهید کرد: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### مستندات API جایگزین -And now, go to http://127.0.0.1:8000/redoc. +حال به آدرس http://127.0.0.1:8000/redoc بروید. -You will see the alternative automatic documentation (provided by ReDoc): +مستندات خودکار دیگری را مشاهده خواهید کرد که به کمک ReDoc ایجاد می‌شود: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## تغییر مثال -Now modify the file `main.py` to receive a body from a `PUT` request. +حال فایل `main.py` را مطابق زیر ویرایش کنید تا بتوانید بدنه یک درخواست `PUT` را دریافت کنید. -Declare the body using standard Python types, thanks to Pydantic. +به کمک Pydantic بدنه درخواست را با انواع استاندارد پایتون تعریف کنید. ```Python hl_lines="4 9-12 25-27" -from typing import Union +from typing import Optional from fastapi import FastAPI from pydantic import BaseModel @@ -296,173 +288,175 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +سرور به صورت خودکار ری‌استارت می‌شود (زیرا پیشتر از گزینه `--reload` در دستور `uvicorn` استفاده کردیم). -### Interactive API docs upgrade +### تغییر مستندات API تعاملی -Now go to http://127.0.0.1:8000/docs. +مجددا به آدرس http://127.0.0.1:8000/docs بروید. -* The interactive API documentation will be automatically updated, including the new body: +* مستندات API تعاملی به صورت خودکار به‌روز شده است و شامل بدنه تعریف شده در مرحله قبل است: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* روی دکمه "Try it out" کلیک کنید, اکنون می‌توانید پارامترهای مورد نیاز هر API را مشخص کرده و به صورت مستقیم با آنها تعامل کنید: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* سپس روی دکمه "Execute" کلیک کنید, خواهید دید که واسط کاریری با APIهای تعریف شده ارتباط برقرار کرده، پارامترهای مورد نیاز را به آن‌ها ارسال می‌کند، سپس نتایج را دریافت کرده و در صفحه نشان می‌دهد: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### تغییر مستندات API جایگزین -And now, go to http://127.0.0.1:8000/redoc. +حال به آدرس http://127.0.0.1:8000/redoc بروید. -* The alternative documentation will also reflect the new query parameter and body: +* خواهید دید که مستندات جایگزین نیز به‌روزرسانی شده و شامل پارامتر پرسمان و بدنه تعریف شده می‌باشد: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### خلاصه -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +به طور خلاصه شما **یک بار** انواع پارامترها، بدنه و غیره را به عنوان پارامترهای ورودی تابع خود تعریف می‌کنید. -You do that with standard modern Python types. + این کار را با استفاده از انواع استاندارد و مدرن موجود در پایتون انجام می‌دهید. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +نیازی به یادگیری نحو جدید یا متدها و کلاس‌های یک کتابخانه بخصوص و غیره نیست. -Just standard **Python 3.6+**. +تنها **پایتون +۳.۶**. -For example, for an `int`: +به عنوان مثال برای یک پارامتر از نوع `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +یا برای یک مدل پیچیده‌تر مثل `Item`: ```Python item: Item ``` -...and with that single declaration you get: +...و با همین اعلان تمامی قابلیت‌های زیر در دسترس قرار می‌گیرد: -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: +* پشتیبانی ویرایشگر متنی شامل: + * تکمیل کد. + * بررسی انواع داده. +* اعتبارسنجی داده: + * خطاهای خودکار و مشخص در هنگام نامعتبر بودن داده + * اعتبارسنجی، حتی برای اشیاء JSON تو در تو. +* تبدیل داده ورودی: که از شبکه رسیده به انواع و داد‌ه‌ پایتونی. این داده‌ شامل: * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: + * پارامترهای مسیر. + * پارامترهای پرسمان. + * کوکی‌ها. + * سرآیند‌ها (هدرها). + * فرم‌ها. + * فایل‌ها. +* تبدیل داده خروجی: تبدیل از انواع و داده‌ پایتون به داده شبکه (مانند JSON): + * تبدیل انواع داده پایتونی (`str`, `int`, `float`, `bool`, `list` و غیره). + * اشیاء `datetime`. + * اشیاء `UUID`. + * qمدل‌های پایگاه‌داده. + * و موارد بیشمار دیگر. +* دو مدل مستند API تعاملی خودکار : * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +به مثال قبلی باز می‌گردیم، در این مثال **FastAPI** موارد زیر را انجام می‌دهد: -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +* اعتبارسنجی اینکه پارامتر `item_id` در مسیر درخواست‌های `GET` و `PUT` موجود است . +* اعتبارسنجی اینکه پارامتر `item_id` در درخواست‌های `GET` و `PUT` از نوع `int` است. + * اگر غیر از این موارد باشد، سرویس‌گیرنده خطای مفید و مشخصی دریافت خواهد کرد. +* بررسی وجود پارامتر پرسمان اختیاری `q` (مانند `http://127.0.0.1:8000/items/foo?q=somequery`) در درخواست‌های `GET`. + * از آنجا که پارامتر `q` با `= None` مقداردهی شده است, این پارامتر اختیاری است. + * اگر از مقدار اولیه `None` استفاده نکنیم، این پارامتر الزامی خواهد بود (همانند بدنه درخواست در درخواست `PUT`). +* برای درخواست‌های `PUT` به آدرس `/items/{item_id}`, بدنه درخواست باید از نوع JSON تعریف شده باشد: + * بررسی اینکه بدنه شامل فیلدی با نام `name` و از نوع `str` است. + * بررسی اینکه بدنه شامل فیلدی با نام `price` و از نوع `float` است. + * بررسی اینکه بدنه شامل فیلدی اختیاری با نام `is_offer` است, که در صورت وجود باید از نوع `bool` باشد. + * تمامی این موارد برای اشیاء JSON در هر عمقی قابل بررسی می‌باشد. +* تبدیل از/به JSON به صورت خودکار. +* مستندسازی همه چیز با استفاده از OpenAPI, که می‌توان از آن برای موارد زیر استفاده کرد: + * سیستم مستندات تعاملی. + * تولید خودکار کد سرویس‌گیرنده‌ در زبان‌های برنامه‌نویسی بیشمار. +* فراهم سازی ۲ مستند تعاملی مبتنی بر وب به صورت پیش‌فرض . --- -We just scratched the surface, but you already get the idea of how it all works. +موارد ذکر شده تنها پاره‌ای از ویژگی‌های بیشمار FastAPI است اما ایده‌ای کلی از طرز کار آن در اختیار قرار می‌دهد. -Try changing the line with: +خط زیر را به این صورت تغییر دهید: ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +از: ```Python ... "item_name": item.name ... ``` -...to: +به: ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +در حین تایپ کردن توجه کنید که چگونه ویرایش‌گر، ویژگی‌های کلاس `Item` را تشخیص داده و به تکمیل خودکار آنها کمک می‌کند: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +برای مشاهده مثال‌های کامل‌تر که شامل قابلیت‌های بیشتری از FastAPI باشد به بخش آموزش - راهنمای کاربر مراجعه کنید. -**Spoiler alert**: the tutorial - user guide includes: +**هشدار اسپویل**: بخش آموزش - راهنمای کاربر شامل موارد زیر است: -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on `requests` and `pytest` +* اعلان **پارامترهای** موجود در بخش‌های دیگر درخواست، شامل: **سرآیند‌ (هدر)ها**, **کوکی‌ها**, **فیلد‌های فرم** و **فایل‌ها**. +* چگونگی تنظیم **محدودیت‌های اعتبارسنجی** به عنوان مثال `maximum_length` یا `regex`. +* سیستم **Dependency Injection** قوی و کاربردی. +* امنیت و تایید هویت, شامل پشتیبانی از **OAuth2** مبتنی بر **JWT tokens** و **HTTP Basic**. +* تکنیک پیشرفته برای تعریف **مدل‌های چند سطحی JSON** (بر اساس Pydantic). +* قابلیت‌های اضافی دیگر (بر اساس Starlette) شامل: + * **وب‌سوکت** + * **GraphQL** + * تست‌های خودکار آسان مبتنی بر `requests` و `pytest` * **CORS** * **Cookie Sessions** - * ...and more. + * و موارد بیشمار دیگر. -## Performance +## کارایی -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +معیار (بنچمارک‌)های مستقل TechEmpower حاکی از آن است که برنامه‌های **FastAPI** که تحت Uvicorn اجرا می‌شود، یکی از سریع‌ترین فریم‌ورک‌های مبتنی بر پایتون, است که کمی ضعیف‌تر از Starlette و Uvicorn عمل می‌کند (فریم‌ورک و سروری که FastAPI بر اساس آنها ایجاد شده است) (*) -To understand more about it, see the section Benchmarks. +برای درک بهتری از این موضوع به بخش بنچ‌مارک‌ها مراجعه کنید. -## Optional Dependencies +## نیازمندی‌های اختیاری -Used by Pydantic: +استفاده شده توسط Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - برای "تجزیه (parse)" سریع‌تر JSON . +* email_validator - برای اعتبارسنجی آدرس‌های ایمیل. -Used by Starlette: +استفاده شده توسط Starlette: -* requests - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. +* requests - در صورتی که می‌خواهید از `TestClient` استفاده کنید. +* aiofiles - در صورتی که می‌خواهید از `FileResponse` و `StaticFiles` استفاده کنید. +* jinja2 - در صورتی که بخواهید از پیکربندی پیش‌فرض برای قالب‌ها استفاده کنید. +* python-multipart - در صورتی که بخواهید با استفاده از `request.form()` از قابلیت "تجزیه (parse)" فرم استفاده کنید. +* itsdangerous - در صورتی که بخواید از `SessionMiddleware` پشتیبانی کنید. +* pyyaml - برای پشتیبانی `SchemaGenerator` در Starlet (به احتمال زیاد برای کار کردن با FastAPI به آن نیازی پیدا نمی‌کنید.). +* graphene - در صورتی که از `GraphQLApp` پشتیبانی می‌کنید. +* ujson - در صورتی که بخواهید از `UJSONResponse` استفاده کنید. -Used by FastAPI / Starlette: +استفاده شده توسط FastAPI / Starlette: -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - برای سرور اجرا کننده برنامه وب. +* orjson - در صورتی که بخواهید از `ORJSONResponse` استفاده کنید. -You can install all of these with `pip install "fastapi[all]"`. +می‌توان همه این موارد را با استفاده از دستور `pip install fastapi[all]`. به صورت یکجا نصب کرد. -## License +## لایسنس -This project is licensed under the terms of the MIT license. +این پروژه مشمول قوانین و مقررات لایسنس MIT است. diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 34a3b0e6a..532cc5cab 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -54,6 +54,7 @@ nav: - pt: /pt/ - ru: /ru/ - sq: /sq/ + - sv: /sv/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -126,6 +127,8 @@ extra: name: ru - русский язык - link: /sq/ name: sq - shqip + - link: /sv/ + name: sv - svenska - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index fa0296d5c..4606c9349 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -44,6 +44,7 @@ nav: - es: /es/ - fa: /fa/ - fr: /fr/ + - he: /he/ - id: /id/ - it: /it/ - ja: /ja/ @@ -106,6 +107,8 @@ extra: name: fa - link: /fr/ name: fr - français + - link: /he/ + name: he - link: /id/ name: id - link: /it/ From c07aced06d5f9216ea9af44f83f65221b626210f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 20 Jul 2022 12:26:04 +0000 Subject: [PATCH 0314/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 55df22c06..6d90cd916 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). ## 0.79.0 From 9031d1daeed911f3f2ff4dbaca0c0a0013694eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 20 Jul 2022 15:20:12 +0200 Subject: [PATCH 0315/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20S?= =?UTF-8?q?triveworks=20badge=20(#5179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 4 ++-- docs/en/docs/img/sponsors/striveworks2.png | Bin 0 -> 34780 bytes 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/en/docs/img/sponsors/striveworks2.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index efd0f00f8..7aa50b111 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -37,6 +37,6 @@ bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png - - url: https://striveworks.us/careers?utm_source=fastapi&utm_medium=sponsor_banner&utm_campaign=feb_march#openings + - url: https://bit.ly/3ccLCmM title: https://striveworks.us/careers - img: https://fastapi.tiangolo.com/img/sponsors/striveworks.png + img: https://fastapi.tiangolo.com/img/sponsors/striveworks2.png diff --git a/docs/en/docs/img/sponsors/striveworks2.png b/docs/en/docs/img/sponsors/striveworks2.png new file mode 100644 index 0000000000000000000000000000000000000000..bed9cb0a773a49e8021902202b8ab21a3bd672b7 GIT binary patch literal 34780 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

n~I<2aQ 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 Date: Wed, 20 Jul 2022 13:21:07 +0000 Subject: [PATCH 0316/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6d90cd916..3bf9c27a0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, Striveworks badge. PR [#5179](https://github.com/tiangolo/fastapi/pull/5179) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). ## 0.79.0 From a1cdf8cd44e088d1cf2c82cbbb0e3308e726a2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 Aug 2022 13:07:22 +0200 Subject: [PATCH 0317/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20Jina=20sponso?= =?UTF-8?q?rship=20(#5272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- docs/en/data/sponsors.yml | 9 ++++++--- docs/en/docs/img/sponsors/docarray-top-banner.svg | 1 + docs/en/docs/img/sponsors/docarray.svg | 1 + docs/en/docs/img/sponsors/jina-top-banner.svg | 1 + docs/en/docs/img/sponsors/jina2.svg | 1 + docs/en/overrides/main.html | 10 ++++++++-- 7 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 docs/en/docs/img/sponsors/docarray-top-banner.svg create mode 100644 docs/en/docs/img/sponsors/docarray.svg create mode 100644 docs/en/docs/img/sponsors/jina-top-banner.svg create mode 100644 docs/en/docs/img/sponsors/jina2.svg diff --git a/README.md b/README.md index bcea9fe73..a5e8cae22 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,11 @@ The key features are: - + + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 7aa50b111..7fbb933e0 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,7 +1,7 @@ gold: - - url: https://bit.ly/3PjOZqc - title: "DiscoArt: Create compelling Disco Diffusion artworks in just one line" - img: https://fastapi.tiangolo.com/img/sponsors/jina-ai.png + - url: https://bit.ly/3dmXC5S + title: The data structure for unstructured multimodal data + img: https://fastapi.tiangolo.com/img/sponsors/docarray.svg - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg @@ -11,6 +11,9 @@ gold: - url: https://doist.com/careers/9B437B1615-wa-senior-backend-engineer-python title: Help us migrate doist to FastAPI img: https://fastapi.tiangolo.com/img/sponsors/doist.svg + - url: https://bit.ly/3JJ7y5C + title: Build cross-modal and multimodal applications on the cloud + img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/docarray-top-banner.svg b/docs/en/docs/img/sponsors/docarray-top-banner.svg new file mode 100644 index 000000000..c48a3312b --- /dev/null +++ b/docs/en/docs/img/sponsors/docarray-top-banner.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/docarray.svg b/docs/en/docs/img/sponsors/docarray.svg new file mode 100644 index 000000000..f18df247e --- /dev/null +++ b/docs/en/docs/img/sponsors/docarray.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/jina-top-banner.svg b/docs/en/docs/img/sponsors/jina-top-banner.svg new file mode 100644 index 000000000..8b62cd619 --- /dev/null +++ b/docs/en/docs/img/sponsors/jina-top-banner.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/jina2.svg b/docs/en/docs/img/sponsors/jina2.svg new file mode 100644 index 000000000..6a48677ba --- /dev/null +++ b/docs/en/docs/img/sponsors/jina2.svg @@ -0,0 +1 @@ + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 5c7029b85..0d5bb037c 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -29,9 +29,9 @@

@@ -52,6 +52,12 @@
+ {% endblock %} From f1feb1427b70df971a28f2c0c6cf1016b66e4561 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 16 Aug 2022 11:08:10 +0000 Subject: [PATCH 0318/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3bf9c27a0..5f2f0e38c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Jina sponsorship. PR [#5272](https://github.com/tiangolo/fastapi/pull/5272) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Striveworks badge. PR [#5179](https://github.com/tiangolo/fastapi/pull/5179) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). From 0ba0c4662da865bffa4b55872030907981c8d01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 17 Aug 2022 12:48:05 +0200 Subject: [PATCH 0319/1881] =?UTF-8?q?=E2=9C=A8=20Add=20illustrations=20for?= =?UTF-8?q?=20Concurrent=20burgers=20and=20Parallel=20burgers=20(#5277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 2 + docs/de/mkdocs.yml | 2 + docs/en/docs/async.md | 94 +++++++++++------- docs/en/docs/css/custom.css | 5 + .../concurrent-burgers-01.png | Bin 0 -> 203390 bytes .../concurrent-burgers-02.png | Bin 0 -> 166992 bytes .../concurrent-burgers-03.png | Bin 0 -> 172943 bytes .../concurrent-burgers-04.png | Bin 0 -> 163423 bytes .../concurrent-burgers-05.png | Bin 0 -> 159869 bytes .../concurrent-burgers-06.png | Bin 0 -> 164838 bytes .../concurrent-burgers-07.png | Bin 0 -> 169968 bytes .../parallel-burgers/parallel-burgers-01.png | Bin 0 -> 225993 bytes .../parallel-burgers/parallel-burgers-02.png | Bin 0 -> 203644 bytes .../parallel-burgers/parallel-burgers-03.png | Bin 0 -> 168628 bytes .../parallel-burgers/parallel-burgers-04.png | Bin 0 -> 219429 bytes .../parallel-burgers/parallel-burgers-05.png | Bin 0 -> 185116 bytes .../parallel-burgers/parallel-burgers-06.png | Bin 0 -> 157113 bytes docs/en/mkdocs.yml | 2 + docs/es/mkdocs.yml | 2 + docs/fa/mkdocs.yml | 2 + docs/fr/mkdocs.yml | 2 + docs/he/mkdocs.yml | 2 + docs/id/mkdocs.yml | 2 + docs/it/mkdocs.yml | 2 + docs/ja/mkdocs.yml | 2 + docs/ko/mkdocs.yml | 2 + docs/nl/mkdocs.yml | 2 + docs/pl/mkdocs.yml | 2 + docs/pt/mkdocs.yml | 2 + docs/ru/mkdocs.yml | 2 + docs/sq/mkdocs.yml | 2 + docs/sv/mkdocs.yml | 2 + docs/tr/mkdocs.yml | 2 + docs/uk/mkdocs.yml | 2 + docs/zh/mkdocs.yml | 2 + 35 files changed, 104 insertions(+), 35 deletions(-) create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-01.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-02.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-03.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-04.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-05.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-06.png create mode 100644 docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-07.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-01.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-02.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-03.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-04.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-05.png create mode 100644 docs/en/docs/img/async/parallel-burgers/parallel-burgers-06.png diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 90ee0bb82..d549f37a3 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -75,6 +75,8 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format '' - pymdownx.tabbed: alternate_style: true +- attr_list +- md_in_html extra: analytics: provider: google diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 6009dd2fe..8c3c42b5f 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -76,6 +76,8 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format '' - pymdownx.tabbed: alternate_style: true +- attr_list +- md_in_html extra: analytics: provider: google diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 71f2e7502..43473c822 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -102,87 +102,111 @@ To see the difference, imagine the following story about burgers: ### Concurrent Burgers - +You go with your crush to get fast food, you stand in line while the cashier takes the orders from the people in front of you. 😍 -You go with your crush 😍 to get fast food 🍔, you stand in line while the cashier 💁 takes the orders from the people in front of you. + -Then it's your turn, you place your order of 2 very fancy burgers 🍔 for your crush 😍 and you. +Then it's your turn, you place your order of 2 very fancy burgers for your crush and you. 🍔🍔 -You pay 💸. + -The cashier 💁 says something to the cook in the kitchen 👨‍🍳 so they know they have to prepare your burgers 🍔 (even though they are currently preparing the ones for the previous clients). +The cashier says something to the cook in the kitchen so they know they have to prepare your burgers (even though they are currently preparing the ones for the previous clients). -The cashier 💁 gives you the number of your turn. + -While you are waiting, you go with your crush 😍 and pick a table, you sit and talk with your crush 😍 for a long time (as your burgers are very fancy and take some time to prepare ✨🍔✨). +You pay. 💸 -As you are sitting at the table with your crush 😍, while you wait for the burgers 🍔, you can spend that time admiring how awesome, cute and smart your crush is ✨😍✨. +The cashier gives you the number of your turn. -While waiting and talking to your crush 😍, from time to time, you check the number displayed on the counter to see if it's your turn already. + -Then at some point, it finally is your turn. You go to the counter, get your burgers 🍔 and come back to the table. +While you are waiting, you go with your crush and pick a table, you sit and talk with your crush for a long time (as your burgers are very fancy and take some time to prepare). -You and your crush 😍 eat the burgers 🍔 and have a nice time ✨. +As you are sitting at the table with your crush, while you wait for the burgers, you can spend that time admiring how awesome, cute and smart your crush is ✨😍✨. + + + +While waiting and talking to your crush, from time to time, you check the number displayed on the counter to see if it's your turn already. + +Then at some point, it finally is your turn. You go to the counter, get your burgers and come back to the table. + + + +You and your crush eat the burgers and have a nice time. ✨ + + --- Imagine you are the computer / program 🤖 in that story. -While you are at the line, you are just idle 😴, waiting for your turn, not doing anything very "productive". But the line is fast because the cashier 💁 is only taking the orders (not preparing them), so that's fine. +While you are at the line, you are just idle 😴, waiting for your turn, not doing anything very "productive". But the line is fast because the cashier is only taking the orders (not preparing them), so that's fine. -Then, when it's your turn, you do actual "productive" work 🤓, you process the menu, decide what you want, get your crush's 😍 choice, pay 💸, check that you give the correct bill or card, check that you are charged correctly, check that the order has the correct items, etc. +Then, when it's your turn, you do actual "productive" work, you process the menu, decide what you want, get your crush's choice, pay, check that you give the correct bill or card, check that you are charged correctly, check that the order has the correct items, etc. -But then, even though you still don't have your burgers 🍔, your work with the cashier 💁 is "on pause" ⏸, because you have to wait 🕙 for your burgers to be ready. +But then, even though you still don't have your burgers, your work with the cashier is "on pause" ⏸, because you have to wait 🕙 for your burgers to be ready. -But as you go away from the counter and sit at the table with a number for your turn, you can switch 🔀 your attention to your crush 😍, and "work" ⏯ 🤓 on that. Then you are again doing something very "productive" 🤓, as is flirting with your crush 😍. +But as you go away from the counter and sit at the table with a number for your turn, you can switch 🔀 your attention to your crush, and "work" ⏯ 🤓 on that. Then you are again doing something very "productive" as is flirting with your crush 😍. -Then the cashier 💁 says "I'm finished with doing the burgers" 🍔 by putting your number on the counter's display, but you don't jump like crazy immediately when the displayed number changes to your turn number. You know no one will steal your burgers 🍔 because you have the number of your turn, and they have theirs. +Then the cashier 💁 says "I'm finished with doing the burgers" by putting your number on the counter's display, but you don't jump like crazy immediately when the displayed number changes to your turn number. You know no one will steal your burgers because you have the number of your turn, and they have theirs. -So you wait for your crush 😍 to finish the story (finish the current work ⏯ / task being processed 🤓), smile gently and say that you are going for the burgers ⏸. +So you wait for your crush to finish the story (finish the current work ⏯ / task being processed 🤓), smile gently and say that you are going for the burgers ⏸. -Then you go to the counter 🔀, to the initial task that is now finished ⏯, pick the burgers 🍔, say thanks and take them to the table. That finishes that step / task of interaction with the counter ⏹. That in turn, creates a new task, of "eating burgers" 🔀 ⏯, but the previous one of "getting burgers" is finished ⏹. +Then you go to the counter 🔀, to the initial task that is now finished ⏯, pick the burgers, say thanks and take them to the table. That finishes that step / task of interaction with the counter ⏹. That in turn, creates a new task, of "eating burgers" 🔀 ⏯, but the previous one of "getting burgers" is finished ⏹. ### Parallel Burgers Now let's imagine these aren't "Concurrent Burgers", but "Parallel Burgers". -You go with your crush 😍 to get parallel fast food 🍔. +You go with your crush to get parallel fast food. -You stand in line while several (let's say 8) cashiers that at the same time are cooks 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 take the orders from the people in front of you. +You stand in line while several (let's say 8) cashiers that at the same time are cooks take the orders from the people in front of you. -Everyone before you is waiting 🕙 for their burgers 🍔 to be ready before leaving the counter because each of the 8 cashiers goes and prepares the burger right away before getting the next order. +Everyone before you is waiting for their burgers to be ready before leaving the counter because each of the 8 cashiers goes and prepares the burger right away before getting the next order. -Then it's finally your turn, you place your order of 2 very fancy burgers 🍔 for your crush 😍 and you. + + +Then it's finally your turn, you place your order of 2 very fancy burgers for your crush and you. You pay 💸. -The cashier goes to the kitchen 👨‍🍳. + -You wait, standing in front of the counter 🕙, so that no one else takes your burgers 🍔 before you do, as there are no numbers for turns. +The cashier goes to the kitchen. -As you and your crush 😍 are busy not letting anyone get in front of you and take your burgers whenever they arrive 🕙, you cannot pay attention to your crush 😞. +You wait, standing in front of the counter 🕙, so that no one else takes your burgers before you do, as there are no numbers for turns. -This is "synchronous" work, you are "synchronized" with the cashier/cook 👨‍🍳. You have to wait 🕙 and be there at the exact moment that the cashier/cook 👨‍🍳 finishes the burgers 🍔 and gives them to you, or otherwise, someone else might take them. + -Then your cashier/cook 👨‍🍳 finally comes back with your burgers 🍔, after a long time waiting 🕙 there in front of the counter. +As you and your crush are busy not letting anyone get in front of you and take your burgers whenever they arrive, you cannot pay attention to your crush. 😞 -You take your burgers 🍔 and go to the table with your crush 😍. +This is "synchronous" work, you are "synchronized" with the cashier/cook 👨‍🍳. You have to wait 🕙 and be there at the exact moment that the cashier/cook 👨‍🍳 finishes the burgers and gives them to you, or otherwise, someone else might take them. -You just eat them, and you are done 🍔 ⏹. + -There was not much talk or flirting as most of the time was spent waiting 🕙 in front of the counter 😞. +Then your cashier/cook 👨‍🍳 finally comes back with your burgers, after a long time waiting 🕙 there in front of the counter. + + + +You take your burgers and go to the table with your crush. + +You just eat them, and you are done. ⏹ + + + +There was not much talk or flirting as most of the time was spent waiting 🕙 in front of the counter. 😞 --- -In this scenario of the parallel burgers, you are a computer / program 🤖 with two processors (you and your crush 😍), both waiting 🕙 and dedicating their attention ⏯ to be "waiting on the counter" 🕙 for a long time. +In this scenario of the parallel burgers, you are a computer / program 🤖 with two processors (you and your crush), both waiting 🕙 and dedicating their attention ⏯ to be "waiting on the counter" 🕙 for a long time. -The fast food store has 8 processors (cashiers/cooks) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳. While the concurrent burgers store might have had only 2 (one cashier and one cook) 💁 👨‍🍳. +The fast food store has 8 processors (cashiers/cooks). While the concurrent burgers store might have had only 2 (one cashier and one cook). -But still, the final experience is not the best 😞. +But still, the final experience is not the best. 😞 --- -This would be the parallel equivalent story for burgers 🍔. +This would be the parallel equivalent story for burgers. 🍔 For a more "real life" example of this, imagine a bank. @@ -238,7 +262,7 @@ You could have turns as in the burgers example, first the living room, then the It would take the same amount of time to finish with or without turns (concurrency) and you would have done the same amount of work. -But in this case, if you could bring the 8 ex-cashier/cooks/now-cleaners 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳, and each one of them (plus you) could take a zone of the house to clean it, you could do all the work in **parallel**, with the extra help, and finish much sooner. +But in this case, if you could bring the 8 ex-cashier/cooks/now-cleaners, and each one of them (plus you) could take a zone of the house to clean it, you could do all the work in **parallel**, with the extra help, and finish much sooner. In this scenario, each one of the cleaners (including you) would be a processor, doing their part of the job. diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 226ca2b11..066b51725 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -139,3 +139,8 @@ code { .md-content__inner h1 { direction: ltr !important; } + +.illustration { + margin-top: 2em; + margin-bottom: 2em; +} diff --git a/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-01.png b/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-01.png new file mode 100644 index 0000000000000000000000000000000000000000..e0e77d3fcec50e22c6efa0548ced74eaf6427dd8 GIT binary patch literal 203390 zcmeFYWmFu|(l$Cc1b4UKu7SY`1Pu-e?!n#N-QC@TYjB6)?rwv-yL^-Le&_zbKkr@d zS~Jr<)7`W8?y7pK>Z$551vv@iPXwO;006R-i-n1@BDfjN^qNJ*f_<=O~?7hh_V1ag3 zgeRJivW6+r6`BIQFEWUAM-jZsfs;(;hjx&5ZCnHoSXJ_R@siID*V9gyQ6 z_=VQ*y1uE2?NN>#OD6sVJ=#whCpY~lqV|SsU|;vaJ9iOy;sr&)q9i(ZH?ma(62gZ< zUqvEq5#x7<2D4W52a*M#?VJMLP=grIzJuvZ^4=5gDIlz=7LvcPijw8=byUL zi!hI-U#3T(`}*&nYaFhna8w})j8^f&@HTpwJp;5|L)_VR;+F9<(5gA9)oz$hrC%K9 zDZdb4nM&q={#3u1xnYH`H?OXCha*=7+Q>%UuL&y|4p87f z9>IrI=j*lkXGf|8r|5yQM%I@xMl(LoMjf4#cIlD#gsC#(%Mg9*jhx%DV7nsRsnh6GRQ>QaF?#D>{MaiW{5L@mS z#w|OInTiiyiy$oeDAko@>k0{sXs+&D;#~~5jUB($LH1KgynGpkkvje40z(Xd;mxf* zULS7siTP;9Ii3}a`eCwdb#E*(Y&eNd=|A>cQx{3Zle4%jg5Ox(EYyk@xE%-jhDsj_jY@dt8mycwb0D_g2y&F z14+db%9mg;|7M^$CqJQbyS z8XlfMcG|j1+B#AeyWWz`h=r`A^$)PnG(7RQVJxm(xZ_ss;W^m(>mMlkksLl{RWK|$ zry)o5bglaQe8}@!HW`%jzEAQXo-(Dlv6AIO-n6B;H(jfQOS4}1{%i$Ta;{un0f7RJ zjH(&08^LjyS^p?u=sXio!P~olHq_e3jl|If{!TJq)iCf$Ncr72X0d$_7KgDkXW@#4 zzZR3hENj}!bXK&&nJpltD>&|ls^I~4*0@4YD)z`Q`xCK|>I!8F<>a}Xac(SG^D6d? zF@0ppkbMh;bBnBjnEzuiFw6;|NkD&N4g&sc(UhYvAd$@D4f5Z7XyYNk_?WQd78ICl zB?xNxIBxy4@%G@Nq^=yi$X#z!?c=yuY2x@g+{ZM>sWR2L|nt@n9>EDrJ&vLcqOk;AG6pqFxB(oYxIpUG5CpsQk zMxU$JHha>!S43=Y721Xd=p0v{@O$Pqtt!^HRJaLM5B`YzgJCfu<`>xK_S0Iq!?UQ3 zZWS+P{^$|vQ?CYV*GcEo`*e*cfCxJB4I>&9}eBDZSRJOGm5QMtXr!6{|=MGs_k*G9>`L zU}8S!76GTSF37ey@q?o1p5e57$(=(lxrbUz5%{YSaua(Ic!>VdlhAp_^gjsalB}LO zHa@fS7RB>SdPik2r`|_oFD}I6u9+Z2);dqb?VyvX>@lXvz*Hvss>=rVV%6b|z}?>Z zA;=*v39j|eK52!TSvjr%T0T+O?%kx1VbzC|2E!p0fWp7fnvGlgc~lF$g~HgotFOBz zjTmq{ZJOlZN86R(cnQAo_F1l2ZpbvHloK_ST`>Hkk71?e2O6cic$ABD{e>e=`^|WR zn#Fnk_UFSHJ7L?}z-h+D0tB|QHF>v8$cdTEWy#HgeYD3FP0UvhjwTY7=ydM`h<_T0 z_n-KG?d|+PbGDZ2{N<=%7kl?|MX2+reRI*C_ho%W4Qw<4_J9ch1ts(yOf zwy6SiNW-P>)2xlc4LbXp(FwhMzNn-pnaW2%16!29r!hWZymWXa)^y+%gjexK=Rf`N5?@Xo^RVdadhQ(y0ky_@hKRpDb>}y^FlsD zcZv$C7!Y7;zzmDcR>QDnaXwXu0Vc01#nkCgK62dIhf`1lhy7Y&H%iDCd&jbICZCK<-Kmvb~WAQ7~6VwI)_Crn4Ai(<`u z7NpK>!87g0vSl0!hL*GbXTwPX~K8gdz@ljPZ4Ny^utGxp2hsRgR1KQwCd7g_m_A`XT@{ zaytwsK+0ZB5k69p$z< z5?u|&F@a4F-(Q~{>>3DdHhZUpV?BH>0ens8Ij(t1{pI_Z)$0BDxV&fvux=|QDKL5| ztJcq*i~TwZ;mi}V+Sm`S3fcCF>cBf>I%;D~nNDE*+uhc#NpmMh6ceBYM$N)v+D-7` z&V=?7OAi{S2o+Ki*4MR2>C@R&zJSgXBgW^bBQKFpU_wBzpJJEmjHbbnVnrrl)&oM0 zGF}gL~ z)0*^S+WBUA?J*O7K+9n~XTpavlF$g_S546)7S(g;4Ls?tDuckNhNR~~UEKqdWysiC z_*_(N9pskIxri}OBwb{=c7zGTJY{m@2+-u;5tg12hHo$SHC{ZXIiyzZQzWFh(<6XN zMUA$c^#aW&a1LOnuwr-e4&(|>5~jk$``OC`pDCl)Uyxd>AZ+yzCzR^p>Hl?mZF`|rwpFM%k0l=U?F>h0j)we&7W%JD)^SqSB-cqKm$1~c^KSR`< zQwgot-^13&PBg$y*56ApIKa-Ay?9!-E+$TdB|_|z&c|SDr+{)5hrNV;VJo1Z^&qJH z8rU2v^^Zv!P>hy+zsfR=NpanK{m$i8W9|K=y6eVxMb5d-ybt60x?~BCe*(FBS;WnW z?k;K`Kj!9{Uu7Qbwy71h#44;2T4Y})OxFyoZNrJ;5fjX4I-G-|ly|oTcK*(~-Ivg9 z;IKFsWjWOs+*@S6;?XC6dtrx$01H;rPwu|Bi+J9Gb6@tm6gz+Xh) z2R{3%Tfp!7rft`nf84%ard62G|I)+d#n)< zgx}T2PWD^C`w7Xx>+eznr)$HIJp~wvt01sDiUV|04K#kKJnL(4iDm_28QB5{Y+1!Q z&C#PNB`Ga;5D1I9>xhUXFe5a91#f+C-l$1?ZgcZk3fgJ%Ka2#8sCae{UM}5b!au#? z1!tm%w#ceWUB_6$k?F9ZGWX;=2j{7IEi3)hZFF2&EG=UwDPz9re@B89Zmk`i62<;+yqdUvftm%sMtqAz+@z-#x|>hP!~lQA zdJuLoSjs7MI?(uNY#JWV#l-1|s!0b=Xw=o@8mtkF9Rr13FgrIE5I5M>LOIAGd?Ute zStlfE<$&>pCEPL=FRV1_a6GPsF$?$lE4=>SK1+#mW)x}g zmU>PHm^t2|T$RJ=Y!wL%{$^y@iPq$@+Z)((9sAA_YFkC*!#o{~4US8${GAlha7!u@ z>;ahZ5%H#711>-(n-@pdjE?>Y(DLjBq7}b@el9J@Ze%Fqxh|9a)aZvbdgoP>c6Q9Y zhk`itSz+T8zfnbkaBIkbGa`h!VfbQPwzza%9N$q_oYK^ef4C|qTDfNU4+VeFo$ldw zCOVkPIJ3F=4#XQ1qX|&a6jjj(*v04SEY&F&(SrWv%pLIzmyd)bW@bfN{6|1ic@)ix z_-m`X2ojv(E$TNZN=ttVJIg_GBDoz?S#~q|m$fGq%_`-=lD3Ufy|0Yt*{p#S)_w`e zjr#Ye7yC`9#M>`ItHXSojnQ2`n@L!>Qo1&5+;j|IH^Q`2 zc%2WnH{Z}+eZwfiLKVDXC-wQrXg-Ol$|zFQ)!IoyhHjMu=jkm|qUgAYdpMgJIDP8{ z)$yM$!Z;?OSv_PUYvO8q-%DTY#>i_dy2sF%SlekSP|pIDSw>6on1qw1aiJBi=p7A? z0f3Q8C_SW-v?LR+W|f6AX>)sk@IK8Be=U*hi*07}-W0AJ#w7vXCuxU{-QB&oCP%8; zW0)nsSH#wF=Cp=TF$1+^jz;tSJ2zIOfJ`kRb6!K`a4HKNhig+pP1J8wKk*?Lf9{E^ z#o_i9>;_RY3t|9(@?iB!*9ZpMHm78H@GZBLSXkDPMyU5=;%MY#N9xKDNzAVCjx~8V z2RyHcY}}X-vfRc7HrqB(Pq{(Ln}%NYI)SNzSsaoTm*$`t&wGjX_KW^z;mqO^pH!cZ z>P_1{qfs?rwRxk2PmftljM|w2e~;iY1DzH@WUQZddWSG60G5(4fmUtFM?NB? zf8^T&=ro@!804E)ikAyvEGUBlz=KFkYtan3#t{)=+Z%gPq4DE{02mpWK^^wjSwfKA zd1IRvDJsDW1EAt4+jzx8sfn*WsbRbm{_eG4!4yBFTaZTIXw9OblAfEWN`#)r0jCQ^ zd!2>pWJJ;7p=s$yN*Y_U7+5q1tQMU#3xjKi3Vqc>EJzJ=WH6=_*utmpNaR-#2d{Z^#{o5 z0r_gEzU`@8$Zg&dqz{?N&^Z$1Me zA%{q5aIFgjJa&)}jrNu!8N>m5^3boZ9n@;A+1|U;vPwvV)Cv1UGYmBr-eq=CXo%+G9i|PXFy}SkCA-G!!S4N7-q@`h7hw=Yp)(&!!%6!G z0KHW;!uN#obNyC6^p9$SR(wAZr~Wn3iVBdb<*<&w!AuWHob|ak))DpAvH7cSB$g=$ z>@WT1zx4-?`^&36+np%@FsOV zKmiXE1djnoP1s59v$?;!exk|XnIit2F5_h%gvm9@jeO%d%euL?BCgoow~`KNt*$h3 z#xSC$oFKn8uBhEpX>*3OnP^C%hZAy0Y`*$JBwC z-d%doXQqithCNWLb<-Lkpt}fl4hRU07G*a>M;<&pIz0$o{zkS{Py@_K50*ARW}u}V zk)Zn0f0P*}R2**o>zp8}Ces zob{|Pmo5#Z3EOe-VlXZEF&<({;or=Vd6?i>F=`P72<+l6i7)dUZf~0)x2j?XRR^yw zNK{$JEDTua2eVaoaJG~`(OBy{!m^<{6?{hEw<4))pmX+0Ya;eEQQCZUNN^TwE8nI5bymgA~Y zcYGgoL$CWs!y|%I5bDc|OH5td!K%!6lL~EX!+AAV-gRZXwkmiJeSBV9cY~@olG9*} z8e;wzUPU`N!>R77Ay1)|(1N=2BGq&0k6jgSD_IICG8!-HuDF2f`NB!&p$9`2Mv1>6 zQ;{-uec4Aw5UDMCZB{(>jW}4iRqaMDu9l)c8TsDat8-6L1Fk zo)7Vf@;q22t-VoHb-G9l3AA0VKiMk=Z&zKeqPW4d?8?Ro$4XL4^*fuGNC|2ggR6SVslBgJUrX^*)U{b8q#k}>I;arGml4Y(>^_WTaYQC{U z^utg>_$pH5K~Hwgjj;IC=a}f27PnZrF5I6p6b*EOWfZKhpr$5iZO#1W&mR?4)q2}) z5ds2&RBjuRq@<+8#6$%Z6;7_<$>HIz7Eyy7cR9Bz1j_7blvwsJNhnB2msKtu5hgia zTzjs43KhCpZdBhwd8oQPPbv9uBCbe3u1Ifh3oj4^)hP)MhAUqVECIrV)IarkHu8=H zbPUcfFYYl{iTdcWjpHQytWXb+ZhaT~OBu3RpGXbq!9t9T){opk%l?V#oR;M_ubH!A znWJxCp|Udi#rgh62VEUpVhN*R#8+)J;Uo4`dO$9dEfH%S44J240*#dd`xSzp4GK5fl4VPRgK**6U#R2!D{ ziOl@qeM!!VfK?QOR9Ni!B1Y|Hcwrl#*mbxJSGP@sdr~VSN-U~uNu}5nQ9ExG?@|4) zKj_}Pz7pWf$vm|^dxF)U%#*t+s^c!FP8 zR~(sil3ZhXCjzz`k4DnZagx`k3W7n|=w+VjjZ_GB*<(k5?kU?yD?Yx*Gg6eBOUywt zD^!dqs>#*DP0&?t;%6bDoc4Bpb93|GzklcE<@r57yC0vNl$Mp{H#8(3A0O9}`ECkv zbSTOjp{Ev0k!=;rFqm?;3$b2WJDSuL4avijpeJEe0EkzmO zz3Gp6V^WxJ?aF$QYS0MNd^K{9${qovCO#F^=ZrB>crhp%$q77>CTPM=7t=A%HH$&k zVSmnaZAr#4a_~Ph?R*j;>=K&dzT4G>5Jbh=6H(9RQ*0vQb`rvJ>5@pW&ZS$Iox|!O zWXW*97E7>P;WJdC}+_e$vz7SrL@c@VBU?cgMgeN z7S;kdFaz{f(?POOuah>Y_7`t(|KbqNLi@|ZN-_c))vCa@JSB$b z0pBJ#!|F;|WL?7+F;QA=1C49*w>XhjcbHi>F3jz#pJ(AzyV94!pTeVcu!m2iZNSz2 zFw{q(R z(P^MZQ{UAF!e5xaTHu3VZGeOe$-aUz?p1$(ckg4b_tpX_{IZ?Sl1HSA*RI9uUCzdU z$HqIoXD3GniJKQm*$MscGA0@z)=vNSPYrozjHrRoK@1|HIS~^J)Ckovlb#vaXkyD^ zmYqwj+(5z`IM!d>(Wyob)Brtoloyoy%eS1f9n~%YaOjC=QO4bC#)y%5C^(8Xl9g zUpHu)sE#w!ga%}IRGh5_`)tTM^mj_CyC!CC%n>6FznN~8-RPXHYkOR#Bn+%RTht2b zHGI*-lJ~)rzA5(fHp_tn`@sRdekRyvvhQrdG${BpSBf&U#oW$9a{MpKYnM@tkeROU zd1v~GO`mU^lE8dP2@1dzap>NKlpCZf3DApfw((&Z515`qOte>ByjX!8wUA5;E1NdF zemhsyX4C3eLWcLeP+wYvb@OgE2Y=rkgEJ<$T={dEP|1qsOmkDby)j8@6YPYHX#ww> zp zg0QjT*YL;_k-(J9=rGF8G;muX5nN(bR{Hr(%8VS!tB;6~%XEnDSE)%^ZZ_J%AcNpP zdjsbQkM}Xowv%3PhlHV-KQaW1u)Mo+MnDs1c&cAz{7uIUKJrMsUqUGO9}gAf8j;w5cz^M= z;|M?K8b#?i3A)U-Zt%uf5Nh(Oe7P%2cL&6%uwa?9V%+OD`%gJ*35p+DZLf(PYfzII z#Z1)xIFupUja;+3i!Mh%@LrX&K&>$W%(4R}96xE%uPIlD!E`&;;l3i);h^^`_wV)x z)UHk~TcG`x@w; zJevb^R=t0{?QK6`^KpaA+xbGr{bkZ~#|96UhlhtzqaoCyswH@_^Zt+JMtj=KjFQaF z3~B}{Dr&vg%Y8$6>fN89HCtKGDXMg6L|Rti(R=kxHxz22*E(%cv*jKJK^a46zX1_& zI?_ls(=@PGzw@m=#Pv*bL$;IVt3h+nuVz#}62}0D=TnXZW@B;oXqVq1(R!IV9czC- zPS5h(SjBaLVFh233luRm#o87L*LC*3U<)cM#)FE=x(9<>Asfe7cEa7n7|VzLdIs$( z1otA4`0P_|ejG7pd*`KX>ml*2hrIwx%4`Q2LsIu@nmGD9tI)buX8ZhSh~7ONkX^KY ziaPZa#bIyp?e@{x*q+rapA}#B-AO~+`T$PZQh?j`d)9j<#QYWTZz3po?KeB@l*`^4z8P0U<8g2r5Q9wM?#TU-mLLOj-#MGNP2o= zVHT_&0tnkuPN05xaJ%@t!Dy@mPnzyjCw^vOiE+Exw|hBjasa>Ut=D;_zkiO^PmwIu#t{`KCysk4XW=)Z|1INLFB=yO4sENgH+gv0bfK+e|8jbe{sSWyRra3=MNKU*lq@GR^uX@r$sey=Y;c!utD2Zl*Zc<-6wA3W=p}> zVQlMMz4r9aE(|?1-cZ_ohC&M(^$s6W&#?Z9DRQk7JVPvFpgIQCT3a(HIA_v=@{W{W zy^vUE=mFSH--pcvI1L|YzER_TsCVPr5{=($6MMeqkb+uLG(rbUHdU8S?$~|aJ+0fX z#LwGrpevy6(}-&GS>vi&gDLvR?e;L;-vnjpTu(YvI6gLiVpM}ciJE-T zr7(1tQD1CPKCWjKnCs=kAMT9}#x*4=Id6x8op0TvjxZoh5AmE;Swk~f8``fnv_0n& zXu96TNvbR?po=Q2t#G##xj{d_9$MYU6~xy?)J6^mr(0uo*ndQI|%+cZ4h<&f+7cqacXV>F~My1jTc`O+$b+$8+8gkPwl!tZ<=0U~h2kL)F;VL}b3n@*Ze zdL*Lxc|WS*i%YG`i@pd2I?^miJ}Sw8WmOHVeP&wd8(hm;3c6Q#O#?y zKS&_ZSSJ72Efyjl`K-@JboM<#8Ufl<=O<%2_qItn4tL#yDP~qjsX9O+nrM4A=bdx& zd7)`w#XedQwzvy^R>&Kp+^zz8MQVq#02qdT`VF3sD>4gyo07tMz_)SQ5F!N8*1zP- zNbPhU*`(k-@a=sT{}*36n9BUvnEy+Pa_4XmOfTr(=SlE=vWEVS(c@^( zS@2dKwbsgnZ+Q1<{5Dh_Ws(c|>OSWbWK(UmWmBCd5&f`}1@Zjw6qfvL`u)f=mk-zq zXN2p^b7#`bJl0c6*FVcHS;v;f1*3C9d2a3=%!E0T(V3dlSX_%LE z6Cbp1TgP@W_T_%m)+GBpourolRRWHcHhv>^VZz-XZpF7(^AY;+qceis{8l8Z)X0h@ zk>PtGz5dV3Ho~C}m*MaLBL0~{p|?>({>#H@v&v_NB3dS3&Nh%af{@XE0$`VZ-4QBN?#C2 zf$J72?ubc6e-p9gJ=b^lpqCoV=}-kv5I7+;^ z+%ZZUprQk2P{jGe(Gfv}$JEP<J9J*z6k*1`Yt7ByN0EsS+R9;Hk%zV(sjb4u%bTpeQ zVR?1XfW)}>hgrELWXWHQl&0X)bHf)#e1L;6t8s0);japCgTea|iCnjhPHxLc|9jddDRfsdXvc{v=pO0w*oEq_>*}-6@h(mG2@%n)k8qL0T0+7+Jx-~{NLir8A$O!Pl`r~U{BfkdlIX;t;86gPIyld9ds*G|7B@^2rbCiPakXcK z5>sGh4v?@QdU0fe;6}@xmYZ3Z-ckd-X9X_7yI%wxF0tZKmpmX^BwFPW5gJh`G)74e zu*KUYCK0`5lf^eYy&B4{y{RC^{o41fm^>jD;tmO)F_4bmm{I8w2nEl__zq0awGqGWi&_tQxUJ_+QmnHlbMorv# zW56en2d(P`x{hcR&MjFE0Jom*%F^)l_S(5CxO)#I*p<-2mDB;`RqeyA0R)wpwCqMP z;QUxZo_@GG%Mu0}A_i`cPJC6g^uZE2m;Y~Cb+t@l+=($#;aayNq;bk^X-;bJLLOe% z#^UIti?dw~pA1mHWC3D|K*!o{_P|Ed%giJoG13{j*{hQ9Xh<~bV@=(A2Rn^cZlFExpn8$ z{`WegO_|n@_ZKNjrGo4h9v$lr{b?AyIJASwh2(97AF-h1z8DwBs}$8aqX|e!HMB^P zNqD3{g&t`7@xq^UVs>AjrAKsPLQc3U^D5g?Y&q`&4fOPUyI$J5DB>h-7qwlsuf{G{ z>y+@a8R`Dg1sn%UbUF^WU^fv7zFi`8UR$0d`hg3xPc0BQ+;g?=N04Qs(w8rFa6|=H z&=h&_0s*3#re}p6B{ACyK&pBJ{>nTvvJtoozDUT8)~aiN6G&6!uE#q;!Pl^m!?W)> z`$vM}K^1B>!A4Q#bj*x@6_$nSJ#|R;{w5{js@9oL!#)SMF8g?L)2u|JMMoDxL&i7m zzrS@P?zul56Q+Q&5_w6Rh;1=mEH9ccDR5rPfGe>rWG3)M4XU?8x@KF6O$(0~W3CwWHwTo%W;=Cn~+>m?` zA@8mbf6vWKt)Wm#tMldczLXp@eH&HUmj#AZKN{4Y;(_`LSle1wWLn+`Cf%MnD=#n! ze?b;j7&OT@s!UZB%EdUixf*P;i_}C8&nBDl3m|Oaa<`i!c(as9qfqkbDad8BG4>A@ zbRJq4<5E(Jst<`+fxw{zdBHU2AHNHMAxWe3Gg{w6zb{5G_0g4+(mWTun%kgWbkuq8 z^wd&1BKHgn9uoP-;Q>*A1Xg|~;T7#NrN=*vs4R6lB29EQBCJZ}B0W4j+&*45ClOiN zyJ-Zk5Wqy^#3gs8{tdT3H(dPD39-zc2aD5X)WK?T=Ia6UYlQ}9j%<0d(oZc{@w=b) zuY%wg{b;y)M{}Gcqnw8e=}XKPySk1DNk+lF&0G)49-;Sl+C4WNYaM%izE=JZC5Dd| z-5DQex}u@cVUxeTe=4+^qj&u4Nz3)|RFW4I6qUxGjWU+Ke({CKpHn2KK61;hq?1}* zP+~RFjo!>)N>6=>*8`ff5!J)fSjIO}9|2vFf=aQ(ct}QW+AAvQ^pw5GRk?6#>*!G@ z-RS8}?^D6gQ#dbkeu6xPUcnbWN{?hnms2N^Gh;UPAu4CS6RXTJ-G@;uT2p!6?Kgj4 z35&5N9g5cm{>T%y3TT{h!N>D)st07rY(`NM7TL?OxH{kULcrn7w+YOcgf7?`kc~7h zknyefx36*aZf~%PDk6F`?3HX9Dx{z69S<11KQ#uKoj92yXHnSQI|eSTu#feev!3`p7F_Jf(HT01Y~(Ue4GMJRcevdh}t6^F#_ zr6||E<$do&jBN&L$3ytyx-?vcO$uh)UE8Ene@|iFgCczdxNNx5P*LGOLrt>D3*2F? zudmYysea5NST^h?h9+2Xh;d^1Lxx47L?0A)SLH*t{j)nn=BO-tz11V zeQe7oOxNwf*v56WGF;8zrvnMOLV+tzBr&faFpD-i5~4p~29RX19H6|D$Epp7UwL7J z1KOJp&|YG=F(XPyZEo};Q8!!yU)~Lf5l5M2^t-{;{`afdn5jATMEkVO6O$+ZFBQ7p zPdu`(G|JM7N@@dlgt~uwTdCTcAMjhBS7|H@z|p4A=6n!A*0u4;+!U#leg0ZezAOJ~u%D&D9v>c-#?jOG0QUG;!Z#gH{spg(> zFPhu})SsLpviO-s!G(LK>9G0y&W^FVXJ`9*E~(kEtJfUs4)p1cc;OnEM4{JH+uogz z!A0#xR(oncyZz2tH$C&B;85>o1&L^kc}#ET+K2{z_}Q4_@BAe`dwqU@LwFS6oWrhl z6J@gT0GU^HxeGqI;T|NX)X!r|Uv_@*^uN7$Ojc}_<>{GyQPJ8d|2QxRKkJJ#&3qwQ z9$3hdXugTIaNW%5OyB8U*8J9R{XKNyyM?fwx0XGGZ{9Xu&huJAgA;TKfAR0%p)Tzp zV$WR)QvLVWr+Q#(M9xCG$9YSL8siu2Wmc?3%e0UKjMF@(Z`2)mw+X!h#N?e%o`j^; zx82!4@g_1A6U+7d;GBzES{kbaEmwOo4U))cwhoe+Qm!%va|%rKXwTT9LAKYsQiAuj zF;_(_USX@&ZBZxHUD@Lq-2dub?HgkZj&06oJ4>~Orz?LVl9O@n&sHUXM8>vLR`~lj zPc>EEVGik8d1g znGRcy%WXf4O*k~l(^m;UtD>PYR9?M2HZ$N(;z{_;*!fZ_VLtb*LgF@FqE~I zubRAfNvi9W4Ys!u8NF~s=@x5Hxtc=LWBYoSQn=d}#8TQyXeqv{Q%u(w(&F$+9z|9T+?^lB z&j=YmCpa}AqTl5{UDCTi&T%7YK8SizRivVtuudN#4Y*xDl0BSV?3IE7kiIcum_hzl zo?h`necJMp2&E$4sI5)6v9dadkZ+pe$LpI=+N8r=T?nL*LLoi_UDgVEzrBc7=qL_v zw!he)yN{hVHiN;*{R;@M05kX zULUJki#=7&^ISn{iT1TU*<5wUOz}CEuOo3d!+y&}QBuww^_Y;LxS%N~__$Q{jA1mq#ckB+^}Mt)GOz@NOD$Q< z!h&J*?cC+52i02`TuLY^D&pkiRM63plafNf#>S?^KtLhkAM%5OJG{7{!TMBWb;hHv zs)`Gk6nG#K`gocg^w!bVmK76&Y1(jS;9hs7sAkIIo|nxeDKB_BHI!|i!;o>)y>}DP zYP3gKw9$xgDYhV#$xk#;6lD^7a(g|VHaU{T{AJg$zv$u7Tvi+UpgYf6e$*{5usIgv z(*ifjE5N2Ct*ERsDVwq>r+#(%%VYP^p~Ze3nen$~Z#^HF5UM6N;ru&yaHmf|VmUZq zXW6i@G;ymOMqC+2{l$Y%^z`D>O?{TOR#QzNs@80GvYXfe5&R{|oxu-{!!=7x5xBG4 z{LG!m^5vC_o>GQhJp6&|HFE{c^L`y8_ljD}Hs`uNz)n%fttH~+;laLvMWokx zqV$g^nDC(?<(+Vl6@4{O<=!q7D9Doe6RZI2Sva#g6_^^2@vhB~_r7(nT2hPQf%;m8 zxR=-W_NwP`FqhyH1A!e>V@JYRZil%4)N$U=J?UeH`;mc=GVZcU9$eymM#+>Rk+UkQ zxd?Fa*_mvyyz5oRPX{5Qsn?{x_8m>G1_3owv;bsSa%fS-RRD0)hK25n#(C$9H4clQ zPdRN$D^0em|Nj1dL(2RjPMy2h9%GDT=uNo=rJSo4K$tE_$(#|UmD~CzoB2_WXN^5- z?D7OkAy-dtrsXGp+^>Tk!?_>HMvUyOzw?4swg1FP=(?tDEA0F0YgjOj=67}FyuW)# zlL+uSr|IJvdTdNK{Mb-wZEZa~J~p`6=E+wX(u=0 z#fq*lS9WoJbVDHLw+{u>w>~c<*q1Pxe9Fq*hHRVY1;-f5W*2$x7*gV`MkAxwx4RoP zT}@v%6iwlFnhwgJ#x{t*&-810w?IzhK`>ic`+y+ z5m2G6F7~xE$JBBsLz@h3tB>@(M&^BRT7!e1KT;o0Ejh?Yea&G~fHI*J0;^~%`2-i^ zQrBJ{ft=l*ItvwCa;9gXKRi4o${1=;yDaIroc`=8Prhpe4A9vPDu=v$kv!(cNg}B-nw!|e*M7SGf81Q;xDEqIkP)I|Y z?I*C7J8^3kHAl~N=EY7&&R4ThYv7nF%0%VRrWmZT4(kJd_+MX9Fw8bCz>MP415L)$ zzGiZc6ZSR7RY%TwW^7AwVt$@GM3dGPue}5F+J!y!!` z)Imp=dgD#D*Y*4#f1*IQdWWx(Ob||rfX^f<{o)5PqzU4y=!rCa7V30~aWTe9Y=R!? zG{@q>?tHVf(4lE5#fUweNeMI7VH&S(3}pY%gp&;NvM@K+xcB zbcpZ%+O3O)%|*+;zC#RATCKAoGrKd4w5C=qGUs)dM$7ANo?}z|1l)i^d3bh4MT9at zKQHFu!VUI_hUEc0da-$V+Gi8Fhv$Sb576wJV33_)9@ zwy-3uBZm^x)tu0Hu9NAobihk!klZI4^5XU=DfDp>m+-n=Oz=!0b?=ysqDNaq%fh_7 z&qS!kf6?pAuGUc)cb_fjOw5~ooqb?Bet~j(y*5l>t>x)0za__sd<|{|pk!l4EUNhJ zuUX}K29xzF4_48J-<~6**Y?-Lqr)YQ`4E*~rqxp4w{_Baqo-Hq`}Jx_N3-2u!WO33 zu|OO#T(u<}|A(Zr3XAG}yYSG`Dj*F8Dcv{^EWhh{izxC~J$8!jDOD1@cC{B5e4-#$8iAgb{s~|-wQi7i>Dk7RV0Tiy#0 z_A;3_p`C1YwqZ*+BNpoWGNb+Jv{g?6VKvWvLr)_DPKPIw%F%S~JdY&@k#L`w_)ho; zpZc_5ruK}#YX;Jcq1Nf_nW-q}VEQ?v&4*C0u~A7l&y^;^=<#;0?a%;jI+Vge?(A-5 z3CK&@?C+eMxPZe&KtRAPEG+z`$qxAM$%*6Q($YR0ZUubs1}if>?{0ATIk@%BeWz9l ztK!e!f_ArWTtSEO8F~@_1rb_>hP#ZDy7O2j(&YIwdMBusJoT=HDs)84KKGBer^e#cSsK%vgTAbllcYgF304kpNKXP= z(#xo#Pay-y639wx>ZVdLd=-xGJYpfFk>Iso@efcwMh2OMn1MMHt2fbQu{9IWy~vMY zBfWfCznJVJ9v7CNz&K>&H|1Jkr0v>YPXW%R`_sQ~j;1Z_$rWy8xWgWtOqfPY*Ke4| z{O7x!7?VC~?CITnc6? zSMgnJwJC%6{_@4Y!t&gvV1h!X3+6_LOk*QvJ1V>8%hFiX`ho*13H(<$9ra0k7ls-O zpK-|?;S+t5U`9i4D~Y4`8NEl5Wn7WwKZ_gSd2PSGNBulXY(p*6I5wa2rU=X;dQL*? zJdn>-%q!gkd4w-#&8e{MQaQGOg?1fb2!^*~`84a-2aqXt#R5kqb!Y}@ru*`Fh5i$il5VM99baX%Sy^XM9 z1O==2mcCen+bH_1;JFBL=J3_?0jaeyF}VJKWY9xNH7;9I;`ZvpRU;U=?4}=de@Sj` z4~0Urgk8Q~U3q3?WLVqUZuCZyxVyWLFDwlH{rhCy+1ox}k~;BjzluG=hLjkRVm`Sp zH}w+oc;E)UGsX9M9CEPN7y9U+@h$Dw+f{{-RcOht*;1xvww@yTczV6IsFtm4+gHLDG-s z<>d~*XhyLKo__1?qr$8Z4)0&p zIEg26QKgSt3QJ0bUAW|!+mviLD1UC_E^@xO71C3`X>1&d^q$;>k1exFF|wV z5%)^tvlzN0o@@99r-gJBX$Ngf zFI(u`=V{Vg;pxp;!U^3eu?iK`c^HCacx>}56}WB|5?y+ZLAZngf~;(oiS?eBuB)44 zZUeDeYWm#mWav};+S*3Yo?EJ~c~6vR4QOl}i@l7$8?*2(<{WQ4p;CUK(v(h&0e#c< z5YEPR;oR^2DRHDMbz*uY`KL~v0PyG!Pjs*hy|t9k*bh;Ndrmo5UsO{UURo0!q%#D6aH?x^SwExb^4dnQYUmYJK{Y2skkTFg4-xY=ialLCyJmI%}v=s%f3 zs(5st8KMX>GY5n|}tj7ojB0dUP zJr3FH-A$OSvI-KSe~=_bKr0`g^~s0e^NreQ>*ql8YxF%mV*COa*{@-a*6fg#7X&|Y z3f7wo0ML*%QKo@Pp6;SsUstmn9gEsorECvo7I-b)Z)w#e(ROnp(gcNnw95I{WQqXVj;dx ztNW|G_emk^&u$~WUJw!SQP8}~dH+0?S;~%o?J{i(8yyoR@qCbBKzEWH1%}Wl)b9RO z?3ti&>bP929ze>JRlY5*sL3L6$YxEg$Jqq?{tD{H0Ik3Oxoc=oc}JZVJ~(;9#vS|7 zzQ&#C(-d@(k-Fq<`TF27@dOO=AhJLig8&rEZn#=ophaJu)9TdDtN+T1Mo{26t*fyid+AU_z7I?Y4Bqp z?rV}zP+Wn}X?21BRrJ_tdQb~xC2|90z=jBD^KoFjYn5eCIi3Q-9AB;{-<*z?<=#$~ z3%PbPuzNdKwi+<}q%!~fq1v<0AIL(bwezrsjb2DF^innnkB*K`{+BC-z*r@$|{U;RuE-Nbl;L-g2*KxFB{eUGwo8!HK+tS)PR;rYyev)GYRrOZFfXwyo^87vX zJM~j?MdehR<>N+s_gES;DDyl1sG59~06g6PvKXDWwXy2NQux%D80f(4 zuru>H?ta*l=BFZlyp3sWk@lTJZ4OO2B@NxHoKMfAF@UZ%W% z9?iU*@rEz0@K^ng_D38f_<}#^d@qNWZg>>KdsO*yuoEynxOd9-RWkKRCv4Zx3k!y*X>mu38g_K&z2}i znca^T1eGfR*X2x)660jO-#NzNrZT3;aGz;vjmhT8G`(<9H5T$ZJ4|^(Zx^sw| z3=m@<+QYh2#Rk{Ij?lL^n-W#^JJkbpoBPF(9pJGC7cA8|Wd3*K&_7-e0D5vP055Kw zU2Cg#?DHncP)13;#DR<{`1pv-%*=e&)O113^IXAKQymYvK}C5iqeu;%pR$^YoLqEA zOrj;%?}2=P7tDh^#o}yQL*?u&J}B!yqC6xd#0G~s3BA%rHEp+`jU__9K8(GB&D2Jg z$07`^^n>efmeE&|10M~$qD@%N(dq0JHU$VxajBPF_9&xVt9%l0jO^Ei#&Ul7asPBR zV$Q%`Xs8XHTdui|p;LHe*;D>;==U&k0=!p|ByZ-T+~8Fm=wo|0RI?%D*kUT4TB*}N zk>W{2WA~r(EjKAz)&*JJ72lkB+~+35n1bi{d#8Ed6{#nw6o6iJoxV>yd63p^_1*tZ zDKCa-gIz}Jag^&k&I|77*UK9XRN@|cR485uPA5=5c^dWUD6vAo-U)w__JgX?7Nq*l z@8HH#W_^k|e#W9c)qVTXO*297?}(mZv{yttROxOK`*$XGe%oUZ(b+0o8yZ!*k2MS) zq5GqYPy06oi~C8W?GtnB_Eh-59!D1|g>XBel$fKmgxSGvxZtOxZCO7iEYKgcBr7Vt z)nRmJCLGXm)Wbi$(P$g(KC9}Mh5-AfU#O!SfIqm)zkcN4;8^y9x1)}%BsWjjq2HiZ5OjN% zzA{e8*dg4X_~RFc{Em*C#YJ7^EaC15NtDUe$x^gM;VAHZp=5{;O^71KWfzY(0v0}~ zyD_7kba7hNft}r5x77gTSsz#64`6TN0YLbmZS&%<_=wW;W=98()XLwco0E74{}8}s z$32z{DeHA9HM;c2lYjeG*uMp+z zSG@FZ(nZ)iKV9JC3uEOU{TYxd2yoIqVBPQA?jNlS#fuLuIdz_C_OEIxH_8CQOp*59)#o-E`77=M)=R7gau9L{!dZ4A2K<0Tp6ZHd95c zm7R8ToPpO+`tzmLhZMjW{W<97)LA$&Jv)20LhO$Ty;u__A_kv==vZ?NsJ9>R(cLdphSesi06e_7Ddp+zD!>qIw zo)}WumGK&m?Ot+sHbmXj15eE>Wv|DV%!{Hn$45ZShVi*uBj!R}XdWq5?7)TU=Y)Zj zdN%F$CYKk1t?4tW2GX+it>k{(6sE}aO&S~90w)&le!b|pM$hwTN$}v3VF1*JVm}>F z#EYA2nYYD-Gu-E=C^w4w_%zP^c2_t(j}NKDc*bzICbikRRY09wT>vN$wkb?wvk0l7 zm9p~&4AK7(aHAlEEBe%R);S$t_0aP@sUkChMZVygvX`>&;y78Q!cf*@`vI4WM0vbq z7ks4u{^7uc&QSVG*0c&G?0T!8Z@UsbH`W-sho7dY=S)+fi{g6HfHiPHKN~j0R_(Tp zI#z#(-e^fV;I=vN=?ktpLvovMTCO8j>GHV1v51*`eSlQFq1P5o!I1PP$`RY?s7n3Z z?dKF6^%b8lXaDVJc1=f^den^v+1Adme#5*w>wt9O{_oel3r$-XAkg6#t+NU*`!#$c zKNI^ttYvYO(ZhM_e->ykkIkx)n;ReGZh5x}-f;&=Gooep=ODVNF3&97XWHc^K_9>W zve=Y9D|ZTrRtY%JxBw!g`m6k;S{0=`2zYxG+n3L=Sht}+Fd;XCM7|Lk%-6bhqN3`x z=u8@Tc&`5X^(`5l{;&^@uz4xzlbcv=-G)$$Vu_Tv0!kv~0^aJUyMr_MQO%}?&*JIb zkQM_ZiaD#zklWI8o8rA)Y!v(NGD~TL`nxQmFkJck-VNN1K?XqUrohH(7WoWJkileM z)3wyo#t1X#T%bE1z@gbpPeo472zkGwR9DDc^4V)XUiQ44?w(&z)l-X7e`|3>H8SKd zHI$zb2SK?KcRxekE3?WQY=h5TIdd-7@ke$q0eeJGkzq5-UNYkxSw%C#<>loD#K>3t zEbd7gT@Q?@FV9_KQ2Ix)=@`O`xUGF4x8jwkJhuRfP)6YvH+$a7<0`RdV^xGwKCWsF zAy=PnQMKXSsf>W0r}hqQG5vyO8n=!_!Qu&sWt;jKle;84TRdhm%%&E{UJsF7>9`H) zoEdg@ojjc9H}(-gEzGD&hGzTuemy-*RngLroY;N1Gd*}Yu8QZ>LeOzX(95J;ffHfs zf4-VyJaw!4??dghhj6^1E$;avowx`P++1%EBk+cCf+`IT8*H;!2Yg?WhF&6S=`;iZ>vY&yo@pnCHkA*cPH8t%U3-3clT_L2 zm2w|JQp0Ila=>pOeASB)Dejymt8mvJSX96BLxIi&xKIA*H98*ccHY6@*Lw<*|9-qt zD6*eOJz;PGR}L6&qtsV?p|3k!RwWkim@2##HUt4b;}JFXxHV+-z?-LP8}HINw^c60 zzJU*K&G@$wNzc;hdw>}aw^zwrOg`?_V`brZ;yl#vyW%(SwnEn+6b}A#RXjCG^7KMG*iESzJ%%z&Uuf>$gV5(945f^I6g zn;Mm_q+*Kf_axtH6uR`%_A^+vW%+ozDvqBe6_}`-d@NtLG71P+339`MMk;pWO$Cp{ zo@{kmV@p;#;G8TFZ60FClK{524_~4OpIL?W6CBZHUr^#`z2VB#V9>p>82#g#0J%L| zmlnis+dIfMyRcd&_KnIu;t|TSe7v@%g0N5X;2Q<2sSs8|uz-2;@bY;_T!jFdSZ83A@?DSf82H>{#BW1zKS&d3u{5(c&5{#|%Y{TrtqVITvhk0551d=*H$x5x?Yrp%FjM z$N5rUOGjVd93frkk1xWD*bSi*WPRYY zfXsC0%eOfq)Jo(#CUOHooOX4|Hx2b;p_HSF3z8MbFZV9`dYRHpGlM=!m8L7NuF;f85?cl6;7WQsBz=u% z`6_s(WNZ)+R;gE)`g87KwUV4`QeJ7yafC&j1nJm+N<-t@s2qPSDS{ApY)0J~T8ujld zjU#Nj3um<^&qSgUd51bnqx-_CAF(Qgkbh|w$wD_3J<#?` zvcII{x7E}?HXqMjGwrM1wdw+aPVw;}l7Uj6G^@@N`IfDFv8P z;?W&F`-x0=Pu8N9sc@gLsQ>NODjnK!6Z65oH$hVJE?_l0HJh{+i#j*w%r73kxXZ66 zalrs-)N{!da$OZCJ|%>fAnTlCA|8%S9`7S_j~Z`F z?Sz*J=eRN#?lN&T!JLEb&eguZ)AkRjwwsG($C-2G4-=u%+ zoRPD;RyQcoX@T}+7az)XHvW3 zAzqDtK8XUt3H?r{WWcvryBF(-oK>WW1>>ogP;rd(_gW7{h{)06Qr~I-ikL=xB7XL? zr{N1wxx_vF&2Y@Y!NDK6xVF1-=ou&o)OBRZDs{RS40q2gmTx$-=w*^8N}-(bmwXtBkcDdYq3KqfY`{}n(Q)tawF)zUeM=Dkl4 zSMVC2o%s~#h?ZJDFXQ0@i&+`4*Q>V{cMu<;u@gyh)d8ArM#vYNP1JCGIwo+ntU5Dq zAz2jM+{DCYbkyC3IX?Mozad_z=XjaCztWJCl%1RSG(OnsU{!Hl#o(o8Zld%!lL1(7lHoeu;k?;{#`68;fc*6Z%BOdft$z3bMO6Mt+d880~4s75ZShyxa78V zfm-S|Ys#1(Kb>`-I%5beFg>m{Gz`4}RnayDrM6=AxGWE>b7V3}2(1K^i`_`hV%)#l z!;h`dK8$3$q{9HZP5N(6%V>)a-wH2|Jh|z0GPa0q^~m$W)Wu%LL#2qHdEUx!)1<>d zpr-E5LQTh^f0g1y3a!jBAMHjEVni-$*3^euf;kW#>sS-UC+>6ut8<=#C$htesrI0k z>F(FaO0baN4Gyxl{{x>(1ADYn(A`qo!)4`eA_T=iE#zpv(iTZ_+6wQ;yn}ag9Y+OJ ztU`giD((?cOg(s};{SK?uBXK5uZ=gDsnOnkEgIhXa~r`+KC9DO6(e6Sf8<2V<~q4}!pyyQhQgM-nyJc|Xu-2BcZ# zky6ZdsrKfZ|Jbay0y#0`2ZpxGD)VX^W1ZbMQbZcrx?{y5Gw(HR5st-RZIGc-_LSVJgFf$0G?=ilzVt2I*PQXUh+uMNwl z0UA66NjfnNOvR~U#t|U|XdrQUax~iBWzVcxxa5eDB;ViaLE`i|cdfP6h8q;=<`N7$ zp3a>)d7t3jvcx4JxehJJd5LbNflB$Jh!m3`jKqJxv%wWQ6I(0#tHb%V0JNs%=|NPj zkqBw%{qcW*$H9ySePb~p-Ljvknb{$3lOyYNte6wWzmT9NLOKzU3cV-uUJ6vZfFORK z&TYoj`D)T9zspbfnfF4|`3Kv# zHJ_^>*h3#&YwlTEiJS^rJ2Cv06?11ZocVk6!=t%lZwZpgwW@yyQu@WWXQ_uWCso}% zG!RudvG+FK{ieoDV;>;SOfg$5{B2E|ow*;sFJ(=^iv#~?n4D{FG9C(*CO?un}~jXz0ZhU0kC`*vmD!)_v=^YO^j z>HwZAxtZPzlPPB{ug9$@#8hUx+mk?)%;Ia?-xJkxBvqC78w4QGpE9qhLnBchn{9H# z`PqJPB5c$!zy&1BdUO^vpi0vdc|-|%rveqf`Xz7R!RdA3vP^cv zCLR4pnH8+%d@Z95Y;-`xWnWr1Kct-fM!`YgU0MzSa)%qwz&~DET2t^n?+}tivmK5?%7~w1Lq7l*3M=28Y*YvmReZ-Dr4>PE2OiIYv)N z5ZD-RZLpqNCLUV{qE1aD9;34visl=tY<(CT^*fzY6qw{J!_bHyZ^A7I-m%k(2?ifq z=T{38i#KLJehCt zLDoNN6bmh0x&nEm{^M~UURhZF<}b-u4nw+ol|!_{9psG-qjsY`Uy+Mtx2}R%%(n=e zW6{BG62E=gqt`Zt^j;P=0$4selS5o+A6RGEVSlR|pmLMuXkgc0kWjGN`)>Q z>Qve6R#B&`3~dA->4awP@vADf;+)T!Ksm1*6aVyR%eP|pB3~^r-N={LY=}{Ya*xYvnt;K5(C@A5n^ z%HJhxng~;wRgodKh+;ZkBqeuMJ+J9B8ZP}P2y#B^mHPKVSxJYp?*3tYITySIJt&;GD0ob+IMVPf7UC`b zHOjAYLurnKrc5-D=?fRjb}f9xwSO*?BhUIL*Gg^X<8>eBS5~Y*eE`Npq88qLh_dQ< z0;sB`EnNf1m4=^<57xw74VW%HfHxpcZ*hfF4A<32JMln;0LfGn;SVvxd7LZ1a}!)urvQVhK!Eex>d6T zzZ4x!eCm%)@M5p?8K=(P)Q+3i*|0a?3Z!#Z?-_7Zj!gP`JlO5@T74CAbe#=&x+udnkfF5WbF1^Kv(?IdO6pt?_h8GukSUozKLDQNNvnZUw?? z0&8xJF5H%1uCaOeLtvq#Br(Sh#I|w#RL000aB{#~Up9jf?|ySBJ(3>cwj>4y=Ku7M(G{2&AJV9`DWh6Ka61P$xSQJg9W3Tm z@gC0-#2m|E${>Nh)0TS2aZZzF0gMKrZmD@fDSJb11lrfQSJ!=l^9$hW!gUrXw~j}j z11VP!t)uw_8WeNjolV%vE#`rnQCK4it8Jffjk@u>*TPaBoB@oWwGQ;L9N>_9K|Y|f zsUz%bYAjRDSh#k+t?9q}M(7+GL39SgA#(lY!BQv>vCui1uMTbOxGYC)8$B>CHMIAR ztJ?ZO7Hhx~cd2?TY{Os|SXpTrDp2(Z!L^{8wV?2KG7LZWF-QuV4cm54DX>v=|MPtTy8is8Qeq4vo`#WBd0cWC zifE;?+RZB`-A+|cGWyg-FJ-6nI{jr%%(JMNr;%ZieFY5Va^Zq?C)dgbhHCnX9t(vA zAZKtPsa>k$ujWmPv#s7~7d^X^{puSj*3VhM>qQ8)C5Wk*UkLKO#9PAC%lqT4J z72|-hAHApi0c2-fl%F%fM&!gS@DC1WCLCLPCt2AZ%R3hu@TZ-rH}PpCbQ1?AejWbR z+I0i3Zgq1OPg`{~M*W{s@#E&m$vxq_TccsB&XFfJi=p(LTld znw&)e;ucr^OsFIJ?TL$oehpgiL&*Doht;JF5$E|J`iITSw(VPa|Mk??>-{$h6a;-5 zakMlvTPLmXcXrI%=0j#wsLCa+DjS`v!8;}DgPZ6%_TAlh+CoW+;yhkRBdjTnxWj^@ zZ>r>+w2$>%9iG(y>9(3$iNnKymzoQeYp+KGXCXs=-w-BfwPH+uJX04N3rv^qwCS zXXd=eznmFpOyCoV0Q^^6$0D1t!`^zoIApVbl;_0#5$;~T;Ig*&2MYvj)73{8XhVxK zl3^ikkC_!l8Y9TgbL^ae>afZnFY5ntU~8&L@7d*qsq)*4MJ!%JVofNvu+GnXfou+p z|8pT}eKO+I+fWByDX@K+M5Z-4v6SJeqy0(~d zd#Yu;)D-^qL3?kI^)4m$3}rIx=Ox7rZaAQGTk{JzX-MS>ByKxCPhWL|)WOIWjo|Ll z?Mjzz6|w720g| zwWrPy=1QGS#}Q#jy>pUlD5=Im8;DMPZI&D)^rXs5pAmJYQv1G#!`XW~KYc;@ISs7e zHQ(#s#y5#V@4Is>dls(nwWnL#t%<(nFb7bciLg(v_;rY%@!;F0hZ$8({Nk0yJKwzf zt>$>G?|kmSe41Z9P5k$)%neb7*K`-4g*X`6JsXo;9*uj`&f0hThgJRa94Gr`fRna( zd5SMqv*80A;;>V%N3~BGRpoDp%eIrMKkn(jJ2W`@^3exQ7kF`M5-wwdcNe3P0N;E= z-xPINX`7YZjq4JZqP62PCN_5-^x&Ljbae1KscK-di^|_1W#d~p6EG27$re? zKWAnIKr}h?d#Yi9K#mKplmKW%>X|Y67HCK(o}8FcaoE0rKN(+~$@H8oCuJ{5YbH~J zjBOR!kvqeZ5eqC=}7UE&x8Xu(mY+O5|A#8L0O`SEiDqu4=My0N+W zita0)S?}@M&|*w5T20;Am1!IzRC8FFCIU&0k!$j)ra+Qe30QrO_58 zd$(r3+PmSHxP7}8o7x;U4RSQv{8H$&@5NlCLhpisDO8!&oP8vk&@GH_c!0)$2!6HHk)JdIiYxoDi=^b}(s6>V^pev>sQ zkf%~$JEi_dNVMFBZrJ(IgJbpqv8F`5jx&~B(Vf6*DN5>ch?O)ZW%Z}s!TFcnzdM9C zxXoqC_rYuGfmdp9=+VT<>>JtP+N@C1B@x{n zZPq;A97o8q)=GX}lea+)8b~EWsOX2{-MDyB_c2hk3EjYxKqMZcE+z_em@7mAiQz$; zwxwga76yR)Q*UQi6o4wUWcayN98b3%jXc`XzB^6AB32n3IXhoR8ub}H_F0yoxHwrg zu_QVkiniN)btt_gVZ#+yPk*>D*T)qo3crZ_)dz4zJ8>t~Gs_x@g0dwOZmePN%vj7W zTB$NGvOla#Ug~C?d~gjt%2kmyD|{R7JMU(Wlag&dlkYLDIXarN zU0a{)$m%14g$b2qO>KV~E;yxfA6}SH^1c{bB~uI+@)p1AtdBc%A_fgq{4#jK$uxDz zj6@^*NZKcgQm9ojpplLQ*yQ(OO=2j7<%=v6)O~dT{oY zzd3j5j+g{DcA|pB_mySL6t6FbR1Gl8xOyBod;-0P-d&F=EGO@|h~CMxdFLiBaXMus z=Xn$K(U!f{Hpm|9lvwEnD(<+dz$SLc0abv21p!PyCNr-;;2ah zZeQuH^U1*q$nk}g@QH*_`0I2GP$2Q}=l=cc4BW)DqQND2TjE&x@4U5Ev9)<=o@Dr z_6gDm>Tzg$)4A#w`Nl!kM$0JaYnhf7V)l@0(v-h;xP~@oeqr?S?#$B=JT7jL{CIYb z@NavBW!~(q8pa~o*=+G$Uim2XWzKzZ1vI>qrPZOv#)jHTwqa-X5!@HW%<$q4i0y>? z!Hj9*y@y)^P-UduY5FYehTI6Ss~gIM&a#cf(*z3E$0J`jc3zz$L{rvP)AEcnu3Umw z3$)eJn`Q2r-*ODDhnG<|uOGYvUbz#i1GKd@Nf&-oa!~_yuKucJ>$QWUDC0KQkiI(F z@DrQZUKtWmG}0aNMC!j_%b^c7h|`ZpqHVF(!}qWV1#F)Y0Vf7YuY-@uFk2$xd);!hs4H-TVgrRlFziLMTW zp(HqOlxDxH3->j9i-vl)h%?}k6vOqHC%!xt&;&`LdGmbTFnwI2{delL(u>f}QU=`Pe7=V#&vvVY%dAYi6x_b2moy)d$ zbi-=zPXmz+;jbOhx;vZqH|z%JIgG3RpHJVW=ix6GdSuf~WUD1A}8$;}HSiHi>*)JH}?HVY5({HC0|27g|rytZkD5dmHE zmJ^KBpx^9cnF&m|+!RqHv3fR3$2VZ?Ab+UoEIZ3)s8K))vsZobC+i_R4FA3ILzce* z7RM!E4S@7>G-7_ds!jFr^j% z1Yw|2oW1Qad{1$*y#}3E9WZLIPnpG#Z9~}$wIYef7pUK4Rl$tAu=W-=d6WS1?RVu{ ztIMBQ!(}BjLo!YCNo3@_l|?@{%V{a&qx(sYBxi?9`*xiyTFRV)dr3pSx8#EmA1y0E z=>Q7q+Rm;c?DErWDFZ0ZGIRHA?dxwdq(^%!aVe=DepX1z7?#e*BWRCQLxp0DFa1*`nVH4Rf+hTgliQ zGr4L)qfT|oN80BajnWiHx2voZyH^b(B(qtNZCXu8gA@5s{ zUKv>o4K=WB-Ng9Z+b=8{<6BCtvYMbb%n>^9bzCy=Y!V*AMy{fB__A!}nHSfU+rToe zTKJ+Q$=2;My023U`+bM~RItz=FQg9rz9+MWL+I&9@mi(Rx0a1sYWR&Qix;nN?i)t@ z+;-;Pyd5+_&a#Y46Nf)szk~-8MajiA`+^h@zdF-B8+qsB?u58W$#*-VT~ah#VQh7t zeCUCvH1>qCb~@i%>rINjQk2=ksPN)x-Y*!M>AYCI2QYz;$(DlzO62(xVMZaZvO#_9 z&E^;&1a*`K`c=EE+G`C=kg^U>=Qkt&b4~xtIlr6AJLHtu)(NDPb7Y&#sPSheSDED4 zn@-_{xT12Q-U6}3>oaJ?LSU2EB1ur8ao;4K#LdK^58_bk!(wL>i;eL-aYQs(#79z& zo2ayb>{ATuvURjHMR4=)a`Ue#>m$Q{?sB|bJ4++o z+2-vmiYkn~oYvAxNMyagdF=}87s#@Qf3T?+&6&**SBQhmfx0Gku2=`Ea`~m?AHssb z@ig$=yn*;wk0yXR`MK$(b;BVR$>;bQ0?BN#khW8LbmnR$cG|QalHQ1nbmuAa`#nK6 zXQ*_@RfhMiQcPNiaxb?R(V9;+c>!xy1F-SMW2HBV8tD-Xl@*Kn+oo(b*tI$px5|jX zB0KYde%psV-o_oNqI~->kyB=^F2KIkaXyRI!;CPPLKMp301?O?O_1Fyi* zLwePi*lhoq3qBxZWO{*#iF9&4R`)8CH?rO+t?P%i@H}@zqQ)8;q=lEavA=(rX_m(; zmR-+reu24sOqgg&MPa+ML4j7O7nEBw4h!Vw=JET(T5>b`1Z8K_5-VlZ&qJzGK5T`= zPF$?AT={OP_SCkAN&_8z1N`540;-)h5gBsFQ)=_Z!qQ{L{C0;O8~&1y7ja6c{j!v^ z73$Y8LKH&v2^SgCAI@m=2kb*C;B>c?UQN(U4uQc@7 zd@X?pax}dJx+oRMOdE8t+dh|gcf6a(ET|e=V_2Wwtt{v=Ix2Jx;7LA=)*-f5D4=~W z$MLSklm+AYvrGjEui%c&leZN%R7qrHlR{fXR+dX}pq;ts>~TSCh+AQl&0$rOF(ret zI3j(5&y1qh?9&m0Cxu`PemAVk(HO(XTBvowT&ONYX=7fIhR?76iW|Yt7fMlfaCatH z!szi4CQfJun-idm*L|BjjRhCk6&d?couMRb|K(rQxz1-p8AWMM&oYbAFLxwrUy@V| zJ(*wu7@HE6%>_7`Gn_-e%p(s|hY6d{631r7XZUKyhh4W`4sG0x^AiS~ zS|=r_ZvF5}F0ymKYH%sq5hiq~zs6We+Z?v6t~Fh!60BT|927>56p{9IEiz_W?^{bv z6~Gl8Ui~JNiBt=vW4>t4Es|$Idyyj5|2y{`j%-=Tdo_mA=aMM1=(y!$>7m8}Ck$?u zy?0oJD$;oI2P1Rnd+UY_8d+V7=2Ak`2`Cc+4(!*AKh{~%_o!U;xNO4(s-eE|)KitZ zKQerOv|tS4tVng@kt+;*t-;JB{~pXlwurO#NQ9f%iA#zDWvO{G>d8e`@PPO8Vn$n+ zVQa5b6qM7I`#hKU;aH@}A@+C6SXWy6uPO=?H_f^-i46&t>XP`1K>EH-ktHK+e%eoF zm6@`uCfw*4F1ir^S3F<2k};*YUngO#JZ1k&*6mU{?Owf+?D&AFHOg}`4kms?>AAzI z*)N|Z2-ZHiQh-olbUf39x*H2Vk?rjvxSohsI|jWy{YL-rC2WH=oUlTlZ+C8M+#!sv ztal%bTZ^~wDw`~zs??~sx6CZP%l3^@#^r(n8wcSG zk&0;eQ9CJaSD(NCrs6cBNR;j;kI5|+4N=8R<&ul%lQJvL>?aZ;&%m}mKAW81@82if zz?bW%-6bd1NzqpQ(YGt%Bj6N-5M|%|%{loi-GPWq5p3{kGkxcTPBA6Hyvm%S+WdR0 zmuBnPRotJhSZs{Ola;8VL;iZN^~;+xY~ZzjZ-LNHu+a|}Z4TihL04W{bI~Jn5qtB+ zZPO^yb(hS4jBYK6F2B_0fsvuYFk-wd=J)PSbl~@N2}_ z808NVVxAH+91&x27xfS;eVq;7YdPr)xyrKl4sWcM)`W0!tC>YK z6Bk~7?S_3cu}l8_P@M65xfv5I%|qn;yXEj4Dyy(ASIhlJPfy}*^a7+;+Pf9XqiGT^ z*_+;yL#YBH6^>bM;!jOgV!osDzFdpv83etdG#b=?0B7=}*|Xg+?@7jg^@z-zFAae>}OK zs{U=RFN;a_M&l!>sdnV3Gw*Ahs|(N6Gh4Y%G!%9u5p|pHYqgUJoz;Tc1}$bY*VJ%+ zoBXs!H9bvvK}LB>cVvwDmW)YkL28T7{mhi`jwMm%Kmm@4jK#%%xWC<#8Iot6|jw-eI*?o&Fn+w73b z#H{Bg0e4y0GwDi!mjzMX@U!@=62M~YgbK!5x}r+Xc44)}J8#X|0@RaqmYuC5l(2qq zot~mWbq6C-&~Qs_7#MM)X|lR?5hr6h_NjLPfiHY^&Qx3M zcXkqd7~54nMzHhc25}Sx&s_;KiK=QXbmO1UIc0vZ=zjo!xMzv}QMDrT+T%}xd%Hx0>TFHtnZ4R4p;OWn#q=$Iy& z521Nc6&glHNKFKf>Ur7g*9Clz+S8Al9rwWISLR@9EM~v5RLVbcBuvyzu zUx)1PhPxB;t?fwi{jI)GuK-s-x8K7f6Pc+ssZn81ffH}std&GE-s?fmvYR^Jh#u3J zqosB-;BfQPJ?MyJ2!bq(BDy(yc3vkp)g9_i3Y2z6Dp_17`>VPhEWyu@k< zD!RVr^wZr_1roL{Kc-qCvt03K)g7ng>1mI=yDt8+Uz8M6xR4nKXwo)*JEl}DI3dY` z4Vo@O(U(v|8_;v($OF!hSnuC__$R5lcbNomir0cUI>>2K`qR;V_`SZZww<)>X))d0 zU&`DNeXi*3{*Ls3+TvMNbSR>_z}fzSprwcPmHxIF^A%>-jDK{xo;GzGWA?&j{_7Sc zi?0vE^<53Avn~q|sYT5UwBhnN;hY@Ir;Y#-YA2B7=D$}IaKU~q45pr~F_`~M7*UAhlPkS2m1#p7 zc0vD*g|lQ8&FK4}^R{Nvi(kbRHlQ)AHtKKe8Uxjf8Ry6!zFI0Yw-7D_|Jz44q+1n> z{zhG?Bb6xJuw-3-Y1$PwfdJ?s0+DN2kn6k>=q5m_g%*l>c?@o!zkmImmL*#II9l0^ zZU(FCZIwV&<=^d{J_Gw&o5AUap;8lXn$1&@QY zj~9lo@+1sTU-V4Hu!Npx6R$_95-&^0-=ZLfzJMEWf!O((GDytC()>#`Qz(r4Z0vSD za^&$4-Iy({G!4dMSi|0v2PBr7+L+Vp?0U(cjaY)x2{#*E5C?X<9auDm*+}SQo@1?@ zDRWk-cdW7dN7qbT>3VGzNVci~r;W5Imy&NJY!ixFv~1l;$F} zzVu7)??+W`vZXo(D+7L%<&4@SZ>8u7UBVT$p(yeMjUX5ZmyL)MswDei5!&2j9%tZ0 z?$1&Z%V5I?fs0NGkDV$%y;~aYY#D;WPJL#OuV}&SaIKBN%2z2kvad+BDtHD81RJnD zevE`O9C^KS0twKmTa z5Pckohfz8;!j;VO=Kd1$fBvOxdFu^Q0-z@NoE}|WLhMxd5IwNwVqv)msBC^96Ya9B z=FcAfNdMuTvjm=Ox&#s+!;lW{ULA#Ma2BccnAzCRd&GxLw2zaJX2Ao=eU=W06bC}Q z7X7Ab#;HwdrGlu1c8jNT#OV6x7ch339RbE0|J7q)>xNcID?;@N+ukF*M%q%2E!q z8cp#((ir&k{o|Z~VZgB^MvrS(DTH9FScVEIb z3Y>ndOSoic<02tK*Xo%;$}Wz{idid=6_j*X0%G9`{FWulNS-%(l#%TVWvj2Ljrz)% zNz%5vZTR1hzJWjdn50{9RF|*bSdU$<<*Zn_){Ih1Ez)#&QgAETwXS8g2#yt~9o7~K zKk&47&}>n(wXF_%Fj}q;+sKGsV8XEVUBzW=MqfHvx4;GOM+J{dBSORTO2SiF9F~Jn zXXx^e*T&h;67|B686`mZK4K>=^ocFISl(-q`SI;D?<&8`+DYgOiH7Ma4w3J6`HO_D zMCXO%LKypwXJp|b%7Z=r$i~D%!%RkR6Msh63Hy~O!e#7C~kT1)Iqe*xqsTCr15L;xxb|+(4&WV>~gBl6`Ig3*oluM@wQ) zr=F74AH0unoEpEguCHDX_6*qF=SnZfKQElnHmmfyF?;-5vPUpD8!b)w9`rk+PI%H( zGb+^XiT*usnuqbx^B->-S>h>Pb#AY|#AK9%PBC*4B}9-iV5QwM#Mn4a!L4GbRpN zJsAMsfa?|B+DlIOo0-m#XjvFpeyMOF)H>FY*p&rR_in7hu6s!{THysS>J*#lKxAV?#8~g5^df`0cpbS7(laIsXZAsS@SB+aLwCJ0uh4DB=@DnckR0B0fec?=FFxe~<^f zes|eV4`vnchi|N|uhj#h4tEZ3v3`FGPP=8v?&7XuCe9rT1;Ms;a9U%t)!_gjOG(~u z=7T>C8fxyPECCl|+>rZP%NO2xVfTt?$Y@|Cd4>7)@cek2Xh=O_ymQws{rye3Mvp*d0VLae2eKg6WV;Jp>`qgpqW$Jn>m*FkrTCBL&b>v>7?Tcz}OzYEB^ zw0*v+aaoJNU2kFj*_`^5X4s2Y_9C|hUajrI718#04;%0YO)S+++-x!CeIlb2C03eH zc}{WX5)w6hD3-4U&3SNe1>Z$VA1H4oUH4)@`4^-(COD}Dda^F z@(E>y_uDSMgeDaGre^!`pgv3QG+&ve0G+zm8-vaJ4-=0BUW`65?;@)iwigq{wT6+U zo;txjMFD-$Z3rWingTQWCKUD;1`xe7UiGmnoU&m*%R>AXh^!<6iNWwSeK0<>zM`Ri zeQM>s2h-u6dRsKgTJQu{!_?e2++fa^W@Wai5M@L2TxiD)keyw+w-hBtB;c+ofSq0`9O*%(DZGj>GxZfUl2L0Dt(!%MG98%lS`|8KDo`t#}wqzV$CQH`tv%Wno%o7~B zQ%@J~n`J&dZ=UuV$N%G(#pbP+igB^qHOm~EX-vv7E~A6!iSqL#sk5qU$IVY-#2nW2 zrcde&2GMfH*@k3zVMFPIMZ$Dz4do@ZE%+amT!)lT=e;MW<8yL2Wjpz$&~_)h>Ns`q zP}8lJi^@)rU^a6MK4>&KK6P^0UG)?&-bbjcx+FYH& zeTef;UZ#^?=C4zJVe|0RgL(vGdA;|imeZAnsRY}rv&kUnFFs9VIjr?9|F*6>BP|c- zT*eP))_fgt|9)a??Kxa0^2Oy9%&D!$V$aos->r0fJN8UIjd>!~M%w z=*F9!2g+Q|SG0!`)Q4~nR*FFxzYUv%d_7opKk}={xIJdw>$A~b7dQ1MK#h#@QlGRz zti*U`+Wl5ZW52TgxcO_LUA++4EcrGvoeSj&ZAQ=I0cl^x(^Jd5&y3@H(U|yk+uiu> zO{TaPGMddHyl#U=z9IwfaH75!qZ+0zJf!H5c7J2YGfz5<-=j5 zLpX9(WD5Q&p)dCpi|hT^N3=|+)h}v9K;jM zVeOg`iQ)G^t<&Ds`7y1Yi4ZT9o@(CUs=yZL{X^)8&A%v03i1*|A@|@9-`Z@D0I}Re zQ=5nXmz;F`dKXdjJvfi<>-@7$zji^{^*<-5mwz^N0-ldfz#8`>-_>)2hs-E&qic{o zJiPh5TS8Af)Tw`iA?OH4qgWE&oHjQAwd}!9ip^leGU<|dYluDihHp#(Jt`6?m&sv5 zz@g~*O163t`0?_(75NdoY+}5(g7$j*WVzHJbQcYkD13(+=x0Ixe;4O@+X1ipF|J7e z`$HB5Nwm|}uD7D*A{Ya9_I3&XruW<7w5nv{=iA1pGAzK z+TVxX57LnQzAsSg4$jQD7Ev)D)6@-PvGypECP&FQvk`&fA9bTDi3kG5m4>G=IRFuu zhnaR!DDZbkzC053W|UcS66#!3C5@JRR3S#)_nFG_rSmHG&qy2vbp3Iu%8Y06kH=_dA63?N8A=do>1Hsik;ck15swx*YI(Ek%Z$2e58_J~W zf#Zr9Jk%;qw^r*zr<=7|e0ixXyU46uRE}kI4lMP=WALI|-karD>vNZ;R=1sirfX;f zZFHPk-Z%MRqcBUuYB^fKT&G16;Ke9`g`-q8hk7l4H}9y3rXPY>nP-5vdJnhesX#Q&-#s))}$aZ2X{U4)r< zAvib`v7f4rm|Z(NJO9o3eV1T{wCk<4>nbn#lqO3z{XoHIbv}BNe0VRRHg3;sk1Ej} z6erG$UEEw1?Zhi#Z4BpevYP^*U8XYc7o0vVC5tkyt<%On9_G~Y=T{M1&upw7>0tpn zTrXajW@ts-V4;MjhP%YfN3_KbC6w*NP@HejCK;ysELtelna)di=rh4Z_IuuiQ~xse zXaiD{a46%w>mq3B0N=7N@*%1roHn~S1)wPWdR~Zli(l%M@FGT;B%hWiCjsfEZkL^R zH?Srw+$5(N;SWE)$900EynVsB9UE;__K}y6h`&h3*zrtFD~%b)iQ{%Ai60PXc1}RK zDPrZ|huT8xFQJ9Sf7{+QY>m zOq3ykzFOaAhk95n8BDVo3YL31%0+BDasuBrgz{H)6X)?LNM>1y%o^F5!iHg2nbygs zdG5YZ11KwpdxZUB$ls4R{X((Gt{v zm3E(_!_(d8lR8>#EbRB&4hwHwM&e`N+PB_K~-76U(RN1 zj!zst_0 zYmSIsl)9qWR1X}5AF$rR!tXfEqULuyXkQ!|v|mNZ4{Vg?{;DK~NoYu+PvBP<^FkD} zscVLdcVQ3mAEzF_C;5D+BnliO*pLa700FKPSMcoBPBT)otF<;xzf@rD#gT#{6AXl` z-~KQ@qGveeZ7P2bdGSI55ZbA>61|^D@~#uDqyO2)wTJ$2lC!}(Zh@M`$)`uTk;H57 zraj(XF%Tx`@H^FF{b~`^)y=A(?<`=%4nl0l<7)_8FN`YW{RC}n{Er~9e{vTM30ySl zbjEJX&fDI~^_=+!D>@m02arPuGds1VhDzS?-$tC}Gw!p0I9csq(rKpris$(Sd9-y~kgO;9@Kv}%iVjcI zdM4s)Pr}5Cwq-3v6^~M%*q)^T)rO#fzmL#SGe%e;z0F1pvZMzoq)}#u zCkMUSE)CQYN&Wcu|t9kZ=%T+mC z##*9x#VhI9?Q}3r(+|;pcFwUTJ>Qo_qkBdf6<2nAhJzC+wqKG@l5L0r>q;kkt1h-E*|^Pcu{>OUzB#HInf-V( zad#@b7(78t#C}9AGs;y86D_*hzlvrt6S|3n&~#j@#RjvvY&7k za4t1=P)gFo;A*n*?kl~gRmgsl*CGq-$~@I3Qi8&au6PWcUC8vZ@WdPXS>WqXAWXBA zW*!X+S(X9hR@n~{ASN0AZuDMYZ>iRf7&6=O)5l5dcB7!d!p2B+ymno9$*m1jC6FIq zOa6dC<%O9YBbMp6$?`JJ*S0cWZLuG1oP;!pYAH7ZGI%uLK&X&ef}IfdU$J(wG;GyE zwi-&mnUl}6_jMX1IOa5(-@;v=mJr#gWgpg|E|%DHEnz)dl#WC~ww|Su+3$P~QICFB zT`BepztK^G`a7EG?6IIJmHpF%tsi26*}Ov`keKNyLrEJbhFkOwx#&-rJoNe=0pxM{X=TF)nH{JjO(8=thE3b1<*)3AU zSWAm-eB=RnXRJ5vq_f|3V#ku|ug}rKo&tM>gC_{~t1i|f)=kV*tZ5=3V@?LTVzPuyY$z2p0Z2ZKQ10{aRbIYp%0H1L!x7k9_yZf9P~K^}#jD zUL;8)%Zug1-u^Nygx>+Bom^ZiN7)-KbkP1%48tv%J~l0{pDmj zJw02DBj2U;A=SjYzUvvIHp~b^2|+-F#BnHBu)3h;*zhuJzia2_OhNeBn8;NdOD#br&b}guHt*7g=@wl>xMSU^TfqxP zo)-R-gAQDt;%v|Q8ZExwo55p$f#l4`IOvfHagWdG!($em)u%C-hi(`q;8m*O&1|&3 z_h5OJ z%ium{)akqUc7Sr0RU%&Yyf;QTXkg z0!y71vZc{sYcvH{?x5DLDi1^Q_y1d-kWz>~UT!7XU1J|FM>dD^1$`gKlmhx$^8HeI z&b0f7e>`z|43jsz@B0zB$G6AZ}}uKn#-*9Yy?K0_jC~>U{G7ZO5v;%MluK=170Kf?56tD;9p2kl%uO{ zye*Z1Z?e4qfFakPNBjk$A-ZNEZ=7m+W1W)Zb-AHU3{t${p2}!|9VGw7Undi1qgiyc zZKSbC?Dh302UVH{Dep4l3L>`#O}U7OKzKN@H|b-;L?1 z;3DZ}@XwgRUQ5#0sg^5j3>iInlf-jg%^MT#>?q#%-ZPJ4K6ExQ-rl(t~s*~tSHf5nPGLG$TJxzz<-78Cg`v`3L+=^ls)Z1Wp zGSgG{R-Xzy4M!Xcs`d$#47SwlS&E|g9Tp7Ifi1Okz*Ve3V@vi?I~|?gvL9l|kh>%7 z0DYc+z-tjAOg;s7Q*bc}F?v;UZd^2hkG-H!_tVtHB_uf$OFRAlJ;(1!<2(VrarO)` zk0(`V&bk=?@eeKfCqILEic zVe}nHnS6MVfaWto1B;(d`$dIB+&O?5Vm5?O3qTo@x9cI9oKQvDH)fvShE0OTJ%JE~ z8ck@hh!u$|{ovoQ`}6M9RYD&Gy<0wg=Rk`jY6rtCVFs5q310o+irp5*u~D4YjfjwY zq4I+J`}^$?v=pbahbKYjZ>I{w@=lb@{|exZX8x^eH8`DJg0t@JG?;W|s%Q;F=7;!p zP`oN^f6~n7OZG5ylZPE$$1Uam4s$Cu|7RKtvtjg#l08IdNTv^hyI4aK4%Aljjl}2M zC?s5d_(xi7=+hp6)CjA|68a;&Kl;tA=@Lqr>vK)udne-fW}_fCucle6eNDwiqK6Yk zRlll{dGz5&KAo{jGRk>jrbs=Xr1(EB^Glm}uNl#h%Hf-Zx3xAKi&c6q{qlCp+)B4@ zsy5hfT6mi|l+@g7Nx;#T{u&cfHCCAUSsy$PJ-xm^ zW%nU%UCp}XL5{(@vxZn{Z4ljbb28+Z-R@X)+H8*Xb_;|F^!u3(K@_eY2UILVC@Niv zGw?5?eHV>MbCQ*UmMe_7>CF_S+Bu^t5RXS2;}%b-si;B|15P2$+`}Dh-g0mnO`jcz zgDPzVp7hWz0w&;5=dCm{A&Wc(52ELyt=$=jH=ji%15PMZLIkV!CkXwMao!{*qtl66 zd(l$4DMn&-sJ&EHgIspQVs0;?FhB;h)WWUdvA*!N_dVs{@)@S{>EDU+4&w@HOh2*1 z+LWm;#@xkqeMM*Hf?Ord2eo{#`viXci)-Xm$bv~WD^%gha~OTF*WceBgI-LiD(U9i zD?@x=9B*9Mw@^z3zI-M`#?&aOTL%G+n@elSuHZT){w_)UrjF(gQ-d6#F01E@sv3l= zE$IcSQJj@KW!6OY`U=X*!9mzcc!#!ZgR6nZMj@vO(kmQq?hA=(kuc@yK2ojMPU=Ka{spuI+<``mI z4HVVJUKCZwDY?!6nz4ik5&5xQ_TFQ`j7g-L4ItFwMUw^+{!3L<`uNy03OT}(lC)3| zMbFwFtD9AooR_iM%A--JoXmmkZC{YA*2ZgM4K&m~s zc|(;?7?SB;kkgb=p@nygdDEuS-z$q82m;EqzGHLiO!+RM9GYkk{{j$ zW*Y<=?a;=HExI4;^6!j)K((Y=PR8JR3PcD1BJQ<8z*40C6|XX{MxZI%P(X_^FRz1b z(8xi8^KypyryJl3(s0SO+g?Cw-w|F#`A zNe$r7-O_2)*S;sUbdCV?PFB^?8^c=o!q(Qzgaq_$gg@Z?4amb3f>pcXjMww=c5!ri zx{G4?2(5JPPxcIn4Z>e!425YHixG1Bq|D{@nwnIIp+HQSSn20=1+8bLw84uW_Sx7P zy>+F8%;E6H_2=qHJ19#Rrlmvt_8B{~W*l_MK!~AFfK>2JDOgyPvd=g|kYT&_Zg!q# zxP%VJ!60wV)iT9dO$%PzLOlRI*2UtV+^|U!-DD<@v2HfsbSFP(!}|k1q2QQ;H8Z%@ z-u=2fs?Z4*m!ua_JwFpI9hBmx7fOTNi; zpvCmBFJ9~nh7&Q;;N)m9ec}$iK@b=4+#nY;>aW(}@rbIW47AO!-6f|Wbo}5-O-a2MQvx=p= zP-WfSTPb;BN=7ag&tamJzFSIzRSFq9)U+JVUn40Q*rG84|0GVF0I{Rd%z%X^`!S9; zA*0HP9SMksBSe5_6PzzWNhV4X-7sqLECE-skmSyUhwg_iUO|ULx4y;+H-m6>$O-MJ zs;n&H;J_UC@enw3!_p;PQA3VTTS2la?=(PPaS>fPsJaQmD_AzV_{E z?oW(2jZMYXfEek<3+YC<1F!{F#|btXsB3$At;}R7=PgY3LP#LjF&VgzqNw4>o}LU1 zuus6ppy;{Z;=$uJdG@kryM6y64dtpaB`)&ye-$KgyYv%$x@hJ z(k>4T8z91X-?RJccg$%$CqG$YsrS0rEyM}&g=Qp~(9bV_@~B;H&nG?6MAsam!b-Dz zAm6+F@oY@h2@27}PoL`Aa&H{W(ZR$J96C`dm=7Nz=o!~eH8xr7KTU1n!HH84kOOgU zKa^A^XR`TwRKbXo-1N4m%Q?dwes&YoSg}=(%LG>-m4ys(c7J#L75s)-iU0p_tK2sp zX~dGhxW#t?sfDB?ac=+N~1uHOHIyZd#I#r2}=;rgeNzib7XRN_XCL#_}% zKR<-5tF2{jY-}9#<-U*qXlFu^2;wJF$LWTy1+J}3z#q&N*uz=IoS!qunM@K;WlwLk zKSqyD3@xLxY)deyS-|YZm8>9EU5+9ZT@uCuqk-cij<^XgnkJVRv-t!iqfOb%TDg(N z5pVAD@X_|_;1N0qchTR;@7lyZ<##j>=|pFs$7x6ZPsn@h?-JarY9YcZoZ_hn-0X)Hjy+(-}L%WUvpMpaP!ZlOVIRedI*VxG0j zgCKY}jRMG|^IPbGQ-ije(trC|!x$L|QD+N&Kp@-{HhxMEZ}>x3k9lyM{T)%iMZ<^! z4Ui+oi61Kf!&HqUJA!44lpCHTUNmIf4VnVk`cXLagJT7TqMGIqoDNF4G_)#j=+1}C z<~0YN9!Q28ls>F%28w17OW@Olz||m5=S|+l?ti&%MMjt_JG^s=!#3D&FQ#=I@m(gl z+Y%Yu<9ZG8WEdp&()I-tE@7Mjxb5iQbo5l1j5Bg>^fv~DCz}$5iRyl`NDTp^E?m`- z{C1?Yk_bMUHIcdWof8`xQGKr;>a4|*sku|Nm-X0gRQ>1l+9v&L4hd^l)rT^K{Fw^g z4lJ*p`yWG$11|r3aac8wQ7w;zFx`h9@PFL%UKtr6Y?78g=r`&K+!=3{2w^PKA z_o-EyyR5lVZ>B#7>Q8suLeszCip@U8GYe%GKl27(>C0L2ASfA9u-oWqsyIN<_fp;z zpM;d^Hp9w&FZ=Z_=TOTgO4palS3c`PvsjK;-(tA3V#3iG=0PxyRI^1&c;y^5+YDp~ zM~xg5b#;XAbUwPS#$L;Q8U0dRqFFJ8t8MJO7o#RToElxRotbQ2SxYS+u}frhwASdF z>0bKz=>xL_CxN+#aGo1q8yj>PZFO2wB2IW6KCqqa>eWjS*ty|)u$^Y}xTF8mKWf%1 zH216K$hzky>6=%N(kLk^IG6T>AuM^*D%?u{_mZJ{Y+e#;`i}gzqf@;>oVa{CW0f-b ztpu$U|JrB8-@Z%QTpZo92Vc9QU4d|)z=`80&Li~HK#A2MggP7p1WopCOwQXuU<|Gd zzt zCzh-{khea=91@8wt?mr(d7(49>Yy|Ht-FW7ks@xEq9j5Jf4N&E(=@|JtB1E>Y65ww z)6ODK=IgWH?cLqgaaCQTVibq1jcv%-c zZ>@~kSsQ?GJr-A73I~A!?s2(xCt`mTCXZrmR1B(BgXi|3NkjVB z7FpGaIY~%x;ygQw>_ybRXW%M!zIg#6RJA?S=-J*TBq8GT$&m0Sf7`4c}hxQgQhnk3H`px0%c9qx;N* zpV`wvw4>446qd&?j}T2#LpEA&?7T33j8&&kOQ`22_e~lGCVWt^NYG5cebehp&&Su3 ze>Gr8HtKGcWxnX={-?5le@wCpF6!#Jed&AQ0{*36Hriu^Jd^tE7=B-9^(KD!a67c{ zV#ZzGU7Ue20)-|v0}kmoUY{4tEPqTeThF~dD+|Ba2*TnJGR5n6$bxY3))M!Y+T?z@ zOU`^{nVvEBRv$`zaIa0a(@o(Dhghp{hrzL%bse0WvPGs2$3q+(lPanJPJQwC`C-=d zn4R$516-`=um)X@otCKEjxfspBeI^4*3!aUUC*9;Bf2Q}KSoYvND-lI-#;W|v1CG+ z_@o%GA7io?iDCw$kSHWHd)9-kR69STUOz4vVM3`<)=?{vXDi{qd94O{t#8PwN&|pi z0@dh=vGP#^l+X+5Y$E~Ub%<)T&y#7PXUXS)T!-M|-Knyl;{;~}CEbqdi+n4f?4L(w zd~-YfSysCxv#bq%hUq3fzWM|;P*ZPIl~|#S*Z}b}YViZxGWFN#%|QD&^8RY4QYbPZ z8f=pn+imxg!B%P0kdw9OKDKbvQTCjA!!onZu-01h6-q{C)Oy>j*t8!))ih{sFjhj! z^OCKc^CKj@9JJpk2#boW0X@Q?zb5Se4c?9jw5Oe}eR+SDzWNxxS;=j75StbpzyC8Q z&zchqgJx}McQ;wjW+J1EfPX9BPl*1Lc;{$f>UA&8M{(h9PH-#nlQ8^_m})j{!7Qw&w}_rTg)xZJUQBo)kE3J|&k%f!^=o$w9F>!4aKH zW4ijpr~m1exh@fhz$WtL9cq$>s)MJdkJTgn0Z*h+)=v(ZBb`e@Y0HYGp@UMIBTddmk%N0wwwjb7_hS zDR_C99D5|FEcRv#BNF0y@P#C%)9D4UNwH76|LAp)%rYTfRxV_b;g^#9mhA3wiZ$*c z#>jNv)H$dmqlg$PaQ}7?l}$N?Hvo~9Ji=fJJ)~Ja9YNku*?k(;m@BEb=6kQBQ%Kx8 z@6DTn`p_roxvge(bw1bq$;%l|eanyKL2O0Lf|H`IW(Xx)&Z{ynPQe*qGHAhl{ah7p z<~CAE$<4yB^&A{8GVQ@&AV zlcfLvh~IX4&cBPJFgyP8>#F>fDY^G0t}y!>(B+ss9w@VGPAhE5M~IB2h*_FyK4rIQ zLBDJE%}`ujp9(Yk@#Rx&?a4299#@Eev?ee95qhH{Oj?Un7>acj$o>-ht#)d(#T?G5 zg~!?GnP`xisHURWVbg$@Ym!#wueilB121qrWP(;)u}Ys+})c+hKkI6T3LAL1qRt zfPnWL@ayqIIX3)9BhXlRFst;am>t)VO$gtRLX09`W*W6VpnF zu^L2!5%_#08t@^e&`A}5|WE|gekv|hyT$Hd~F&V{l_(7 zv1ad8`55|{NcU?JFdEKWQcJ!8MWjwRDA11FnZ!-MysRNjp(>r`ZbK&|z*!O>3am%X zvKTW?h%6UhAy*KxAttj+N8Whyh6oeZ6X*n#2@HFDY0o7 zyhjJ^+UW&x%A_>DHzP|g9@okqiK&@?#z?$81RzcKA~PR zNugEAyxOxJ6Pl5xs1Pe6Lq7Lx=AuEh+Y|{N(N^f&wwysl4&f|Mq@d?vsP31NH?JMS zl~K^(+%@BWv;cAXt|=G8ZF@*=#gQ>YQf{-F+v?w-I3N!3S9VPUCPTLGk1O@;@L?Pb zUjZ9OXhTznE%#pD4?G12;ousH?@p8Vj{^`U{@d~|&Vnip0@s;yNr;B*Y1lDwTiy(x zGe%kh+}7pZ`P7ARU1!*u>}&9waveKqKSlx}zvpn@^BV$U)!6zVm!R{Uvz_t<3ha@m zB+GQ{jSC%1w*U&A`urtA$Wl$ZwE`=|4$MNv-v_y@0W=t^xBlttn(7ep`@Z1Ne;#%@ z^+T&lXN<~`CX}NiNyq^tCDkjaZRbj|lk8V)hL+ie_2bY=OSv9SPUKb!#Z)Hv)j@DJ zZfz}%=Ip5|55?7Y1LoZP-Tl%D(BO&2zHxolXC96`0HI_u7En@(Tn7bqfbx&Jc>h}f zBX%)SZd{*JIukcx24>p%?4uZEy{vP{H@z74Z_rb-sCNZQVFQ%=>xngoEog(9;)A{) z?aW1`5JpG}g+@-Y7LobenXlGD+ut+rGj;R`i&7fCB|ww12VOFw`3s{Hlho{Uy=Ax+ z#OzLhgj=rUN1IKPpJI(y0_RHF@5h$4JLv^WwX@`|RW>4B&q(aS9Q;jDf;^+0IhC~T z-=GpF8iX_7+^ldp7}hcuVW<0k+}BepSGbzgA1jhq8Yz{{31+>(2K2nHBN648Dv70t z?#DwC*>uS}ZCZc(M|o~E<{D}3jSevfyx4Iil3JDiWYC~YGv`U>k%RaA#7$+3zfYa| z{7ntwwuO^o{~eq({w=UR;~=paXlGBTqd6Qax$@HK_e2o+1Qyi&7E&eGLqbhqixhX^H zd7ZWplGsvW$^k5Hq^Whbe*^s70v|-SRLnn z?HX>9hvb(ZqS}7(`>fK;MckWKSOe7dd}*}f#Xq>pWg7=|WrVrDV_Hz*DqcT-PosTa z_(uFBgI&!)AZp2;deHmSa=2AH!L+-9^XYGzS@b^y1)T4XW|J_HTA9Lg32af7C%IM~ zol-#yf&l`3urVlADyN#2bdAdBxHG-Lv~1?b6Pd%i-eK8RfBoxCZ%QDNgAR3ze;IW^ zVZj#uXPJ6?sI?w*n~o$zQf01aBdxL-(?a`RiHv#a@_DKBFNwq%2Ov|qx~h43;Tw7= zOmtg;#|&4h?_;8qQsEEl4Ka?Lfe81o1jtP)#*5k59yMRK;=3pZWxU@2akcZ@lT7?j`j&FtNC5 zWa6ObnZ!XUN-4kz!Qy7ErsaoB3L}>KL0!4NfXA>5DEz~?KO5h)6NzF3={qSmvAN_^ zYiIX1vIhlWaeD)v(bDIDU#foYCDHPcd3Y$a`IwNnynzj>a;x*h&k$Xq`k3!?-Y@q% zm-*=_8GBm+Mj9&g9CxiLh>Q?|G>FW*)vh<9%cS!zNjfGGq zVn+OHm(5~`4{h2Lgatfd1}9ND(p7?^%4}i9#~1)M+0!_vv0Kao>0|%6I<$1!kYl|a z9-At%$BX7)TK{?_oW_yVuDd|!_F23?&;M-v-eIxt*z6c5NZ8yNNAaP`gt>+W)5h6R zcIA$xp(#pZgeoSu@fpQ1SLn0?t{N5S5pp6*Qbp-CtAmIh}U$<73XdEJ=2gXev zhXs=a<1Y$&B9jE&}OrOh5Tn$dij z4X<_V?#m|e+u^almMijR@h*ck*6!DN!nr`)*(4zACtQdD+v%b?w$<{1QbTi3!k-)h z9%lpuL|wdY!)~ka9EeEoStb@7Kti5B4_HW~13jwgclrXm(;&pc^@1Vkwwe7!*cqIn zW|HIUmN_Fn3Zb72GwS%Q^%d|=Bhfo%})TO*Jr)G9k+9=!KcGhwS<`wL69gu4}H z)zBnlV>&t|rNG7SlL8jIT;9S>qqV@b`oD*jtYdGZzYh8G2@mj4IJ#7dLdFm-Iwa-@QLOzHE96ll{Z{T{;Fdah_60`32^sSP$`ZKN%yi7eUZlvUB5812y%5 z8e6EI7Fz5%cHR*YF-?g)qwD4N8|%t2X!y)BTu4YsP}qn{(tvjj~80s+I$UgwOH!SzE;}L z%44}*$|hyv8QJ_1DE-o1!JF#je_8P}^n;?|lvenodGdAgtP=3qz|Ll^=TJZC;0n8f z3A?W=sZ?YV~QFAYxB>H|6mO`vNZrwW}w@P?DRxCkVo`$kL{U^b2E>0}iHzg4c(pI{VK=TJ}4;B~&mHkc7H+*4CASrVUAZ zBZXCEDQ9-ISxu5bgXV+0y#Av9O-xx_n)i86(h(KTmX=f9Z-3J^Rma#Pd)1Qe#H(mR zkaUgSx%a>==0Pg?z>|QfLjT@L!6MQ^;FI&nxQ!HnW{}@c`7U9?W!6vT#eHhLYlju1 zeNKw;SbWeH>#dK;#!PUFk7y-oB&hkN=^*$GLebu^%pn@8D1Ek&qc1M>iB>!L+xSYe za0>$tV}&yef)Pm0r!6>W9lYqQ$yBLLrZYfHZ$5{7!)0Lz^I8hU zqwaD&_zt|dQVHDL{{|(<7EyVsb654^YpxeOba-a~?SD8#;KxtFHupcZ*TXx|Vd{BA z>3k6iQ-GBW3D{$u-&Mi*US`oRwMODIi=@BB_+R{AZ?A4nMOI`=Cr7p$KsAPXeOxDU zY((KdyQ!TV`KSPlA~(>x#eJXWNYwpxmv^G`ZpcCfJnoV?I?8%e*s*Y#1# z0umnoZQH3$LMjXpPUI87{#gX*N!WXfj~QN0J42H=en$t9z%K2k&n4T_MVh4T!gm)9|RoK5>7moHR&0&8F4i*ks|qA3O${}^vU8K z3_#_eyxRkAeC(RF_Jn*$x=E6?)b*)DPxw#4W(IOZ;8T$yrjBM~?m^~!9H~-z7P|8n zQYVkubEL^7m!K?yeR>ZM`rKvwKTLL>^{uY39!we5C$>c!b6?1Je=RJKF zbBL_t{;_XRR#g_OZ6q>S02o2Uf;a4dUZkVR>$ku6vjVs;yMVKy7~FNW zP_b36xsJIfNInrpp2K5b1L{W!({kpNEw}Sa!edPpuqy(cln_0E-Ec;%?74N5;=ZNvix*373os+F!&@KA2z1E=a%~t+odJDfQ%1*ICmSonEFU|h z@@|X%0t(snw@?4sdIxYam+f72Qj$vk#R5@CAX}bnQlM|Ddb?I|Fo$&zG5IJ;k75|u z?}&8M5J{$Hp2bv|;Zj(G7|1F~PrR@1u=5=g0s<{3P0!0|FWNuS=Hy&v`O+yW-hyCv zIZn`M7SE$=fitg>3l(Cu(~Sey&3?1NU5tQ^Wqwee;P5)^aijRm@BFz!U1TawIf<<6 zhuk1EMA%LskWcr~fjzAB#6);<@ihR}wm7(*Dj=v@rT;s9IX$-D|6RBz@)!Cm4kE&r z6e0t??F_S(!t3yu>xRtsC^-cimRYw#P0!bNQ(>qdCdxjV)}3 z4)rUla2}o_S@C>={>Cwf8^PXZxesVi4XU6K_#QAstj6FqgQg=1A$#NI7= z^eZuM#|yl$Nj7nh%0%s(3rXr%9x;2@<@aQu=QHae$9y8J6tS&OB=T~5*Xdn^(=j-5 zmov64tL4TXhD44N2S-pW7QuERXG9)SakK+;o|@}DoqzGH2SSh^ujBoQdQhAS2crkJ z-T_8JE$gfg-5WrfzP@Eh!r|fHOZ1LH4V3Bg>@Tc9@{!m`CD-=$d}VKHh$;8Q|Lm$N zy&s>RmUPT5crW3lpz0c13XJKATl(^mq8|@{Z3Ib2z4)ELnQx2r!t^bMuShw zO=Y^%-~;?4j`I)K{WWHJ)n-NMF}L2*g5-t+kAoJ=9E|0dC>Zw8Ml|n%C}@{=)g~i zO$)W3jipR-$1*??F3i6lhD44G&FC@pEh4dkdr{0;G0(KWbLnJ++|dotADrZ$Xsi z3#QLuu*Cz6{6B0k^Mk}&Rz|XLiSU-Ie&1(Z!Kh{2DWx+jh5Z6|cpJ;14Y@OlW`Aa_ z#f0c(qvZyouRCym6rG|r1oec7UlXMkw*1UQ$liK|0YA`1s}Enq`0fb54uDlz4Sf4+ zv*GAlo4C1{!?hiPr_zM@A~`7?p4c70MB~FP{2c1V1}X!B=$sBZi*f@##| z!AAD&5bEvyP*i1>lX;$J=i0l?1gMONMUJR6gp)E^Ymn-6-&K!WGM*I;>hayh>@xr-tY20?VxQ-a@r%c!=|?%Qf-A?ch>UR# zg4-aWj<_L$>^L^Rys=w{HZH%pbmPfjmRhHll#m&gC!oF4NN#)xcqL>)PrjG`-8 zI@vhW(dN9(Gx;()64W0}`}k&3cqzKeac)Rw!X=f00A8m!?h5q<7_CadmVuQO- zQ;>b?(d}H?%_IAczOAd5Vx?4`wz+PHi-lz8ciF+Rbv?+;>`r1RJT2)~84HWk$fL`q zkZsO4e_9L{xLO=HXZ`G|KbJ6kFnofZlE-!@S1a=AXJK_JFq*PHI;FrTMEp)8LkZXc zbG#f$7eJrFjCwCW>~zPN-*%kzc6JB(+h1tANm=yN$KW?yv1HfAAv$gRF{-D?0S_>@ zWlht6Lyf!ZOOBVtW=~i>O%@_W-72H*D7B1ZQ+0@TH1NXhO|VG*0o_COPh&J1$ZJ;z z6}xp~-XNaMERN#WQs~&FF(8>pA%lrp%_Vd97JB*Y^Rl0EOpR^Ywx~v=SywOV3r8IB}Ua%cqT*j_iU8#-hlaDB~9HT8nM{!ng+OwbKiJgox{F2^EtGC<0xi~#;{&i{O zhN_=6zW1_y_o~4Qz)007j*Dc3P|BBCOj*^a^tE-2>(&J*1d)+!gNmGgCXD(n}2q$aoSiCDnv@4cH5M{L*wHR z$Po{JPz=9$yWdhL9VPUg-KngsPNlMt?Pnwk_EWK({%UR$VtCB3h;HZ;TzB3%M+^lg~YvP;)}e=NmmccZa3vN8*Vb__8kW8T1+7FjE%O&s>X-Y(*oOskd+X z1ikKI`X=FmlOJb`dg)5WQ!Tm>M z9c_WBN3b`rg(%>aQvdktE3pRFXkM?|fFpYYXGdmiH6L)obbd-J`;YV`4hS~#F<~y3 ziIm721>^p(DKy$zmL>F5B(J}+2ybK|javvJeeG~`WbwV9p_1wQS5R`2KVDbt9``>; z4*s+ylt~rT^l!x)0Mq5H1tTHU9ov0Yu4+Zae*=UxItp;yEJ}aR-LYdpSLH7}mw{l7 z*?A&?W(NEi=W=e?HvMeQM#*1{@`+a8Dn7_{A*$e@p(V9sC@W+g+=y@n>kstO`-gU3 z(6;WMM_d={GZjLsq4*tZ3(f=RpqatWBL1B}mVvx$xs-tZ#%g_ltN8PYd@|(O2!X4&p z4Ta*i;v4{qa_z=mPS5=FH*2C44vl`9Q-TU7z*rndgvLpvjC4b^J;;$b@ zQt1lB2#07I0dw2|mlYdQ#>=Q`CL&KYX4h746vEGZ0T%OrPRk761`d$WklAXgDwx0h ztGpggTt;a~iVpS8FQp@c$w6`PT{7rJ5L#!IBABfeXD2cj{@s}CfA@y+xU)J7=XZGm zeK|@rcmXJB1T{Kvm9kqUv3*#DU;%Be1-g6O@6=asD+);kkN`?4VKCzX-{w5D;5_uW zCTyKNg_?pQpt^xN*^Z;Jp1z<6h4FeCTvAw&XfHh{6(UeoVqVG1l!?mz;4neCkk7rS z$iW4y>*Op8+mAOJ z(zVj45;sRach*4`egJIKp3ONoc6iz~U zAS#@Gd5{A0P{X);J|{!_i;0$gBx0Y}t=lNnzOFCz+(kMW+wvzjq%kN4rFMgwVKtDC z`CC;#192G^oHJRQ;q4zVtbByFM-&~z+tr-5N|-v^@h8dObjho?K@`^{Y-{is3cnWc zpqRnDEp~WTZYwc<0f9vv6@zw}L!^&Kz)4Yyuru{}19~xoYxu9ez5CUQvyg%=5DF0%@jIsSah*z9UN8u8&ai$hLM2)3 z%PJ2rMYP8T{^&SK;UBizz7|fmBp(l$G;F?z_!M;wr9*M2MtoNK>$)X_h&jNXFDX?R z8tA`)w*EY4W%fL;G=D$+_pGqfI048)7}IYhTkw2Y+ffJp2b>@67C|46Hw2{XdU$Re zNz{w&MM*`uqYv4%?8^T{4Qa{pKx>1WFhAP+t@&sk@2*O0l8 zsUUsc9YBY7lEdeEj|n^gVU$~29#NdKq)zPkqt1(E3{0kjMY3bD*D5I}uaT{b)0lfx z1sQqKVp+O>9siu|9cd*_ALMHH<)`j#a*%x!O0%F6%V!>CgXIeaU0qc&YUeubH73qE zm#^Rjng~Qy*bKzKn&tm$taS{cW4^tvUu(>Opl7I4Q%RRG2rYX)X;)MVJowqNaB%vc zkNEsuG51_}RTxOKXGRf{YDylq3k&*)s$gR;1&vw{**2Rt@;hsD)aj9!TkWf8YV(~% zggF>Y?o<$Sr&(Nsd01p8kJ^h6bbYtMoq_T7Q$xnnLJK5G3>73Zc?Kpu{|WWbLZby*|<$ z;^Vgn7FadbRaXT~@a~u&us6V1O0>^?p8S`kg%}jmaM)d%e*S0)!c7gHIWaC4Bkg8h zYp+hR1>YiinwJ&=G5s&% zXjA4WD**vMm5e4=e7_ZQLu=9Z+=<79oIPYlZG9D4ub$NX1m@>l$JgFZa8kXt+{rm< zVk!Y>J~~-cX*bve&b~HB_*t=4^z6v8rIzb^=n>w>Y5l+574^HgvrsxOf{EH}9@>-P zJ%miiTEx;@eLa93!`|zJ$@jWu@i-v3EKxyd(V%Ucb{;uQ`zmalbrDjG;(xd}qI1O8 zP<1&w;98~IwYVX&kV|?x-(vP6n6D8~k;+{0J}g$o&$?6F^%WyiRzTr-YG51whLh-Q zxNg5KCKXX?9ttZiq6^Bz{m2)M&`^)dz(`t7+li*^B~4AYGsgM~P>AKE< zBV0%Va&jsJ0> zSyi04yh!VY0AZ1m@KmGUkyIFlyxzIX`^|_z{iM!*!U@k!H@ssE62soe08SfDxM#q_ zJ=g02Bh%~4_#~kD?^Hn2n)(#R6QvvGU-07m2LmE|eI(ECz4$|R2{gowVXnlu7~!G7 z^Z6gqE0B2HqPb(mgiZf`B*iHIaRS5ux`Q%+m_e0ZJ7SKmcT?>aNc~Pw7>CdlozFTMLl?uo^|lbReu*M?tg#n8?amf*uAh@B z(NiM(^(D2}K8{5oBl2(ij^xeybX1X~I5n2)XeeqtIdQl(WNW9)7nu+*d*&_e*w_MM zYZ}aphicophm@q_jgF^lxf>QdU>@X&lrM;yf3-4&bf)R~9WTW!OZ;UV1YG$#1GSi! z=H2ZavdcMuJ3oM<%v3;1j*6fb(*i)!lKs5Wrs}zN#SN^X`@Jw?>1$!!w)@8ldJObq z^)rh{qyh02N5tK{f#2Z4Ja@D6CwYGfhfP(zH_j@{K)Ib5a{iGy0bZ}jjxL3v+eJ^s zWnXWWX@)D5(;c?O54>x1$|)enuN$o0zd6GeL+b&EZ=(fC#nRBVoO$ZCMiEjV)-{d5 zf*d)?Uh};Ne6)SoCZ_`D11Lm@IEYvn36kDNm!B04D~R$f3J`#VVS^)^(d$6Y=vY+x z+KEg}m4kG6Pe*f#p4RS_(@VC`t2lbzx$>)&BeA(mpJR%SR!Z-Nq1x#E*b863? zH@Muy;MEs7cZWlycU@f6Pzmej{2;z%lFrJ}v{cR#_)tf-*{1~-X5~z$VRO~X=dj!^ zyFcI{y02oMr?5|FJS;offGwbIEM_dXj5@gAcK2Dq;&pozSp{$4-jJ-@7-mKNW!o~y zw^nBWKozzr6m9|EF{Pd zoOh9z;ia{Kize(1f-JFSXF_FWnnoBJd(%g;=Dhay`t{~x7k7PySh+Pd!;VeLI;taF zISVN{I34&8kFT$HgV3#ARC9q+$vSPVuBy=p8gt%Hiv$U|U18Z_gf>?pl-=(~EvmZT zv2HkI;>l^2dYI%e?P5q?FHy(a{u%VD;raU1Hy6yFk*(ibMs`|6M6GeCd*x5C+``L@+ z9{luUtXFv*>DTESxm$i#A@iA{+P|F++rL&!L{bM;^j-l<(2s`!-ZmqdkOmY1tu#E7 z9Jth#tRcr1=gArz{T51SiNGEE3&m{4Fdq+EP{kO9$b$4oP}$$MPKx%t;8w&S9>bO} zV7QQcoDNxIK&GeA3Z#g8eD*O5RfK+$2I9A{aI#=R{1<2dOe$L~U`_NK6580MGUtby zaKF2*l{%&73uJ@{4N`$=`~^V;74w>&Zxpf&aEbssk1~hRf%749NpFWjP?1rz0yI8e z+P_oGr)cC#j=f#d|En%r9%Q7=Rzhm0FgxQxk}*8Ilr0UgsoE?SaB;Qb^gr4Glhbh; z6`RmApdgM{GN=ZCqbeFM4hhimyFXdbW=0jg+P^2VT$u@!I-Ou0b^6 zhDs{O>Bof-|Q|TBE`4KmF!&kkH)`b6d3I!%b2w+KoSL~5g z3HVIV{12K-N>f>06GY4Slm3>F@}}K@-8E^ROi*dWpQTkZ<)S~?1?28a?-VJl5HzNLLkrY3p+Yt=FWgxmze*bEzv zlvI;8KQd!N0ETDe{er8maiGMquyr-e^F$OsL*Dos6zjFooB|k1zAfUJH2h~nIxKnA zNPpEQS~$%m3qZ2@3t^sVajsJkK1hCL+5~^W`56+WVW2bxD!12Jo)ID{=9S|maqIpG z=#9VnIef`f$#5X<&q2WyS95a9xAuCpq@`V777}hT=zfRlT-px#i;f?|L>are$}oay zFn>gpbpSb1(fQicK(#k8Vr}e~h&TGeg)Fi2h%K)zjB$lF3OjCOw0LB+2hC0GTUptk zsg8|?;Mzc^jXIqW>T`fab+OU(8qS$Rj1?-e0P3PFrfm&IKox(b%H~Rl0$XR~ zWtdUjy`$L3PC+Hafg}2jL?VMOOrR{qA}h<{(V!Cfv4La6imY;o1dRPjIt3yduBA%G zVYyeHbu;vU<lkv|A-S@GQ~v{*+q0?Z>I}@U+39q9vJvQrW0c4t z_4dIo5>Amvmev}uP1>dUq(ML|Hu_R132RC4R$;`H3A)rpd#w=~k?N#a?L-!|MojQp za%N=1o35T9H&kbqvNlN0x&KGdz0b4VDOm3g2hwSDUDf8=+9{*xA}**-%Z*_h!oJa= z2OZWIcSW}R03+MwPQKNxP6vV1k;(b z{mrU5ZB!}xL2egA_PBVNKKmg#%je`K8U>OD{Mq)g(F!zAr%p%I_|XP_d_snX!k!UB z_sou~_0gws|7fO>Ws!80b-yaiMj;vj3k9GHD;iWtEJQPQ)PO#l?~MJ^{j_Q*0?fI` z!k_o)$$}-dCuoH)=W~2E&nt(?H1MOzk7~~<&W1O7^58#Q6Ws8mTs0Ov2v=-Z+tId=00a- zC`LRuGL}r@>~^qlZKKn*PCZLp4g)*?)ezHWCbU$29mDQ>ARn57KNf>8HTk%yshW#0 zj*goM21tgS5sA-72c)k%wQpcouB{z1N}Q89iv!)>i*N)3jCt5EG%**9x{_Ay8S~cq zATFlE6$u5_K=d)SPOJ3{o>gf;&Sa%Wgp>;wLUPcI-Z>v-(tx!4V>lIHVQZ^rdn{o= zKmg9w{{lro{M-82b#{5Dq-)^|&|oCi9A2?4FYD^?eRqlWt!;Z|=3pmT>v(7V4+>n? z!bUfg53}6J-=8u;px#m>MmZ#T{ZkazpQB$4l%fK9Vk(YOW9R_Xh>0O?fWm>b99=T6 zSR>$iXs-{yKQ}yY^@P_bk@~D*QjN<2G#iakfqP`tzIc5f)hqI6{yef*TFGs~N$tfr z&6ziJuV~2Y9s)2)e&+5gNJBfdz1Rxh-s{nJ!V#CH~a?IKZ;kn4W zkE5e%F7Rmf>D&4L^C(pIK2`tSrk9T6C|j>&kdCP&gn#jE=Qh+y9?F=#8{lF%RGb@! z2^)pwer8}(>$OjM8YaTU)Z}`1FyaW7G*g$rP_enVkHQEpr`9XYapZYD!Y8i4Lkg#+ zpR8-Ft}<3kcp_>qAp+nBw9swgeBU*2>%FCD(qd&tN5O<2-34DKQ)m@pCh8gA2dZ>* zMoRB!{=;~*qcN=+2E0wsyk9xwq^=KL9XkRa+C-mD-#>}7M3gLUt_0u=rIy(PG+@>h z*P=v#${s7%sIAY>;W*EdYh6-E)!oSsuy!Nt_N}ET6tbyDf79C=7@Mh;#6uc|?Z30e z`%@hIO5DDv`%%OPA!VCNYH4jlrk6^6YNx^wprBKj`tkG>1BUoT|KE z9#)pQq(D?u0F9D9m*rjhT|THQ3pHOyGg-z>4({!|E*J0~W|!wur<09Rr~yf*&EbI? z$+af;yb^dVM#V zlgZw}g>7(3MnQ}_k|Qa_w?Z3$UcAm&b=(S-%Hfiaq**+X;bSrR#iuMIXFq08zuZWS2H;#4v{WlH>=Tj-Q!p1l3fiXKCGwe?`Xw+F4zDntQDwMt|n zV4@nsfiYe~i2tMfop=qK<(|+WG5~YV<5+YlbxwsZv+e-UBN%uf7!@xT{D8|ofCw0D z-49Kn9@1elOFsQm=TwGJ8;2`8T?i%7w^ySm>O2fS&&YFt$y0aGSBw1d@zSj&0uYoLXMYsOgv4 z(c+_h=}Me5wbaqz>)@`Du{|~7#oPjrezn9Ras{(_w|+iAs0G^VV&9D{pg*j)=Vv*v zff>8s#l3VcAf>>3JbEqEeK;cn(2&L0=? zZ9Qg&p;T1dmrn8HH`n;2V)Tm+7{-{bbJSF~@gMuUeL0ZXTTVvPjQEr%ZPKojtm$fK%~ z=`H39;PS!myAVKGQ?v{%AX#w?fhc-0-H~5oEuVdDgH{TLxR~76*zG-+o7U>Ls2zVB;c9bK& zJZ_&Vm-FECa(8*S6Zp)+@by`UorK%a>&|l?WH6F-{0-3QfYc#h76GV|;a5Yeb002= z!YnZG-P$v=vOYefOcDQ-X3}!~-m*GCx9%p89t}+5B1-l!=l79ioIaX>s^qi_|1;e6 z$8&fa8#h#unhFd>Ei$Si>BX)q02I@f2Xp9k*YL3$3xjoA^`9W>ycRNU7NMEMGz4|e zPv>`>_O%;>b%aW2wt=`OuzeV{M+k%>S6Pn>OjyErYf{NN}-rq}lLGPwTlzU3rsk%T_sUhRlEf8@AMT z<%hSBSI^nU;rUge4L?hP<3VYlcbL~|LoR@Ybm3N=3-@~XNh+BHR|INh#p7wDAsDacMc^BqGsA_K&*Pm@f!WFOo_?1N zDy^C8D3?z9e+s{t3`IqS$=3#kq7(kLH$}9(?mvSOxUa$gzLov=+X=>2Z^_GETNMVM~GOz_WA%XYZ!!eWn-}hn>E_?P{!LbH%AUWcAy{$HDlMP+f71(Dq>V zi&o3TOM5ePp|_ldKbr{ncEPy)11vnzR|Fm;tJr(>+N_0&mmDb{8d z1M#?5-;25JyVPF17zK#0DUZUK1S0T#MbAQEViB;ZHbUezwxV{CvsK47d!yUOzlH|FMq33<71bihs()r?2nH$TkG@Kx3^zGXjN-<-(NoV z69&KpcTb!eQkvNA1`-QWFZo~LCe~m(h;Jp@VaWl3mjyV?X*n<*aFA_G;(gq~`n|IP zB|h~&K`ig5pJG5Job+Yc5v50x>i+sIbMiUSN~;|SDV;Kz9dVTXmb=hi)%$>21aN_X z`c$R2h04bejQ`Z&IJHbxXFi*YyFy#SJy@*qU?btRsoBV!opow+o$Um@bQKkDRI^+_ z+Altgfi#-a<#em6S7TW9zu^{KhuC$!+VzfT#6bvDWVZdxW%1uu(D$0ljxHd(HnZc6ZQ3)Hy|~fc zA#pNPQj1N!cpa{M{BnmK*_>!J}I{E=<^WLEQA+s1%FDi_oD=pGWj&9qr-2x>TAPaLUkP-Dq00)Sp`Y-$m z?4loPvS~*sBJ!0=`grwW%QK-rOt3EEqWXlZ@hEBboQ@Ye7PyC9iX`JA4`E?59U#;T zf95LjQI=c|XK{V))o;9QA>Ub`{7obj-#i*{q?w!4q$#~MW#iesM8_@QVrXvZ6Zv(( z&u8>c+8NuJ*ve2D;B8zma#3bYbF&sU24YJ@SFs@>>7uvz|!o)MDO>~xdOcgG`D($yF{8H}Zbp>n-F#;*hv@;&o7dNi;HbGibzZ);q1!Bh= z9v>2ogtqMbdl8JDpD1$IALl-{#o;e}PKJKJ1ZFd48;j8tzBYUYG!u0CQWVu`u=WoyLyI;bFBj)VR<6K0K zuV2nBu0pn53K(q&hk_NW6b{(#26@8eI6{`HE|mfUQG1ocE^JMX z&|S~c0qk#sH|~Qw%&%VscxXa0YF`Y*!3e38H#{rji-tZ~zA*~Yg;2aIZ`p3(DHU=s z1d~L|X%qwTM_z!*Di5v^0x*Z;J*&+c;Bs`}^E-u`;aKNp#dJU6mvoP!E~icTBpaM3 zw~3l#k}@>0-4*=SR1aBN5j>Z<2>qi;MECO+tJ36qqNpnUKqg zrA(Nhn$+Loj@?C%xwUO4+S%j%KqXqHJkrN=I1wAZdB9twS}WGXPbJCb{)xy7!~XvQ zgkzkr)c{cPx4zunJaVmf#ZRzr~x+yE`?|=YiX^mMJU!K{g#oF9MuOAaJn> zK;nQ*Yatj%GMT2i8nkP)(Lz-Y{^(IM*!|Ns)hd!|5#8aaHx*No`hBECh68;%&JwCb zZu-|~utBoLdAAKCGQbgE34G$SV&PJ zeXGd^26!QbqXepZ2bR~tEvpk98}33`2?G@fz_NaNF);53X%^^*p3Hg0mV@)Y=jO}n zQ5TPwz=K6dwRP@wcQw!*sncan1yo)2u}gW3sUIHO9Xv@WP0ualUuvh*L&)~Woe0w|-Q}HL0tA!7ENAmMV2jz!Vm`fr0 zJ##K@FPk;2^RR8p}|u)*Dh&losXw3pR_t9EpAuAyXposHf6R$ajD~p+@F~ zlk&TWvsX$9!`RfP1Y7P_wlG;*X>c3)H|GPbu~%~t{FFBCMTm+mQIZN`v_MmF;EDp} z%-=m+*`$5pOE@*|d*}hL9)FD0Hbr78aDJ>$40W>W_;RQC0-HCSr7z20eF~ty3<9C} z=SAR|X0rps&d1xO#xdbDA`BH4?Z_!EmB`b}iFZF&Y$?+tSrIjO=s!Xf+4H)bAkcIL z^t|G0`dmpedIh|@a5GOH)Z}Tb0>fOKwAqvN+9Uq6F5%-91LE5At+_@L;9F@iI8p?k zLNF7yMK5mkcS*>{bBA4Owz_k7eSy>Ky|(^AU{Dd1z(wG&C5qecN#N3WM4qcBPwj@J z`j7W(%D{7)#_Jxy9`SmA!FWIL0%Ib@JE7qqg!6iKM=bAiH`nTowp+xq=A=~mS!XM{ zS@C?C(*N1q7O(8>Dy=RZ+E>aI`*;g~v32aQ6ZdJ>10_G8iV2U+i;JSX(aMjjpgt3I z)pv~@$WQ)4=fT8xi-YMGQ`F~`l(VAh;5Jl}Qkjbz)6r1aQ|;HeFZhPo!c%{gPu;tJ zjneJ&@^{?q6%RcY5jUf^e0f3t&b}Ph8PyVK(D8?O-zPkD?ZvX%maAQX-;4w!%6wG( z{kOrn0eR6zSfdN2R1h4Sw}}{aA-uleloYPA381yR)XT6>ca+d%Vv12OW3?npd_cIx zUwH>oYv8H+`i&2@ld0*)^ZnSxDHEWKU+4cV_5B=KZ7dC9!pZXc*uxF*>5Mh}<+Odo zQUUiSeD&)aYcHcrXwL;q&x1KD(EywGjcxKLRr4(l40wgvqa&gXH7nubN1+kOivcD1 zR9Zx!CI=!6bJGCbeiPNJYPVL&aN8@8zrQ5gu>D)-VS7SNERyVpXwZo9P!nilibRVk z^L{fh3aB^{`Ixx-uTbgchK>a&yZLb>jtazIG!21Gd?CR9J3y0>YU7r@IR$+lHx*NX z12S4wme7F4w}(7)fwp}g?(!u52!#=1k|z2!ju@q~@8};<)aHHop$kCuf_L$e@P;;V z&nwG`1Ic{fUwN8Cb-w~G6cD6(1<3!JU%+UPfkd4G)`&7`%IG_u=y!xT$Xt9_L_930lKe{d{ z^UBLvq5oo@@9ZSKfq;a7kdYKqSDF1MP5wRG)D{>Z(vN|+6}(Qj{n1yysswlzkw_wX z*Z*EHwW-C~m$E4h1BMk~F1gXk-`CS}8O|?7fssIG%G|;f)(ffSU{RC3P7_v#2d3M& z+Q1&k=H`*VI!`_wv5cR-3??u~+U?nBO&Rpzs8ZF|Mrx|A<-_@7$`Sf@@ci|n31+nm z>!%@t#)B^!H{3<_wy1gU(2|fC1&7`Ct@|UA{0|W2yQ~ zQdW}l>k3-){T=~qkw^Hg36bZEnOmPiGj&WksEX1bj_SMkI-MXHH-0>ge2e{+7M869 zy_#*J!~(O8t((VJ8O?=A;RSP%A7PJORiI!YMq>l1TNe{H1QEm{u!T&@;cyTb z!N`uj(d6qqPCCHL}ZiiQ5=$pr?6Rj=nnitf4{gn(Jn`kSqZS%DdCn zwNrl8a+I>*&`L@3B+Y^d*1F zAOyR-zKT8T3+-qkTF98nNx_M`HVdx|d7%?Ni9Zm5t@R3ft7Y|{J4y|pZ+u+MAEL>v?np0q@V7r-!Ha7%x%mnno3ncH^pw^Z*f6tb zsY4kf$b?+~l^>J)^3AMZMUO}S4PIFz{wqUGaVl^mxVHzH%{D;g3(WU{NkzcPv1_+& z(30wG-^|^W$zq}csZ*8{rO7&rQF?@@JbKNOfRB_j(9I_7yHScN^S67Uf^qs! z8$xJD^eD?Ln`9;=8C&Nl`jIRK_d=(NtlgLHx1)y%!_(}7E{qW2LR8# zPQwa1sTpproSz2|TZCCwyB%$Dz9HhXi{~3l9{vP~uTXlzAWQoSww`IdCL-nPH z^~pM#Gqb`v)Sqc*$YqS#C(eMPpI(k2uG51AqGso{1TMj+zb*k+!+5+N-7ep-Af;hz zbU{!bQm|NLWPIEv|9B%HGZK|~T?c6(uzQF?8?BnXfWDXkoGgf_F9s5U|DgD>XAq_~ zQRYD%(LDNHQ5~L|fVE@49yq2=Bt*T|%B3t*P5_y@G%dK)Ey1&dsP?$xunVq#WGRz$oxLD~2 z1iw;NmISko8mdU>OaUTbT`pCKu}s>-;d@e54Kf}31hwc{b_Wh*QCXHov(HIN(Z!I6S-g0y`g^6vL90Gr(!4Y>t~@d3D88wFB){Q(CzJ4lfM=@_#M> zLkQr6pZN>>1tWC-C~LPP#7AY94viZnH)ChWL}B-gO$N;1eZ-8gI+|lhrvR89&}_s^ z65?Z^hJRxFO8mzddw*1AvJDwC16(|TPphQ%^NhZyve}#xna|6SFAm3*IXcajTfglW z4z6}wo(}CV3LBduOWV2I1)eWkJHtXu=FRz8GjLq1y5K*k6<%m16w^aLp5`N$TECgt z?|Ca9JDuBq3hn`?rPED8>3DLGbvq!U+k^JUmFBKS+Rj^hB8=%p4aR8*mu*KT8Gcll zv&Ax|t0k$06%806S!YL#3y4}d2>?7nPA|^Qdw!Ls-P8jurjhLzFBU4dQj`zcq6!#7 zfgc}DGuhioCc%hPXiaioIA>BtzD;aABYA@zAD_k%QRe{wt8F|hdS@UJh$UZN?vHyh z^xay~#)qG;?ai)0o~YEV*)KgQb`u1t2vnRaJy=zf2`?H*&)L1J)GWsdgZM|r^kO1O zD3*T5J=7M*gZmR6mwQzXmQ#`bo7()+>f*%n0KDvqLfdcZa!J%OUoq)vskV%BM?V2& z@bDiHdu;>b5Rb1Egu3n8syJ4}XN!hQZO*T>!Ptl<4wYGFYhT^M*}j~QWRP9pH~v)C zE-CCEypB0tTA~OmR0GVnG2-w%4j4QX3JZWE4>-|{<4HqNRseN4qAJuNTG*XUslVpx z%sJ5}Y8jD%?}H}#-H$k=sWMv<7fWOF@oEK;`M~E>4DE^St{G#DrMRg+^*lisDTXc| z=@XjNO$1F;(>RF>$?RNvQ==VlK>f z6Vke*0m(!6>jaNKLufPx>w7I_RgcfM4G+OP+hK^Sx6`UIlr4w)& zR)GQNGfIJUUC;&qNGN2`oT@)Rde9z-t^*Y)fR|PjfG`V)0^S;Iv{LNbMA{9WuiolG$^nw&w^5i z*C18G=iQSb@W&%$TYXSp-<7w?b!VkKUN`bXSLXkqR!UR9H3o~ofOD||p9RbLCQlF& zLubM$FaHi6UIlW`?$S~K$Pu%e

V+hXZK10wT3Qy55jO^XMgW^Vw`Ed)>yU9{&pf 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-02.png b/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-02.png new file mode 100644 index 0000000000000000000000000000000000000000..27f6e127179cbceb02da41761f7489f10ed086a2 GIT binary patch literal 166992 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 z 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%0DR}mUF@EC3cQ9MZZv6%Yf{n;z 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 UHDXZBiVbUz!t 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

Nu=l< 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 U(Pz}jp#T5?07*qoM6N<$f)@CT2mk;8 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-06.png b/docs/en/docs/img/async/concurrent-burgers/concurrent-burgers-06.png new file mode 100644 index 0000000000000000000000000000000000000000..4bbf247c0f3b51a7e0e4edbd454146442785aaff GIT binary patch literal 164838 zcmc$_Wl&sQ&^0=^OCY#~V1eKsJV0;@?(PJa;1WEzy95aC7F+^>;1FbR8{FOHp5b}l zTlfC>et%V|VWu*3PVc?DS9h;oCtOKE5(AYO6$AodNK1(;gFx`WTUbqGMBvW@b|MuB z1_Y88|ETJod9d`=opk#0St)&F;rLhyWXgb8QPDR^ z>F}ZvyR#R@-${8N)iNDg|2+2~r?PT$+me|r6Wq@Bsx(g) zHhmj}Es9JDyo522;2grdyoJ8xL8M_t{`b=kA~npL|NWv1Ts-K1euGTe0wWFj-)~_r z`BQ`b_gi6hA~>M`u8d46&hrKZ1ii`2l}TdZ{r~p@6Bttew{O#EiS@G3{*}bnq z@#u@!iv-J>D;!CNpl)-szr%O1h%u!8@gpU?3oClHW|wt95d6*(nnX+xW#Aj3%Eyk- zz}r|qafI7;PfoI`(vWN6fOwqQ4qUrta^?7i;|}O{?XbS=_C$lb55r$>c`knPuQ(qa z&G4G`R1NKZee`Zat<*uA%6QKd$g?H-Is8t0vWwHdFUEiFBuCHd%?A`jEcEthqf!yC zLqyh#kZsO!*OPo4l%%9;lV38lx}L90%`$JU7nIHL+OBI*&+adSGpYiE+%GkK+FOe3 ziq9XOR628@hxJzUDIn}UtuS6=)*e5CZ%WTJ7r@Q~*DQQ=l=lTe}dyn*s#2W(z zlz~C&CMEwFJhG3}-2R5C;d1|I2`k4Nb2DkrrVN1;^Y5^KfCsBNwW(R-)5AK>I|g-_ zvs3prrDM!zO?iwji{;;ZKp54U(~`JqD2C~K&Ycuj2f;kc0XCIEncPq4I*{wK`~8qR zUu)}h9FXhfNMAvTFm$uRP+m6cl?ScEI|}s*HJbrFSL@^jYnAT1;Sz^=YP^F+^GWq2 z&yM(?n<-0AJA){raQt!^&hP}NXr7}^jyQpgT!p_7O zqZni+D?-==Sgx*nHm!PUuQ->7j6Z*@8$pFTn`-!dij>~Lqh#|mmQzvss!dxLSnYLWhy29AkIWhQDJ!`ZJj1kt@9~d!@Yh2T)yE*nNCmzxB4YzhDP_WWcsYa29@fz}9EGXGn zrcfHYcke*eRm^$qeRR}b`nJ1(zKhE$-HHbz34OK=wzh)67ih`-WdyMpMW))}O^-L6;K{TOHUg$Y&TI*Qh&k*`cZJO1{e)Hpkkp zop*3dINTw3%|R+EXg1)UziSh7B`=6TxxrHl0OQ}&`?Yff`R>k7fx~hQ9c=}w2$fMV zg+{;gs-{~W9n8)e@jHLVCrmZE6J#L?I;TgIgrKg2?Z1t6sH(w5ZSFEztRy5TjocA; zWb^$jiEJ;`c5OFkP)8VS`GJEjn_O~_NjBw@H8oYZ7trFF#K=1V@4up`6hfs7C%U9` zU1hnWkqbj)6`MHQxbJsTQu+pY2O|oR7GMbGY*ShQ#=SrXuiTW`!od}YWRYM7=$zRJKGn~Z7^9Q#Yoht8u zk=8T!786bJZV+J^#w*MDPW!y|?IVu!=QJN-{tA~LwH}M8Q~{6>f`xHti>=VE^VoUf z9+)D}`JBdy_=JxUAFnF*DMmvLnRD5G9F(+Z`S3x9^Rm6Du&ar!8-fq4M_lzK7m1q^ z7BaNjFLI9_`OjH=^i03wW5Q$R)kr$-e6&+h()o;%n|Cp4%fj0aD19*jQ94s& zy=zJa>dHH8iEL~0$nCQn!cy}5|~1%6$hOx@0(Qy6K`C#S;pSDffmN}wDqot z1fQ-`hqe%QG&%B9OELV@qi7F13RRQbik-#+uDvcwLq7uFiBC}k2!<7eL~yWBPD1K&-(U~>BS|qJ~WYBwf)kZLie^IJF!qu;kZNV zR5!cOr}#1p-;*52LZR}t z!`WS#845x$Ly2R75*Kp4*2g+d8frOQw_}Lq)FPn(c&aP;u!0Wc{j@65+CuBjwD}C( zembiE*?!%5)fxL5krkneoH7`|^!=-t*Tr4Ga+MG+O6d18Z+O4WfL^C`ag3Ew2Ua2> zjS-g`8_w@CG>>bW@J0O8g6&epc!=ieJ7DBuR=Tbz`OI;T5ZtZzKmzwQs_+m20#Q3z zP8W$l^-pf4;mx$Z^4*#mHWd`dD{^ zdJc7YE?T-)xsv2oD)_8a@@Y1S(?~P(Y0wZUh^sZEpW2tFS~5bw^&#H_w`4InFQ2&Rak5;!I;qYP&Ky!qe> z;thZoIo}8jVJORS3)u@i*(G4O&ELl+1fb1ASaGem7qkUv3D+0wp26-yf~|Jfa*({0NK#|H_Gu7W z;#3>YNFXCN6mwv;&m$gNauP%*9Uo64$0OBH>)u9NoBoxXyE6+B(1sCl7(c@5%KLe8 z)oT4-Sm~p4&a%UVhx`H(w~P#`ubt2PM|e3Et`!@i6EA^o10JkeT-+(asy5Z6N1o&( z@N|zx#M+}`^io%Kb6;T)6T(HIcvvCVW2a3s<4dAv6JvljF5(i$x;qJI_n4etGBiLe zorQeo+gHjk$JsvBjuA4jgc2&(PtF#>$1XXki#n{SV__Z3oR+h+3sF1vY zlJ3yiokUMa>GyyZJ}KG2jo#IMmH4&zb2V6?Y-7;MC1H?aW2vZ_l0{-DOUFSY zW+=`PfEg9SU5ETOfKz1^_AR$gCqU+;!AnPa8hO7%7QM<4N~AIk#!F(glbghZTchsn zp&=;V3-~iMbp%2|Su3Me5MNpTp+l#TR?xTZ)>Hgyt?V5%Xea2c%x}di4sxi0PcE^K zNJbOj(hP5t9w8;2lb;2}Y4=t|Vs2YCRr&kXKYk3XRClvFiSLu0=($#(*v!_*a_zM4 z2<)uzT(mv6!z{Q3jgBuI%5z)VuCu(73j?5aZdy=lqe}0X(fsONs!7$Z!rBV@^@$<-}ma>7M-l?{E7Z&K|x$r9Zi$wh5-5&{Px9LD_T}VHYdoxVUw-bfeR(PkfHzA+wdc}2Kqzj-clN8 z{ht{IL1+lq&m}YI=4h8I$<$Rdw9eHF7%SD{=2FR+W(Kw z*o8736%Ir~Gfim4T;7DKa#6=((M)XfJvcAlUoQZBGR)1KkQU)nLt+wGMeFcK>3m>6 zt_T>TTpi8Pk(+%IA}t@DFRAcW&^bO&&~)Ov=dC%i6@hKvNXm!1g3PgJX?_AA-);#9Bw$S4ih&A6%M$jzs*|yL|D%6dNVqr!&*Wa~3A)SeZIl!tuQlCEveT(pv-6+hMr5`?Ms# z@J~;-Tea#h-Yk4gQr6n+-bXVeq%HJl+ujGk!*=)YFZnVsEFJJG4Q!)b8`W4AQ1cV9 zvj*B!5&j9@MmL{bWjt_8vyCpuF;g(HdZe83L=B8ak_-GM!el@7Wz7U^S4elU>9w1X zmc{E&9YH0Nk0f@Z4V;40b+g6~ICt|ZH)5k7 zyo*)^qv5T^$aw;YdxnZC1sO|(dn1E9WE1MDq%=Z9A`~(VghtvU6uJp{>PWC*Py}!QyM8o{xOT_>wE(${4hR2$*^+wrjklYF{{WLhfVhlmTOnZm-%;3xwo({LaTP)~kc6 zRRfkvvr(Olcl2dqV1B#C&I&o)y}d{88cPaUjVKB%K@Vmo6|=ivjz)KRa^UPZPIEM7}S zN-6hQi4E4T!YD{|y)?Q@fuvm*z){kz81zsK;-70@Cg*DTVf?M7J!y{|c;Iv3Z;<2ez%@8egs16l7hK8t>;TAG@UE+IZ%6!@S}gxkaVj)kSDx;pmZ!Mn7q%&~Pz zk_3@FX1bKk+!Tx4XGhn3jMRUGHVT$sxqWRr{JYvih4BZPfEIOYm~UDWgsW29l@|kx z$=um;Hr}Bzb?v$iJ!knj_H(r~->F`$PeqXn-tBDXv~zQPr~1I=J87tY=;o`@l1kQ5 z3#>82e67Ao*EhqN+ky@=)|v_cw0>$7(8M7bbMe}QAEVjmLI&V*q}Sf^lTtD=#^bpk zGd;G_GJUS*UDS4Cz2n1M zXT%SUy_T3N0JKK}c(>E|;2StbaJ0!2!)&@#_#2XZzi{YwcYQqei9oNn0jxhXTVn#8 z$8Pm0QOL*7pI@DCk8S@UFfd(eac$DAG+}-p7E^bcl|grQ!eSm5N6ue2TvPgPbv*~~ zy~hu0xIZu&8Kp3wbuN-goVzoTnY3ZKolyDxHZ>B!XZ55RX+YkEqYGRMH)3-*ZnuMh z=8uQz!ISG==SnkLnOVFbFFIe0S3tNo1DNk3JI$s4^f31GVLPcIF?pKeT(@;;G?mSy z>2@ol`KV?8??{yQq~NcGk-Se4?svOITGfUy%qeBl&bHiWDSEt-MHHPuzj0;UoJZN^mY$L`@);-_+ zy(^RXBS-n^VwzA|j;TUa$)-4vlK7CML1A_l{YUIaK0D4^EVEw24NvN2Nw26R9PST) zIS(=YicLzw>;4?F?bdp`k)Y^)F(Ikd>SkN!0}CV(u-=F7MNi%+CG`K$8F1PPM>Du| zJj%b&FX)s{2;yVx*eGs3|5V%ioO@p4KdC-(wYG2W zjO=|UO;6%jzdv!fVl=0{N@;ikj8phd4HExlL`a+C(~v?Td_v!Pl{~3U(_UzGakP|? zVb>DgdfbuG;d3wHmTRk|tvz_`_w1$bduvu&TKYTNdBTx>QdP8qv+ z(;&&lqr-^|Az+@H1+Cf!zp(FmQ)AA;$xT$hNDKeP0bRD>yT}a?I$3mvfx|84c#w?Y zal3i$jGSzugP{MbaGW410P;yH_`|Xh>O?mzOdu4xp6@5_4;CjqPjX^oyOK2Y2kwtM z$Eaci{?P~e`5zp7A@@39%l6!(M?@#Yu6=w{Ope6T(Gt*~tts zC&%OEa*8|s89tS3-3N8l(7MQR5J-0|0zIrAM@g=W_v727^yL^{(S=s%q`a~H6n>c+ z`X0e3xU_~fE=PXkn4y6r_HA(<=UZ%6{|c%hu3oSY5QFy~Cd)JLBCGFf^Vpaxkrc06 zg1+AA4s8zfNH_9E-wWWro7}%unFH&7-LU2MlI8QQ2HG}y)TSHhQ;S8d8_I=tGfCMe z67b!#fHU9mb$ftTxQAxJh5-=0{5y-UqOptKVY9je_C1_k8z_I?#q^3^va`54m>bUG zPiZ}Bi3kcBZ9u$t?~5Yo*RgN^6G_DJn--?DRB-XRl|{2=%i*;Ixcvj?`#PNS)X95> zt&5KIa~!02!w*kR=s6|l%{D9=8=a&3Tsre@Be~dbxd~H;zaDPZK7emDHnnvnCe^FB zM}GG<=|5Cms|jRs$eKeZH1+R+fM7lh9CJGz&%syL!t2Q7qoE1TdAcZS141>DgopWe zzKyVlhsVQp=d;;hBE3-x94UaN9ig4M#RZp3aN_;a)w9i<9`BAX+M@}as|?kRmmP6o zXsgFG29|;;&O>YeO!2%&!x!ykPFaJ*)zU1wm@?ZeB7DRUOJKz8$l}h%x?B4DNjlJM z2D(+P2>{U5&e$?+EQfA|Vt8`N=i9Gz2;3jk37q|^%>Z_Ak<($%+8iuapI%SeBv6 z!TC5ar|ww9?5|0BA)9e8vcAVAo?X*gNHxb+jrGILslemKq|q-%ba-_AoYvrL;U%Kr zxbQ7yMukd6q7kG=yVXC-{Z+^(sWAHG^Icz2m-Bg*2)g4?Ddz*D2Qot>{Bl~ZR=F~8 zY8=y}WD`Q!tBzfPM2P7{HwPmQ>(5AMLQ&SGq9+3T1_NJwa-W{;S~?`Gc^HNuQu#3I(%(aCo3-SV*)9Lu);z=v=+ zsp{Dt{Kq@_z-ybYPLpw8d2x_ehLw>gZl{QRef1wce8uzqD`c=aA>oJ5c{itWYZBuO zc%HQczjK~|GLT!B0sz2DIUh)A^|<=O1<)c4B)g{SQAv4~G6-=_Zz+mZ+Mmr9)y? z)`*It;GaZ#l|10~GIDYoihg&v`4Um69>d2xH^-Tm|FElZWvr#(hxfh6>@S)_h7v*( ztQ`@#U?D5~@RDaJ_*xP|8;mNRSG@H9W-e>62~}Do zNA&-S?_4$FUoMK-|LNGpk`2$oDkDAT~9X@ND)+ z&vYA`z0GS0n)$Z_>#Okvlx@q~8Q%Y%&5s6C<@~%DFh{;4?i6-MsF9=|&yyVf_^vtb zI-l7KA49adygqho*L_clGG(0iL{OlextPu!9dp0bOSYRZUr$pE^n4S`P#BDCAVG7# zJRAXfj-Su?5y3;ut&u?HXqDx9u!$(EZ21lx5YNW_)jfv3w=LW>*Ydk4x%yz0I(DuO zEXAv~)M>cm)m{A*2mOC<{#Bgb$Y&WOdI$8Qi+iQMMMceKv7D6T7~M$y`F3farl#lc zB>6N-tcslJ7OW)yG#Yl?^XzHx$IpqyB#i3nYZg!tylRCS1z)AHT|<}rS(9|0)b?U$ z0vgRm7QgwwS*34mZ`Zl(DmU6Lg!JRKH_QVi*TYGuFf~wXZPe&_4c;10bi9j{$&CDB z1G^%f(bfNGXKb~62xNAmDxz2hf4c$?hn)MYMQ)!d0c|PeH&T#`dR`jZ6W>Dx<`KWB zPnSriX?$)Yzkvoa=9)K$X2 zu-X+C*|U9qdQdm!G{OaTxZO-~UiR2#bKO%tYThP?mVR%rZ|CNTILsa9t%`z4zj{49 z9FNEwWzc*d9s1tqV1fU<<3X8o*%};tiIgWu{R)N-=Yrzc1Ut$^133(GmI_18bf3O} zwrtW_1+Xk0SDe0aT!Ad-c95gU$$~{F7#2~YfL@)q>1P#X>&TlW@$WQETW==o*sawL zF-QF66(GmrS$3xqemafOYIdU8+}wQq`gOHY4`R0WnRLf_*73>RewCytU>6D5?}c4< zlmJnM{=wDX;kLG$^@!mN9;_-J7rk~P!h^-;1OO6Zfcp89slZ5<-_z~J=4LP{OI7*y z$k(4lhKxgvwBwV>uMdrS%M-Fc9E(Hv=3>$nBeb3J-_IT~LH<2}Q+4XHY8(aK+xl-A zi5&PlM}-oeuExzYfC@_70w`%-okJVUB(?7SgDeHQ=WD-b$xh$Lx9lNjXHNR=YY6C& z+0BdK;NXYT7{7wbO6Io^>(>+%V?g~JF>Q^(SGiA1F}m_e%fm;!pGw@qk)30kjEqzA z?M;k1V8czRoiGonM+SMLIFNw92Yaln10DH5BdxfXC15}=8&HY%*8i*Z?9gq)>Osza zqTnz`=UMS~?(O?*U@+F<({EpFkxsimQvq4+4tzzZRz1YhnYpns?ldKM&!Bm784wmD z8D?o6b9NVvv%6{#53=3n!N@Mr?E$*)hgR^W8gw(xB!vAa^BSFB;1koDuqE`PF!E#J=Wbu%Qf!Dl+TRFUVWM=HML#Kgqd zG}ku=HHnF@GZieTL-5w)Wtk~?crN69tvtwv)HfxK#6`sw7>6@+EXP;K!3n7TkbPTO z+PU?xD^`G=2~z>6-B)#(WR>R6-N4?swPu}<2@9{Ae2DWGVq#*-8^TQ|VKoFL(8{BJ zI6B{D)F!^|fEN{)DNh{x+Kc`et`7e~=k3QsErgOfdbo>^!fb4DR+ELF%mmirLFTE! zmuYDeOcd=0wL_wf5}fk0we9dRJmZ6PL1B7yBvWD=7UPPpeb1(qEJIMzE$m1P2lUpw zVnsIyOwe}Jp;4tDNyzq=0b(VK=U-@LYGxL!M~qF6OQ%2$P2o2I4-D2e3p0O}!Ze*~ zZT1weZj0XHbT_Gjpq4GA010?_z9pa9GDL5ZQzC|3-q_e!wGU{={}rBl0x;W6^`Q|4 z4ly<%0oQ3!Z9Bg_PY9I?C(ooo@`OAb)n96)R~7*(Fq-gzI&OL3?es?-uofdeP=pdR zoC4(*={0G@A5nF6b$E%%mW>V5+PXTiXz=u{DII8HQ#}92@__ZR)3eEv_{3| z9qp|*A70YYi!)B-OG;XaTi<@v5pRVBz2V{EF(vwDlujwELTRie3BRxRBQE>@vluxB z2FK6)XNY9ZyrdV03p_GZdGgnd3x~G)4C8VCCMU&+Un9aICaGnsXH$xQ+5O9PU$ZE6 zoR~l`VPgQ5XN9>z5g@_aH_P7+o=jFo=DW3r4-o#qm|IB#aW2%v_n zPeD(GICon4^!Fds0960A9O;{*h5xI(rI>{*(&mh{HZaM7G|2xu? zI{DL!j zcFJNf+l0TrmEknw&WI2!wHB#vyg(wrKF}$WF$9|6&lX2sdd8m=s7w5acrCxa<~X5Vl!FXu0yR&jKK28nuAwzOsiaZBa$ zR%)U2r%#_$`=|0Hu{^JjmiBo0#9(t98hwQ4{yY5xZzW_H1Ae{iB1| zN)c6{FUk{t$k6TwcBkc?;*TgW37&LcuIIEJ{ z-~NogvuK0|R1vQr8%u>@TEM*G8v_V700bC3L4wWDMZxRvCWFTw^WI%SK_N);wKb5% z5>rw_Nymnz09~KWCCDKIMh4cEFcqG7ro*~;D^y*+2mU~lm9@r39bHatlG;CDY^sFg zVTw>+HzWpiN#3ewejfM}UYTyHMnZ0lK|xOC-;a*Xit$hwjRMbF%~@X8U`ZTeWjx84 z>MvGj{E4dDxW8oXZ+%~aT@>9|wnR2n6lFB}*@0DL`2NQ>YQ*R=j^VIh*RE!Lo#&-I z4+qy8HIq7hi1x2C1pbg(X@(kD+yG9kuhK6<^BjrEI3k?gcds?h%2q?_gP~gGqdsNx z0}li4lK3ZuPla+-RCs#Uc|4+{PyB8Qfsj$3GJf!g=^ARHbHbB>mW(|MTt- z41sH*@VhSIY!!?r61cm*Gs|vj#Mh+*d z-S46b2X0OwHJXe&E#xk`lbrDh3Hh9*w1DsQZJ#5MO>#yBc+}*8G!L0qLqFwIPpAYf z0L>RK-A^|Elho3@+`bd1S%aSVJ23%`=4cliQaIAxVVS>>gI)pJ{>^yvL+vCIx_qXO z_SD}tuiEU2N4^{#ZTVtbZ$Qe^VMN9ILUnHbq`0a|tTfT^&_XkQ?PsOw8H-#6Un z8ZNtW>uFU!){k|#K-2(U)I+RDp@Rx=As3BhFUrwQ<8Hr<~s6N&&nWvmkWT zNBpwL><`kt{d0i~puxdGfI#OZoyhn}$o+-T%F2q;>u~aZgAvbwuFnvIG|!fxv@Z zbT?Dh-nao)3tGW1(goV4{UKWl)U$_w4V&vUK!0ofqm4dM^(RY3GsQ_ewu_S6d)t=U zYMwzjTo2Oma1+EM;3hex>-vm7CeV5TN5Yo~tqu34InokM>3*7BO2#znG6NGCX&~#( zHyya~cRp;g4E+7Oy7YDBk9EUKj|ihTazqw`PWTWyW-HcyLO!<1w#W(Rd z6JwhLeo-nBX;^W62z!=`RwWe>ErT0bKf|<6 z9l8IhKmrEy`>kzk=ouLcfZ5Xo2_Znu|AE62=mIQ``ePh!Et=OXpTYeZn9?{2{IXzg z7R+DCSylCUZ!54(hp}FfZtJbuDr?(g<{s^~116c-+$+?!@@tD;WU@FUvDn3Ba>HSC zb1Dx9&Y$gX+(&?Yc|5e*m`{+d>y0yt>d&((&;qKB*5N(Z)C3GW8k9HEP+k5D=M!e- z#iPgi%`{Pn9++P$uwfFXwDSHt2y&)wauvU1;vyiZ2yxWAhw1Z(D6B>dBDWVf;- zCvQ71CRthOF{aiyTuafhrdv_~e6KmmV^mT!>iSf%_EY&f*8~9gFBnVJ5Wqf8{sDC) zEBtB0vLdGNFFLU8w)D`nRzLgmy&IhaL$|FY`~cZ2Qy!ttTS;lymy%H&8jAHsV(e=P zg;lP47DLHQ6C_G>h5LL4eve!sM(|``dCN1oD*O^9rX7hb7Vqngh6HfA7OHp-;ws3F zQSwwiOy@SX2X06q%#p>t1}Z>+dYs`Zd=US|Hp=A|NOI>|ZL;%Xz6zz+j7vL-;D zqwMGSxBkJ_`1oC9@i}kHVKV$P+pgwETb%|jDWRlO@!uu1a4%JH!;5vV#OlJGx$$aK z3kR|Vy&PJXK9j0#UsU-$q5;*Ai`?Yug|yEOxYhaM9rn7ZxbfjrIQ&2sO_R`^>C3|q zEzAIEoyzSgjtNu*ANx^JQD^Hc6cZ)j0f5~Zr`#IJ=oh?R?0LLi8LQN5&EqEb2M*}$ zb~`6jRf3mPy~ATpDpO%jL%X-~3f+8xO6n+b^D-VDx>9&(pWD<6&rcO_ zr53!-6|7=2l!k_ehh0U#^~MFe{Q7#%Jd^of_7wIVx8_pk!WkpKFEyfR`QKCKL0;ui z)qlnIs{QC>VP8#$0RyFDj^u&vWh={hOXQz2$Ob^s0j zc1xacG6BHsz-T;t{4*%M+|1eEJ~`XujAMN{EGtXdtpF3gM~x< z4j%~o-DTXCU!KiQ@tZ60z)j7x&neU|y{915DFi}$1}nf(J!_WMrbYXF_5iku4;`u~ zD~qNdSZVJRI)!3Un2)4$kqME<^!Mb)2o1(jNs5w_2)+$nupONfQ~oT?#d)4>3C>%W=FI5)A+yI_4ctktct|P$A61%kV6I){QMc0oxQvU z@p!xhr}5b9|DAsZd_cI_h5@MZv^@&3Yl`wMmKEy*^vmHV{-HsSaz`J^Y6jI$@!B=J zF>>on8dz0%ieheXL`3p9)M*P0FClY4Bu+S}Xv z7HvbE=`|x#`Op`J+1`mAhabLruNn`^(kiC~I2R!EtV3@RsZ&Klpo~!MbML0_x%)G0 zQJ9M>DU{qZ3_8wg&<-RA>IGmX-*LZme3r5DfLJs+X9Ri2#I#u?{Ae{x8}@itaJKI? z_Li-C+31?!E%94C@2?H0AYQjKYWzOveZm@nR$zx6aqH(fFdH`f#q-#3b?Yr~+KUSi zvy!Ih33Q5ESznjeLP+f1u&~4cdGGA(tO=ky+;@Mb+yJ2<=k4tsy0miW=Dgx_eLAgS z5X>kX<~)D1MpndOBsYfH+KD~!x@5JXRSo2FbqZBGk#u6R$BOEGDK1$S)gPTmL%!{75k+Lzfl7uGzkKR^)yUjsoP0e<2Ww%Z->$A-bLI zx8fSOyF>?-$}r>aX8K>Y4+7mjr^*f&c!F#WUdzH(pwFauzUe<$Zs-3_wNDrz;%|}T zlZ-^+jSZmNnfuBz{lmE;!9ERdS>`zl#C`ia4=+fv z9yS+F=$ULK

4RFXOxbDw5LZW!icJgDC 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�@*isWQ8&$(d@c`puDn{l}0L5dTmS? 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

$${1cpK2$znyFR)oyi=M{`Lf^ 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

bcb6( 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 +pM3o2=1>m{-Y4;yR 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.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 VzBl4^-eUj&002ovPDHLkV1gk&*@6H7 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/async/parallel-burgers/parallel-burgers-02.png b/docs/en/docs/img/async/parallel-burgers/parallel-burgers-02.png new file mode 100644 index 0000000000000000000000000000000000000000..9583b84dc239e12f7b4790393e2fbeb9dc07c16d GIT binary patch literal 203644 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 z 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

Onv+Y%F zrK|myH2hA;Ul>E$t5@Nn7D0_&mTy%g9+&H2vhOALp8pc(G?L@@$NTKHTkSILB5V!ow)0Hzv7;S}FolZj!as8P2cD%{>p|J(@*M&& zw0Qvr*d~)~l4FIK1J~BCQoBO`9fok&ZNC>UOz2?r5k9)@khSsh<$x*sG{u**%nug> zw`BgeEC^c^y$GJ%f4;}w;}RY?wVyie9&z|3%xesH=>4tl`qk+6EaS5V0($k(o>rWu zJK03Z%M*_GkhZZ@?bpunGJ{}jYeQbMV>%f=wxx9TJ1WV5XF6z<_FYO`7kPZrbOVD= zH<;1itzhR_g~lexw($0t$TM z<>fGB=zM=}?(3$%j!1zdYpE?wO8(0KxH-STgHaWo-e18IkkC68^BkI0FwnU;1AKmW zPd!-AUNA>*OmyY#%A=A>mg(niW{(L+>sEAqaGGS^Tj@FHk@SmXOHRv3DI}x(D2JB^i;Z%u%XYhrbb={)?)i;oabU6 zFtnl=ecvkhMZAvUUVOA%q`GF3nf;$T3=h71R--ogpIc&avE{~d8z$VR=a~Ud;$cQ0Vg4wTAfrVc)Ti*}P&=`2rVvVS5nqkW zek{wq>zNWnu|UiBFjw!7d^FP^E5TZ`zsPP<%fV|2P;BkEWq>A{hC$EZ}q!uO*$E@l_@H@#;zBV<9W@_Xhgfifr_1egCrDj^BJ@Q^U22 zC&;tE_jKTX<;7yyzR041f$*X<15xjfCUX|T`2xXcw_VX~zl)p6i@JZ_AFR{A*sXUa zt>ve?NNF8PhUyzUdR6%QzlOZSAO5{W*^_bmgdbE51D&q*Pt_jxnlo*D8+)h~|La_)mbggd#%rZGnT(o`<+lDnZ>` z)Sz|8wwrl{E&$g#ZoT6w_ z+3vd^-xeIV0HAywtUcfaX8N6@6JH=BOgGl;#!4ytP~)Jm7ZO*gOZ{2xW_a*?&m$Ch zLq`byRC!JG1oHPWHUVv0;Mdo=)|Nlw)92RmBz~Fx$AS*v-UuaJDd#pWy*OU>TX+!?q|0_)}l+M~f z`%Uu~b@_Rg^P&>3A+qjfspopu%YTuWk4Q=54$MZzLAssRk>_9D zQR@}g#Fot&?OSVWO!|fGH%}QuaqB$1mHZzh_Afqrpng}46%{e0`p6%-1V zUI?3-s=;osUWqV*hyQQG;n$TO!o(iA??IL|5yuM3vOk_<$4~|D+NBjcj7kYLruXvO z@Oy9C2l#DC+Uvnm$TR&ri-)CuAzP((Y)jNTR=!2AY+kgR#p+Q4Y=ga$vxAVRX)3T@ zMw(G^%9Uoba`vZAsh*0DvW?8Iyl{y$ayQ4hhbeV)BmyLCd_+xt^gloS*me^m7n zf?eBII@8mXh64RUVi@2^wlQi!GZHLZ>=l*w6d{(K^~eJ%IE7N@DqhMt>rURQ62dc>`1qfxir`9czJKp$9fZI#6K;Ukkf6u1)4yyZNr_Rn0TZEI!6jCl|33eRF`Nj4n@ zhC-Y*Q}Y#T$P!kF%G%X1Vt@9#?|fw88OQeRKTp(!$Y~FhXS`h3bAQXL4jM&5bpJog z;PHAJ4cJW(MO(|O5r&)V+7$+AeruSh&Z5$cE75-*eGhc`E4LwM+`SSfE*SP$*mc#c z_sh>{1NU55+%U3s@o7X8+Kmiit&+E^RJ(z()7L$fPVl!7t%l8@)z0two1@w#=Rbe` zbD*89bXXIsEi;i(75Il1L z3(s#ZlHsCfdnYm86sqS1Tzpi3Op8yj%>@sbV{Fkg^);`U@mpn3=iv6*@x%sGnLz+zfCo@7yr-HKl+_m^r zH8OVZ2W#cxVl72e6F=1q{0lqIuK0vfAJh*L7-|26TI&0`?$|tU$oGF?z`#IcuiyNY zpd>*a!tz6DGI&bu?0igp_x+DZ*J%-qH&(JArBpll9?fii_r+KEVZh80Qns>Ct8Gd6 zc6?tvB(gg}GVs-}$B6MJ23skVNhH{2w4du*=y%dP>3+*lObJsO?Ig8emh06}X-|oj zDc5|9E0~!WQj-Z6!NB-nlPNp8F3PGrgIPHkZE;liXUkGyuvMKW%~?xtexvZIb*49$ z=xwF5NV+xNt2oD}{d|`}A^%B#e2B-@L}oNqRP;Nmsrp;9cx|O8T*$*|>5X^qQ!u7P zT>BN;+Udk+P{73XzrA_N;Je4m^=J8>Tnt0Hb2G0+ce#02c*4m5^?$<_&}LOzA8fXw zPk%Mr|LTmmr&BUOz5|k<+(Hn&_ibULThm9DDTW#;FLOQfHO99BwUz4M^XQ!-x4HEf zITtA|Zfbvexht@i)FK3-+KYQQzzU~X_g7FAko`!h*H-GHtMPBTCc(bXg zhL6Fi#*)ND4@i5iFd~9xCOSe-Y+G(ZTkTU)Qu?PUV0|ks6N#2V5=)(o67?^82qYXG zJ|QPTV&Gqptq%XjKGRzZ5rg780k#c&tq z?r3x&G<3qqTGXe+#Aire&nCcOoMA>o^>x>oLvN*hDNmj9*AC}PEf}eTPr~H-O^4&w zRM7($Vju&PHZ+AxUWVw=V~5A(iR#)?X`Xn#{eDke9XG=|-Xuln1Sz zr*}f`hWH=YP9L-kA&uEL>^Zoh?rJL~St0VWogcCJ1Cb;Kr?%zTFy0DBRT|0K5HKNDB4*$hPoKI`h;el-H%~_99TGK2zD}SNhcomHD6EYh-uhPkL z-M5o~iK;Vg^P#_lUR1N*9wH!*twD=a-y*)7A-gJ)!+^Kp(ke zdR?zv{Ogv~J%P1LQ#Z===F7A~2eZjqBJNGe+oLnkQzHayZb1fG($yFr41DPEf~Esg ze=@lX%A7LA_r7^ty5oT+PPg3U&=WYxiwfg1(jqn%lHno7r$2f|`Pr;kO? zne=Tr#U*Bl0^1x23oS4;iHlE)UOVqTa>=L0qL&l(XFt2C1H3x(T{l8m13Bb&0LtrD zI9;V4K+ChA@c5fZ14ygz@Wh|nCr{do)drU*Is=13cYVra#rZzT7RRfwLAaGA=$M4HBe$@P`0S=(#pOvc*-^MU7Eg@ zT7r5QMTZJ$K~xy?K=CZ>*Mc>y2Qm*Bj7j)uSS$C9H^|^{m_=azcVB-$Ii9Sk|8y`( zha-RP+pJ{ z-wQcqE5n-ll~KyN*iz-uR@8;caBLJ>_{L+!iZVh~#GaWQk@Or=l3F^)tx9?+rdz;* z6;MVrRLYbWmAY{<#GHWHA>l^FXUuPdG5^H8n4hyJ1gor>sWI7aOagZk2>cr!vk1OP zjYAR$CV*(4Ow35T+ntqJU^>Y4>-+a;n0RvFU4RjGkQq7yz3k-QVVu55c&>(d^um8naBeU34B#A+`H8>AdL)&oHzYk z%AHU%BwW_O*H=$qB#s1UdYzo@J-oN^A(CBs6Eg&8MV_t`kn^+N=2x9W40cX|yR&=KblN?Zj9S0*?QZhJWSCH`hT&|9i?+ ztX%S8!7Y8&1H?Em-WmhG6K7GsRCfi1{Em>Wpvu^lAvaU1eIDfft@B^c$+h!~zO4{S z$I0V@dIk01!plko1%I{rK!p58y)+c(1w#l>9O+590~$6%kZn{FervGnL2&Vp0F(06 z7NDaeL&`&^UPkv>yY=P{jx?VmfrEv^xCrB$uhbj+Yi%FePe$;i_P?`IGQsGuktuN{ z3@aBt8uO#iV?13=7X-Qyx$QUyv{;e)M-4#C`gh&yX8w6oP%61dP1HA6Z_#qtqNMN6^d8JGj9j*3K*vuv{)0zc;qN z7y7Ip%Kv#!CR|_p$eo+o02y&!EhOHtg+>=?C#RII{>8X;`LBEP*$xq+6bjP5 zD>Jz68@kN_71Fn^Gi!MW2j#8h1QI5>*%zrK%xAH@KkHZFvqs!)JA6r{w13r~o`$6U zQ#>;~ek0k5{=_mX#NSQaQwF4N1&vnloID-^A31k{_*9(Rv6LsjFMu6_3cJ- z$Ihi3?f7n1QK_*>_T}OvP|PBdIo?32Grm_}GXvI7AIAiyn$mgGD|H#Lm| zgLvThHZR`ae0L7)(^1fskv&n6R0Sv8gFi}>F&{zt$uWEGK7_BY0z{=#fX*F!4tD(9 z{9|?JqG7!9XL&OXvs&Y_XcNsR|fhUMg`2Yv@yK0YR?1c_wMUoTJoycPBo8K2w= zm=gUj7&Y@5$B#k)pN917u%e3pz!jnQIZ#Uq?kw@z0x1N(%Kmu8GkrLuxox||(aE2Y1`ckzH8zi(AD7@m*YXQ z>F-Y-Vmd>r5~B<*k54};{e;Jfg;~hLsJPBfh6z}9LZzg=>N+y9i^u z&^Si0*mgpVOv_SB?>ORGNnyLWS^NN4HA{MG5STXT!`XM6K-8e zm*PG{ zl7srpI7fE&1b|4F;4i zPc#Dc`LYbqG1w4(%jVHuGOmd^mV1I6Bx}wV;IQFVnz0Stdq`P zyj_TC*!C0b=ehDF!2KfG7{%~T4R+&9`#u?F<;KFA@G3+ofk!Y*@oly`PLs#JMx$u7A%ktV4 z(U#Q}yOI2A4#FxsG>QEdFu;reKsW&#*hT9zKuZb(XhS)KTB5vpru@50ORr~@6>9Xw ztR%+(kvxu|r`x|1Mdo4Hs;=o1!!sn=WO`DZ%ck~5lRv$WoP?lzm2O$NyLan26s38# zp`Nj#zMRoVSYG{5b*%p^OYeGjvim|-#4#S-$i{wGBM1Xb>sL1xLED9>opsT8^6|FU zmDlHYDI69P&jU@q7gAPjp^dSEF!cX+qPRZOu9^YePCrLYUAZNs`DFr8>?b{Icw|H$ za30eJ@?}sE`jQ3Ie^@qk8?)(|buF6amG;cL)H>k3Zl`tKIO|_lF=32HXBzjCTpm31 zJL78%)>c74ShM<59MRF_SQ|K$hdDF^iFzZQYeg)~bP&}ss?okVggB?XTjTKV{qeZ; z3JUxny>f?S$@Uww`B<(WOdtorfu|9uEFUwfvpL3Me0>jSgZqt%T3I1%Pr-J0A7USq zidENPd5bZ|MapeQpH?0K1VWTTe*YndhNN zzSI*UnjnA{$L$W1qE6Qv_o$JJy)Y!UZf`n!ASc@YHrX_%iu6g=XRqN}$c!u?E^3Vf zeh&7$nLy-#P5YIklHkJJ@*6Gk@0SSppuid{92h#)a+C!zr6}f3kMInPX7^~bC2XDw z4wUf-HwxIGInwPeGg;56ZWGl0WVODJw7*!Qw4OCoCOV+_;nQSpx{3b&0*~P_)YW}e zPsF=5<&{M8u=gj!nx*X`2&wIdIc1H;Dy<2a!VL%*U0v7d%#PsaDlcH_3R_1;2s#jcMOg zn~YKq6qY>&wt3^e*gkFrYi2O50X#N7p=aEDs_)V6x(m)&44VCQ6N314V?c(20+1>F z@1*pvgP_~a+%F>Yg<@`P(f`CFL?gz+1+DdNWrW<2ToNUVLS=iW4svio(OsJfg))~N590ltVvWHPV>i( zih|9+>J=;?EDZDz$Dt!N)0>E43&h#HC{^qxdKVbqUf2ofDAUlazyC&mkLceC8wZOe zhcDuLhdg(a3f}>XHF~m4IGaqaZYt@s#oD;B(GAEAk~sE853Zc5en!7v^M-!$apd@2 z^^qRTk-fRm23=8uBtVd7KjR>i{nSNG<-092$YEk3yWC+^6U)C>5XkW;^TVPZ$(LLo zG{Rl_M<{*{3Fh<10g_=rKdS)*Dwe5L4K`Uq_S7D}WO1-zFARg=ABuweJfGkBIs~lH z6RI$lS|FR>4_sq2(9TwGUfw<}DgY%i!|@xibD4eKU$Lm_3QOs|Z@RoniQFFU8I+Dc zl8llSn{4;(GMOEg@kEzsF7u!4dUg*(y``vGGX$|J(_sVZ@}lJXP~tXXT)RIyn4lnH zLKwkI`6$H+zQw6P|>ELa`11(MO@nZ$U&OngA zoF5CF6x&zQ?b2PiZ$g~A2*Z|R!?EPIc5q`Wi{#6&xell$tn9zj2C5Wa zH<_-iG}rOwV%kj*A;}2xj?ao{Jhp5ZN&VZ^#zw;vAcB+@yL^NI-rW|U+uZH_%GAGel{3GvxrD;N zNU2dyE|)fQj?-kVE^EA`rqDgxU%-{xw5iS5etnDsd-eYI@-$cF9W*q9*HW)p@u>%F zCd)VodU3hAYO^J1xN_<5)LGG*`JjMr@!>5H=Qa>p#j?bqU3DlyKj*(=c%o%s5+9a( z6i64eQ*nOdO6~Q`6j;W~sduLsTk4wKSJ%>D4MhHYyS9BM6Z?4Ne-diJ9@H`UvSFJKJ?^bNY2hhvj$}r@OV**(kfhVF&_+P#ShtSI15WzC=E| zqmr;VMGYeUW4=A)9o(gH%wHyL{8O42+3+vf#gdCV3icN^(0ROje*fONKRi57#yU&^0-y-9 zT_k=dfPt>8GHdPEW9YYlT~N6$Yn_=~dMsI5ibq{lykn=q5H#@nbXli2$vUX~S6c{^ z>Z#F4^2>jF`2jyw>UpF<8VV{@O6@{sdB&w3OMCwN|We7W{3@Lt~Mxnz&|wA zXC4i?ue8O%PeV|_VqXzx4VR@XV7;%t1nd25FLi4#= z!6F-Lnq~Tsi<8AWWEIN?-YSk%~xtz9edqjU+TiJg8VKgj<+3V+;1<4j7Y5FCD8EPB{ z|AlK(n{sCVZSa{_(EZ>pONbb#0X#`}mYn(z9Pz0nl*7R!KTjF3g7l~(*GwodRPEbN zsqKk0&kLMZM+otojQ@lz))_+n(j{xP5pNh##=zA`QcTMmsin`w59ZDS50?NuK+Cb9 z+d6Ir0}78GJw?bty*r4TH?SoYcbivGX19;#X1B_^7Xt^#)03N27^+t<0mY^A%s7%W z&b|7GK=fvsjeJ^R#6s)Tv#lI9&4S!PK@-atdR01Ho-3_2Ps~_yn&Yi<0AJT(zW(kW z{MsW{vve9LM#79R8;s{a;K9N!#|;JXt90EfdK0+bRlKmrH;{R;?b(zKOKI% zhcl?x3P}fmHRHN!4-4dx8v^Y<*eKv}6Z94P8mM*oC%gcrlKlP#SCr{U@*gBDaXuT$ z>`&Et!51UkT2h&}W6v5%U6)dzMg4LEqDf5EPtUkH7L#RGZDd(r}Wh1-tBN!k|{ zxb@*YJLzor`rbTTekf~*n7C{x2RyKIDr#os5N6RgxdcjobAp3|OpUz}2I3M%!UaAUCp>Vnu-RF}i7HZ>93Cq&U=B8?fYUvdiaG-yl{5*! zO!em+fN{CB+=m6sGytVTCzUa^5ux}bjEpd{&Lk+NN9y!3Yew=(B#EF6wvxje>Tn?t zAaO~@)Sp%_;WL8JS}P4dZSUY8y`TcT+1G?^Su@fX5{%&s%wWOT@8Y=-cHyLa1IY;> z<7c;H&CUmwGomX`U~=tnPVBmdOQY({`602S-}!h5+4EX za6@)p5FGg7_>;qc6J-*f=6JV!%l!+e6BH8L0FeCa6o7qotiM+ z5Ma_Q5$?tR+Y5x2N+}RU8gc!5y49?vllttN_@LOzcfZC0?yUmU%j2}eE>s=ZGKzgi zT7jS9hF+ORV9YE>BAQtl!QhC9K*oUQS;fciLas<3P&_pb3xul$w(aE*d@cD9w>&+T z?2DJZoI5lS*a$5atfWeMEPrPgYo}r^8x})!nG36F2}uzV6gk60c@feTlm0iGe~%ny z?VVZT;Vp49_l><$H#~R4`zQ1(z#sT*RogFnzXH5AfU#CIp4Q`--7fNR@t!yH0?OhK zS&sP?;ox1LgCB@A)F@qlfZWGn01;&^4v{|f^S8o=PkKb>oxo`{qqyz&@87GJ&R$Q? zyA)L&LOFCjvGyW$JKQ-oKHeYxJ1!|2{`c>5 zG5u(9;LI+*C>NILTi-}Bh>5L&GXE&9fBQ?)$;EXglF3=K;b!rH1r6g`J~GF!AMLA2 zOuvp-w#Wn`br&*}78MzyHbD-NNY5o9&F==Y=%B78Q2Awg*s5V;YB1e*8;`#XB`ztt zK1nj#b=X^Fg$*RUZ}ZBNlJQbX#e~h6HX8Nkm=_~9uw#Ec&i&?IJO{2fK`i-ku@Pof zx3$(9-tDK3`D-CG454!;dl43iN?Qjg)n4rK`M}&R4S3O`8Z3jraw<Q}f=?em_kTtQ_rHHbJ=qW9{xppA0wuvqnxUko zq!92pXqxcG)00WPO2^+enC;DzuFp7)hY$FdQ>&kGmb>7OlXF$>8vU4_0ga843$zjA zE-e||O;D8i_|!f>6m9r|D^)pO-;5A)$0{io+KszbZZGS8gtN>P5$>>^No;G=VU=Nq z(22YygQ{3d9D7^{2Y;#^ZaMDsT*6oJ~0q5rwUBIRA({@Br0+ieep*~#;V0ogw zdtuYB2}KiqX>r_Vvj=yVJ@{4k=6Y0UE|W}aNLq2X9RO>+K7C#^#T^$S3|EmzsI8$ z9g;bC^y{FjBlu7(D<3y(PK>aNQg7pZt-G1{m+3sZT@KSKQKWJrKQed|48`wH58e@iE2Cz&z;>y`dk&=S~4AVN$FH0J)P>{oLms zp3`boUQCBjBs=@lchKVGl7n;G+6*{6&~KlVfFyZ>!Aa-Y{g8@EKk(8*eLcOy3g=q} z?oB zz!$AV@o_-A%6xGNfdoFU9rd3(ZOX{>8r11GEX5i_59HNzOOb;Pv81IBTjvY@(*rQM zc+!08zYC;@zZMMGz!3USM4c$5GHKWjCcE@d+M29k;7Pz1Z(&W$EdIF~gT~1JTjmGD zTksAhy3SX7qV~RY34Y(wIDLbaI9mW{Rp1Njpd2&X?-ic5`evH{jPwB48hRbPDHjEm z60Lip5-P^S+jmxM?}%qi;z1@zX}hkPJAC246D4Q;T~nXc4?@LFxg*_ z9+}P`_2F?X@ox7oX0h4*KUpZn1?Nw32w-zMaMrXj9>ujjEXTFhe{tRtqW4Xh8)Lg7 z`Moc)<%WB&{3iOj`S-9(lbog`1I9vsRSlD^ua6fcDR6ZRGqY|j^JoP-kVm6=>&R4p2Zq1sYC!8kDW-Df_ zLF3VAedHLt*(1<~<|-iLEXl_nY}CW>b@S>EsaF>$n=1VEc3k3eM%(TrRn+eT_y+b- zVn|2cswhWsZg=5B9w9k6ipCuTWsIQzOtTEWHh1#ib=*>!VMa6#<*o`;K6=4o?=ERm&fQ?3ULGU%d+n)+&Z`nNI0EzFShNOQD~pYyc{Qn$ho%sZRd#{^$uBJ3jYAHU zp()a==2V%oWAPt(FCF%3CGEHOt~ollwpXOKrARpMzkK>h_>S9LW6?X(`59>(#`XRO^&2E-TEK;oT7g!!{>sq@$hyX(7 z{Y4lj-%{^ZOb&kbV)UP%P?XQ_7xv+=XFkSrk_bPC(-PNb-=qaa&Xht65tWLW) zSifG!@|kgf>lgOfx-mIO^aqCah^W$SndXqqwAZYi6}!rUv%eo0QjrFX9X3H#(AS|oH@y7PKSVHe6c$2|q=*O@45%@@$(vJt=dHXaY^!(j=# zjXIWVOJOOXIYIv!0pP0Jf^AgxKkGEqUOT3F6uEw;u#~(7RXCunW>Z9KLL{J*fgZ1p7o7 zBZKR^Ep7+Qv|EWZ%zZK?Bj~nNaJ$hIZZFFGK?RxWpR>4{(}6f~sJ99B&wzSgVIWBW zj2s?dwT%r`5qE$$1pwlZz<-f;iuF8^VgqF#Mhh`mwI7vb)LzFEvp!GYX-#pn8m6(l z2j#`H+9L)%Jf5DtI{=^G{AOILmR$ZndK&)jG6f+M3vi%IAVDgMu>H6!dOZ{RFus3f zt64f@_{T112;fJn-Y;iB@N(2HhCVX@E+qq&s2#5F1YJzth4QwW^77^!l6{)>INBOs z<#0KrJug~ujl?E@`#}gq{Oaw*KP43I&g1v}(-i`=Cj6myH0+*nUjg=j$z%@Y(0=an z{JlFgE`aeelk&BSvz7M@@##OUpU04KVgAxZ{&?0-$N&{6LVPp{ZwbgP7gex4bfn>ihA7sCA$YRl(6h^C_V!n}-V+^}+Da$5KhY!^ z&!}^D$_^23jIwX#M|iouC5!I#PxFXE(Ul6&%;GS)K%a?PP+?}v1fyq2PNbv`V6omGBzUO@#ttoM)O%kOLw`$z-C zV8F8u9j1ePhkzIBpxXW{n#_3qXz?pr5w>O8AYd2SQ^-Nb=i>mp@%8|d9B6&77m9SZ z3i&QbY&Z3Ck7mT>7aX0=b;fshRkh^j^7H2@ zoNMmZT1qY}Qb$u+A|xWPYRqRSiF{t|F*Kr8FE5b7>BVoYu+CdvTtBngi~xY(dnPZy z>gU#8Xtk05qRpjHTrIV%`i|0GE18Jm6oSx%BBZTL?SypPW-mYDf|G))w$<5M>%R^8 z(#qBNYtwG3XOTX}#0pC(Z)!A{RJ}on^G+8c_!fOb3SwwTEeUEnA$*{>$>xz|qw5K~ z<^~67R)~6>z)}cM;n~0l018Wu(f-H{NCq_lxevwpgY=kzvcSTy{J6uiAF6|@@PTaSR(9DMUR_zE=jOw`T0x>1Kom^lm4CGCQ#D;p zM%c1`WPHv4`xn9wEGQ^wv3wruKo}rpMPX;{y9&L419>zZf`VoF9DIJ&lu-A=+z_Sw zQ*^Sv|B%AJPwr3vSp)?IBpwmd)zfpomMg$1pKh6c)XPCcW0M>~7- zJ-M{Cq0_wk)*qG4BNNG9-@^Kb4$RB#(n?zLNHKM*{2Iy3>^m?XE z!tMz~{!CD|)+k*o@d>mTw`7^ny4a-x_RttZa#@38u5U!iEA)JvEJqCAVSPtXbAt`p zz?{UWaJYNTf=5ud9H>}~b`J~H7#KMFndGRnQCH+glK84A#&nRM;RyxZFeH}+0fO`F zQrPeJJH3~MR0h5nzb1a+o-v;u7Bh0{FTsU*oU>?^CEwH?e2*V=3l9CXxZNfiPFwcy zPoI{cPut(w@7JCf!iirg5G{ou#i-uuBWeyPDks&^27^T8Gt5aftP3W( zLQ#FZGQk(#@PLc#usl~hxgfsS0&_3PtsdBzvO;rdBp^${TVTQ*_j~b?1de$_xeKX# z3a)P}r|>a*ckI{CHwpvJk-)9ai5L(cQ*5*(`E)!J9r)bvkrx#-;=GMnLw>>!&U8sv zH)TvadsM{MN7R!u!&tIqZ+S-*k}eHYn#f>a@U`(0*3G2$=Pdvo<=*NYu=xxd;1G!A zF#^l_w(&IX`Z*0_GaWC;&CLZ_F^iNwXKdLWA093{%7rMn_WG4p#^{TBTdMjoL0wp3 z?4jzA@Il8LT+{_u))}uISH_eiFLqfCy~S%~;-rTMjf;`1LXT&{DWiRz^!{@d zw5g{udSF_&g3D)qT!WhsHnIRW1Fko14KSrk@m8R_nGyU&01l{nLR!`osSH&E>Iy*X z$ZY_rG?D)y(Fn}7HluxqJQL@NCXE&$8|J=Yja^=^Brkt^c6Aq$`9b@}_(;_Eae*29 z`fOhdd>+@|sVOif;nNni{Px3Yn7Nx-@uZa6=iL?xnV9@+_r@+N6|1X@YkPU4+Ja{Q zcE=JOV6hP3J_UqauXMddcznD;%*gY@w~5uGS<-czCDiT>{QOq zn)%i4V_f8=sRw8!iAS}sL8yM^b3HeDxj);+T{dbaW?2U-#`6>QZEbB0h5lh+&m$2W zQP{m%ZYE^kK`gk4HxxGo?hGjr<-Tq_qft&XB*Pa!8i6?20wZV`y+aKo|V^%o*G5!3( z`$0Il@83z83Bf4W1}z!}8Ug^oTb>P|*e3kWBx40b$ajvPVh5?7lo>+Z(wpNBM;Bhm z#lsOTZ|H}=DT&07v~Ag}=2J=GuFuzhpWe@!*;^YKCyy^0BfEfgWxMWa!o6Bpzp%GZaNcF)Uay4Aj?8ZWkBaQui(VJ;|e zs$KXHSYIIoy0$}<-xOQ1p<@4;!#B*W4Szq>Z?sXD?M@bM>ap+>kwXI0r7rwK0;8yj zJi!q1r%m5Al{s|91YSH3pH#Mhia?^HXE`7bvfm?pYyCmr9(FuGYG8ZWtS@#ef7Asy z@ua}(h6f8)pdkHHX8)Op=ITZ8){WHU`n(GpNrXK`lWRymyj)sogjpI{wh1q#22xN& zDQckNz({fj?pvS!O!KlOTOmYfzGp-dy2FNmbukz@lsZ=t8e(yqMLkoiRj)Zf9>@|( zOvEi}Y_H;&C($TB?Rt3uXzQ>Hk(x|Uuxo}zalED^a~EAWuDz8dv;b*;XZ2sFv$!D} z3yXO+KsB#yf2SO8J!JhPYq!6@Dibg=iYw0*M*@f(PqQhuPwSb9s`opKQ0K|RE+Tg);2L!H1=dEo1XgzH3}s^tyH||9 z903&y)`FVN7j19RjTywt5b>i>o6zC+GRwfe?QL^XI`ViJq4HJirgxv~(Io~A0iYsv zga9t7)7JEymaCx=0~Q=@V7K7U2gu?|{Q(Q1D2`}n7diTKLpWPzD%-cpOub2&F&uQs zvn^kN6NG4v1-Ns*eg{;m-vZ9v6G>c!B%W!oZhzC%?p!BdCbekq?MI zzuJDInxp{kkqjql4`u~Z0EvtR_pP?&{J`~cv@P4=jlq@G?$EBh@vuvwj~URDXxuK# zuDUI?cwP>cNtMq{F_{WSe9MI*4`C2JWyk{4Gty~e8}AUC6R}mM^8pl@?**2Fu65U* zrqy?oIy6#ePYX(0(DG_t53kjsfcwaFqc-hu8c4lgBp(t{>iyicK-eX<2>^ z8r*ZthgB_sSM!Z)=}-uN%`k%^)F5&!reJ;=f)Raa<9O-VgGG)<>8h=#9=<&CPx&)B zybOYs3r;@m1lTzlN!HtaEvYVwu*Ox+BsI)K4A!^Pdl;`gD+@6%E;D{Rm;9n6f&sCXy0_Vek|{b^+Gtm2ZfkFj^qNjq$I)dR zhg1fJG_K$g^{{b~p7iTC*6Yfn%Yk2U;~SVT1r2y@Fm}a-mMeVaH82$0Z7WS4Lo8?Y zKU^MBe%bm*ev!b+L|JAPM7H6TGlE1IMKQn9afab8XLh8{ScjFkzMj^*zg$Ga$OpCQ zJ^gZX>_7>OoWblpx3e2@t?D}pPv|iKUXPs=Jg^~>*2!Ryq)Q^cakqiIP3v~y=cK&I4l7w9z{^MZbwsYwWssQ_zihy&yN`}g6W&P6S;c%9jLF1GUQ&|0a=1%g~IUj-Mq z0MG)-5-Q=PL>7)f48JIw;$5*+|I~zR)`W zG>W3WszfQc;ocRRB9(YTBD+1Lk4ZsZ6z!iN>2k)#uPKd|!Gd+K+C49JMH~Z9lp0UY zqJIpZ;dRC3;T#Gae<*j+=iV%7|Z92~O?vjG1Z z4%+{)H5d!irUlEmI284SdB(;1}D-+ zVrvZTxcNBOjWs|S;ITP^%e#l4)a_5y>M>pGonx-HDBGQ+lkfy+Z&cNS}Jq_cgL zdfy%&Y>Qq8V5eRylwy*i!M#FDU9)=Hya<6C73|v@7Bl-SGY4^JI1!p;yWt@Yy?I3f zZ$lx(SVA)+Cx<`)IJEA}`#2?~7ee~lcmZzR<4avtjn4v7miM8zpyvrkdzK`y$cQ0N zo>v~)p4S-f&$o1=DQs0UFGT#FMq6^OafQMS2J%Nu{UB2?=(4L57yz~XE{^S@A z{d>nfSbL{DPD(BL>~4ilN@{NA8``oY!hkA{I zcy@)%%L>;FlLp)p2Y!8)FG{%Vu|FFb**jZdXJhbjxI?^hbQEL`AR6u`#D6Y=H|!9Y z*4{StB+3Y3eis0rVytGN7PJLHq?2lr?iYU1@4n~dxrLtAwwt4Gz6glspd!WLwLlPR zdtm~e{@wZ_P(AX1R@DnZ!Arna@CrjqwUQJAp?-D|Uv66%a1H_eNS?-wDx~h1|JhUl zE8%6pP44}|Ph>$yOS9ejR7R% z&Tn;d2qcuXWizP=4NRd-{SHUI4)o*?-S8 zQZG)+18^Z`>US*e-BQKn>W3Ikb!dO?6Syv|bk^eEvw2SJD{N2XD-2A5!_aqPsC6Ql)ovF~p0~-1LscVbNB%p&&aq(80=tI> zo_Q*ZDN#rSQe!=kX8pUB35#Wy>tnFZ8nIIcA0F62*N3)O7i*&T#zKJJK5E}Q=eXmb z*!=~!cWzG)&NPOCBf)#4nP_RfN%Jo7Zv;HXS?kxu08oApVmON zY;OcPWevL?3Z3PNU3B^WEju8}@C%j4%0b-|ZAh(gFl234eG9}4PyvZ_dhT00ep znH83Dcfe)^H`vs*OC@6l=0;LfU&XScQHY)ZBhwj-+q&0piar!)KFBbiO%rO&h!UPk z6dx9_waIC%jmYyCkT(9${1rN!e3`~2se1k?GMuaj8%;E_WPBZaLg44hF?YCloCGL> zJnUo*OhT#o^cX+I>lnA@Bt0eNmbMTw2N{qJuPl`F6mD~Vn|*V&U7gK^6kcF=VgMS2 z(o$-yn`_>A>G=L$TkY`>+m7I(8j*H$96;@Y%3YuBpS>!(m2E?AXGWaX1!C}SH zvH_nTB}gpzS&qAe(;7Ul9+O*A%z+7fi*lC(JOIjxCqv5NJj9pB0;(n}9%zOk7WUtO zi$W8(_;Q{)eIyQF=YzsxfB$YMP+>DX_m@8Z4qF)sR&?|(tK)VN@p`k)w6^&U>XbD- zdWZU$H~XwF2cPGMh2X&3WebO_gIs^6aA2&j(I3x<6kJy=BOl0xt2QeHN%rySi=8JWHVAc7TCelNvLM#y0?JTUa_W+{y^+ zC?d%@&U2K|@`5TttjwUP)sZ=gUBJpK!^0~34TwJ7O+Tj$y3MbfazyMXrwG0!Kl>ZCxk)i(w{EJ>z&LtzT^5ifLh-jt4LD;@?)1+hq zW>0D=65Kew77E&qlp_w>h`>^7x|5|7coa^g4qdYB0OLuCe;)4L3TH*OikCY=Y?sL& zIlI|iJzcIq+=qB`W4xV?iY2*he5|!gM&DrE%bAvR8Y@?$fF7~%4X5vV;$BYaql@U1*MP%D)|&$5r8Yy$a!BELz+QwjpsZcwR_4P>tv>Fg7GA8a z4fQqa+X%^DzqOYlY`a8CImZTRD#sqiE`IkU_Z8OTU5z` z4dc21Px}k8tPSZb=w^(VI3G4){hne5D`SiMp^uoQ3r>7t#-QeA%LdpULF0Bfjy`aH z)$eO8;MuBY+1~JZLCHUIuU#T6tXu8h#nI;ldqzQO!uJp_BOSQ;Ff}FxpXj7uu?rH*w7Q4ZG;E`7+?H$d1oVy2p8`_id@tC{K#S^$P6+Sh9*i!H{=vbOGG$^b;o zlKU^*J}PB+Q&(s_`XW0}fRDH$7e)XBW6DWP%DyPpV{T%E`^_M6w>}GyzoFC9QK~nm z3y*jkg?!aAVf^&A;p6)sNoO5aRntZBOGrqAG)R|#beBp?qezLQba#iqr5ouGK}x#2 zQ>42a>F&DU@P2>$JbFE6&di>**ZS?{bZWB>dC+{5S-<^7Ez&xh^$W@|!H)P~dN6Sy zP0d8GfM?8koOh)^7^=8)4kU#wGnqOoV`t0>%{#b@n?8hebL_$ z11D8%NTF}tWDOWBZQ}TB*)7yrBGMlNK8_yz@X^}y(8g>}^pJ48fAih;$7gOeN3Csh z(zcf@+1^q8Jg+uI4QB zl`vdsiyfS|uoNVt-)?rsH($H#w%)(VIYoXTf@Wxx*JH#M>B$bz9!#M1*Wc5#6j)Ky zK_g8D-m4JA`1*-Mr7y@p{_n<|%XLP^{tV5498xgUH9GZVe=NCJqLqtYg`(xQF=gLM z{jP1|rZTTR)QkHB_YS?R=q-yyR2hktrStUR-}jML$tWMy$%Qa{%j5D+hJ18`t^A92 z%6`iSGx84msp?Q?XH!vzudoKK=;8k*XQ~iZ^*Gj48HKPFL#vUq)8A8ycAELSE1BAQ zFmYe6v&DU#(LgLMdTWI}OIGpI_O_ldCiW5^Ta=qa0Fwh$VBzuSRSEGvnz739C0>&D zA98&qRiab>ApfY2|8cWyRX$gwviQtVp$cmsGxucqN=8v(T}bSnsr8}%LPZne1g<+< z9n8_>inN;TwI*EVUKmVVirtkba!vD7KktMmyvkAj9lw;H5>#sz8u|Z->W_ccKNvduo5~AdkCz>3RKsxqS-oS1|!!*!zLsKakv zXIP7&#cF;6ovUfGy|=3(c4utyrJlPRw59vUF+EG zb3^DAGiBY4KizZ0Plbht#M5_rFF^QhDn?w71G~()5m)?K%jz{IHiwB{&)qvEu93Jp zAXqYnj>iQH zJn|cg&h)XuXl7>4gxBY=?^@wLa$zUSr{iP=N_wf<#Z&oJUJ7cc&*1qN5AyVSxh@;` z_}O=KwlW>yj+$9Qi61hAHac+m@!j%qbXpEJXI`&aK{v6U-m^J41DJmL^!%k>WUAQr7~v=_X#D`}JG=^)1GCe7wTX{wN>uPn(5kZAxpG5Af}bImZY3SuMvY5mzd#h#y|=ckH&n}J)C7U= zFU%E;J2K8~zb&*Hg}0%~e|@Y2*)vSA{AW7@gY`__Bf#sQh%~jTH-UQwe{D~FccI`Ax^ z-zE*-n!RsN%n3danu!o%5_pBs#x6JnF}1PG#rtst8gX`u!$*36t~l`#HS49gTEWe= z6L2CDcBhAKS#>p9&@7Sfm0#|$h3kvcX;fr_Tw_`%!+D=ZA%RHLoU%Xd0EkZ#+dh!8a8eW&hWLCj`(Mn<fhZI;Ys#WV*ng~CH`eGNFKZ?bDF#!}d761%yy6+MjP1z+B{~E|`eGMV%3r+LQw3Uu#u}Ts*rLFKfkp zse!&Mh6**h*s^VecD@Q9^;av_ysj1{qts-T)#GW?Aw5%Z~Pb6jKkm9)7BeKqez+w0w?j~n%5BnDlgDvnV9}2 zc|4cxXFB0y=im@9PtuP~iT z>`CZ2seMpSLrJR%7~LZ<#>6i;=M4+qI4s4jUTgj}v7`h2Dm{H3#1r#uO4k|!9Ec8*vCq{5*2=|eMzD>5hAiEygU*dRlN$eC8vzWe4qzwTlt=+$7 z*vgaO)-+JMHWBzQwWl>imZH6C?3M`7yEGGG@Sn*hl+Vs(zL6PubvvDQtMvPXjqYBz z>L4RNjm(KcU3?XjqC^T_sj6hJJdP?(R2+8ix8T@&^z&s``_E>XPeQ73CxugL08p2! zcC*ZWd@xImc;^lQMT#t*Nes6eX8(sRgZx()17bg}OKYp<4URO{=Bs0Dg)U%+8!wDA z9Cua7Vt~w`OGy;`fncM?zZt<_Jw{TlE-WwPg;gUETSA-=BJrOQkM7(R(sL5b(O^fa z{X9}cdeb0OvJRT9=gt-7kw~3V5u0RIVo3w%eAkLY;PJSbn0l`Kz}% zF|=<#t_g2m`+ctq?$5aX97o13NLa!T=WUlT5?^*v#-%k50FvSq;$}LHJ+MAIyn2u? zU^kr!&LDpZwnphe3vY>9dZZk6KoP#`DEQw)V|>~~Bl7QTc<$$Yf@O+fJd{lR5t1q> zJih|>pnl7HR#!7IyZMkQ5 zr#qgNFEwGA$cJLm3HCasL$Jk4@QN!)AZge|1fMdfe#Z*PmV?{0Hipc^*F_?<4u{pA z%ovBu#&$hhzQ+{eE&exY@-m854urGD7OC-8_EBy7&u1(;to=Sng;6NIsnP(rw$Tgo zs1dD^2zn|B9TX-ld|QpS1K)k4?ab#l#A_Z07+GT2nkYwB1dC|GAp_!cvId~fb-(2& zO)OAj@*WBFecfMdsC&m_^s${8$PS_ zH02h$lFmhS?KZp=+Ij1`g`pE=g1?+^;HJ|an;w*%o1il8xU=PS5A$_B5f$4O4$xY2^x zw$nxyc)2r!p@DmaW(!rqSv7&QvVW6(=oQuPbhv;9*y65{5-{{LBZ%Oa#Z`$uNUY~6 zJB^mueBjt;#5X^9tJYL16P*q(CRFK=jtm6Hyj&(mJ<5Q+ruHtc{fDe=bV$fy2t?45 zI-;3#ZiWvXq4eKOVz_3}CvVqzo0(P*hTn-^Brb{)GWhRdOp5%5mTSXQ{cxS+?85C% zdPE8-kn^Uve33t-E3Fp#gMM9$NZ1ZzW|c}?SW&`~k3XoS?R>%N(CMxkxg{zrr0(Y& zq8+{CRUvPsmDOM+D8ea#DWC=1`h5K9bFS3H3CSCv<3^@NaiWCJXG|by{+55mda}&7 zqn-2B=ydOnLjOFV-{7>+BL9M?&sl{lKS&%g^op9tClTsr(<=x-r4%RST#Ai#)xUnw z#(14v)b$R&0X9vY&RQ3=K%ZNxuWa`i#pQ>=Q7$>xE>^Uk?HE_>e-HMY&oWSS;{dzJ zIsRJ0?lU$1xnDpB^ejgPgJiu=Ufo-vvMka6>|f%I**Hbgs3>;$W=|Oap~<6J!v0u= zM~f05)OvJT)@S!&*1sKcQOzS_>r2DHB@{IOc&!esJn62#-VlA!Mx(F}V~yBmN#Ea{ zFf?tzrfY&BXq4CLtm~@0P!v3~JoLI37{BHNmx?D@H{5vBOR{`L@qH-HmOs6;`-g$n z8YdV|$V2*%Nve?|s*&)lJ#s+^?&LC4DDE;_M+hSUnv@RR!zpqt zBi3)8?<%9-)#Z^tk-Z~b_I6*A2LYJlW7MOs*GO=JVUWcuMxPz3%cVz!!X*#IZ7d=Y zv85MjGHbKO9az2Fv+!i$>fT;sP;_7QlXBBuY963fTd-dc6=Kd~ zb+#sUu<7Eps_)|=d6zL^ESuq}Y=-4NAAMUdtLCE}VoTx(UHJHOi+~FM{T^QFkyYig zvD)3?N=3K41+DBZ9x-GvJcB(sbNPXgv*MQSOF!W^STUYratroiF4Y^9Ov}B6vpL+vkmyI@NQ18z+U9PVXe752wZDP{a zdQ#)VfgD)XgeF~NNrZ@bxYl-5LIrFvAW7bwOx#IexO@>RhBSuhqcr;p}-2H+I2|k!x@a9qL{&(SMGWuu@7Dfp=^QHgWc@p^jUMN&s%q63UggEuohnbSL!dgD`5ogB$8Sz0RH%(0e%52 zmW8t;XDh&)`T$lZg9Bs#v6)rK_+B3MOmq@zIZq7}-o3(9+TN@u8T0 zxW@$Z?7Nti9>PEtp;-1`mcqm$yE3VHf%}o$t~v`n@Xz%N-)YY{AiuP0ZUHS^q{S1I zAlQS4aH=}c==6wyUbQC*&M7|C+a6DM zVR4Jy$`WX2j)?^G`Fo&6lxd}+9}%o)BC5dp)XTptY#pFWe0sZqvA~Yrk!Aar*D53t zPS1rF`x)oXSx&BrZHH{$n3xacJYR7h=vHl`y!qfR`h=xxNc#g5Q3MKjob#U>qt{KU zj;Q?K`h5*YWRR9G7^^dzWG6j_aafj^$Br<>`4P}DXjW)m9OD!+wl_Lt*ExTcHANwk zIyaJt4PgA08^Rb*wNzB|GU*bdWR9@_E_%IWrKsd=5TMs@X>wcPjd7!S~7L|uL_ka=FeV~Q+9aBvav}H zbMV%%vt?po&kYr#AE0_uG6cdZQA(jc5mHLwnzJM+t^J4HR%&Ke)@xk#hgz?^0}C4S zkZ#;`I7(Sub7VRIN={ASh3Y58jFdv2i9_x7j*iw3Ge0XSs7bV%beiJqTDZx5Kvg6A zd0O3@e|Jx=YQjc&w$E($B7D1s<|#qoHl&s_B=W>7QLa7Lx%c4Bb|Yo)(6_rcHVMcI zbIZkq=AbgX?XsiO#7X9GW&lxaGJokf;R7R8lZ@AY2_Ls1N2mx6>gvf{VLHf1 zjl2~Rpi+Mz9ZPyo-7s(2CKtRPFeJ#Zz^f8=6w!xXbP@UyR)^{P#&JTlf2o<*7n{%F zP3-1OjfWQ^phiX1!V+v5yM7M~E4Celwf@ZQ%%x^54MNLnE7F&)rKQA%0zg_bt8HCL zrz6am^Y-gmo-}N{Wyudb^~@ee$T<~gY(ukZtMJ|{Dp+aK%c_1D5PPPXK#d5a91;_J zd)BZ9wW(5J=0&F||0f9lOF@WVGogIWy#TAYUu6^ASU#vaM*BQ&x^PcTS2x)Z3082S z(vYEgz5#DAF7IT9uG*ufViqsjc7_w>3p`Sd9aLR75CY z6h0b6!#qn*S9}(kuY!H#8L{G(Pm!8*G*G%kwB^{ZrM*;(DM&Q4m+a8JghsEzK zvMMo-VL$aOK0O3Y_4#}=acR0*7jg*%H1cyK=7ED*0(9v@G1n02cR88#%4lyw9e;4N*7gG5 z@QnTQGJtb&U;CD@zAef|B@7!6t)jl)=E|G~(>ea9LKcWqn29R!rpt#d=1-<*k$+GlZTdoby)M_%1 z@4~b8)w^ynA!7OsFnoe2e$q|SOCjj0wqtB@)M*i6OwWMu#XKG5;xUn(ZM~wrh8YLU$W?MJ~b{Bs>IUXZeE5Tmx3iy-ublZK|slzpT#_hrs*`a zNU!oaf{yk3hbV%(ul^t7z=_r50OKq`XaFeEXwW$+t`1;N{q(%NO^qU^{`h&t&<$@w zRffv*1fyrb#{;2Lv@K@HzuAbGISbW0?_zqw+PYvqxxqhkI8A*XYFfd4y!*Re+*6<* z{P7VWxYQ~QJm#?Wki~@6P40t0pGp7saWTYh{+ zQUtRTConK(A-C(z_^PtPzI%1c46kp2^bd>|3=VDV;RJ&@E#W^IJ65E>U>|0^Q3~S$zp7uedn@~G>jLoYr4xFG3rYCLR~hE3>wz+c=BZ^L zK=5hQshMeU8M?UoCV|%S6WXV+F{5ia99EbwCMO&&PQfP9ioM{vDa4Tle&$jjch5bK z44J+c3t%3%o+Yw7pK`x^9JjgNPUi!|4D^VuFD=`745;gu>%W7;6RJgF!fCo4z>M|b zam{~%Ov!oC4)?36e3tA_tU&}z%8q5y)#QUvp}4c zQ?Fd3Oshzt05%A-*y?NQ7<@!D4c(~Sfgd3bT%2L?Z^=wB854Q>SBGKl2|i87Na9lA zxR^|xZJ_m0E%!K3Ce}P+?bV!Bc#XCHWBW0)@<@HTr_xQbam~Fq1z>~3&*%>w($i9U z`q732LTNaG)V#NWMbZ&v4P$jSNey<<@b8{q`K87orBZsH`9ha4m755l<~(_B9Y9p4J#TpSghu*tqFUAynMlLO?yHe)Vrk<=uz;hBi{ zz(m6~s;uo_7>Wz&{;5|!8?f6tW^^wX;|5y{yYI~wAbk`0Q8AI$Q6bE#U&HooBw+{MsKqN} zXY0iYZ(d)&>u^4TkVtGM%@R{bEuz_(OIaod{S$wrAM6Wv7 zq8cNXmhR9Te3SMIdQVMi?`fi^v{Gi21$#U-Mdz!*Su|M=Ssl&?{v?2L@UX@6I zQz~=7lvZnp38F>(3wArc{U-_l3Gd^6E_Q#vJ1|pa{46Cmi+5^mz8>hT-6K3kjR;`t zs;+s@k4Vjd&aF!W=u+#mO=%ludC^XXE`ZeR+|}VF{SBya@H#M;*2jFOU<*+zRCm%S$0C8ZJ&-+VM5y{TgskMlM7>u*tJ?k{ zo!tsMjUOFA+)a!9uwy*7}Lk5ckbCG1|vruCksdQvT< z8ZKqjF{@O0&tK&0nux6VZBfK)wA*UE+O`=u+bw>gOa{@tS4sKQVVMS@YIZP<{5> z{r3c~o4gIik_=9mbTFEj5Bk`rOu#WaF|XuWGY;^JBX z2Zti@^Q$(TvCZ z*LdMep?hTK^BD;x;0C|Cncr{Sdv&?^sPISEE56MyeLG7pFOoxVMelX7d4a33~P%U^Z$3FEF2!65B=zP_!A=$qN$p!fD|fp*5>yYX8#6z&_0oG?}R2MJb^AC^&7o zbFdkbI|=RHotW`c!KDg*57SBq9x^GbJY+#%gODRCYbE}Xjo-b%n^=ETka&HeFkJ>x zMFhyvhi6Z_;Z;(B3<2}b8C!?QBVyr_XDH|o;5l8QhFgoP0f?XU*M4tvu}6v$j`2SJ z1A|!lzg@8jlRJ(?;?v5%2Tm-t{Y^Rvxr^?|s%vnw{VjxEDki4d==k7)2>?f@HQ?dt zvTda~lE9o{I6mTrYv0i&tYF!h?EtWQS#}L=7=#CW*n3A;n@ViY57k{!ms>JN3plo` zFHVkzKvHYqxBiaxDvBwV8XgY}g5f=*^HL3bTvno?n#YLQHFk`5+}i^=FYkKAJ4-7=np1YWSN#$ReM01^Rg; z@qhOf-jIK?u>svbM`{kmvYHJgNN!neYHnP56@7^?APQEx=>*DaC^dndR1>yDzl&|I z-1v4hrNW7apK!HDHuW(e%Skv?l*sV2I=W$MQLwo=Rwax(W(HyPsRb0i0uPxi-SqDVb4e+nC-D zERr<266{f!^B)87gXJO6aH8%NmF(GaXRAFgh8R zQ=NKePzly|5Y9iaM*7Aof&?5ej*yvjOj_Ac95il>oKglb3+|+H`7!x|xutEKQA_#e z$_AiyrNVAyIZI;j>@Z|9LqB@u zVMuMZ0ao=q{kw6=+eZtFRb?v!>4HW0_Fy+ZKjm9-$_}NI4s6_90H7Qq>gAY=t98Qx>DB6?3je$%~p1Bw+3;K?A=d6OII^~r77DA12 zCnMhKrkR8HW1exxJVlC;$nvVsIf$m`nynb)r0WmlQzH}hnNx?ZZeVQ(^?^jk7L0GZNlBWOpP?ll;UHj3t%PP;u<~y<{`%+zDk&ZM38hLmH+O0US z4A%J8b1d3ag#M$q7)XNFc@xCIipCXE)V`kVdU#RMjqg*vrNz^Z{K|BQFkVc*Gy0G!m@8w2Da}msr*9!6j{F)J6qY!|&kiQ< z^6DjezZD1l%)@SHzy4PCi+0W~dJ7lcS2{GeHyvft&0n~dS4MR!hGOmd4HDAZIm)C4 z-?V~g1R|i*@AZW~)Ahplc)absPIal~c6g)p`9w-*+U-$nSpV-ePUHp+%h8hq012H7 zv|~_p#f}xwg55Duk>0o@Q z_u-lsbI%ahPFO8?^%gCtOdyW^Td~;>DSls@;RK+8$ynAS5ZfN;7-d{207WAiz_@T; ziA}W7KvdRC-ALQ#71OOuVrYw;3HxWCp^L0qLWk>p7gV^)W;`^)`=8e7TjNI}``+P= zl`)qP!tWLh%_Md~wM`17-%z)J}k#~!*^K#qMJ{pT((+NMG-@RI?)SU3xPtTyYp+-5LgPTV7T^;)`~(iA{kqbv)VF(JJh(jZt4n4z}p7>ez-+wuv82Znr$ejq`Hi2_w zJzY*l*1r>kSi3lyR~AA~cxr4uc3U812a4yvrQlV6&(LFJFRZHDkoleMG1-fGsa;nW z|7#91iN#L|N(_b0RvU}FE309O9$$+i>b<5TWx$P`VD(CTbFxHJxNJix`%a}O=U)`1 z&su*t{|+fS5f_>TycHZCD9%gRAH$DSt4_twbHfD-{)DYobzmv^C4NGC+geu)j}h|) zwp?B2$(F{c#dw?d(l2WhtvwecFOi)SLbtu7Yt?GWUuPgWNCm6<*9amtz*%6Sylpey zCrA>^-?Iw`o;bS!;t6G6+qQWGWPb0HX?2x4YgE!DkEGZ|3rX$1y5&5?`U0K#`bBC% zeoMB9>C=#uH60d@Q^ilMj%;QKP!7{wGxQX1`q@tURhgmfL#C%s_t}$x|881qugIu? znVh8YG(ce=5k!^GY}wn6E~*>()Kzg~Sq!UG^3;khUx)W^xUQ|FbhCJ^IlsEH>ItS< z_-9{l%TG2jHC1B-1>vZ78>MK|)vS5GY0N(FdNkEX%5DvJMSYNuA}_Y#Y@+z#jl%Gy z?UA0vZE+YJN}{LPlay3ByW4?YLXx9@`??YplMgn=oijoQ)1MRs+X49N{+;8oS5ys& zGImdWeWWiMsF_FKG&}W%Z2RPQ@7G%^a6XayPTm1NA9hH9tza7OryT$J;_~@ton^lx zZehOMM2iSJu)!`ITyAu7o{>kg&EHV{?d!eY`7@0%UIWX<(|@Lq)zqJ3KrhRf7Mbyr zIk8)2(0)0c^k$nOl2|a#tBKDZRu}o4>8m;fDYKs#L5Bo`;0%?zGUBH$)`0>0|EB*@ zOBQgRzsNf*{BBGm%6pnQ({#iyM_C<#0A>cjw_uV%`ujP0k_a$yPd#i?6n#ryG~`yC z#U)7zW0vt4Sgu$ck5yu?FhqQTsRa4l~RFC%F z4FtaNC+FZ3aKqftYi$V5E*k&YZyHqaTxUBOxJ>7W(NmlSnFRd*#^Qrz`K2=^tv@>1 zq|ywQF~#Zop!N<~PY(%ze)VTsD^bn8LdernUWZ?aS0B$R#u)gF$z)Y6e_CvAXPcVW zr4vMYF&%t${#sD!tLA>|&C)>`0Cf_SXZk*0Mfn#fNdO-hIrIAMBE902Klr~LPh2c; z+%h)gj%jGh_b0IN*FB13T7H)Qk%gJfHA^~(E`x`eU7{KdXA%+Atu5L3zEj3izLRqQ zO4&&dZ4$Y*;=(^qc8Iud#mt2LGW;l-%l?&W;x}z@pf?9TT^UWyjpPy|7j91y8N3M` z7m!BM;{$#&=HvWt6BmoC{ zCE*i9F#q^|SG_e<>$PT)jPwjKAj`<6xKbaUFaZ2|L<-iIpNCN$T##1J#TcH5ml+x9KdOF{G&)Zi-Nf z`Jh&EJgW(AC!3sT;F6M|v24>go3?%w-(OD+-(4Ylgi!fnsP;@wBFCX!Kf*~lU~+p$ zvO&9wd0*CE)ld!#-Tw!JV)LC{Bmw4HF>9VZ%=mqc=iAkusQlN9fx9QD$cpM*D=eI8 zf(;&kgdlEDvHBd4`^2l1TwrG+a4|BmQhj3wFKSmh!-3FG-3dNb&p zr_w6*hUu>2Hs-0ddY^y$Ev8|V-9pA-Q&ei)Qj5KF21H1R9ov5H@ZZ-S2@Ppoo;6Am zU0M-LK;R${S&6qFldQB~NKIV|6&p0gtQExs#nGv7MlW^h_*TRkaAI-ugtunpnnXE- zk4&;(d`0F5yXf6ilVtypT7byXP@*|)M`3Vm3|?d#RdydcwefX%Z?O4I27O3l0bUXf$Q590~->6 z!;MQx2$fX%84vTyxFm-hZWsY1>IARI*tfwPrSiQ>^@4?3ro%2yCZ>>+_1^W1=N!2r z9+xX&EaWk?mi5m>jt&xy$i$`Mgp9Z^m=jlQ>>+@DO2DGgw$>eniU_Zg8sHgG6c~h>sO>P;#7Pl z<>ivm`biR{Ymgk%#wy$yvMuG<&|2+pW>_J=Pz$@gptZWa%a4Yw{=bm;(@XOIB7o>U zkL%p*^CDi4aQ(bM`24p%jq}N5y8HM0-;<6$s=P>3WVpDvxIc;V*~kidX>fmiQnzSb zg`y&(nPB(gyd!`;HkdIv*X(bGNb1gbo&)UR>VvK>MROsE@qEzmnDUptWHFz#s-`b5 zQ$xVaWOk!lc;2QHb}HSt(G9frrBlErZcFi3KETod4-ef=acW z7zdvh;ioB{B?=JBb0yQ*c1IwkE8b+406|%|onlw9SsKr&Z~Ft6$hBSD@*&Os)hK#A)b&IeUwrPuy{5PN(WDwKhVNL_m<*7jfuA z?CM{;@^1kme)E{J&c_meWoc1;ywrv%ekY!Mq%a=*eN}nr@AHjs#sMmZf;@P&)e+v_ zd?xZ@x@}!f-{vh(B6v8&cBk zF>r{DpUwOxo5yZ>30h#8{)>yy5t~bA zo4HF2kCE_MNn@fJAuN6^BD3OO7$4JOd8lt*%`f~6{q5qb_TalL%gU^O^zD70lRi_L z*T=0(Ko1f=Rl{ToNwlw$EeXGQW`xdI4X3|I9US>UDymh>5TVwmMfgE;~H~FD2}Ob{OK1y6hxSeN_5uzZ~n> zN+fv8Oe`rtf93aYy-hTq9M56*t>tq{rrBmves1&wU0yu* zp)r7g)gDCSuKEM|ZaXDm1P)*Z*GwP*{s)nL8`tfJisGTykHB}i)a3Rzl@H6;ZoTJZ z_jlaSZ#ROW;BEs0#8T-W&gE>QQX|)PrIgkr4F6IfS%_M> z>1b$ld4!2=cr3SsYE~MbEO{K+Ejmq?yPm9F|I~u|l|WGo^vyjnCSJ~UUeXQ62Ij`z0$+k&=mBaS`3; zbi9BsN1j)hO5W$l)|Svh1nqpLB}v0gDq%Ic0DsYQ+h9=8PbBQa$^KjppXL56g4xQv z(1P2f7GlGnFd0@9z)c7mPYWT`&ld137Y{dLZ2#obzLDJ_mWM=|PGF z%#3us1U1$FWNUbQ6Y8C+|oGCR?9AF@^tX~;{=Zi#S(NHIqq z`N{TTGL1?i1q^Anai@1#*ot{c(ZBNY9Ih5!94==~&YpO|QKGjfrPK4cc6^QzwpMgc z2#1G1bo@c#^iGGfAQ_7ZW=qq&w0If}+4#uHw`B zH(&IHg^I9I!lt|wh)^?@D-7DxW5R@J65oyw1#1}-3EDi~zT3q-!rLR^tcm}4uZ6sG z`bm=Ly1HfiiWEUvgOv z#PT@JBW-*?fEzjxX?P7$-HPnyvBnN`5ed`6qnaurEwkj4C#C%(6d51=9Du>Ec*FK6 zF>4-)(cO-b#)i^xg^U`%pI7F_IM2}vTU@DL&fHCSRmCWqDOlFa;z6@MG_8JEf(^I` z>=7!(aM+&5WCnrF`zcC;g_E3NX?9}j^7@ow`R|d&b*Y<3_D*X8?}3J+g18NX!r+#z zUU|15vOIFziGtTG+^3dfhRafSEFUj?=#*Bk2@w*7*E!DBk_Vg1q7&g1{l947BTf^D zdpb+H!wAp%XrjtIZ|v{AefFxYfbD_;{V>H9p}h?H+U zTtNWk#@ajEtj{`-z`9;Fu82ShP=CEIJ;ECr1QTBCkW8Zj1c7|Aul{zx!^>Bf);ade z#!q$czJ6Si;}RB;^O~E+ez^+M(@ovAZp?v*nxdorELU%{mrwv8~m3QyP zJ3?HDfYIt?D!w|NI6z#qLyDo|G?pxShhRN~Hm75)_}#5qIfv z@t5Vpidoa-uWfVw9!5+=c;V|6f8n#gTpRLUE&N@de@f;23Ah7R^pPHPuS4+_v4 z`cC-a;?QBgdD8%M!GRp)BHo-i>Hu=UXcUM-#ZXW9H%DN2!tR#;Wy0^z7onjKL@JQ;`I7%CGtfQ1 zBhZ}Kq`+9G09HLIU-k_P$`K8Zc%%^#uvm<`8-3(VC8Vij`!Gp|MsGGeVdzrLj-+_% zkKZ|LU{JrS&yu@Lcx6VxrvkJ3G<&CU(M47n>PTg!xEYV|yA93I81|_b$cy{mVWxv4c;ULarw- z2vncw-FYuT>Mp^}Oj!J7SY;x=c9n}fM?X8I^`OQ_j}Bh`1?%`4J{wu;5e+;aggDVK z{q6lPwCcLo%Z#yt`6>K={-m~U zJmCET1{Nkv3Z}M?K^uc=$NHIfnT-F7fypEBrc}|j;%X2@$`_QWaX^a29&Q$ zHzz&sxP!jQ)=2a1t?FuDZE7GaZB@h%0*QABoUxJEsX1sTQTo%RUC;D!6l{=XW6H6HsO}o`IBu(jlCC$s(0xuB zjJyqbBeUxJ?EX+dZgn`Y+yJg0z7t!MF3KB8?SMikRFI;n4)?J|;&U7-CxPXSCOYgY zD)1yTpIJgkh9EFLu9s`KTByJAf||vw{Oz}`GsfA|XL2yF#sb!_24ntDZzq7Zf0WwS z%9QkmZ>@>2k$VU?`zB-Qv(C(^RQN;bo>t?<7#O~61R;WYf3DfO5zH>2db&+eh}t4# z6vifPzD^7iyI;k!T&RlyRRjRMgVgG$Pd!$(i#=1t+IWZaoA!32l7uJVLj5!&rc$pR zer$Z)Fpv%dL4@eZt+L7X;16(79e$0iN)Msk+mDe9?3YdL=R!y3v=-+Q{d*Uu=i^G! z%`TH_>Kp)onlL@+<1&co|8{qG_v!5?i+TxS2GL0E{dD{Lc>44$)mWNM=09f z1a7U(@Jy5Vjmms_7O2AK<)-t~FW;(}BHy2gxcbO7E|(jczV4)G3^@-R7vkx;EX%1U zb{$qj_u59Dxi#t10f`{YBCRe^X_13TVDj>axky04G{5}$UR6$qr)%n74hI%_zxX}D zhjpG<>H`u);oxyCC)P>1GFQW3{FlEmY1=x~wr$|gKUf>I_9fLum;zq42FquML7w~j ze`3-qFCGB~+5V)P&|#$w4!u+F-%$Ef@xJ+8{R1pFlTx4t85`MjC;IoQX)N&Anh-E?VYr58f(=Hw z+n`fzjt{2Zz5JKvfJs?_rzm=3#A~w@Ua|))CHD557#J9=bQq$UQ7%hvL5)W(sbI+b z7uVHiqOf}h5>isbKQA2>ewmJ&G+p)DaKL(DdLt9#LKm~Co?a;2Q`nf-%HrPe7n6w1 z7uUPY1wu_xJ%7Xlnl4xkQVD*O|K>uKevdRX!SdN`mj44M_(0{|+ZjLjo-+G#(ZwaJ zFSWM21+@kl>6_=sUPT@cZ=plN$S%gPmqInb(u)5SvNC-Q z7aY!8qjsmnazEX&N?}z=GzdeMxbiD2q~_VduF9_^bq`A6Q4Jq&W0!+O;@!iBm)v^$ z)e?N}1i$VMUAClJ@;1jJQNiW7;GjuLN)!gezcTQ^S5dji(!j=+$dL^i5tlQiqNR#B zU#h(l^coTvzU!S$Uz|;zwNxPBPt2^_|I0M|H$&iT;mToo*~S8aLiAd$ruEJ!kyQ;# zDuSdtknju9L0G1U2XF0yJ(}3#NtBs|MJGFY0;_fp#p5yZVbj@A(uNu}876Wb7kT-a`x(8XlnerCvGFng?NV{&3`{!_ayoN zQ)=;;;oDsePN67~*8EYK5FeY!P7yj zwCZ2C^gMurFTP-OW{9Q5JLff&=rGNK!27-AVA$&zG3*``eAJFLLWGy(5c|NZ{yZ|? zvE_a~a6?J(R;F1vjLP6o!LrenG+VjJ4YP6EYedUee9a3&(b2Xh8y7c87!3Ddla?8<1vu3?`Y~}5h9@!iMSQ9Jmep_acpmuo--}sK z1x=>hXk8Vg@Il%9-DPIyN#W%~>w{14MqF)4wfJYgCi1@vM)b0Tt%T%+F~$}upRbpq zzq&^LpjUdGM~Gcum9@2OLrpEBC5mW#(IY;vvhKz15Knx6RKx+RMm8*Bz{?PiIUy4Y zv4h>e%y8>8987c@8V!eCVeGf8QAq5r2j-*>xRhJWqW!(u#pty6MhhFp`vk8?JE{U@ zk;S#g!l_tNf4q}R-1RYe=9{}0K!fu_GDLHc8y&|4ZaOYw%1_VOB9L8_=dmeM+B0I3 zrDabLRNgdZQ3XO=L2KpfH%nJ7L4&~R!8PG9t zVMu8O=`QK+5D<_CX%L3)?w+&x&p8)!CF8~H{l06hC)V@7oRwq+7Sn+t@~cJa1k7kj zu#%19NIc`fGINN30JH85+g6#PDmwx0?Evsh<_6m;KDeh<7TaUePC41C2# zxnxOc>31S82g(nxcRk8(Za>O2+}t?d-ta=1gk7Lm?E#Svdm_*W4Cq}DRK~%<9}=W* z_#gT6lVx{Wy7C%$DZrPNNmxL`L4Q~V10p`KYe5*<>NPkeB)gv)jL4Y}lh+c7;-kOx zx;CQ)x6T<{xy*a8EHY!zmKg!BKi=afiBx5WrQ_r#*zcjC&J7S3M^TMI9!- zSXpdXv^#gJ;?AExRW1`yf8V#Oq&>DHbwLR(uVS>etCWB2v~y*DSjgfA?In|bk`D}` zPs*?)GS-Wp!miwogecz24g0qG5grGH#;B+kp+7_d!E_F(CoXw)hU$l ze(Q{g^5sq6Y`;{2J4scXMHpZGss2=xQCXU82h)R~z=`ezV`ZY}QFMYO$RpOb*Yl6Y zPjk?Xk+Izk8M2*5&WY`RPJzTq-BHm)IhZFqPIh)%JUtH|oVp(@dxn76`ZNJAmba(( zm4J2kX2E!`{ixdPU#m-_8 z3;2W|KUbLJE1m0Z!WmtIwYc^{P)m)0RMR&j-s%N2gk>V#hkJ7G$4I&1jBa@)8w#PFJuQo#W+~g?7C&Io;)G~*#JFhhPtDr!f zPv1l4pBT7c!vAv_4i&Y7|A5qT1La2N6j%!yL~F;P_=)&(ME z=!CZjp(h^8N0~wOe6>L?ot<`L<_55>HaYB^ zC-nDia>l2?5X!l~Zxjei&98s{8oeR)P~}vJ{s(I{r%?GMht=M z3^nC%3R_&J2n-?YFArut7JEpO1N1LmGWAPI>YNWntLrDBW+>2YNRhESso2us-vy)H zI^rHh-WKo$4y`ow6@Ve%;$%BtZdc&Ra)FS`w{zP4bZR!7O!Ew!WvhuSK3>D>>Nv2d zokM}e67lz!7fVkAKYmT<3l4IateHb>iU|+~-58MH)RbcUa;R;;SQ}z7nkw~8U#aw) zy0Q?SlGYwgqQfl8kr^cc%=ATw)2p-lk@B?Q@xG6a1e8{r;u@Vg%e{&QJ|ZzszSJR8g>hftii%+@*0=mVR{c8b5<>^w*W8tEIo+dAvX z`KO>PcSzzaYmPFMCKwas39D}W@WDqrDovs}-9kCN7L52E*$Poj5}U0S@j~&L=^|VWPz098py9Sedt%We_V~J(zjg? zg7LT*4-XI1FSfR}t%v0mI<3wdloc}Q1FbGw_dtB`0MJ>E_G1YBj;70r8k$kWtSEGv zM}D;NelT6u>XB)Eq$ntSluUk0ROsEGHq2RBX?ED66L~<<)HEzk2Lc+hTq5ZI{YXg} zFor%Zl;!1P!s(_?bHyzf2W7vtD(#RgJ~!{}B$+f03;BPG)RZE2IZt-aMgKNACHh~- zJAGFce6Cov{~FX)EP$6)u+hDLE_NQ*W{qy1v-!>s`yVVQtMl$GS|4SB7-KVhuye8s z(j3C`)jRA+oz`6VzCv#eZ9S{wT8^6No#)-vxt!RxOZx7V*co9T^-3Vd2$tNv#F?K% zdT)&wN-t-|ZbKXi^}rd~c9B;0@I|IjeE$y~(YfD2f0eVN&Flj=_mHA9vPTv>sx4+& zS*6F7PW&#VQ=PwWt6dk?umxO@6S_U^@XAM8#YR#+g>KamSPqI%A|NfW)VY^1F};fh zj;Yu_*0??FzBRmT<7Mvir!}w9o{14wMIHrc$I7#u5H?JEH5Tf%(V)GuNfIp@=@y$} z1oNYk+m2(Z$fJJWYj+zLp_s_Hg0b1;WRKX@6Dzg*hr925u`eeNr4i z)z^Q1Pfm%Tsj2x!6s!ZFG>v~0YI1jCdipTU4}(5|v(Em043V);pDRG0!M=hlQM$Qi zyaj2)!aCsJo0cIT#|S(R+rP~ysk2|I-){w0h#WUe#>-(d>C{q4-(!lZ;x4bOv;j8& zAVr847IxeH{Q9`ilbl0lJhEg^_jPsDh~)aHg^&;Uk1hA~)&MWFpxeywvF-(GlJKI& zEvWpCYP768rr;y{-88uQ+v=c7%SwILb7c~z=R4!q6Y==Ad%D;h4^+2qU-(jkJD%gy zYO>ecN7q!LYdfR%ozY{xTXF54ATQ8Z@XPmM2`)B_EI-^LT3y6c5J*Z|EoEQX{3rDF z9E&vdG!)7b)-1Z|fcu}c)qrAzUd3J9$J~41Aty5&DtrpBWSx{Dss^K$bgk{{vzlIh z6QSO!bJ1po-M3rXb9Sy9lf{JD!1WGTNmE9tIl^F zy`*Mi02(#)ROEGJzy~ME2m@#br>~T^VWx~nMcfFmm|qqILW8P?4!f&lhEu?j_n2MJ zLgDbRGp7t`fVIHoH}ccU{duqPUp+sHjVnM`kdp%h;KpjU61>~U;9xj?!3zmk`O~v4 zee5&v{rQ?)|%3(FanEt3+d1B z@J0+{b=3RT>W))>BmWDj!=od1H28#}u*p5M$+fP30e7o!Aw$y+ll(_aEZz5C$O7*- z(!d$`==B?OPwBpoEh_ePDDS^2laJ$d=s-sT?1S#)LLC$@Dym6&Nzm&X8nDEH{!t!; z9sTafvxBr_RY_hR6+IF@cTz?|0)E4pkP;!LAQAW>fIblkog}I)ryb|e4102OM#}c| zqtfeIwo5`}C$qiZo!qDHTc?Z>PtcqrECuU$;{mZXinkXRyw-6@xLrQ?tAL^B;-Bty zZ#9C?()B-OKXKaFf~l@191}TTtzOML?L4O&tv{?abxf~3 zBd`8S{TAUjNXVg-DRt#B=YC^uizn>9|1LCM^s*T*zwc7#~``aD;80D_&_N+ zBP^RZ=4#a>1?|z&%)45%uvVEb@dN?#l0j#V4+Mw%PiN5ugGNA7E)M!_4$OWf%)W3S zC0ZYB1)@oj$v)AMQR~+LMgp3w@4)gB*9oBqA)z#IFB^5}$zIF#=B82r?6GwyO z0Ih?&VS=90;;DPaOa0T|uJ1F8pw!%TSHqG%u{1@bFPm%n3Un1j& zv_S%`b%+*z=_Q#I&*$Qx4payqdX^A4;{D%o`C!L_5WPRje-`+uuwldrxsD;<-D7q$ zv_iYsj%wp%#$_qD#`w&$*ojw(CF|)4yD^gurQ}H4?hYPf;aVW9O+C_msl_5uvg>@SmI+2S4@yI@m9~J1FmLqb2GkNGVqnp*ZioF{ha_jXyAKLe7w|P zrBYL9vIBaLjz?zyu(Fg(kH}%Z%6iLB9F5?s))hFL*RIx1{w&sJxHTZO@Z3u4)N zaUj3!Q)FB6(tl|{TpSX)|1SVK8fkWbZ?LnqvDp|Bd1Y5fWs3g!Ing2=_;m+qs)3&1 zH;(e}xeYR4a{zz|P`@FSz!4?!m{ol~GKPLR{nsWp>GOOsBr^cxVJI`hg*z94AASD( zo^=q!g)6lI@a^%Akedh;phe5?621UWec8scclxsW-s@savGlz6g-5-|NjUN}x@!|3q9U9rp$J8+N!Kk)L6} za9SyIR7M)iIp(A`5n*dd`C&H9Ye_ zdVAKwvgc4nZ&EY}!bP4r%%B&Bvz4z;brnlLao4@p zIH(tJ7S<@hNsV16&NEtmiM1jZ&ocEDMPp1to$!yi-+=uMC^Z<&+oiTELoz0k`1rXW zDiMAm=d0WFxcy!DZwzhlvPS*f*d2uCzDhxeUUj>zq=C}A-aUkO%_VZem0#3%MS#G?pPJtHKz-9$nV0QcX@g?1_+E} zQC@qQ=7IoD{Xb6Hc^;kAowE8My5izu!RPb-&gaX~&WGL1EC7A(%4w;sO$6=+8Ign# zQ5a=kep&1r4-dhM?Qbq->3nXM|6G<2N32Oggy--Lf%dvx=*S29ntR@3w^w^DB2sD! zwQR60S+SBy1|53}05ii_w!n{cPOIe1%*@&tNn|mg8917+G8>8~We)*1HF*n?0dVBt z_VBm5C3}~rTWQd`)cZT5Y>SJ7{I;r4DKEo`i2-G-+h)pSYZ1vLn%QHG%NoINHcqS3 zw^DS!V@9~2HoEUj-9)exut?XI;qHQ9njBE*D4ILb2A?lDBL5akR~%$j`T%J=WnYyFkNRizqMRM{3UEYPjqzQ1*_`3C`G! z_xm@+eOn{v*kEmHIxPU8cVG&S>qyT6Q0>-H&5kkGJptS*Cb(|{8~|OdTRLXP!7wER zhsw=*3*5OZZvBG?$<^z@NWYcDrPlora4{69vj03+9I^s#m%7NzHh)LI&A|R!#E>Wy zwOEzlb^@rvY9A72d&H(v@p?OCBZmVipV)r6HfKxu9uD|Prvt#4Kw5d20!tg12jbr_ z`6=X$odw8NXw-F?^e{lQDhEB{Yz-ys#S5H9q4d4M&h({O*j|-@v`b0w;&>PmBg)>GK>sV4BI!lgGaU&>mrwn@2WZu9($H5Nk)(_WQ8 z&6f?YWObfEoSCe}D=pc3vK`q*w59Yb>*!ehY(HG>HcOG%| z4e9tr@`CrjtDqm>p0a0>wc#{c`o89hmYKa$$+0nTdNP0zJp6bt$zX5$tGJ)cu2I^} z82|kCjCzG;1oiNd^gn8RVVxLE2bul_P~ulmN*q2uewP^yj5PA64j~3?no-v*!d$t% z7DEUV^vmOG6Y_@aW;gV-ta%X?W<8NA7`>F!(PN3tUA#Nh=&pe<@KetNX4Tw9fU9Rm z_F3Rvhk?rgUI4$6r{||TNRY@6BVT$?Kkb8JtaG)y=KwBdA2JwyN36o2Yb%lgv*|w5 zHUB4vZXh6T2w_DW`SxUG=q(a`D?x;8W;8d7RKU}Dh}3Ip9$#N|)0wyRpd_=P;(jmN zi%i?dhlY=j?{hYPN)YgBsM_`QvH=K;HF1b3p$VaE`3G>`=C44bz=xEy69Oik;1$x2 zFQ)q$KT#dn3(8g+LRz4nA8MX&?SMv~{+ZdsWoJIS{P`QL;%1szyy@Q}Irid5p^K-N zk zgL-gAlK5dLeeB~>_-CI7e9Ni_B(t-2{K?Mf>0pRz#^n7|&5)3XyEe8c40E8jE#9*g z{VW}F{0CS8+x1+9MB&9~*{QPx&_;&N?;^TD`FU;pYslIe=0_XH!PgU_OBMmo#%b;r{}y^ z7nE-Ly-Y?&=uQ@AoKuxb?jJ?eZhw$(I@qia{4m;mJgRv^?+ObFDoC0N3PLlTy3+>- zr5%CB985FYUj08w)oO>=kRaefu*!0I$Fm9`DJqxVg3nvz&~@PJbOHjIs)f=w4^21H zHa7I>>FM5GB?}#pmH0zBy1xbq`BL`~&PmrL3rPGsH{c#GZfyLnnE7ddq_nFVO=2Mt z@2J_Y2teu=TlZlNS$ zVf1Q!@!oB-2;j=j@2VDbd1h4rfCQy><8gQ=wI@i+aL;X)aPH_7;%f;7GoI11-7|t- zzqJ^t1Se*M=zC&tXBF3vCNt%_q~tA77jJlDDDJUi)0z6H#MI@)J5T?UgjQNS&aE(k z2n`dqm+ysTKIc`u3&=v)CAKT_qozZBUtn}Nr?JF8Vn(6D{8dGq8h~Mjg6SU#P$Rb< zOTKM5^KQ!PVbH@_x6sv^aU%O-7tle^H^7W)s|vmV%o+Cx`#hFxG)-tq!JR! z25>;)9?0ZTO%GXDbz?JaIe8nr)qy#HMifmvz#&2&=ru83Lq}z$i|~Vj&uuaDZ;SdGffC z27*J|+S;aF{8m;}Q5B-O7Q!&?4WK9;o+mN4uU1x8oObh-yqSHs5%S{QS_U@Mqrjjm zeR8UNzn^cc-hB}U7P>AkPtM7i*q<&O9v+s{+k`|{A8BZ6su~%^Pwsu~AdpX%1i%KM z-qEqL)=cia#uQxp?(w*fW9yM3ml^N-0>z0pu5Hh#W zPGTv$yO@4c!lNN`qt;^-LgVi?aZ1bxipu}MFGOpPFgG3g&Q|=x>4{3Wtz0BIy)ABB zJ#F}_b>h`+ussp494?!` z_tQKKhc@IS4t0j_nwl==^__<7gGT5}e^(L8z!_CoJs}s!h}-+Vh#9@CN*xUdbGGOzOtZ+)D&G!Q1Now*j_SGU#(RYU zN0?4P;~4QjmxQlT(mTe+AcH03KKtnXI1~Lect*R$7e_?keL#8oh5(gg;|ggJ{?)75 zbZnrew6*pr7LCjgj`s0)v2lApL&Px_Q zKG;6LSd4t9pN7tRkkVs|AL@{Dp=B47v&0?Tp<>hj}O**1<tS85xuF;8uHl- zqR>Ty;wMH6{5u#W;hAtKty3({jG`!)J+qRPRaWo;#pZm>R>}8^h*Q^%);}-ZKPw~E z#gpVENrO1IEjSl6bub!uv$5VUn2^T+uC8{}Y1OKHOHhw-J}*add4!@Z5l=|wvC>i-v3A+ zLiG14%V#&;z-K4Lu;M{{9nu!Ei(z!ie|GI-p7m@223<`EoYlG?sBv&|zOhkl0NO>* z%zRQ#wcq;MYhZJxtIOf|k9UIq==}X_QAx=lXb{{2*zC)!Lo15g_@v8fTU&W)+h8qG z+BM6)39&*0D%RHLAcns~n!cLZzW^gBpoAp>sFPSSp~pnHpls`mL13O*>hkl-@(Ulg z{Hpf^cpIyY;WDFDaEljQg>P%hXs0*?`*I^#%2t-dAC~zWRiMc*?L1LfhrZ*|AH`QL zPbB!eXA!^Q(1O8iWy{UUvVU?-Sl_?@YOaF<6n)4F-bb09#rA<&OK#B?P%ub}8QPu- zm6Cx3-6zi5_R0P`o1+(BB#o@N(}d~ZG~&e^3yy@X4%9`92T<0n@8j=LQPYG2tTnUn zq&9Xlo#Gu3qrf%>*SvcALx)A6Z+;t1i*BgEg!s>6!4R3UcFEo#@L}goR}RQ2p^1z+ zF$K>3A9&EBn~Z}F+D3|y;DHT)2hWL*FM(P$@gFdOb5E?TxaA=GkJy&Ik+6LbcmSIK z{!sAK-)a`6uz@ZJ026>)D(fO{$<`8P8J@~q-b#N>kaWnt3r z;HVy@s9_`kDGA$#Y>_blSFxi}v+%9E-U(BxI5dOt5Pdf34y4p{tXI%H_%N5 zz)FnvYMF}9?RV!2$$&{5V37bhHBbkn-5Y%Xv7}@Tyj*ha$_a4nGXQzKur}6p%7n|c zbY5}y!k)+^iC}HARL~=S$O{s-KD}&m8|tZwSjzb2eWIn_uGHq}$mo1eBybv)J*N|6 zCn0JwW(iM?ey;4zm9wo5O@p4E#t#@&HxA}m+*T^Z54N5R>BfQ)W31>^nmJBefu6>0 zJP{(`)Awpq7>pg}yz;K}ofrP-&zIA@?^`AE!y~&b3Rw~Kj=r(fmirurz~BqzwnMSwe!Ii(+hqzYn;K&tm`M1O}DC@ctDYkD`d@bQwZ^55G_6)6_j1 zzm^AfN6Im$pKJl@own>z9zdu7g|#S!sJ_1b_1bdVVXo|v=|?^dHOgZr8Dx05dWNcY zb;FKysh}sN&2>nLs2D$qCfe537*xqCq+C=d4F1FUzN>W%p6k`Two01?-#6uMZ|lL< zldVOBc%}_LPi(bN;l1(>Bg)zaY;FO(uDPJ!zE^(zMv2iN1!Tq2$K0Can_@Gh<1bU$ zoo87}C)8)_5PynOK2DT93S9JoF>W=EtN()jJ%kD0iJo&W&;(2vVRqD$S38!xY>~2? zc`1IBbn$93{Pd$~rm?3AqW;sx=D+9rGBeq_o3)r(eDpeJ2)fp+mK1j(6>Qw0Vw|_D zLD_^vOX$a+EP@R~f7{&^uGzbUf(Z-c6O{z7=m0sye)pmjoMD2q^GnBP;hiH1UtvJ^ zz%gHGia^QengF91^HLMPw~d}Jih}a9;o5isC8B>{j$<;QxT=Yp76@HLnWUs`^(=*- z!#|^D1Y6Z1S{6CL-m4?{p6L0^ zw;uu6f&72np4172zCq%E);|$CZljcWVYJv%ILlqKd;N z&(Bw~!z{m(l9iMW+?Co+onq7d1{1S-dfu)Ct*kPkasmI`V=+71geBUo*d^6$80I4N zmIg$jzdb(L2nM{49NLQ6!^JqIG|zWc(KEcXHHnIf9`^Q?2{^HiRkt&G>5JoeboKrC z+Br2XPiA_=5pZ|2)6s_X;Jm=eccb`oeFfxi?SSMR#WI^@B%`PoLGVPbr9kk%=^9q> z(w)=(W1hA*@e%Qtk%eP)q@{Z?qqr zeE#|DWe+vl%oOwx=fjMjbw^Eg$n$Vcwj$4clLpShMJoGu zKqr_5b~)GXBrFw^K}Ayr-JC2Jn-|_6nLZ1K5HSD^Hso=>WwF*YOjinkwW{oZ#m?&= z84!Q*S#reJ%^=$wjstkl#5fNm%NcLZvccuA9NmW_@29YtgIFB%nG9m*xNM>qAxyM- zy9E{{kr(%0_I88&d(g9Sio#=di)V~t z#Nv6TJj{KB!8(vFhRx&Z` zI*3DdXf9AO(z5C}-+AQp7KFpy>*2Kc$i>7aFMGTkI0SYQ-wrghkZ8u(75rqU1m``N z2>7<pxv%v&lwR@OkwwZTY1`(PNtbf!$?+) zGpe8hqHl;F@g8CucBihF53467k&^NfN-ncg(SyUK$luC-ROoc!VvhrHAKU%@c#F=} z+SAAS*z$7O`dHI_WQjbR4BhHEzL1l&R{3mxSobJ6q6m7z#U&&=Lf|hj`zpzT%Nj)Y zUN=-eN^h4tj}Z#mbp2=>0m4IR0^u0YR&8N<;Tq{wGA4(Id$$fU^CnGrfqVT;z4Tcz zu1&tAsbzh&Rxkqwu&Hn;y>f&&*e!K$w{f0 z&MC(9mbi50zb+9v@dPm2b&@W2VCzpvPdfMHGvdaXWl;Hu^9XK35oCJuU}8-5{LuH@ zlT>+u`uKUBuO#d8gN6+GHx!g}o&{{{6D=}kIRN8Jzx7|WesO-E4{unJk=&2pGA=7F z=cewxO0d*%AUuP5TIj&l)t!1=!$Npn*U;S_Jkrx6*~+o@(6^#9P+oero|P*STYx?h z=*9-n3jWM;Trl7$8(8E8E}Z2BO~o&)BYo_j$N{A>jGjtqS2I*Rf=`LSm|O%;CIOX3 zk&vmTdi(Z;l`B z5$ci`6VnTuep^^VvCkr&WYzH$ieDwqpZpt}nx=SDC_w?gY}d5zadl&Bw}AS4MwwCT3QZ1{KY`Rq5T{+O@x`m=1>8NBON^;02r*o zuC8q8%UMvra7nM4s%kHwa>KO3qNBnx?+BZ)Ge8atKr+*O?RlK5N&c~*Nkos9{Zg?O zb@AFvNU|0&Lg+Uj3&OTq;l+Fo6af|>AtRhWCBzP0=QF^d8B$}{>dAxIbgnK%khQ#U zL^REL1N(l31_k%I|2cD+#0@<>A)j$JIm#O(wziWz)AD4d=QvF_+iJEmYJAtFFD~IC zkU0C=ltb7~u}D@7G>Fg{$H2bA*|;d^+)3s|P^cN(9mJ^ zW*D@lcQC<)T;zL;&w%WRWYLzHUsvZE3y>UzrumLd8+flajqx2w#tfz=Cw-HU*CRsz z@xG}ar@f)nqcsT>U8i2kW+ny!FBtsHX_W&{ji(}h?f;4&i=pM}_Hb8kbHe~m&-JuzZ?F-AOY^uO5K_{ANhFK`es%!A7jZxr*@qjSn2yS+=cHr zvBqKQOK^7y(Y0)gJfd9RQeh;K#8kVxTWo4bM#?gsKDPvc_Yu5CWsTL|CP#CZPvT{h zN8`fdv^alnGxCj}-WxQ!VkGjX+hUIE*n#GF2K)Wu4g8V~9dv|Kaavens@sysfw7s~ zcSwRvhw5ylw7?QnZT=Mjupe$zQy(^NI+csP562dvPshW~zW;POgzT|eirWJ37bwlu zJFRsxH1(+cn){`}WxAE#oBul$F?3w?Tn zC1zAviiW%%!#6-N-1xp*T8aE=P+d(O=X<$4#k)Oulj$JeR0M@&NlVMlygBEj>}*mT zCYeKq3nzVEse_?x#g%#~E*_pMfHuAY=;Qm^xHA=SziCd;tB+&Pi>K$=S~geO zcYE~j-?d_5m26tFxG6RXd4%I_b;oU->WdZ5huLV%&_u6&x~1Gt&E0pPjd|KD0u;g@@3nK0>BE?Q z3YHU&-$VNYgno<3Dyu5e&_sM;|;KoptEgPE|Gt=`Rgg#(a4=N}ZOlyFM(0Nkl~H-6U7 zNT+Ty;KCn}hly1&Ae&KQ=B5sNp1znQZjU!5ykA}sLk1!}BB~4R_{$ZgQt`~Cq*XNl zU#8|X_Xmju6eI~rD$_=1gs#HMB{np3UFijn`m3v9W|l+k;*g5W7ySi7YIciw{K!bj zob`SO?fGnA!N(8+z$@D5T^f9b`^6nd1Xxp?72pA(Fg9Qec`+r>If};&y}WeAXV&{o zogmn^JD!O`qdyFrXs%X59%uJ&wt7--Xm9H5Zm9L$8#?QM(0S#390cVQX=xt>xD?Av zdPtu4AWHx9i15>&dTh6G;s~sTYIe+PbZtCJ1mRp(X~}bEmye~e5?`;&`Com0O;G{f z1Xpdg#6xgJw~5}wpOd`6 zuVbW!M)yrl@+-_P9O$!UJPU*qHIYG%mq>&&;aH+&W;$?`<`PihOvqdElh zvPCRe({IW2D;6voPzmI|d?d4q?GEdqPviCZhR{*zJPfwk<{NiIHj$V96eRL`opu73 zF*u6kdGV|HLx4Ptb$+*@f~{{S`u8 zmq(w(wd^JIvHQYx@A@`7tPD? zd5rV1^?K+d{ZCn2ZNy+xLYHW?j@1c3g{Hn11PwqZ;e|;P`ZfCc1Z`cvU8w6&uOsBG zx0N`Y|3V3-AR+VLteX*n`TTW=ItAtpc?U}?%uVT31?chBI-&sjkzLkgga&B|x!Wlc z!8w#~SNY|9mQ7j-;5Ug$NzR3XMn?lA@}^~WKnsTVgdNUBui;1P-KT-?&{0RE93LDU zvYM(v<)w1HDaxk5uo0@_>BqtN1%`?46VzvyxAxT>+pn zDdJOO$s$6;rVCX^J530-4e51XoFVbZNPzeie#dt=hN zpB$BVHxS-Dm}CLhssQh?V0X?ckoA^0HLMI(I;erh`CZy4rc~E!QASx4g@`_vzfJM? zZw}*C#{@_HE;DYxnP9hX{BI9!ZbSSu|5*r^2!`E(DM|JWkFp=U>Uh?KDvBJ|yj&07 znaU;b3_K_C-GdC~_o3iKYWaC8p)77K$P;1MZ>=v6!1J5ioroa}YX^1NdEzYoXNOZ} zS-O#7K;l)o%fagRL+h{B7P zTe9SILU(KfR~=>+o4aS{9aoCJHrlJf3kds~=7p(T_va#ivLNt2$R?n^qY*+}T1pXA z8g{*IGZRk*u@Lcu!+D`*V7dgnni}q9$G{t0ydDYGI!HjmfRhIMMZsL znonu&bT?P-Ibu~y~NpfP-SXcYoT$kqOBLJ0wpMg@X zd@BDD0;uDgET|8lu!TSYD_!|EL;&Hu4p1h$`-KU+r6Mo%Zv=E#SC=-PJ&-CWWP@KA zbjdRay$CYTau=@e_ZO-kk1ujZM2g5`{Bzdp)$?SyJC=^~qQ9SnncI169-@dRSIRe*HPtwkpoFU5q7?b?zetiES{zMytz*T)tMyd3+iu8|+?of>bD zX+@OpGwpsZH}|2ngYtOcX=%{WkjAz{y?J0LurC{S+j?%7@3LcW+b`2ZRmbHAQgg9h zx-JIr>P8-t!B(gDyxGC?4CGJR8aUSf<=D+kA$kbte>B!#*i$=KGH(fMWc|zuWqG5P z$pKzkf`eu6UOXo7Qd8ZU4fb{3S4;3wAI{+-HKaefC7;6v?pJCi*@wh_O~ojc0!ST{*F;KV<_d;RsWbM+k;MjuwrXc>y&i@kYyzwU=>&L5E^h9d zgBM_eJ(eoo_CF9X&k2N;FHj564RJ&Xc4ozQUs_svKDH9sdb3^8$^n+6H~S+jCnW_M zJSCbZb3hJYBIkW7#h_%IcWCE1&T+@pw})P<7@VypOxqxN^EPquyYOdswPQ+#8ePkJ zix%(adLG_64X&hr{ASVc4B`8&M@2?Q!UM~&FnB|Ddsr4LylICxiBj>we(9pZHQBab$O1jT93FT?b?yg$ zDdk{*kRxF2Wv>4vN=?@-(c7}JHwT)khjI+4u*71Mh$p48p+|1HQ(aYXI80byi>$uO zE)*9P;rziLg`2?E&dz5mj%QOVxhXFJP%r|EIsEQUrFan9@k;_BwC>FIG%)BP;~Ua- z1J!CD!fV=Q3uAU=HDF+PNxGDOg`JFpJDJOLBO%4>vTs&ay|Tic!Ro#-gqSvo-is~M z1EsQP4+CHeMWy~dhI|5F$K=QspePkzOWi*Mo|vnKO`~naNk6ls?H9k{lvv!qN{4@C zr>WoJ5fhzrUn`p{!r{uDs(%GzQY|YuTECmi%sk(2|3jFv7{Q8lS}3pj_n#$qSnxc? zr^1#;mA^GJq>Cm{!? ztmOYQfC@L^y9yfXdbFQB+iXdc5jIzC)4ZXqWE8vp*U}tH=4w6kKfrghz zeR*zUM#u2<%6QmWILYKyHGXtv3w-vzyu7q_JpB}s0V7>wwEJ;qOQLgBqpr#hW|A)(-K3ReRek%sa-clbuAf-S@$cuBTDqMV?!`&|4^L+q z6=mCY;i0=hx(5)DmQZPsZUh8GKtj5uJEdWemXvOglJ1c1ZX~3o`@4AFwLX7Zi#7K> zSL}1|V|UL!a?R9omioEkRfrZU z|E6_Ta}D=NTv%XwJic}BsV!SL0cXTyP0t72A_-dFmB>2bYcFYdB@U|2eXG-3DM+JE zO11NBFoeOP22ox5 zva|MAN+9|EG-L$3?Vl8Q&I?)B@3?yb!qIXo`*4gYQSkM%wce0XO*Jgw&&J%1d2)o-O( z)`KvlnVH!RSz^g3$T&Y)x^x7-0<2@G{uLR4(iKb8H~NEXE^a^q_<2%uJ9jmYV6W*K z@;CK}L;Nc}9Xx73FT-oDY8eI5Yd3KWB!zrZxSZK`p=$39uJ3}R(^S6A=Ug~@vABEB z9s=x7TV*T`vsiIciZ5`{zJzYbITK0CvO{#jqc3X``|7?ek7_HuJ z(YP$-H36@zgF1K$slO~IEcp1Qr2`(7bc*kRAGD8(dSyHw7yR3Luf;S)MRPn z^XF`|ABf5oamy?sN(WgrJHO)TKk4lp*KPVo0&=qir zzWws$Xx#-2Z=YzAOGn;`amvEU#bpg)PlQQeQ>KfR$?*4?+wI}mbCt7sc(d_%B~mr~ zu;uIT17YB$e~rh58BkTlKG4Y^pJ2C}n^+SNdE|gcba7tq3uWSJ<_PMGK@Ah$vMewYeHE36u@a3)6e?uux-qeL`zC+scrq zqjVzh=sIDM|4JJR-~SA=eexRH+uIAt;b`D^a)5DJTtcMq7ij0! zI)vOb&n|^LE>AizIJ&H7=0qOjGVTJ6qJGgh-;15C;L!QkoP5?Qko7AxB}qXSj(F_h_pVmKc%F5X=N3XIQ>4Sy9Gbx z=Vv~8uvJP)T(&0S0yLF}W|Q3cg+>6`QGPN&bu=e1wVUiF)=odNto^> z`u%Z-)JfVM&y_+647aKl7qq_oyYL-(jiesME8oa~rUS1s$KAyA{SER~WTd_up+ABp z@#(m2%M-q+|B~3h$eI-oNGLfu0@eIuJFKlwOq_th?akaIUxd{i7xmMLdlzFE3W78Cgps?t zpcB6G4`C7+NyqoeqlG#!Abw_lKBYA=OVT(_KfwKzv2K05=Kusv1X!Xg)8E@?!BHjH z;NFuT7*iKyg7cVCYAZyhF1|#WKhgi>FNMV-WPKhjHJ8qt`KbEsBP+y-Eh39D`EMZo zPywkvO}s2r_7fZXCf7tuADWK=+ zb@z@!NSHW<^ZiG?{)0WOs-`@D8S2B*{{}^EaSPJ+^=E|KBSI;wWpY2UXmInuJq`rf zJ^BLYK_pu4@yNm7qX+;a(~YoEPU2TB zEoVG%=k*7M%U8sf|K$IuaaEmA2gw8(XCk~YKw1d2)WtQ7oKA7qxzOlHvYPN#=g*wN z8S>6S=E$zaVoAv=!_q;pcc*}$iV2wgQr3P?fzAqNzy$^6Uhj0H+azIM5pRxSt+)h$ zk0v&Lw)HKrc27-BA*jt=-rkpd_yq$`xYBM-9K0bvlCKYC{_~0*ILpb&^$q^+*yd*{ z&Q6S1A5lF8K!vX_lX4%=<4rz1He1GWZil#wxZlJY8#hwwI>VKP^2nw+(lJj&x2p;u z@8WiVZ&rdjNm?H|?QD}9Polo}D@DahhI|;*9$-3-k2`UAyl&2RHncw2BY`Prc5|~( zLt5_!Eq-Vh+DNYPGhDO6&m;d5?OnO;b4>ub%6S&CERYlRR^c91Vg?LCpt4dk^x&t3 zvZ6{zS0;xtXsfEcd(-f&{qP^Ac2&pByUyFo$3Q)5c7^)CIC<$E!0p)A*GB?#yij<$ z`atnXVVmMUAb_l)5%=27GhL6)c&cZg!zW{2C?gXIp<)a2&A9UwPcR2sOe*?{z_W*Y zxaw|{4KP(nxWCi_d*Tq9)}oZu*wv0B5aX_N8=>g+Bj4*9C&4_vDl1+5`Gqom`9c^aM%=fq#Ygt*qV0FdrB3-# z9Z%Gy%2Zx?#Y|p#gqQp#9Qxh9LXlnCzSyRX&9x#k{b>x# zkMk1H5O(56jt1L5$;7^XAP&F{ybF=`9L~l_<>~(5_Sdv+_41aPgX72%K64`B2>9NN zcgJ(VwR7$A)&r|62p5=A^UOg~m)>+r?Jt8_)pm3buMG?Ewm8y_uJoq>AdPy(Hql!0 z7kKmDbgAEyKU4(+I1r97EOhdA#g@5nSOLeQSPj@%pR91RM$ud#4wJOqay?{=j~((v z^DqP0Ht_pTybi0C%JnaMphyhQ>x^*1bvhuad)B8aT?r6 zE~YKJSR9CAwmh+!8FrazqVhs(zKJrU(iNq#1|B?q)5ziveB#T`I{cgghrXh9=nf$E zNOTYlcg$jO@$rMH-~g3Jj0Cx8$PX93+mt`Rn^uLAJrK8m52cS=d?uCr`m9&#&8V1<=9X#mU?!z-&E8A?Fqn@Pk-NT%m98QzpgjGL7*fV!XcQ9ygG`zUq z6%@@|BKbZ;uW$ob~@JTN$Ln($7G_m=Txv|D35AL5DMHVqMemF^qz^+V z1zyLcfnHbU*G4iN6iwY$wgPJ7uWmZ1)$2TjhT^b#EvxQoApv{l0`0XDd(%z0`DUv}?f0ngrszYizE5IBwjBIPf{jFDb&$#!|h z{(TDkLcWUc|8#JyNiJ1Yhf1&dsrT2?=Jsxr^n+B zo+44>ji*Sw@lW_nenB_(9iUhHOu!cTF`N$K4MuU;+4^$jRbS1(nEvYs_*1}M;OT_yT6Re9v2J0 z!g|)7M#i>wAp80F@o6D7AJ6$1z2Wq_5QqxkkZ1cLletk4#1&9A_vO=R@V1J;YQcp6 zZ^QO*8__Bd>2WhMuX_f)6br4FQ5GLyQ2BSy!{QdL63%)*yXE~{DLf*SjquTlGp0m*aci6PON z#@8%w_f5a}XMb{WQ?bJSu=|QoK{&Xh5Tl~;Hhr|8J^A-55uZSpqvqIoy*-8a`)49n z&v^*o@H1IV+sc1$Da&%lK@5aV)oF&1=BScXF{Puq2oU;Jj0K!a=F0D89cDiyS1`Q- zXwan&AC+b=BIiUV(?9AC^Y-8#bBg8jyoX0mjY$2zRrfrkm3g*2x=>HE-nirCzwcYy z5)gIns_Wa6{9Y^K(U%{EE5cD~t#{PnX_DFWZ2M|$CKPFpt!Jfg{~Z7aeEv#r7!_l- z>f@vud>wn2z9vltox82JBG>{`BbAL}Fi*J6al)ICnuX=s7GSPD36>lZ=mF)#RTOIr?k=cPNN?FMaNyA^aW_x`I{_-OcN zqptG4IbRWljJCo5U(2^)m#FQH=3~ugo?rL|S-1!g`}>pw$d&kYah6wNE1c#=d0F^4 zBvh@qE=?Jxh_LQ}6=7#cub-?xZkYpH za#x2-J{?$nO&6DM`I(*K&tLkz12bM?7StaVZgzjd7*#xkK57sUJzi3LdGx?(W;?k| zU@DFwD8C_Ko(sCCKBjs8{dES%(!ZA|(@jIgbv_6}qqny#a~f9Hq+YLUnMS(1)WZws z<`tI8Ogmcq!sC92Nm}@&u4})_ANSR#CvG+k{Gk3$m5i)f)GQ)fNnbV|;@6rtB3K2Eal&aWdmS`AeHB_ z7$9a8QDPHTA7()O)0kiZQq79R(JeW4?D)*O8A^=3_pi=co%S zUSYO(2SSU<*Uc1tP=;dP37h)V%G1`q2I9kP(_RZYF(0*quQ?lN<`s3=hmUO^{Cl58aGv*x!yN| zA`=3V_FlqKhgjoaG*$F)%>vF>pCkzyH-+si~ZG&g!L8ywYr?sN*`Uj!5V6melgywzF0 zIV|8-oC<_=f>{mOJ(N^){a*n+bu(F7II%q6ps^o06Xv${dpFfvlq{Ba>Zhearf%~; z;j42uia4HIUab_}w=?t^cF(yy2CbR7IV}n1y3=bgO56ZqCNBq|D`sfkn`krg^C$is zejWbA@!5!a6BQAG=_yS86qyEi)m8z!RnZlALc`44g%fLi>D4xD?&%EaF^xwYUY^U* z+bEUE62Y9XW>nf3XecR<=`pbDaf0_mnEI))U~=ZNq6h{Rl%@>v7=HmFR3`gH{89cU zR6plWfR?%5$umEMMo#|+VRNPkCul0=(ovAXy5&DRq{o=|h{rM-kiIHUNkr6WLY;cc z?&?BMCXa;HjzJE+g?ulQwl3;zOw_OG z%>$=rJq9*kO12MASX?8tb0QVzM@B^?<`W@G~7^{TSx~slyP)#h%1KQ zD-Dsv%jwooXHmZ%CI8>3x9qzaI+B~+f)v$kZ8}S9-O4kEs7bi$Gg7^Hsx(74xi z#?d)5TxQ*knY$NO&7+>eZSNaScQ@OmNFLyug4CT%Z#zDvk}aGza8qmjR{W}M`w zRwk1~O5PtntTSM~mU7e)x0hq!o>XfgV$JIfKP1*6jKyZfawuOFSn|678|iZjrE*zZ zbvB{kuQE6WhnsLqH7;O6ApGEgeRqTR_u;7fK?~lgm?D%8aTh6f@bzPZcMS#@T_*kS zjW~?Rh_PIQ#AXtIw(wl3>pmVWL|l$D;QU%tRH9oK^+do1Q8@~311-WJFkA%KJ^Yr_ zv?ES%LMfoT2JV}yKO8>&PwkIE$eJ)4M9-J4r|_{i7E+HpBeNLt$WGtsBZYhj)4ChQ zQGt?iLvdGEQ?qt@?Fz!t8}JIUHL>>pu4RNumiOI$^xfXz^gWE5AYRyXHhPmT4^a~c z){`$SSd_U&Ri{6+7?6CG`L`+p05CuxUsD*hc{wQqBm4me!Mu7+{UW3a{_X{RV0-)d zk;A0h%$a4-$ksp3tsNdx$FpP_#D|2}@CS;B3uGsJ@5XdId>iyKM(ylaA3(0LMH5pm zy!;s_B88bRN=6M%w*^w3H!tASiIbK8T7fZ*sVKxtBbS2%0 zujGm_7}zuy=t@T;KgDs~#F*Wy?Yf3pV>=%#_6K8;&(<3v46(6IR@YS)Rz|U~MVQvn z=DO2NA&{EKq$9I4<>Yml;hc4)z4G1E-9Blk@iaiyNw5;NyvR0XEUQ30EYE#7xioWq zpa-%Smw4>QBkRKm!Qty&i3nZ0kB_|qUBDUPu_KNO#iJhh&5rU9(v8cY-4Vam7QaSg z>wt{cD&_bqvB%5$iVVJhJ^n14(19suW7s*n;2)lDT&0OCKFTS6%wKqqy1>8z8+6Al z*UZ|XJIssOk;z}zgx(;o83Fn4fV}3=xWPCmKJ$Xfw=;rDwuu`bJ?K5qFrPSlAkBJM zv#Ae*hJ}UI15n$q!-hQ)A$O<2Q)=%k+NZyvTWdvn_B)5usfk=m<$hM5#A`x7ma>z2 z5sSBMoLMF;Y?KbPj96N-rsQ>>#{#ANNg=JXp?sQyzj59NS9Rk1BqgWRx5ihJI zPI5LVc&T`KQ0C@pSPJUZPbhe)fh>jxa-+c(cMH>F4gaj~-tgm_7x6A22EQR4T@uUm z(036TNiE4;vnvA(i~{&o83qVs-}!c%8_pLO@YNJEYw%opAjN@DG?x$~u@R#XgpU9# z1?ZaMOX=tPH+Hmu&18f`gp^mXieZ^GWLt}P#)xaCu7KBW9&~&n{Jx2ZC1pbr{_Xo8 zTkUn`-nRr4JaU{JOBC?N%vFPaK_Kp|otJxGLWuUP|&VMegtXasNvX zdtrDv+n13d`!E?>x05{^_bv)`wJY>ES9=S|l*Cjwa#EG+7#iNYp<47v|f9Y=%`SMCp8&~9h6O5V4F;svi- z+ZFFVsCa(Juc9JgxpmT0u8-z)($Q*q`*$&}@AclEmuD_Ligdpu&}z)^0&yq#U*cki zsA^1vl>(_9fX6ZbAS?(+#(7yA9k2g9bCrWZ;0s5HC*dT@~o#CuWHMrEX^e`zi=zTAZW6x3VXklrqS>vpH#~}Y+`W|N;b^U6%1JzKoJ|xSDS~hU7=*p*ton@46k*gPzuCte&blc zr_hLr_PSW)^<#HwJz4|R{`9vO{dyvek8Ih!0uHR!0OT{j_ZvAKA`=WXbjknZk`GgT znU2Qf8T3lOG4=`FlJ^#8)pmCbMlilw$~L+aFc1|Gadp&0^1)f)3;G~CmoD1!5_077 zY-%Vkc%4yB+6##ysorX{v)kyFFbbrQ~kRIPHR$F7nG& zZE4n*?gg;x(0|bKLk+oMkD4Bs6{UGiI31^v0i9RgA!}%Do*4qPS8yUKRL`fHUZsZ3 zrch-OxxQ7k`}HAoghQ6|);LF-$&bpfL-cAZ4=D|zpI$u0T{pd{onW7&dS+>;sP4olQ#i6{-^_Ua z!3{m+sxYsv)p~$-D-_L!8dd`RuwPM8A@K6Q7sSqO-Qy}kCdt4<-M^GAm_SydLGrct~7}HZuR!+(n`$MHj==E z0<&XuY(dvdzmf1Lm`FgL5oSLQ>KyH9YZtSxD50dfN^qo1Y05pa+dpbv;ug(@+xEIQ zktYz8%!W^c9As$3bK3UdRUfb%%*Mq>Vbh1QK%DfXXU^-Vqt>Jn{%juk;jTJ$7Ho=J z1e9vY)VJc|?#h@2-DR3@9l@0+KS#Z4Tkt=w3n(8_5SA1-UEQ`z6+sX-vkYuN=kbd{ z^vgUEz4Q899|KJucl&%AztY;b-l``LBDMc*@RZr-mjy`OSlDQn1ft(rapIseIt9Zx4lEHJpX3kSyP# zx-FVjjR+w8;wY~{xyXLcL<1NGJ0%u)*~%TkGD$ZWb5y9iqxh}!)E9Ml|7s#)5Jh#P zEy8ymztGC&5p)@rvAo9bbLMr!t|(0WjWr}wpOIy`vqr<$OUq&pJd5iUPYxO>kl?Ax z&b+Mnjqau!HRvncTM%`n)?nP;IE8w;~9Vh8;$9E z8^~oyM~OUK<=pb~@%?LTUFl@P5mLx$VKFzapPmn7%je9S@yhw(I$pU=h%Z<{z*$T> zKkf5!-(*5O9Cx-ZaOOK3yw4t1em6TRKcmB;6xtw&GW4bLr zFnZ22X4;=VN?+m4yxy7EP=pMlMT|rt(4xd*nqyGE{#QD3(vBcvG!ud`M!&@3ikK%b z_o&(SfLwoUc6?STm-`aM+a9}QF{Ij+{M}1h_YYdZA~LtXDeLnh!rVSo_5=Amr*xiL z9f@XHAMVX1w%75BtbqTI=GX`>bA@AcT-*RKSSK3fuhY+g5@o`)%KR?@zPb>~G`O z-USyAbw5CV&wt1NuJ}IW(NPfnLaVJMQTkq}?S+oHXv6=+&uG0ru(fBpy8W_U*-P~! zH3Nv~E1ay9fWRVbAoxd5zn9OcM5ZTcLq%!B)ziRgR$5l{ zg3KMlYm1waNj)Fk6Rvup{{DR*&^mB#Px^~yNj!6WphUG zAz7Y7o?^4xg(pdiKNhgB-?y75?D~$MGsBl%#x8AX6ZydQRx{Zyv{zq2*N+L#6jP# zS`XM#rayc6NaH%*i$bzWpa+&K-!$<`fq0 z#uTfAMYh@c%Eh{))skR{y2&c6`oJQLbN zXUFqfyKm~~Vile^H6x1>3q>xY&DJ5ypX);i(I#5FN&W5@`X77FjWcdby*1PTa_ge$ z6QDmjwfaZ+5-%NrZCbXzmZG#U%1(~~oPc6`bTIYb#OCRMogWVkU^r&0PM_`smz%Ey zq#}R(>3sY0#f^S>J189Z_U)UBj*h!zfDfnElE-T}ihe}V@sR>(ve0eDvlI)w@C@H2 z@=pj}sR)e2e7dL?y@|Id3uI{3e5?IRqg&jAvmdCUZZB+Q23QS2n^EV23TdOCP1_dw z)$J#B3cKN?mP!={Mf+7oYj@mup3CNX>O?;E(sRUk6Nkr05i$1t1Cl5u*JlQ*qmBo|j04tXl^55~_Q zmI>oUrX}75ncFT)-SnTpSEBmiy+m;BHAzoXh}er5F2f_kN zS0|?a@fjGC$|ew4js$+;L5oSrOp&3pNAk7!$pQiWGGG6Dd(_l~&`N${4j`?f8o1=s z(k8R)qXE4KfU=^2r>SOS-C{=1h4RT%07_s1aP#D8xB=jaISaO+h3N_Y3hJ{9T#!yb z;-&)H}gC-?>LV~&&g6h9!e!;;XFf1yZ-Rb ztu@4t7gV{&eB|T5m^zbtY1DWFhfrW*F-{RZWe`)fa`z7`>q*;VZ?&LdYUT?G%-Cf@+In&1KOa#u+Eu$&+*3-+0DA z1G9{Fz#gBH-LA%uA?ZVsaO8L5R=F`G&$CiTh2~2?#SZ%#(|QmyMN~BreD4T@#8{!r zuHY*26DWsHv%}0F;D4U!&_wbL>W2JxW(b}*=vi}EyX;B0>y1~?!F*FxdYS1xS)lq< z>(%?`?=A6m^5^t^X=*P?6B7n-QGoLD+Q$R3&L$wbZa;zO|NQxbrgfi-oqGbeF#aW> zADt3!n)d10;W<`EIjGBOzJ|9Z>faKrO>M8$mQ{IDu%ASOc>kPPQIattbXm<2f%D4Z&M^6b+@V5 zaFEck5t{H(N5FNnxE~q{dzF6@ztY^c?&BH#n{8m@2+oi5#WH%M<=O&vW+)k&1dJvC#G_x~?FC_GyKTG-4~c z#8N}QWGh^mf^9wjv&5NDtG^cZ<0|f?K7FKPU|$6+iu`@ zIO$#eU7QV+i|YNrhTY>60u(8<+l7iolsqlYcarY1uFiCG?U-u&+xDtYb}r)^8=h#l%YonvskyfHEP+B&fOP^!+ZtnOCZTpscbMiEngQ;o z(twG4<)ce`d3kxj+*7>q1gMs$0zhTK6U2i9v=_@zG+ild?aw|4{f9Z{Klo8+zR{nd z(&6tuzop3b-WrCA^&;x+kOlhhh~^Zsn<`K~!l6QC58uzl#X!+$F`?SK_`75UYAsNs zfxaG|OG$3!dot#UC|Q>2nO_c3ohIm*&k|x@Z%h6|5WZ{V8^3!PsP<|?EUhV=fd-Dg z)Jbd&!b4a27uT9)N+kut%Vt2sj>oy9D&@TvYTI?<{_^)7unJ1OJI2|X9kk5kS##QS zKd`R-C|KRu@3j^!0LQVu^x0tIANvf1^8mS5N&04d3b1gO&6$%RS(fxY8$!j#=-ZE5 z+QL_t${XEq?U<$))9rdkZF3WAgS)GT5}Db*k8_hNnt#6-I1)&>%$=G{Z;tS{&3IN9 z&PE|ReqP3@pJ34byaMa3noMT@>$^?Nt84LsJ-PMJiUh$L8p##!i}+_EFXmBK{^nW2 z4!H^7&reo(fT96kiX|-wH=5Xu!!Q+;a_%MznH}8Y!Sml#g2;a7-nsIw1ui6!|E_!B zlT?;^T|-2qhXeNZ=#TZ!B&7TI)q!ygP%3(a5)%`9fz6#~vt3hLoAgNV7=;83DM zWL52z(-qg^TWL&sDj`o-G#z<*sW#~X2N6(3b$qlaS`Q2_6}l5|nSSVLAf>fRZx_4Y z?d`I{N(=^A88>n`V!v#?6$tC4WkzT`|6N8X_%o`P#}O{8{LweTqet1&5~UGl}q_WSCRD$-~_>xRIO4jMO=9E0uvdOwBSdoR`#b zZs-?0XC?gb;Apj^nRRgeqjEbMxsB%*u?@3UBK{T)te-wJQg?5JX~*A(PtSiH<(2f` z?D#4PtK5DDbNj2+|FE9BwMS9Z9|qlc&cYk?FnrCZXe%Or&x%7}UZQyALL97g8d7dJ zz?`}IM&zxUhqfZsi;hgt==g5=lALbnJT!zu7J#=n-TrOn9OBXYNfJS%i_qIle|ASO z1k0fEpp%KktqGCim5X4qVRcEGVb{$a`P{i>IyGW5h^l6Jd{N@ZlwjiSpfIr-CX9L< zw(Vz0?)WF|BX2qMa5bVY8G>b^G6k=niov9?qgJIHi1=NI@{6TVETxHRV#skB3(Lci zThEe*UIeSJu~sbMFIMfdKwz_vSf+*;XHsmD}Y@Ak@F@Se29=qIo2ri93xizA;{7VrBs+@+U#W zZGJ|SByg`7P2-UMgI=)DC8M|o3aS>R9;+?76gA6m$H|S*b3+|TVw+=1tM8jhn zZ*HhUPo4}W=8tJpzb=%Q#nSOc&nW$vP%a(IYW}ayJgCzCf&!AAM>5R9C>dI?; z6%=uRwA51-m(q5h68dWjY-8;xSqsWSMBk<50ZINHezsNQ_uAY!B(JTma8KIBziJaD z(g>07PKFz75#xI@zV`2cW*DKN>Ai8F^X-q(AfFh%;+r$dJ9%jJR+zeo=5n`4bwWoJ zZdjZ|zWV)r*&|JzqK|m6(=9sDK=+3m@Nw==X>97bAs1pa;MdDpL>uVy!x9y6klzM7 zM)?=Mt@ugyJ5_PYS&@Gve>aaj=tVMI^Z1E6{CB?7j0J2Pl$_b2M`x#Rfp4t0oQwQX zrDCx8ve*}*y)*Mz@o0Q6e3Mmn6(EE;soVP}?m>p5?^+ypRsy_f0(au1BKDoT>KQx} zV)tv8<5sZ_LQj)~f~ela5T#yIMF-xdgR~jup1KiNPL8Q@mk@x{0v^QEh*svlMbQlH z#{a4PjNf)SCM_Ry(moN%L47F$X2fK*LReXI{NMx!XZIDdKUBJOHwj_xL)hrc>I>8A z9v%Nb9>+)XGN3A1JpOj9xs)Vp6;BqmPl-Q$jw$BxoO(5#`*U0F146qtckKA1iwLjs zU1Se`I(HDf2f)~=-JS`#Mx{gxV*c*8wp`8!)m=RnFPtP)3BeeTM8x|phA}HRd|2ZW zh~v_O$e?nKe!yY67 zA$vMz?j{-l;_&fX&}Own0sSuS*pS-aCc}afRg<61G^*i#rcNdM@&W9UcySn`O5c6& zx-7binsHIZ8pyYzLZ{UFe`KAzVREFs>U1;OQZeWIgX>|90nEMx2U5*iz% zHEZwkmM6y$iBZQ&&g3cThi@&eY#HjO5X*l(VdVS4i2I-fuzRi zHyVM@fK*BQzLxxP7z>QH=qp^v!G*S=?#uggL~w<%TzvPB{Q=JWRGB(qwSnWQIIZj# z5-)w?{O3bivqs|CpUGf9lNDL}PAgPrJ75928+l`c|8^Ov6ZU`~$lQGQE5WUNa8c3o zFmdR3O}0l4K7>z~qxf@d1v6rjq}XG{sQk(;yt(q*vJq7reZ2{ z>!nuU9w=r5z<;1)8xAIMWu&aO#>J|^(FBT~)mFPh2Mowrz>Wov;?l)9_wLQ$dtaxX zI-1<*j7X@-Ls649fGTTD~upGvw*h2gxA+giNxKuQuqjlxCdrt3-1jUg21P zuYx68`$>RD5f}uHA^IA<Mk+} zERfaOIILIay(ig!_c%EnQ$~<}cELc$(vug2t0H+;twNJ^p8a9~(2w0@hEIV0dkQE5 zp`tVoM=bs5z@rx0gwe`>T=keG3R7Jye17;3IQTE*&z9D>$QjPN&dFiS~T&eie_ zo`PmS;c&~fnADIUs1jU~Q13455?Tx9)Dph^dWeHpX4R$oDPc{Xj|db8RhKW%5j{UB zac}*SQD<9L zWbKz3A^XN=03$2$pNDmWv%Q%>SsQzyO)xK7^Hu8b*IGO5QESJBrW0r9t!Yj-?# z#evt{xx@Y69@3a)@_i7ZgsuA!IfHOJS@K>{0Hw{;=lV9{)eR;nT!l0y2rGMdb!RpW zynRnUPEwqZ?%eUx;_>vE)aT5}^^iXvtNybCTixmt^HXj4?VLd}WNFOIsmt zrj?Z$Hgqy3^_#8oK%1v`UpEkegO#i*3{hmO0@U!se;;TVD+5wvrKu?8xAB@L{* zf`%Dg%3nhcbj7Nwj%7wIEiVtV)h%{JQNF|yj0b=|H$mT`zn zYY~UI!*_@k_ec=oKnUM29W#fg1?{I2*=i-q-R4(@pRRQIo<~Zi-@TCb^ji|Wia+)) zy!Vocca_x~{2h``_F8Bed4!8V#c{l09)byHdC{3Xp-wZOZFU>*u$l`H|@xA{2bS?FKP=jn1lCS~DIU zVm0l91gixE!CnfgXQ(9~{jYY2zWyqgkBJR%n$G1k!BwJnSeHYVg-KOow9&>w9eRdW zBdG7ggAqgw26UtC3=TfcTVQ9t73b1`1BnqHw$!noN`@n0TAU3iRpK<~uF(MbV5dwv z6%T4lwE4@q^6D{ObdkTbz7fXa&Moepy-?;po_C{D_#HdjkX12chWhTFj!lt z#NTmb8InnC!7C1S_6G&TM>|)7r?&=t-N0LOa_!liF_4CSIxK$Eaecp6NsSqcyBF6l$7^W60}?Y1OUOx+hTMFxo8b9S-QRA-m#lCP05@M$FkkyH5&mN@kFy_QB>f^ z9JXSfRmB?eKAnX>txvW|dPTPNEvX+eB5E~<9c%S1`;l0g=KHZDT|fFiJi;ErVDBB> z0-6WgYtsA~bV#VHG>Xrx0%n&rt6|-TC;d6%Nq<(`S7M5yVma*q^f_#%!?VS}<@zj@ z=UHa@`ffW}A+vS1`ga7NMeX<4rq{*|-U7~}8*qat>`Mz_CVb7PS*0GFPCmrc!Vsmw zHCSo!nw*|ybei4~_uq%l;bHfabyP@g?({1sI=LXIuqJX+W|ZY7jC}k0u`xyjk?p0d zXETE~Bfi2q^@F)2?AVxit)D&nEa@k{joME~YftJ%#41+dwK%uov3H9P6l;H@5-|OC zIV+mUOrsAUugIE@Wl24oW~zxWrPc|89xATOi-?vNu-(i#g^8o6j2IEP7rjcbci7Yt z8gfzGt!Bb~vpeJb4p8SmS;UUg>Oz1ikI_UW`N5}yy??f`?w@Z&8MX2Qa7SC5eTXky1(FU&w0H47)IYFph z?QTg|W#T8YpmF$GXPvcm$GEwyX2b8O?o~O^aluRtaJQhAGvjaiWBTmD#m>C~D%U2O zuToYkKrXJIyL;%LCmKIb3p?v(rTuK@PIaGMNp>>tljW&ZqL`80k@HJt`y;jlLtifR z*;^h=A$#rf&q)Tl2vX6KX~uO~dA%sLQCz=_y-!FisI|xcAmc`~VOSb|uu-dQdM=(+ zeEX)zaLRjD9mBdk3OMs=0=1b{mJx{*O>0EMIu(!yDG_+WXcywDn>TP#l)beqP9!AO zg?;jw2=cf4Lc7exd^}g@H=eNu?&uLc@_gNQfIb$*=3IF{8bGtKGNG)R7(kq+xq4d1 zurX|ut+W$Farc=CO}I)3M+9T4^$J)}q?vW(KWm zqB5j<|As$2&fV?_^q1{@K=8is%QmGjC5%X7>xaFqwMw2l!J4m(TNJQwfnG9Rd!8au zR1+{o(MD}_5t4RSbyp=xzx-jCZobtaaeHe-JX84h@1eza_cP1$v%(@rr7v1(^OmJ9 z%>#bvoiS%F+5DIRphTp~W~l*x=B``mIpJ?cNFXk+B19mn6TmP9+(DA*hlq;bNz{PG zpVq8g(``=XTYahJ+fudsM^RMv7PynG%Y@Q-)=kS8{ZaG;4}Zym*>5n;a+@VYv%I26NLG+n>3m$a>|zA^sAdKz~RV zM{P1br>9ATr!)9A!s8)qglAn>0^f$2{_z0~%$H#9@HEuQfV(jB7P3d4I{8IfE`}v0 zK0PD^G1|&hSe;a@QAR72o490fJ9>0H+%C3e=C`R>(BJNOHzAw96&9)6Ztu$^e%sp% zX_<_L^JJ5%$2T2`&rC+Wj1^M4FjX8F;vY%3ypdwFrKa<0(Xr8|V>@6w?EMxM;3Sli zB7Zx$#|Cj6=#gLU!4SEF|38whGAhchYY*Kpq#)fAN{2{yBaL*2fPj?bP=YAk-67pb zw}f;_GxSi>9W&qkyx(Fie!;JEpR@PA_7&&T@Q{Bs9bG>VE!(iClZbWQ3-ZzW|E7^I zd+By98zDes*9sIki@r-yKcgSv|O8A?G69a@upFoqi7&HdOHd~K{POKpm#wj zTR2Q$DwL8^8>t;eA3`5ivN>32(8;RoRoLVaCyIFeEoEXP_P{{-A;9|A>3CapL*VlR zGsRP=GRQhkOP%JY{>a^(@Z-%tD*#UWY!>_;^-V|hO|KVoCDX-7`$p^I4&?Gp6j4n;A_-pJ( zRSUE`3RzAl^qMS4#M3qismRW_H1vZ$MXKVY3dT3&gS`>iS)_t1QB)sZ7y3Ezu;J^> z5)?cBCW7nq>XVuy5yJifepUYX^k29|q1vQOKb%0X>*G1zzO6`Q#Z$ppkd~g}2h7^5 z1c`%6EuEDjTbst78KXh@Wc=#<)hF@ZP}SPp$3LRF$)abXTrZwpm_XSvm+G;?$7=9& zhcQ^2#8rC6XNRoDA6% zf{f?VuM<4^2_&-s`F|<2ZmO)CGY7ek46*haANfmb6LapiSs@Nen3mE|K`cKIkF6;Z-TC!?AQCf$wK_ zag7U>uKhUA4!DYRGlfEeT!o{mkCy&)JFk2Y=<%p^&IFPQ-JkBWY0X_snGB&XD5$p2&pT=JlEP zyl8Zx+#Y(*n~&eRy}~xzb2u;smEO8~Ctd%?2FJBn~HK8`0{i%!I@6E4bdn zEbZCPT(G6|i9LkMtmW*88x`_+igZvj-Xm=tv2He}nol9Frzf>Y+fKg@W zWhEJ?F*NL#T54wsl#3dWc99+EQA-@k9e8r|&0lg{!Zq&?^jf%;o5jzanaHN-%l+zw~LN4c-0h>dBE2k_la0I}=c}$BoLCb=$Y2-pvus+Yk9$6qLKX+moL=LQaS<{$ zKcdOgbVV=bWE%tw>Gnv>9yOIPHk%j(>^38q1_qS{ap?CC4=ra))Kto0-Q@k;Q=Vc* z_Zr1oUvh+O5@pz!`}_N;M8Cw<)zytiaCi_4{*&z3MX7aK^U#1CW-Zo)AaWSLD$(|b z{d-Zi(XICkm#h3u=eB=cUvqw+btJyP#2T(7_ha2#8f-lK!ObIUo!qiNHITT9e&Ebm zV|&n_=|a0B9MeR`5p((rTz>geYf_4%SI`m9`4~<3sZY+`$@XGEA^RrxuE6`{yK%2% z_RTr_hsdHDwqTD9TK<`TiZlb2^~n~DvDZbz-ISguc4TU(WGsC5XH& z!nSS@Q+5l^6N;GEd6}#EySz{T>ft;S1R@XSwoUiS7x6q)jp147?FNSgQP7!Z?5E3p zX1TkixZHYi^#i%ALq{2vsl)=zjoa7}@Nf(Un9x6-e z5`O)2BRFs|HaBB$Ogjt6Dz(6;~i53{0DNM4T$sOQ*VqCOT*^B_9$A?FQnW51mSr0;ABDdq@=&AXG> z@n>&Lc{ECb^C^Y585M{6>^~{|;NL1vSVUo+&RoIrsFK|mnb-$U47}={r<|tDI49z% z3{E6hb55fk1@NcU2ujK!r&D>Ck0?4@+T`fAkG#mE3t+>}$eur*Y5zuDS>!2v?gZs& z+i@yxg-xrnC)@S<2OBE75sP`J1>#ir4FQi{s_qW@7J%n^i9>>Fx#S;D6Q`9NEkInU z`uXn6!q?BQ!k!*HH5cA;#nQ6xi>XJRsjDLT!b{?Z|6K$(uVin8!7Jn!Cb2DtYvs2P zGW)}~(#O3<6e3S;2%EbP3lY3uHghw=_+1tG> z+%S_Olo`fI=+XZQ^Nq>7ui#RJaKC);H-7h%cj6;G!Ko@bpgMa`t!1F0PD#tb>Mwfi z%+0fS^l1{A|L~jUn7R4Ur+`aB&gm9sDag1jr>3%#TuAS({Q_Oxf*^~1F-488sLnSg z`IyynD=t<)!dJsS7}Uo-&7k%r@|CHmo+VtDIK*OL*Cq6$FJgAsdhGTVn0xf5XuEX? zrAeZYJxqQb7F8xC072{cwBn+z{SvQq2iyKBUv&G9u9lTh;pDVo+>cG><^6e+S4Ijn zgS|fb9194;#P3$(ezYSnT66yd2cDQ@S>@BlJI*>H;uav(>uWyodRJTd5YBAgV`@#| z_F5{>!&wFH`U81gDuG2dRbK;j(YEb^avwh!38kUp>(m}8ze#;^iOR2O*LC?Zn<~sg z|9Ta2k^JaUEN5f&+aYKO-%DorgXUo1xyZK02}|=O_{0F0OPBIA>VlQQp!wdv8a@EE zd=qqkR;`=nN+=Cp?g-h(dBic~I}UcMTql&KOc}pso4}DbV(goX_e$>d>$@6ke_RJ1)C~z>GZ$CE+CmlU=4?tK)KQryDos6iS zNi=YaS=G=L4|tjol`-8g;KGsS*EJhmn-X~{@9sKAFVuwuP2lSIqCOZHIUS5b7ge~@ zvnnC#5<~9!4#HM4E$8IE{ZFqzMMvfY*PBsz76dot%1?LPpFB976NHMo2yVAkqqfwe zbg0iJPa;PPP7nP%bcnP@60Bv^umpP07W&dt)&IHmy5>ZdNn-|EWQo(%-(v-Rj0S0! zkwg&C?#-2HlO_vdBgMJr|n^H)|ucOKEiN-wcNf-IuS2zeh zRV6%hyWN_IP_G(5t#-B;OTf0EkEfz#Pxqzsh@J}H)n1NWpOGB$dsDhCmTGk!6DK+) zb74;Kx6ERG5=zccTaexJ$E$Bek0Z4AfF*}NczRv-a^7y%|9VMrRP!*X6l%V+{zCCs zT>iq9_7neymA~5b`v!jKF8A4xe8=s#O)ICkug)u`P~-Xj=An&;+$eITF!|KN_*FpU zk`!_Z=cwP)b{`FanwxW!H!iPLubw>{^wtk%FAc^`ODhRx;pg9^eM+skN?ofdVVjjO znY3vrGjB=dc{u>M$gCLdQ^aANsTd)P)~KGDMGw$rng>N)L9caopKk64`GkX~~o z*NtT|zCRap>16pOg8zL^yvy$2f@9^BGche?Z zwOjh|Z{ZDZ{#1eWD~4b@Dt6;{Vatec3qCKk zxzgmdC)kX8?4T8ER}=g(C}#JYdyQ5vf@H;2wy139Wvayejt3otgpT{botBc)>U6zx z{`$dDgwrgx1ujzyAFE`yRu^^y`9Obd&RNVO$3fI7C*@b4IHm+~f+Lf|V6>8cCxKa! zo8?gA(%BFR9zmUixcSUJ)CF%1zue$mXVjEm)Bd~Zd}5vDV5Css_Js-7ygv+ze+696 z^FcrOOK;9K94=I8`&m8F1Nk7&7i&Gb-SB%f;wO~FNQ2FnQ&W*yo%o$^wWeY=S+oYs z-bo)_!^ZF5PGA_yV2;n7KRZ4)MKL`ShxUe(MYU#p(<+@HvQ`&YLNm``>6N5gyVXz& zf$Xxz;lnRul9v|dDlS^hMTze!x)A=e5^sIZ%$}z&Vlr{yRgI%pmU6S#353Dw>@mq{ zbo?rNnx21Vn|(PJo+rA`8oD1zp>;9pymw!AZ@3so(@s3Rye!d?udc4> zs52L82v1Fo#{Zp50a zi>Rgy83G&H>{;v;w*u9iq_GQ}$!9R?nw_bGNBfr^)jG**+7Z3Vb2p*_Ceaa&PQk8> zjh+?a7bgO>9}6FR+G0ksIp(`AnX9B#mrz}Jh;jGGGIC(a&Pc9l$Qx)YDD8%bxckaMt zi{v>+8_Q*l^z}xr|41=C_#}_YUuSvt550YK?v`(9waUp!mMCM`)+s|W`J>%?NHNil zWciL@{NKnpqv4b+>Zm^%Ru8)`l+_7pnL+q{F1q+=E*%MA{Q=pC@9E->9w!qK8mX#h zE+s2ia}m-_1cQ+Locq+Vt7lj`d}M2-gyu;lSCT%_*i|ex$;RTt-vWHN)QHVE16mMF zgjvkZb1!BdW;`C9`z#Xce@#Fx(nfjEikUw>RTrWnex*>k?nWAUP9G&nKFT)U0w_w) zOCb-X{D6L-TjJ7>X`3gG$j&1!cm*CBnVOq-pKvrqnW-BjA@YrqdQ$nK(;8Wb25q5N zjitw3*EQF<;safc&stEpt^KD6*F@}a{@1_m<~&KKj>4BqO6~L<4Gqb5*eRNhenK*( zWUV+FJ$wH|PjyknbOrlgD+zbtY!R0uMC>@V&<-3+DRi*D>Slr|C&L9+@``0zn-)F` z`VD=bK@Cb$S-$U1lcbB&1?_MkU7U-?{x@gsPvwsoq4JwV&$jP9*TSP(s~%R^cDB8H z+;63V2Smj_pJm+kw(8Hrvy3rJ{jl%v{&CNwQl@5#bsu!<%ZGQUT#Jor4yHL^K3YO|@X(83H z|7@*bKB&k@|JkaZBH*W&72OXNK$h;dhWPK>u1D%s(WQW`jWyn*?15dZh8ff`t zd5B{U@C)O@^4%r3fQI)Li>`u}&*|V;X#|h34|vI&3SGTCOp>8I5R#LL(}%0AYei|4 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/async/parallel-burgers/parallel-burgers-03.png b/docs/en/docs/img/async/parallel-burgers/parallel-burgers-03.png new file mode 100644 index 0000000000000000000000000000000000000000..bea9ff0d8039df561b328eb803fafcc6f1346a0b GIT binary patch literal 168628 zcmeFZWmr^g`!76nH-a=G5`uILk}3!&p`r-V4bmVzFo1+eNjIpJfpm8dJ@nAc&^gpl z^Db`h=h^Rnf8EEiKkfMdbIir8b$0yDa}oAJLz#@2kr)I5k*TUEz5;=8fw$N?g!sU( zOUn4WAS@6_Rq@el&-CqCk92*v2Fw8>rLn<$#E0SzXTabc+#FSy?IXNnZ>1K)BJV%6 zz3>n`zR$!QJUp**B=6upCf>NyfY|ZP&v5mFLaO=)Ci@-SD>*Ox7@}8{r2aq@vXlv zm52_bcBZge!EjTN8}AfOA;Oet z_&=6|)Ii4{6JpS(zc1#!CEEKS_P_ELq2>ASTipNm_C_co|Ci-}d%pCDiD|RMKFw3? z8T;*aXJ+a!aEmb^tkT&aNkK?OB&ONj4w4wH!~b~;44UY#vY{fyFR^FJf``geHoF

=#QVj%&%>%sJeT;mwXEdseSj}_Ses%Ki?V) z6GP4)Zra)w+ovhYXzt@i(DGm7o{k$DDlOPT{3x&j4>K1ZUmC!( zwmUiS2P)bUcuF0(PrhNDNqqimydU~MD{?sSTT1M-yx_4Yv!9lTvR_7BII|GIocGac zV+ydQD!X3|4$HLyg1@k>2cU0zM!&Sw zm4{LLF+k>|RL#oDYD_{#T!3T#zaCbl&`crRymv}PbU_q~jm&BA`bbW>FXWieC+( z*vdsqx&S7R+M*`Kcd{dxc5e>!l$osUj^>U9S3VK3i7RJuGWcE+M8sh%aUA1Re5`ba z<3}CFDVG%uTxSPtnQi}wV{%-Rol(6U|MpS|Zsqb9wTJD?s&=pzdp|#zvlDYtj{V3W zWp-X=%;u&yBcqv^M8~HNYEJ6PnyejT>HZ>|V*C7K^b}^+S^wP7@_naHT5zGxH)=bO7mDVnK)wf539Bn0SXOr#b@=(+`>oU;MUL<5j5sElIj!0ForM2T_N{g+e(x2pfd5h<;Vkc6Av~MzVdT%E zKmaas#hXi$Xt79**4A<3C+->oUT8c(=W!Wv z&)IM1eEp$A#;Ej<^6_KjQm~!sOZm0#$7I;Iveif_e}13yjMFYwHEpVvi;*#VbX?+< z*0hSc`rR>cGNh&(j(9~RK)AJC1XVbBrh}_sa9$pGNGDK-|B;dfOQgE+uis48))5u>6sf68Vq%ImpF6`~aqXr= zv6Zuo;NrCByv(p8t>Uy^=7$n*`owxmdG0d|?08%UpyvJFWXUD{n=w;FK=^k=T?RD` z4e7kRbU_hJ(gnR2$;faz^&v252o3jgTN+@BXDq=<&l#i$77zBJ2&>?B_399YVNZsmsl zXlHmMeBNrsw=?xWph8p?%Ed7|jl=)+WVok`% zUNXb%uc|C`gB~yb&!bxjZs4dmmlS!*+h6}>Q-uT%#F_o;*QT?9Bv+>X@&(|L81v&=2lNC!XvD}jFwD|aZv?*V5(R9U6}-_C%L?8%Yhk^4-y#tSrd>CyA@KDiq%1D`*(X z1sFRS)BQ`upBpNE{wQcupT)#wWMor={$B5q!p*Iwrt->r4@ts{`E5bkcmEQx1Na03 z5W&^afPz+#0Jr~|NscU_h`T_@$+MQ(ISl;1gryVtSF0a#0OAytEM$ksu|U10|7jr@ zIB#xlZTC4w1rUL*99wWl+y4^5jhd+5yg6>C>5lK^@8yu@_V};t{-NRJOWn}roB_}g z8nQx9N8T6s|I-##!kgoXO2AOyYyU&&|9EP%=p)kHdJm-HdFCuMsYzv()p%h{$Kg=0xTHXe(tofj>t7&CBzL=EX6ne zj}~LFLBqC{JAQbeAIu1A9Zul z=bZI8pl?pM5zaFI^(a998x_`K0Xro_@ScG?e0mK3TE@qKn_B@U%1P}$wHmW+HxreO z^;aI|3V@3?jE~Z4POk%{q@f>HB48T7{9*X9FHQgS5b2o8uGBB$;2t`7C0Tuo$!zI-l0)dpnUNJtub{ZF# zBrinrFH>SSSG(~>ajXw4p)$w+wv-HF|47hUWeKteh26xlg^!K3D9G6X76`O8iH4X3a45GrO`{8z-i9L!Nin18LJ zJq{q5k1SGn+x(oc*@^S9yTk7P_Pw_VMVS~w?1fYG!37RZF~?u!AAASscxG>JHbto5 zUy#n+N|o?$(?)s|Uy6X3P`qbQ6`_-N^ynHGlrg z?LS)J1EST30+5qIQu<7NfbPEm-t!em!TVlJ@jsdaO5y)%F6W=SD`p>*_v$0=y~X;E zw=3O9_2?S2fV$}J)*|{>2gWzNf6h=7#ByQ@ow*GBUk-hvg*x?%2s;L;`u5BpK6s|$g#_+OhDW&py*vOeMtB`Nul zNDhUUIF$BA^f4_r)}EjO_RZcy0V)|0#aWrKsxz(=*vMe}M*`->B&0{!D-(32d5%Xc zFs>98(&qO$MT++Hy@ADWY1N7p})wG5?I`45W zj8qyaDw=;Oj#DLPo6(e^%dY*N{(>_QzLx#b7Gf-QS9Z@Tpe<7|G&^b33ROXeI zPIph)nP~s64upvA_>i3E>aCXM?a}kEasm0PIF7x3Q))B&{=2$5P>O!ZNP`9Oj&$`3 zu8&_)u+6}K9EqRdoSffzRkg)3AA*$5Wh<~d2|gL2xP+Vw>>pvFJuOqw(oOk zrl|_iZZ5y&=74=}MmW?&qv!_~15rpyV%|Fn-dutRpBbZaRV;tAH(q^Qk_%>PiE|Wy zY`pK%MIJ82MIWAhJT2Mm&mA2f_urvBB?UQj9$j>bMgl8pa-u^>aheZR) z$h7IT^zEW%Iltaw7`v?^AvIYO z!6=={;EHrvyoGR?k_z7^_`t*NQeSNC$5+DN)kBObw1qE)oC+{5SdcL%C?56EXcnyu z%*(~lfzCRk{knbRAS0kcHSg~-mRhuj%LsTQK!4He!~~ZugnT~=CymwSqy{rkf zy2y)!VCsxaVCTn_RSh@UIra;cf>gP9}(0%XT-1)e*%-0f!DL*=jTIaF_m z2DNTnWht&qP*r2DFTAdf5ni?ja%en=918vI z`;icnc9Ho`kK{zD-vhvC+Lwg^T!%r~2V&4*lWO>&mk)A1nKfm{Zg z0ca1Li|U6e*O+)Bepbu-44C727Ip^)@+XUt5|`ac4Dx;RLYtsOvF9P<=2rEEmAo-B z-K$Ru{W|k&t~|b{@edikA0e1G-Co@P&-rSr}Y+Z&Z{B_v5Ty)2E{_ z1urkJ(C|JcMK7-htE&}u(|%vpjNU!ba8EYr@aoGD4WqG&UJFWDzHRtQD2ECEGrD1@ z}0`ddzhKAK7eN*8;+!CE&lR)5sMgqA!O2~N3h7o zXY8ZfL(uT6hq&>0Op|h;*94JzyitTuzj73b`TFa7I{o#Ppmsw^5QXm{31k5Y{}rKN z4q_?E&EE>NmmVD#f2oIPFO{JrUgMo_LkMlzO<``Rdbmo_AP;xX7Nm5A{2;Ehfb zg-ff`i5S?*8%~crZMo&T*qHlb(patW&jR0qy zL@4=N*L5fH-b6=}nQCeQ8K}TomJi3hd+4iTI|hEc>6iG;!U{}KAgMN@&Y_v74(F0? zU685f)VFfF@2X`C@>3!+BV298`f+bwyriHBP4RzxK;`Xw%QB6p&W0M(ZTKKIB7@vZD9B{qwCN)JmM@;n9d@ z*@ul+_xbo#IUoL{Iiw8do@`vEtaDE~jD|LJUV6nz?WNO59RwZFu2`Ke4qLo%GJuo% zD{@2u04L+`8$29VgX80oZgp3qjtWZb(LFA`YVM&NZO=)nhK7g4~b;Zj>bra2-znHKQ^5EhG=GX+FC=RT*Z@7bdNIXvnx zQWIa6-bp5PR!m&tFJ=#GIIn#N1cLKhanGV0Mr4z5A(q<&?X9v6v93EHeBK< zCIjPVM2%=d=J&m%ZU#wijsp3m1@TLZurq}6%I(p25E??5svlpGzpMh=yUD|lj zOFI89&^|vGG5*KC6wy$C0L*fVRpw$CcjiJ|)+J{gwS_EwGxyL|ZSG&1D1)mX@_)YVz! z0d+oKYrYs*hoI(LvtN30P^~RA$}$-K`1AorRTpM%WmV?1_;|M-r7`EZOtxFQB_L40 z|2QOp6n5xV#GnemDILG;FIVWJOWm&u=L&W4sd`IglpnoIR1%uFGQR1tx=Qi@JiS9u zzOC)TNONz0o-$NXAY0n1vJfywAGjG>d3Y?pEvU`pSH~A?xCV#8?9C zk&_~zkZd$lTPVfa6Si~8J)ijtohCn7%sHaT^RSzHvfBCXkac^FvHa@dWH9WCZQ8rU+XlBY4_UKHdW?=Kx}l((!rNw*j`y&larR%xraC3>0+k} zb%}zz&ikG^F9y@cCM0AojSS8Cu3jEwuKg_dC7+s~_^5NOYTqX!7f3R=Ph`(lZ`??v zODT=f>nOnItcVZ3bYX;%sY5shbylAeDhMTEtl}7&JPJ@3U)g_Kr9u6Hu0LEKvAQ2{ z#ks`VPotEh$+D_2B%!3FRFBHoTPcCD0>#KV4GP)cws-2`?Slv&v-K!lIj-`)7!@JZ z@w5Vgqn- z{Zzi2cvNp)9;VlLtXo-MS+J!A?G5y&;CM8`&;%x+8(8;Nn|twM&?~HXt2UnXVKW=_ z;BKQgieR$FLQ7si&D?tPB~9~gPfyQMqMpOI@81LEFBd7m8s}*iT4DpPf`yCmXsvTo zqG3&an0lHw4kfWNA@0YkCia_li1giIt7!dxS6hbTHICfvObQaN=Dwjk6GO1IM5ENA zSsmJlD_vwL;)&gQy_i^vO7Ynww^X$UMb*{PX?pp>k)Qt8uEAk$ERawnb`zk+{li|t zGB1=ReA;fRH2U!KLm>SfkBQ_&OBIH1#Z0cAUX;{x;M|ywF#uOiG+EJ0SJ{0(8pP@$ zXUX}#E%fHrDklwBMFY?!EoMIzH8~XPR8Q{%TAkbSUYlP?w-IBnQDxE&!9SGAn(#<_ z#WN1z8FMJ4=aY`sjTf_oMUIjfH)i-xP}Fj<+wwyP^o%;>6ah6IS4Ca;{^WS_C<_nN z96?sUk(YLMnAD^;e3zxZyddO(-l>*|owkv{lk+cc&*c@se9v#1*cdF@NQn}cZoUGe ziol&TNf)O*)@KwfUn^@rcZPTa;jyGY!n?{qtZDVM3cd&1JB>>-YA%kYO7}ecneM*J zp&_@-+!>LjBJ|-hzrxYS7Drm5A)(RaSXana3}SvKLg4n73Kr2rj{QPBr}5Vl+Qez( zt$GA$Fq}L;69a?=>Rf#0I#<&LmUTVQCA^GpJ;VVkk|P6w^nH)VF3(`{$mReX8^pah za@+BprESq$Ah32#VcH3jjB@`J9i0dm@0T}v21+6^>^9sC5)e+mrYykBLZ;ps`kfFZ z*6)+rCJN@(mI4s-=VdT&+GSz+%zy@0Nt`NxQ~%`WvzX2!lR1dK07hb+Gk8``-Qs&o zeMLVc6LG|ROVW0i zQQ@+O_qN5T$QRu>{8{`b*krNyJx2lGgj>%IEfJLuMqj_74u4v*!wJ;gwF1C&4rrY@ zmk!2zMY`P#s1Lvvq2xE+YUvp|KiRL9xzO|UqGripPhJ2AEA(U!udWRtOdWWdQP__e zD)pyAJGJRL4TH~6Ky&)MMpYzc0S(23Q$Ww3-(XaI50#>{d|5&J7gze;8@V^Bos)XQ zOpBcLS566U9;jLPe#(d8(j@>n($dwsQF#49MPC=#>oqBXyI zyJEalhQ18P_PS`R@~VsgfJ|7!OH7WRRodypv?wqcOb+Ly^afW(iX1R_CzxB1Eeno# zcOOeZoAN0=`9x|IlN^agCTJ0Kxo-0*H<9FRLefjWvo8Rg%P=nJH5E?xz_c$}czdov zdZ!kEv~JyUqopTXME`&ldva``O*qrtE-w#n8Ztq^NdQFQ`Xtl{b=1Ua_h)QRoTLjL zxN+A+hrB&*!T5UmcIe^5AJHivwsV$s=MX81G6S^Q8*@Xf{J`Z~qBOff;qS`Twv6MQ6S$v5vp;aTj(Q-^S93?>hKxgbiBm zj5HfB(9Oj1x8`q|yINJtlBktWHh#jT_u1v-R)=lK0?n~Fn;Sr zuiwO5Nd(V3tB+LF@<8_aM;aQBy8{%d-)CEz)n$`Pu=)=_{9z-uyjNQipVx_hBYfRc zq)Vgc76o{3srP^&Y9~|S0S%?ZAH7+36B#0mAx_-0zKsfI7Fh5-J)oHWq9VxK-w#J@ zG^wzyClxXRQ%JtT>Og;(AQr++%JWBfyg0+C>4Da6U1?B~xbdvR`MWYPTdj@Hz&df)I^RI?E2;6@9s>T3`grq=jB;p+>N7>4 z)O>`YG74V?Z_kv5Ir^SIz1}ZlqqKi7v*e3kwzPP+`fSr9uxVD=l3u5Q-tU^3`Par# z<8ZJiHLDTXX}x9OD;A)|-1f>BavhktHcIPq7PxED8_MQ)yJ;Ri_SR^2W4%gr0Q}Jj zoGw8==&bL){V+Yuq6_rN#~gG9k^2WUmBI!Z?2;7=KkqF}iF1D%kfY0x(`EkKLTXGV z5}P$9pSZpicIUiI8fyu7ANQSAJ(&y&13!MGeVU5UP4-t%6-XEEup0)%zb4sQNNgS9 zmU8Qbu3@KvJeu{mLVV_O)iE|M&0}SaIq3c~3e@@a8?G}GY5TVmBo(jHYee-$O@yYU-cYgknyi z-2)mJ`K^`iI0o_ew9{3NnsvK_i}y`Sa2Jaiu~=>GrdzgX6dVlwFoI8kKyrI%;sqS1 z33Kc8@;o2FP~3f&_D(A@1_V=ymc!L?Dx#PXcGu5sO;9Ei-V=QWMbW9X*GtS`N3ICo zZFC@tgNxE>lXJq5SkNxe?LsX~f%|#1g!mz%ZkLlT{IEk0>ZztqSZbdpX=K#*w((Gy z+;MU-<$F{(QFVPigPf;3RPbQJ?FXIV1lR#VQ-P&F<+i$*1*L)=DK`3F+N_%VFnBbDd_4x0Oni~jJgwPt6d-=2DYC& zAFl#HK(CEc$pz%CmhJtPtIHKVBV;fPqa-#piY9BrDw~vdy`L^Y^z5)!19hrvnVm@= z@bhkShS}!1M|{fH=4#VqVG;3+hWtypWZ@n$ni)O3=)york9;>LN2>6r=rvjBSMzor zhs(>i(P1gOQOo-azD;Y=RT#n7qii*pN8CjXM(wd&UCXsxqANPdzup*H`LW6ZhPgo^ zxPTOdqLPVkhcQ0~n4BoLq0y_fvx$6{l#t+zxjOa12l4ls?#|TIANKIsyxg_s*Rauz z#0J6Gdz0j(yMWD?TQ`2DT6W#Pdbwb0WZZvJ$VRS9-=v7c9_fSQkDkQc&`FG+f|mL7~jhiFJdmRK;qcpTk^XajK@ z5{b-f2x~eX*V~>fgP~4F@-y$Jbs|)h?`%t7l*+ zvP9C*VVvh|MoGvuzMFjih4(s0{(2l!YKGFgs;}Oy!<;r9X=qjnaU;SlTj>RZ_pa$7 z8?TZ@CCW%tCiV~MM(#E7>5;cwIRbNS5_+A+-XrLG)Oj)k+2nknAF3>q=A`~1qy3Vj z2PT97b5z$C)k;hCH3`dK*sSwQT3VWmE=P>ivogf@_zL3Hw`wDRYjNj$P2*sq>tS-h{3hZ5;~q)wW%{(ef|3KJCi zxt?K;y*+VBCx3QM|Ks=1U3h|9?}<8-*Jh3KFp$&bz|Q=ad((^AYbokbXN^5?-Vc^K zQ8uM#v)p||Q2S6a@8JM?$(9Vk$0^ERzuzIIs5kMJ|B!bFU$Y=sIx}EThTL%*?2`3S zP1=mOevcG|+C3;624Yjz?xirF(dnGlr>>!`H=Z?Lzxo%Dqvlv(i@8aL9IVka#JPEY z$e1J|Vq>X$dWJ`?D3Y|b-Wj&FAGGe?c^v~*-v*q#815h`j?{$vfpnH8w8=L z0Q{O~V1;*CZKW2n1>g2wZfYQXy6WL=`tS!S{A090P?jvg#qW_qcY~x{0F-C*JHe4V zhE`FNl4Vkdj`bSBcB>aF`QtXBD759a>01dFkrYbh*kY5nH7QdhBxx-xfGSu`0@Ls#H1wH z`p|}(rqAi_>Yu9Nkvd{x;$(4K?Wr(f1ue6PKhxwVmQo@hYE|i-i;Xu(V0%DhbbGqu z<8C8690jeM+dbB>mOiVm;y z13yC@1o`fCoS}RceZXm^-%7uH@{qf-sr3HD`?C&`{qVKL#e|K!_>TG$G96f=!N#aN zIyKV~1~U57m6H@_YTX@m8o?*o`9QKpMuPjzbVHmoT;O570KAHch59*v(V7571(V8z)u zcgUQzb~ts1ISrJL7FBGeu6ersRy{SU#39#Ci-f}9QO0O)Qj zC@TpHr5Uu<(Anii z+|6t{gc#;r4FsE|RjaIK=k)k3Y+b5!K4h{trjYTfCOL9CvCUGn%F^*vBbk7NqO;hB`Rg9#`s=(g#>(MrO?c`>S zaDTYny7XsU05*2osz3%^){ZP5#x11Asj~$-8jaf(z<+@AHnrutZCsd?Rs@qh>$XC$5FY+`N;k_w+?o-`fY#!xLF>2NjT$~S>vbw(zR28fX;HnD4xtpGdLFcbWU7-fAI5Y81@x! zs*nYh&;8*-Ae#b>)hQ-bG0F^Jy)RuV&I#Ng@T;^m_K#jwo`IZcJ0TLDwTIRTQ;b#D zJ2`e&9gYSzRMJk&(2%oB1kS@ro=f_ksHVFhYkulRR6bcxu+K~M zbF^5SJ$!C^XHFleo6Q43(08Vc;N9PW^86AhSUbI3&t3YvU~2c&ks<23%~gdWG$e)T zByrExij6;D^|3gf<3Z3OqJK<2eDiWZh{u64b*AZ@K69Js@m|_9T%O;f8C(ws0TuLF zKXf6BPZnDFAS;?kE6?KEODUw-(8D%O&iCQu^=T>A6BS{i-n&qR@| z?m9?Of-Ba!&{3sE)(8*GQ`|fiVD_(q3o{+5l?tWMqdL>bpkfYw4v+l^&$L7}k&&X2 zc=j;6Mm8^+xrfGFRNH^VCW%Xe&{7cBD&AXQ$EV0~@2^w1+A>_A^4n70{Gw}py)&+# zRrW+dArx%8y7-`CU5_%0meC@&pqx)RXhIiymSJq2-lcb|?63F&jpPr#z^Rll$_I|5G zUz9ZT+2zown9oN;dUv2zW(^} zDNa*P>d!{nR;}Gcalql>QR+1w3Ec}FoprQlLc;O(be18`s1bm^#i&i;)P|U;4p)xM zRk2KWy5awbQUk&UfTp$??bXH||JKe)zgF%f&p)4a?E}{oz7|gvJJ`u^{PgMG*8+i$ zzju|nBSKvBJ!%&XfId2rEY=W|q<^K+=mjfEk@4laj=C2qz;&y!mMRlUVmOOkq0R*c zaS?u@7bZiy+(jN&)|+Qpl;Rg=&Ns+W`u+jK3bL=@dDFX^%43mvMhaX7(^i3`8J9mU zaIgew4_P4F;YN2?UcTq&P%c7Xf&57X>jzy=e3X>%>OAtJJ-T~)%??+(Gouq!I43Kl z8@!C6@5=WTS$#|y65imjg|TkyN0Wk}>RJNqCwgl8duVd)%wU$dFA&>O#z_XbS zF3_N=W{{u9l=%p_nCGIafv^>;tLH_C)kP)|%h{D*p(o@B>&D=F9NM3q8Ryh+IKgUhBpj0qdXpObZ?uVYBOW@}?KQi9e?u7Ma@s z(Z=8vTU+L&)n)ok^QO3sxkT^c8kJhVhfsPFEJIGPZr5Nr@lNXjs- zU}8r+1GkV}dsm-K(x;rRF6qIugXI)4n^#k-mG)(JlOpg7&jhma31Eh&0W%7`+Aa{a zEOkYgWI}Cr{0q$nQD>~@SALhQ3^yyFupKD?$9#-;E@K&B!wpi4>Yfr1Fj9UB-6oAEHAv^jg8F* z2A6z{4otB+;obH!KY+q3Htq5~?=qg?>Ba|=Neo(m7klJ*dglA3ch@z^ag9Z9^( zL>wm-sC*isQ(FrNr5mA*WJ8lbi8ss7I-1_Z0){D16!{rfTjZ~z&Zknxdm&iz2fLh%YTiy^a!mFYT}>22Y1d1=d1Rknfcz)L1e4dj{DJj)L74+W0P`eo zZhiO`2yrd$T3Dd6pGM%e(V-+n+I9q;KwjHb$$0TlYkgt;Ctk;O%8_O(3kP!(PU~|D zeP|w<(cQ@yE^zI&#}@zc{xAVBzyP1lxQ;O8f}}lmb82fJ?i_Z|(+AYmB zJ;*B&mUSA;qRSLbJ3r^TKAvF0sf}vsJ6@k#LZQzqfByl@lD))!1V8bCAFoCBQrZhs zjgW@Xr;Ck<#cN5mb12Lxn@;E#$U)|&3*Hv~$%|5V7Jap+VMO+nuMr zVRroN*|X}E*yr_Wzp4dn$9ESNVTqr7A`=&e9|cEY>Rf zuj+pcpdoT|$1O|@(k8EZR=drI?UpFUa^}0;)LonDhHx)ebUnSwHvF6X*kruoee)6> zLSfozG-w@a_r#VqN6Sjd_K27bT|gw6IwVj-QxRQ}jlQ2YS0K|89_dO?NgDc??4%Nq z!lOr6079cVeKuLy-;=;svU4PvJE9>SSp>b>T3PjqKKt?0W+k(!v3>DXwB)#-%V9ll z&|@q^Zwrf68h+E*+FI1#Qb(Wt$5_2KhqYbZ(?bu2PU z^UIn%f}XdI_RA}16KZ6|*6K!aFUGtnu6<%~Ub2!?^5V;m*Bdt_DM7gdYk9#Rw#(d_ zt9gRH8L=_h!$n8unGXxmepNbU^tQIV^ah?(jy}`V#l^7Auc@3O*C``rG_F*(!hNeVH#O>g|-;Js$W>U-VB+u#u@?9;=z*{iCDmP zQY?Muw~}vbY0HhKx^!<1PO%!%%SOUiK~kB%MZ;p1wj5W7bjbdU-lpqgL00IcbK_Zu zU}{#FdZ>5hmnp8#-{l-?H@KcgF?R#`vgV+Z=}w~my=m_g9RnhQ1=;+T_{EJ!{yAh@ zb?Z-FPMiB7n@)-+_nT^_aiswq11n%p-o(#gI;q4?FgQxR>Or`I&=WHPrJOfUtz!Gq zB%{B=FfaM|_$I3yv(|iD7lr1}$E(1oK2g~s59Xw|iFOMWxf{N?=QrC+M9a^C=U+<9 ze-3|ncsPa8OI)3uesoiIzI?0KwrNpRC4m7_vnh9kioIoG@#0?ZVlgbts+mCaX$!Z2 z|7nKF!y=D7Fu|NN@k(V~WVn>c)z&L5oJRBuOQ!PU&=IaDgFbf~fT<0D3LxMlo2m1A zYu|rghf7~yvLoh?7`QYtG=SaCOY#Hc_FGu6eQ1h2R8Ch{*WAM5AWRd|sz$zUv*YI_ zYax9?9i7%+x>cJuI*xbpI}&$h zsx0>5$WmbHU^tuVuGd?7TLNN^G>rZPV))y?eStBV*xNstu5@EfV3vb`t7hGAb{$UZ z8-MZ+G*DcIw&jkcyn7$rC*{mhxLkVJzk@Rs?<90MwSMt;2obaVZX@7|S#Hq1nu-w5 znNCfmVW=M(P(_}X_eoFTr9?gI_1V%rr^6r(+eNKDCJKxj@rv>*Ni!lQ09DD7Lugw% z6@X2|XuYC?9PI3j4^W!NsK8pQo<16&%GzBh49!{$Ns}{s29^Y)(SjWhM7%E zL`FthK29XrzAuRtxce(SaA!Xz8WLns=k7dhUv-epcGac-cFed_(QP2H37fC-WN0YkB3k+ysCfc$lux&{pX;O^mKoa0AZWPav4Ti84SeSb)_K%nZq zpJ^HhaqcF;dOUeZj~Jm0T^=s2Ij|9yS+<&i6!Fh{8a*8L$aJQ?TP;;?1aj3-&K%2J zfCXEG$zKaUPvrk+J!iWNE`bhEl*OQrtu!SiTHIYq`xq-w_H`LtW*^6Z!5s-^BD zb|vtaRQz{r;{8}2Z#|#07C$Z3X_Bwd|E}BJ^)ez2TvPR%K89R`5yZpGYtsJd z$Xj5#Qn$g|E$Os^KRnH`F~t=wi$EPsIO`jp!QUA|voUq8Q7obnGwFmSI)UnX(TkIH zzRg=cxg)@`nbe&9e0|A-tNGIXNP)mh_iRk&?3YI2zQ|^RfoG0K2m?Ph{$xU<`qA_r zI?H^AR%S$c>$Mlr<>o!tTA5^By7Y%ckLN=1>#BfGf4oJMthskCV_KR56qKsjPl50! zQCC;jcb~MFacUz|%M3TjHrG2}p0i3KK4qpqE zzq|v~F30taCBmIJ6pfE;ZI60Hahgj< zabGAZ%G6}oCq@ZhxbLViU=TtN>mJ;javzp1z2MgoYs)!2)pjP=hjIbfI@sq|3hm%E z!dVQ9&mCB_Yi&X(Xq|6teie2DM+h9%|N7P#HRf19-n6yx?Zkqe_Z_1Qwph9Eb!3Mr zC-qL=vq=XyHppOh-0d6JM_JIjGsw^Ua{(^bZ>v(jnbgNspMPav58b%C2o%apZ&O+6 z+2jzH8_CB@*Z_viZ3PM?-!|+2oSOob<#?jsi30)_=g-UqPqLb#`h-J;Mq6v zI~f}D-vyt^KSe%q4;A0#4<$%!5i|FLjL#3Pu$vn}6!{1Q`R{m6_Qu-`mRm%yWyHa3=`lp3-Y_u)}iRD@+uRS(?;= zCr}j>%=8o$IcE=EacS#nPgM#ZqrY-i?euJR9;wfNl5us(vcbx-;XG?{46T9(sD#cp z5iuq11!l=wF;bI1&DY#5CpCtRs{<3z6Cg(B16yq$L#27ifM;a69)B2KrxYrh<`Zue zjrn>?m`9gFEL0&`(|%NFU3AB?^f^$1wN-E}h&kVepzc>2C$5FbCE_JzS_k5?jKS2L zvRMEK^SIL%SkSQC?*NvM=kIk!v+YR{7nhX8h}({T*b*itAwiEM_%{)(D}%TtxbymoQf;Mf2xxS)ndoSs6Lew_o~O=qjcy+0AB78hzD`p)9kCTnn?( zwmfpa__z$t483~b7IPCL(|Qg|M;VT>jNV^C4a8*MOuJpUs~P#>&iT};mI-w42vzor zUxECNIqK(>`p*?oBshR>O#{d|+f$9XS@ff_V+pt7lRT)>tr>Q}97QPgi!R7sue4VB z$i})Nf3JC}dFs4X&)bKVSk(@CCm+MRJ=Z0L;~;bEVo0xy48%UandZo6^EK07v$KaJ*Xc;ybkWa*z!lrcdvymZ(v9}hdA zaBT@FOPSl4r@7_|i(+=}9=De*|Bw^nPEVQgP^NL3e_$jvcc8pmGk8L8wz{CB+wonzWhuw3e<-MR}+0HtztY9KP znT9!Y>&tohMa?4js~p{Ll`wT54S6-j@txz-u9}g>VW~MLNUumh*vLcpii-0|dFA0J zI)N8s{!MLbe!zbd*tvdiaZ75>LIxycGtAi;p}Oa=RV(1wh~^?AGsqg`j#JI_-sCN; zULt1oUjN2A=G$RPl=x?&CmMRd6DpdXXY@MxInQknu)QFoSU~Zykz7WRp-I33#VP)JSGpWp#9IRF`&U_r zuP7Oto+sO3`{|VSLokVZVl>R1bS&sFc>2;Tf$@h&n>;;gz+hlDzf~D7n$4t4pS&Gx zb63p(qaMCc&oJv#5UYQCtzg7DSaQhg?5=G`)c3Gyu5U2e}letDOvo!;B+2b!`yH-n2tn$at^^)bN zfCo>6_077Cid>eEBP0`)wmPH*vnVh2~S`Ha4farRGFg zXYJe*z#?roa%4HN{~AD=7(^{eJ32bjy^tyC9&=tdmGIeeG*EJ!76a8-d@3quXMc0c z@!TvtaAeTHSl|DUsHExqwzyc*$XaU^XgeKw_^@Yd*z4^ zsJieuhn$Msi<>!XKzsfvd6)}!7+ZX^SypuSN&OCZ|~Rpb>H{%`Fz~; zH>#LtSL%QjO|^J-=Uuy-x#sPcQV8Q$UTa*2GKH)jtgXg|GTw7K-?;>(vczghn|uUj z4zIEO5|hHPV-$+>{P~WQ@zs~&Qln3Ink)Qj*$SB=w3a-iPyPlI&1H92MgfFMj7oDq6oSeGGwEV`C zJ9I098GT*QBeA)?81H9B=@+nz6YQ${Qnbwej(2k!%EbTvUBfJZU@-PqPm$u5XcpRc ztLPrQW^V9BbBaygXdr!Zj+@rsqP0iY_b7bKVXho5$7g-9Coxcv<_-uHUzcb5Rl)hy z*V9-0?DBIKBL`1Nwa57Nd(oZ_z~55~dZ!JZJ#5Y|2twv7(Gq_a7Ue5z4x*E0v-NLc zw_16Lb{&vydw#`Qz9o-?sC2_G>4?Rt#mZ_yrG#?Rbi0-e#UT@Ak2zrx~6h|RfEzXYj0)YMde^z*I&XbR;E6pnA=C?tkW|u>}=ks-H>VSWL zcpo2VX%(J^+Aej5`F3*DH>OB*E@JUMdkLO_`^PDXng~>rT+Ab(Uw-$&X@*{Tbh~^i zsqXI9D8g^~&^jAsVlEx9a6^B>h;O^j+8R&EImL~y^fr(({W4b|)t%=2+F{(7k{d1RUBiTbah%-l*cEisN2)+d`v8McqwxC6-rsp%ErbzkrhAshp$nsh?#}}=vj)0 znCr$t(3>yaL&JPT!O!?Rt}q(SPqxBvJF|rb6(20cd)U2aTEWy?9USUJtGM|1c7?e; z8WX?uyU@!Mq^`ccPsi$;1|PA$#QPfcihK(F@Aqc&vNu9zjOdFzjFZmEPn~$zC&b!) z+{e6FPP32?5ON&9orB=;QgPrK|0kOmpM(a5N~vyIEBmjf&cw-;3;Booth6rsGrpRs zl08No8mTE8ss*B~Q1{yb>)S5tY{HFeZ<{X9F%qtekKNCk?(r8$z&K4Ecu6%ma8-dmVE{dwS04ofeMn z1%csR8L>2{`ekjYXh^I#y9z^hSz)|@Da2q>E?}M>zSN7GnNP91cc*d01|tdloOfho zWVZKDPyH?~aLriIqs3uhm-x}Ku>~AHMbw;F`J_kZc+>Qc>HdDdB&_X|67X5B_1(fPu_82L*|;q>30gg~Ym=Ezw=+Ld5= z?{*au6e)2ON%DB1uz@MjF2QiTf}r57a=95jKy>O!m3mjRSzO;gQE|k67W)Bhle!j8 zJmb|@L~!JpkNtX;?$#@CU85yrqiQ;)KO$CG+<0OdQ6i(5v#pSQian!FO>ydxb* zV;{39wH$8VCW@rN-LlHrnw9fO<>8#D5LD{R1~rMg#cwccY`D*RO@k zUY=onx5~O|fbIKgL_0_9iX-U7giM_#x#;4C071SAZfr)PPZ*re{$Z5RI@%X_@7J&1mh72&)mYo&l?-| zk&%(dGpo?9N3=>EGqvviXKduO96!s&m2SaCyZ-}Ye0<*cY(iTo+u6Xw@J@seJZCMI zRpPUnZ1?y!`6n6#b0fzM4rWNq=JlwfeaS~aMZ8Y=A=EMFpqO%Opc!|MC9Bio5!{BD ztPfntn7Kyqin`)vz3F5B^jq`O_ip-b|3>8%7hC^{yd`8@H@cP6C{BfleUHn`oEo0J zBF!F}JI-#b9IVTm3?;215%m5YeRu0qe!EhAKVLkBm*ezbY}Y3DbalhnNkY5Z6wAHh ziGzVF>i?^cTnEFG7I942`*%`CY%Xalgrcy63bFW|dfI7itvI03 zsPVKJd(ZxnLK(S_3P7!_tctiv437gCq6U@94lAMYk&$kOtg9Us6hDTWm_7CMlOOTl z%qlOBiR_i{jA2e+(s7vH%Oswsz23v}Hs1kQi+&yxhg6+MZC5&$3vTG*i6CYVpJwJ$ z;|z^_mlJ@+7MLC9hF)7|Rn1=6un5=<UOJrlGP& zGHG*t<>03)Z$Vb&c)E{r)07dzTC4BK$AE6?+?q>C4-k1UB92B;iDHFxe zp76o`K#qBBK5+zLEmYrGZRS*DOr_qof0G?atW4Y;ymNVru$^IpID^`ne) z?~j4pllpsqbslg(AacRy!zg}clRLw`dIYtUuz%VsFS8E0W0qkZ;9jV1X9!{R(yX@V6js7 zIExhmcMqv;Bh&uUbxHBzX@zBoqP&Gg4Q_9bNw@yp&OE%tB4yKDc>cyxGBT&02F$M| z=$SkiUbc&vZcVP#cV}P5McLGOUh4~u(%$VRo}i5IKbEu97rt4Ar+PMK zOU$e!RwrlpBkZwa=L+>Z|08{nujn#3U->&YGNZck`-(znP>p>PbK^wZW~!FEq`CG{ z-=?|qT?hHAo47RPG}_i9ary^&c@fd%3(Et62fcpZMHQruZ)znA1uHjvJ*{BWNu6(L zJ%Hz@&nPk&`>7}ky?%VE2yp>vM)xVZTwjwU&`^MbWos{A7ww^|OWzD?sgtAXX01DM znd%;(H7%N^c>@$poUE6$4Sf$YtWD)xH?8|2$O!TO2|2G$F*+S3G5OSfxY zR$k5FZmXy9lj%uo?6~Vp+7(S9r+3#@UlD())RlSW`MhS+s$zZfDa34?A5*u#liq}6 ze)nYecQefRX5#kN1vZSrgX*13+x^Vldp=BdDzErTlY-f3$5RiDfF!M!z3On76;1UPx5c;@bRvYqvdDYcj0s z>N+rT{q?cMeUQHS-a1^b$EUn_FLIkM}D{3YULlw#us(yy;9kO zu*_2G+lPki@ZS_QfNbl`Ig*>qTK_%qf2(OeZH>BeloRW)^MWV2>3c`O(Nxp&{jVGm z(DUV?kU&XEkNp!QMnacXK2?GN>iNBgEcuzk-$j2Gs*j`#p9Nfoxg5UAR9zp;Of(5y zl*S4BII=44BkZoGXo|2?tKMnYe8hQIPytKd@R-LKP7Z+xHy->24qE^a*>9s1@xYf! zJ5F_I$;@_u)6_!{1MH51ty=rnEs~W4D+V6#@0X{-7!3obNQYJ{N`nDUkcBm!kP5yr zom~y6l{_~BCGI>2)rcq))Lv4hU9&gs`4?`@{ zWF@T`L*i@j1s#^UHhg70?Yu#>|In#BeWjyJG|+P6RQ;X#SmsU%k(tqR!}&JPg=db_ z$*nsq#%s)HdG|!J^Yi1$0?y5FM@1PjTYcA31+JQO-fH#CZPgQr5HYd8fWks4692u| zn*W1&ho?VC1JZNiNhXLYfj9!Pr8~GnsK!USpJU|2GE7^YSEz6Cr>^$uzIRhKGLi32 z{%IwJSrqVDNWd-vz|be9a?@dY<$DCjq6GXz^9?&J>ca;zlfc7QJGHAEnf^O+_wL?(9~HT57!G_7%jH>J5` zf`Yb0l2sFKRo6rAzx({Y0x72~WzgYQX38~OtK5jz8$a*Ip}@aA;aB4rpH{hWxV>^9 zH!(0g0Q|k&^@&=rz){J_zU_yi*%SWRhPtnws;ryu@7JHCiaUUIDMQDB$=LQ($sYnCXhlPrDE!9kqAcBO>X3Pr$0 z;-#v|LvwVI*5elk8W1ifD!1hXJ;E#I6LUUYZbiu|?LcsR2qbkaxn>6JKMlgBp?9a= zl?)FJ4J~&z{{jAvniK9Mare_?&Bphy!^7o*!#C?BRhBvypSE#C+xrzPdrrkX@^nNW zvaC^0dnx8BfK2iox-Jaodrx~Xfo2x>zW_=4E%?0b<205LX`Csw#e3-S(hr5_7U&j- zHK;#|k+O)=g#1h+3I;)1>=@?qb8T+sa2F+g+|lV>2n0cuRa2Av_wT_IQQ#$?@kR#< z5Cz{ita2epo@?2$Uj9q8E9c%vg|6x1Fm*&?<_|7q0HSSkHTwFrzNNlJt+wb5kF{=s!wv^Dm-6R9tE;{iKhgKr_+lQ1;>C>0vxkx?|?M9 zMcY91ih4!74-6L;ewMA{wqxwLV}f29y}rsY5Ino=csQj+L7%ne1mP02Bxbr#O-CO? z;oy|dkQm8-R+vKIt9G++IE7Ts5pbrFn4EYVdi1H zEDQeE6tU{|Ng(3qpYncW{ktdO&8yAMi(8b!JJV91)KY`iL3r75N>W>Xod>xI0_y9z z@Rj*ikng0pI6=bP{@l{rZ6Tq3W@?H)SwWYV5g-F1%l$Z2uUBy zXBT;-Rpf7>3)HJJ$A}Lfngpc???|bV{V7}5UjG3IFv(?Vrpkwv(eqW4ZH;{Hc21+f z{PA3vqy|Q1Ue>JT+mYo9#<_kmZJUfX+G1AM?mLjrO_+Dqd#bi1P5m2m?a!;v>_+Zu z`h)3GWO=)ie<789UIDi04%=2x{*~kb7;dmCCWf-1hT;MP-z`#|-;+|pv=7lg?b0k> zo8Y`HNO~DaW8{A=9m;HqUF&}!+6~g1cqb*`1qq^rfTR{^h(_cksgoz4H^pCzI9mYWs2KzHpnM}OowDjYYX>EOYO4DR%vOy zqRqW#`DGQylG;ZszqMAty5KF6_S{FOSc^96U@Rn{?SpdjHJFiuALTbxJN>(b2cDct zw^#PO{WO>F^@mdG1bf4;RfSEny}ugHO%UucZw+0km9fm->j4rVh3H`zsdaB2zBW2eyPr>T0@#p7h7S@hN2a5$L(xW%`%6PS?Hg8 z{*ykCiw07f@t>K!PeON(9j2tm72X>eHhJP!r3IhnT}>| zrd4+{_g3lsQ@rWs1Ct@43pCq$|FqEOJmFvj@AFqvfysEnYqBn|)UrU19c|!5%#V@% z<8g>tXKLA*G9hdBnR7Us)|i-Uq!j-Sy@o8r4chJ+tkilZ(#^ckZu11xX;9mUS)X+@ z8N`0{$VZEV4~TZglpQnV!Floh45y30IEQP44exu<7u@ts`;>00nc*UAM14swh=<;8 z9+5o8b15maU8^K>(BcekdYb#I*e=TomZ3SN+SCpy6~df!K;b3WtF?q0&+_jm^JkV- zF}E5n%1%aqwB4lkgz0S635YCK_@Ad(hfQ$)UQiXU8j}3|?!j4Mr}x^Hq_g(dKY`zG+#VAjacBfBks9!sU_`frs%XjJF}- zGLvNNGM5azE+cx;WHKD@2Txy}1eE&Gg28&v3Su1E438=fAU_H3dbso`&f;0KHRRvQ ziW|6o%Po6uU=D&}buIc<`4r7rvZ1>w;2|&-JaU)o!*uQEbcq30A7~d9m*OFztq-td zC{oDje*|3ZK)zd|RVG&d#{i%+QQcb3iI6Sz*E_w%jr}g$h1XsPp8QsuJa8d5G9T${ zJ))$49B6%j-cWKJfUsKZj)!oUR4lEEM-8fiwnH|eO5_`$c;Ct)L6Mm47 zCV!{d`8WuB^Cx{*%PZ`HsdK7zfbcWc@LW{uRHMtV1GW)K!Fe8f^tz3^z-){I!$Hkp zE#47fpMN0Lif;H%oT1ARdJg~p4824%1KoOJ&;SHdbGF|H{Mei0 zj)4ZXRP~Ewh#*|a%}jsasTIza-|;nGrxdZf5*c{VsV0go872Tt!s)}csYZbp=+uLKK{4hXm>!QER-r&BYk-v z@$JD+WA3J(7TRU8Ja*Mrs9PKYNL1s-zOe6WCOF$Wa51nall4?+ry`%`rtbt!JU2;t zQ$CMN`5Hr;=@`mfB=EC%J^3;TwU8Fu0&+a3XFD81D)glRd^1O~0ZXRVk?&Vm(4q8Q zZK{mE(l57um&+f|!p!=<4?0^Y@cbwnAGl23qd!@6RJH_%E5#I?1dZyL#HBsiG#Tek zP=-S}m%3sxN4!nhy4rH#poXjPJKPXL2btgQ#MBpmH6Wlsjl=ewQAz*G7Z`M{ii1GNVFLBL9 zPww#&@7*(Z#5xa6ArA=F@WsWp>Uh;pT{?i}@)- zvGwk|OENC#-%?>fpcoLS^YwtG!7+z-tm!Vk9TSGNPX6%aQlFy4QR1Aqm+oLeV&Ys;ayc73SWT`F-+#J>;;f06gh}D{Y*n;Rx9+ z{xA^1yfnBPuW{;Dt3*$cIxo2bR)Ht)*67NIVt!Q8eJSvB`kZC7q)@kI#wRuvoe6 z8Enr$AM(ua@lRDn63Fl0zs~?7XoaA7E-x?JIXg*7NfnwlXXa=m?Gx01r}4apYfnXL z|J~TBS)$pHPsw3Pk&Fth5Lq9$xO=b9g?@eD;w*@>wl)Pe0*K4@j=eTSAy6joY75lD zxF!kQ>bd^VQ|bkX#L{&IJK{VlFwkQr{w3J!s^COi@n1%!NwvJiwXJ-~*^#9xx!H46 z8ZUiTiF^lVbUwG)NpY2yPn)y>#P|y2tM*3c5JsZ)eP-`9zr^Q~Qj*sl9e6~VZ6cnf z-010DA^2PY48#Uh`uTcJa+cEbWjy`tTYQLbT*6SCCTFNl5m*^`xsPDY+$MK?$R5o;#6#! zBJ~*Gda5%7YPzh%QXdX+o zA)mL=Dh?Rwa+{2R4#uGxE9&C3rpf++fw%o@{jrRb-dR692fjb!T4Kit-rLM8v}X-Z z22(MGdd69E{1>Fn0#-KX8G+VIb!*sG_tYjOxWqsAcmZN0hm@oeDv~f>BA>i~Q~cpg z`(AE5bv~!=-itQSJF&fU_;T9&8#Cl~v(0=~ep$!|?Q%7Il$C)Ic8+#`JXMB(UC&9n z3#vdtL$h_7nlm1NH(XbyUx1`5_nqa(rqV_E-_x=WRB2rQJzujDRAs>;Kz4qe;(<2l zlYgyi#7ue)p=yD{YqXluppG0k2tC!$l>x%SiB0-I_0BQ1gta&H$nY`Q>o<^j?`AwZ z&{Z^Rg#-noFo78VfO!&5+r}*d^Q)v`{~9$l^^8BJk-)yuPgHsIhy++FDZOVs8Nop> zVzumY^#PK)&%Zd;nv2uT+WAmQg50K9K|Z5Tf5@81E(ZCMcQ1Qq;7{h^3g}R8GO`on8~LXqjW1svzN(^7cZr4tm~3HJ*ma`*ZxU7(^ZrrX<1xLg*^EW@u9KZ z$RFitm&Z8)fjr=1`nP0y!Jts(@OE-Bslos8pLlCclOP5o#d>VXBPUXXirE`JW%AKp9E;SsNs^GQApRtSUd9pxZ27IU@{OYnwrGVkVDL@zK!^=O8TW;+||S zJV>5fHRizR+vdb*iOJ&RZ1cHe?}abpr+t7)GD!R*fJIzaO!cS`2871nnS+K5H*c*h zB3&}(I<@?#wOC4%#qBIRE2zZ(fbj_KSY?HlbocO~GbsV6!FzMR`)}0U1)qE2tdrkk z8gJ9&{LYx0v4mL8`;tJBFOT$v)U{q&k!5D#K14J1BsJsdd6CIuC(%DBBsMUU)T(;Z zw2nkM7pH2r3J8IPSojB2wP)=K4u3Ax@XsyC1!Y=!!YD`fb8GHv4-Yl<`fpr#c(|oi z&J$nmIJM2r%7%t?8+c$Mt5Zz~U#3r}e*aH$0s~;$xf$SsYKWTAAd5l>0)i_+956zF z$T7U$mRjY^WneJ1e&{K9@#v2(&VM|0b=AuCqD{`YVJo}B4hv;rnX*5B62sqpyb^#N zKQGGN2!j7P$hQzbJx&>Q<#`*Gi1`g_@;#@mj-F$A>jtUL`WewffSTBCVYPmJHtYmd zz4fIW`Umy)hco4C$v!(Q))Ai#to%fp?8$OGl_#ibv z;&>y1;WyBI+KzaYiIQ?O?7A?8QPxAxlVYt5(tS@g5jw21%aqD2hSi)rlV^P+7_6AN z5u@{g>H=tPBQ&N>biKT4bWBtc|5k12sSr4=3yCTt#qhG8G3o;7|mmls<_v6YaL1vAB8{LoQ zyo@yGZm956vD|%n5}gnqKayIEHZeA4kDy_H+wrlg?x)P0Pxj$Y>X}c&vFPIxu?gZ5}C&%pbeyeJ?iFQoS53k+IDW zwuU&6jFL`UqH~_x21@7h9(^CGk18|$@aIr~o=qI?LN^>jetSk2T?z{iS{!uu%ZT@X z3q~i6lMZJ0+10Q5H|FH8T9D6sAAe`5STJqslT3U! z`BCO>OwISr(Oty~I_Ci{mjSE?qU;gb15q=UIreCmAxlUlEwTQ1unf_`{vd6@`|?NR zHLFS9*Q2F<(Rl(im-=?6EDiXVx_ITG8`8i!5L+y+bv5>j00^TcD_StX@#DCf`SJpC z$NXO(eKaV#fTan1W4<22{(*2CxW3}uCZj}}ChGFN*>Vptt!7^c$4w__KfXxImEcMI z)3!Df9m)4xo{ z`v&*^@}pt1?F;Q5q_H{k9e192Bt9wa<$5|g%*-i~Y-hp@Sx{T)5+THkp3oySmpV=; zUnna~(lktY&U$o?Nv4#!Ut$D&?C3|OQwEyDnKgVaG?N3rS%v?p&7VCs)=a_=nwT2v zZmd=YW7Z@fit6OR4a$IJYL;#JZ$Xq3{})wzfzY9k@hB^cn> zTbJg@SY_3b!D>aB%hoLTyAs7P>|n}l5#yXLp7!OlS0m$`=K8K2zO^aSYrOH&tmvuf zQ_pLp0qKg)>YW_TCC7yaRFIs@sE^&x7HCOpj!rMNbY65 z@o_l-nl%r2ayF%atyHTrmF)Tz?V zzm;`i1Vd=zeBnq{QOTML1~70EUoXUpyaiK&T|n%0R@nV`7wF}p7C)bU$K(k(bA{2gR#;$cW)9 zf%P@E#2~P_DwCRdU5QGuKRNIPYJf(?a!7*`0^C@i=yeq1^h^V#miHU|aWCGyQ3qcz zTIDmBHx&;6){GpwOC$_Bo|e&5bP#?U+mOsBW6E z`Rrs1?Mm;OuKl7B$SnYedE6Z#%)i-lJ*q>gfCke=D#-k2%@(2g=b5X?gxmpN= zW^ez5EG2*MhuKQUt$1m+ybxw~V#ruDui*E?CWf1f8~vd$>8bX=BGbzL2!~V+Us3azVsyy0f5qd0&%T= zr>?;B-h(K@&!l3|{@q`_XU|&N!YLa`Ai>6H-y{R@^4H>bZDYWza!~c8#S_Xq>6lsQg5KQir13!!C(GT-p*wn>ZzrC1ao5n?*~$;0Mhm>YK@0Pa z+V}6wF4gMR|8MrCp%kdibAw=BG`-kQ5?CfOo0})(5P5YnHSfgD84G+hc;zCI6w;;j%FqI`$frbCis z0Ho$8!cga4h=}nu(^{&*)NAc-x94Tj&JxXnw=1n6m7|Bb9_?KYU%D9Efov&L5?YPy zIa}e>WPZ@)5gPg7U^)SnvK7n#rHu4*Rf}IX<{h4x-&x-SZS_01==r%Dpn1E6pKsO+lY`n}E! zf#hcT-HYedQkPrT;f-$qkCO1)t{M*DC!80%5po`D*9qZ4+?>BNm?h4?r`MjJ#ROHa zVxvKT8GH@A-YtX#!z%`pdj>dbzb)K7nXS`tZYT>7+*>i7O1z@W8{KbvewnFc^(~>= zQeHpZZ~tXvl$*rSabVBJuHlKkS|2^2Hc~F}YOlnbo=clvOi*{EPDO`kOzifPRS?zh z9E=W(jV_3K#yuUL^XUF;F>UN5joSM;sab7FR#QxI1B`WE2EL#GWLyuN$i|NaBi?_W z$1_XBQqe0?ze`4xU&hy&Z#zP`Za6@%=qR}Wd>Hh!iw#1kg<~&|Wp*wA6soHcmQ{6n zb@noCk(b_V8>(yV+G#24S}2gaCiJKhO6JUNtAQp)2jIN|P6KN&z7SZ#Ckh*@OQybwayDS##7}hT1GjAToSz^deLt<$ zi2}a2TY5&Vi+`f>^YXGPE5A)mz1i5@{M5BHtIaCH-@UTUn9+|%yPGtB?k*^aEe9zy zfx}DpRn6Wt_rf>zVYcq)upkqLBohy}C!Eb9lk7kKf`u%c@3Vcu)pFW?;@lj0g_3b^ zszN8%B(79z>33b;dbjC>c{@N20{K|E>?iY*-fNrA?J>#Nu_)4kCouiud@L=wgzgfW zeIaXIEUTBs2!qkfdnAqSxR?{kYPa`G~yU9P;>EX}DTBv@}=ewY~6y4iiLm05TGSM7DG zWcU~j;J!?Ko2xW@9CJTo<+ic=fD~aI@={N)+T^uC%7wLp!-VK~kzv<%HCp@CtJPsm zlxHn45rPjS_zsAtI4Iq(5-Px^=2?Rx2Dq~Ko4oJ;`{x^vFPw&{0uX(bLPkV-x+usg z^-U~wy``8lY9v8jfg>d)(`6SX){CdgBnQfcZhX@2BnHM7wpHR*F3e>&pZ;l^`+CMk zJWWFryL~n=iDK3vX5yqDRy~6PVUc_bza6bD(`Oed-dM~y$Qs(8Ig%gg<>)}xCX}=I z?hFjjDf}!!p~k*y@?0gwBZxdqDL(vXWHN+by$9#nmTo7Ci@A4Sdh`vP7WCQ;@Z)Ug!9?tq`7dj$HO-kjjfvh+dh?VJ(#Hx zRL{#zGAWgd?R;Bw16Rsb^6 zwHerH2&<2h`*Xex1Dhsb!bNxo@=PO2o#L=lZDRZ|>B}mG`*X&b@?cr2CLexIQ>yn` z&JmMsq9)H@k{ex2edWpi8_Z{0bFkfW^jv#r1;dR`2XEiD(ZR2eaP5!wSdUyzMlDqK ztW#gQac5|of>=&-FXVBItMQH{7(l50}Uo7Ssf6G}s zuhoHEVskAuCD^>tQjd0(ei$n=(|Z3fsRS5p7G19Z$eLyMJ~?r^`<|6CPR30v8v;B0 zv3?3o?ug&*zBi%*ol-G?Fa2jw4{1sB?+-X(c-Z_@1VbJBzqjP>RMYUHm=M!7*S#^! zKN(u3*89rMQSnGcK5R&E7AM1?XgnX(mndDawPFNTpq;8d{#ws&D!yB65NrG!3_-yA zmINsABbl>j_2=^g5)0gMS3zpxCIiEZmoGa$(s2_a#s_P|pIM|LLAYB20N9^=W*S}> z>8m^C*HLWzVYp13Qtq%Uo%+Mk*)3a@LCoTf2rTZmt{$qR_0cQRFrs1~eCTJW7iAFD!pW6%R+&`P@lZ#KlohCFlg>`if{4Pd=yC2?#@@PCEm-uc5bEmL% zq<4@@_Y;2hmE6K#PI?gYPD9HXe6o>9uC8+6k}Iy;0qCuv_?e5odO6&W>i8tnFDWSB zMW*95U(t7!0tK>PLxqrJ@xX}ot$^ymzvS^#nlGBXATBKh0}X-?aH`7s>~ej2{G*Gn z7A)}zICdG7-xSi8T_6P^OC(5`2nYy#2NRL>bT&}Gfd!yAyenHoVWAKhECB*&zD{6d z7l*&lwyQB}IXnqfHvi2o9pp0Rn)kUsKA{&ft^F-E57seaSG5M$j+?2Sw{sAPkXlfr zuxYyeR`a3dFP?%X!BiGpxcH0`L`~SBI(ZHT%@Zj##!1Y-=tAz&LR@JlyCG~{@oay>41D z9G)5SO*>k!Gk{)~lYeTo8)UixoN8sbNs*4(j3^jn`S+GMFOv!O!HW83kz{SeNfG$njyb4RbJZRbhvgr7i9%VN(!r^RyCnq|$7o`v&i)O;lL0f=qkP*6u^F zb*n4@`Xq*^aSeXoH>NP7`gZ2ORO&Y}MnAJka zIedZ%Z9GUu#BGU2RVplk;vlpW+56!FWAjzMk{J!0KEz@z_7s*uH z?ss{!i;Pp0?iyd4K$7_WO(u$(;;>EG<1-?bcfFIR@fbgJV{T50#Wkfg8Tpv@K-}3C zT&2#9p3UiYSx_SC?#+6PXgSO^)OuopUk=1B%|V!(0O0@3vE8_ScoG26IdQm#Ri(3S zXV?-j?M-9zru;EUm-kB(AZDt4h*k)B?mlQc9Z)MQ zd3U~V4>9qRv;3&zeD?(MrxBaLBn!=!~1 zj=fB8^cjHUcEW6HnM8l2*jOchzDRvPak;%Lo^9e-9)@2zd0e2>>1O!gsS}c;MwAsn zgKPL@fBAan^1wg{kEa!wtxC1rI=1cP7k^2*v9SSafCpTJY)g`fp8(3(n;pC$JV5s_ zbZ$%_q?GI?iNM+c_d&S`Q?Ta*u$C-6o)a_?gL<#~1kIyNUugRjtaEi1LiZZv;Q2YJ0 z$B(uQPKC6wtIP!rL^6U`Ih8p%^vUW48+3P!Q9n`2m2D0~Nca^SEvbRYnAa9;<6BbH zFB$J{xyGC(FmG&CMrs@!pvuj#GONOafR!coMXV=bCV+QV#v4P1CLlhyRw? z{|1s>kMFc9U+mEW4=Cu!<+xR?og@J6xDVHaQvx4!VB3?~qgi@l#Q3XVQ=!%y!{Zpi zXlkO`;QKimOt%%i#VwxL91U#wOq`MYTX_ODq8ClxxPR8#FQoz2Ae zxS5T%c5kj~3wnlAElo#$Q_s+{fijS$7QlS?Z znkfFb04B1<pn-*oR7$s7N++2zA}CAzHM-!Z;+Hioc=?C5oBUphz3 zi=&hNrE1BAC{j(X7AKtSFb~hv6%k^nthfAQ@ng9G?#)*1j*sJdHal`;0$tiK^s181 zH%+WiX~=T-Ly5=l3S8^s8;9R&ey3nM96f&PgJ<)xZe-S5ugg%*-TZ3%D>q!0aT40C<$j{!4<4Ut&VyxDii^>}jQt`AM3SEAjwCXmZ;-0- z+)xC8If4v)wigSt+!)PgHZaki-Aw8&G5t6OQQN>IsY;N8W1gjJm9&SG#EnP z*^*r_KQNo0aimbrE^8EZcxG4=RlV5P$17=1dNISs)~r$3z?NH1W6v_s>v2);9cxY=}<|ExEby?17Me!VLAD@=RkygxnjMC$|;nnEV!bh&c>Wy0Zqe(Pm~ zH1oOZMkIe!wRo(+ad+-z2?t0N-DyeA&wZ}w#B1d&r6cQ>+O=Zr_ja1B{w}N8q;?^! zxZ_^Yrkha3X5DqI`N!`*Rvp~mPwksF2Hr6yobyD|B%H#Ne0o$^N|rfuJ~`V3BD;o=s<^>Pc2m~5Xj%>a4P8|&V) z*Z${`8TDZhSgohBTfyg3a4*XDM#I zIp3U3qBkX-H^9k4_+?K^*-)Og63f_;2t#2lmJ^;h+{W*w({KCC==W$}bi&rl;Q!6}9VWbPsxE!gn*kkQ^K7;L!3h_FPlAiG+ zOc9O|BhEDej4wx*Us(_4V(e8C5Cfa>6QD(V-8VE1dT(iE zY%P6ha9g+fa=CAQ%PwX652Uo21@H;2G8``I5{J^ENd9>p-m@fC3L#gf%9kT=?@&mwj9EL_8A-3v--=T_^2|M(_7BJPuQZSy9 zcA8o9odpud)q~c~fQyEnfV1o**LHu#y-6LDKY4NC%&?dxt=C@3SzQrC3e30~f?ho& zdHPE<3GuUR0s82>VtNva+~m!H+7P&5L;|o=ap6|1Nb#!S#Pp{ zowqg1EH$>$|DN=(R9M76=n9YY%09Z&dcgHCYQYx}%-5+YByRIj)v~it8Zv;3ECp5! zeQoL*oD_st;i%ibN7Hmd_Sk=@AEO3VIt^pz8*}}&wby<(ChSgMw#1YlNEtH7VRLqIAT<1H9IjK}vSfpCHF6G{y>z$byFA+)ds#f2oHB9|(?*mm-{2f7YDn8|T+$M9 z4;id-w6&+%jEnl$pYeEc6ZQOX^cv0ijvWgM(^eeobv^;Mxq1p{T%H=!I0wd82X3tX zi%tG=g5tXKfR;c{aV0qV3H8=x1#ZsOZRYU&5-TAwU4px_uH9(hCgN67lV+_+#S{Ff zPvp9q8W)A#KaJoAR}qgOx}U}7=ltUwr3!F<^6MUr7)NwQ3c!HYR#n$~`y2Biv3p;3 zoHlH{ZET>JBR|^6%Rfu5pc+Ba!j2!!o$q+-nwl!tG=BREMzT@F7_}zm8@xA1Db1tqWviTb+5F(0Z}{EquVznbN%OO-69M0eP<&Kf)2?) z9V@ICqBU^&?WFQ76kgJCAH&7yUF&q%5xML9S0Ap2C;i2hjXiGoG@LNcZtvG#Hs)+~ z_oh#gX2|sT8-e-OiJ>4pfQ8PL^ZAOnmBm4cOc{4;)avrtZ7$~X_-VfQUo^?pkVQAM z&Ko^l8#JvN~BbHz$9ZvJC?3;-_flOiaZV%SzV-gW3Md2LI??nP0X$-*Xl0(!x^5svhxd?%uznaw+uKj2FU4Gax| zZE{vR?NVl397Pd+9vvoxhKpRcI53w`o?fVXXf8NhFmyK5!@{%Bw0(mcukzo)`xTS; z(UCM0f-v!~rt${|4?~33UAvOnStXE9{Bu(Fn7PN*bLZ-|9kh4`icCA&B0S{Wt(s+o zRa4WK?ujORF7sBdFm6@qjrz9@L!zP-u(zB*xOjMutF8-p&l!lK>3vraU!NzBQ*e3v+AhS^_ZK0*2ER9g+%4L|6AH)`DZyQI3Si`(f{he>P!;(hq)#{^TYGTAtPdQ1YzQi2N$#2m~+J5ok= z>Aqg-ukWV@FnXdj-!68h~iedoL^cO~2F^ZXN&JiU^@6{11gf3W&Z zS~lJ`=9#(Zw;e&_IdeLZP6r0$FUqifW5AR3#6aWkcNO&b>_kP!_{^9jCm@hq{ki|x z8TW$jF%F%qCgw{$8rMR>@8Exfyj6h`kZ0=t)8?;N3mcO|z#mwNNX8MG6dA?H%srZl zqHcL(3Bds?q+;0ZHbYqaOxrE&(9U=95jc>*X1?;H3{;JE|&l*j}sn$eef^5y=*qRzCbP(@Kh+(w*<@NWqa zXV_8lCv0nOzmPO6kVoA9(^ig06NWdZ+MJ9JQ-(F0v3`Z?OAVp&6*L9;J7*dOEw@$h zb=tsV6$U%8iIlB{y*fiF7o+HzfMmqKM0(i z1D9IB#NGgFE+>X4CYnnTq zm!x(xb)!Bam+g;TvSPxBED$_)<)^HCw#xx1dS%2p<8L zYe~zTdm|$=IPkXfv^9G;ML0D1aFDP3DHDDonG&%d$wBS?x*W~dal2J?{{(d>HZ7Q$ z4H%%MvP%8^iM16i8}M+Ay`K`{4znN-B%?SyrF?gRbK^d_tS6P77wa(;lcO7)y6doU zw!J>i&=YSM(3p*?WWail*MK{2C}gga`SMwtVH(S8bUGTM5zA2%-mG+D33+N;>2@3p>;-O;$Q}3^Nl)i* z-DUnF9pvFTyMRr}tM777z^nfE!W`!}wuF>rvW8F<9IwyuvOvyCb-tab+Ec@I_rlo=Xjcmf17P>)=DA|LHqmr>r+zXI{*=EG#E`9*t=1V=0}mx z9g@N!wW6HgcW~wKH|rUvtnv{tb(Gx-N&C~>mmWdew)@eld|)mk5OQsIZc&QH&K z5CuCst1^PL12Gjz9?*l=<4X?KF>Vcvvf`7F@1 zS`NueQM-NR5eu>Ht*JJ7U7aua*dsT(zMSG*_+d=Q+kS`Tzh|uq8kzwc1hh8gKbm03 zJNmGHMbMTbI%!Uh8118L3yZN@XOwDHl?<69-K*nu^~BYezT|DrmS2+E66?^?=+j|j z59Wy-V=LicPhlb$LiGnYJabAu`)sVnv%Pd3Q=p^3@}3i z#|Dj$1xD|LSFD~<#N&083I#(-e>zXGz4JMp+1BF`thh(E=gaCBerA*szR}0Fto8I# zB;6QFi*Y#d65{{3*I4t7E_NPfa&E347DJ#5AQJ|GtZ;Cya6(sEvOgRJtm};~aaoaB zZ4W)yb;;mi*^Y$=UWZN7sUeaphvTr+%iEmj%?Qi@XUL^?Du&JOep(DIsX1sS!DiJl zQYv%hkJQV*4;-!hU6C8|RO@k}L9x^6?9bn9U&C|2E^L~|QHNiS<7ryT1CMG|h_K0{ zAGyaoVapCZ%NxfppM((%eJ0&N=`i%EKNy6sAy*uv1($ukN6tzFEb@Aq^vJy0k^=ln z|Mp6tPoOZZPNq1_q#FH9?hb#X^%2v_C6tL{IZkvmoVCgjv5y)$9hRboxRj38Prga zRwKgam0;fFO{ePhGIH9fC9wsQw4au6sbL2i^?nN_FB4K`b0VL{9MyoUO#5sOW2>C3 zh}|+Bcg;9>)|rgEn_nzoxfW(TLo(H}U3)#)Hz zvM1WXMuFZS;C{WF==hLVr0d$$bT|VkyzCQdAx1A)m0Uo}{ANdN4di!gyb-m&I5xCW z;fU`wCUmLD#GL>BB>=Ksb0Pm|NiN|tLlVzp3=QI0OYGjn>FwFd1t4d3skE4(W(r3DCrHFh0JcIyMT!>ev%t z_~s;LqC&w_ru#-pPpx_dONfN<6RLo`@>apJC!tF%|)H5~r+i2YH18d>_8>!CZd>vh__Jrk^oli_n zM-e>j!kj71C~qI$7?XM0P<1Qs zrZS|J^dCDG?!P4rBe>u=9kYJDNwwt#yJNY@c9s16NWC)eo9EuRrt)uGaV<<~ce?9L z7_&Qn&T{2Feb}4;m*{7xLaX@nalbl%sJ|GacU*Xp-4CWM-lajMxn8gnz)7HV}nmSuekQg+yT#|HgZ*7R~IyeS5O zbhSdau*Q9E4eQodtedxc4P~&4V+*@;G|BVty9)ESc-p_|-PAz0gPEUFp2CA(!OoNk zmS@`0;!|tYc@eY0=K{HNGHL<>e18phVX0O@aHvzx%Qbq^EUhZR*OYvFdT$07wSNgFaB}25m zKNCU%tGwaa`KEE-#Ud~G>1e}thW$6vw038VvbonoqUcY*F&#aW zB07^#4CAI7?O9*egndC3JltM;QIrlJ=?e}oPtC3)pfRacez=;)|Gui@cAl!HCkB-; zDZC?)fEw$WPsQlcCgTqrb8G@F7OI=IPDn5QZJlXjMl9mAz0; zO_UP4q#&ig-EV<2*O8ZFB^hvdcxlZ=3x?Y7ZN1Frfvg!f7paTx{5}qsrwTfcOMK{` z8Z;$`9lB1Lxl&iLEvKfYJ29yI)>t@K7MD?Vr!{dW~BCCZ(p^7Y<_h^EuBX8{sV1n9d7sFRe<=Ucj+Rq zP}a;&h#}%8!!GnVg`YO1)W#TunD0DU5^MgpkZ1;CI!p!h)_a7L?9j5n5(59Os?{e+ z(%gjocFNPs3jvB59aMfYI=3y{GHd7N>9sPmMga!O9&~kp;Jw_3iHr^aw``~xABH5{7scM z=YJUu-1#3fA5904RP}urq>gm78on5~od%8tPI1ueP|1t;?oV;xynW>scx9Egu-t^s zO@M#_n(8>I52yz*B}Z&-e#uV&=0jnJ^=0Rry6`#jWdJ?o^qBC0X)XVu10Ge`&+0V7 z!dZZ)Trr);-?C6xiqWP~M-XTDW5%F*){EQ7g;sSkY?VOQNs+Itdv8uY+bHhHMLCxb zjsgsMv|Z1D$=Mm#S&`5TUhEMwai6YND48|I)P~e; zh@?9*0qs8?OH&r_C$_T!)_n%mqg`0iDDj7uA#kPas_#3Cs^yF+*8Nf@+SBkkcDCpM z;t8s~%^kQ#vYf|*3b;nn{(TfGG#n`zvb8!Z`lcHjr-h0XH~-FaRgdMMdgw0R9}6dI zF+sAOyIpe|gzr&f)RTdji0Zu=x_xb&n$-`%!sgxI7NT^_h2!Fa%xg}VU!pWSpntoG zIH<5p22F&sZvxy^T6h`^i|ML>BIje7%%?*Hi^Y4Zhp>c%`i0}%#(S+eSP}XUdmWEe zybrrn;;@32gSe=Yi`56~mdBvbe%IXCyKK=)Fbz{tuwBab-3tB4qNVPXMSwJiokX;} zj3_sYf(&Dl2sf(Bs`vt0enQ_CUtt14a9Al~Af|^P6A3z=y1D}oCx?3fPW@qJIuk;n zW$tyv)6Umo80Xe>@YDye!X{QLT-U&W*$K{xt@*8*unFX{GqwJ|YgM21R-vWe_nT>#se@p=Jc)|LEGfRL4nV=dr#V%@smk;>c8|#j-(Hq)9lS^DEa}uscd4svK8;)e z+d`$*0WHdFO`4w!Y&_9{BcAIhaPqHx9TvX9SS#3>2b+p)0VycA4~GwUt6GeoIEd)p zErboaVk7ySojOoUa$wUD$^Dep3T~W&hz{;^PZuQY2lFg!pz#~#R8{zEWpjJ^AR&=hH%8u!(oI!8 zYwV(KSgw5`%yF$&)zDLc8}9OcoNT4;*^SZqwS5Kg~BT` z!K3+_l3Z^7?NQMX8E=kIBL#`vmQda49)We)@EAJ@1C@Qx<}OL?_#WBm%A z_Z1aSJpSs?(+omXOZn?q950fBK_iiU_I!>pX+o>LW9r3<&{jPe{Evz z3e4+Af&Ip`khdmA{r(Xgra zLD-!?h}*C$%H{0d>uikx$eU3$R{iAw$ z6XMtbEtVW4go_~ZqQZ+@OdXd2nYoKY{^}+4DVr5yUo&b9?{hPh0Eu^Rym*Yqk1`gB zS#ew6JA+7(Svw#)c@JjM!w!x)cvE3DT7L$zn_bkx%xnNr-E_JL1KT0c$X#wJ6e>dG z2UwE;F_DUyxk@=&RDG1vhPmu?!r#g2AnZ@{wM+VVLX*h&JO^>Q812kGPG&4HFNFQ&`;_*+TIN?z?#bscZsxPKls#>DhrX0;TC?etE# zj}vma?B9eeiTJK{v-Z;YU#yZO%}m%Ee`$3<>F>fBc%jdRMl+`!Tq(8hFmBH$lkL_Y zCZH*%nrpw@gp&bhrSJ>1xcy9$FZ!6W(+La6u&rhvG66hw_D*xtUMSO%xWp<&F?mNu z6Za=}Kg-7-D=f;`NJs8qtU^n|u|i9Qfrmt!pi-S9b=S$ecd2Gz2p>rk3HnNE@aTcK ziuz~ue%yfKTuhD6`Iusl=mfjL>SXU-N#Vx<85_T6L*(33WrZxT*ME{DzcP;}BioW* zkuH4MRspj>eBg_;zSJR~83*4T&P-S2V1@Iq-Sy_Ll>Qv3f3#2uW2n$be^ryUsV*Q4 zhU99Q5OX!-!Io5%TD5bB2M~rC71qb#x*>fB*geab5;_asE3Ve82Qc05cIo^o2HTc~ zTzU@yGFdsflG0Kl`MC*kYV3{08WR^6m+YhADTbmMnQz~C0ry(pai*4^ttAn`Z`k$7 z_mxr}Tit$nu1C7_b=)|B9Mj~Wfv4gz`ZmG$j>w5g>y2pjy0DPA2Sg@-|1g;3G6Uvr z_~@+Ta=mmXsoH?{A@^)jqc7NGh~xSNRQa}m@!_Qi-17WoNHmY0c$S2l6LLpyCR+5& zU6nrpXK}8p7^Y-^#AJQa-zI87GLMvP$o_#0o!T`k7450wT4o2S*YPv%CPCgyC%`wU zoc~HIGO=(*H+N+{DNqNagT7bBBLHY%<70Y;Mi3X5^DnSF2vpv@gG1td{mGLk29*ir z*6r*NLwF)L!hS5Jw%7vwUAi+G%6Kzv*`)`v>~NJ9Pbq9y`w8+pU#Qf)m^xy-G!aRAvYVp^`t(dCWM%2~jE0V5vxmTt2)H+`-((ufk?Z-bql# zJ#szZ6II#w{D+m=@eKG;CSxp#`b68~yjfUggwsWKgd$5izl)r6u9#8*&lEPr#r+AS@SFSF_L@3a3xa`xgWX{4v)yOkw_W zV-+P1RXRwu=LV;`6uDC0_@nM;E2OvRkG|I&WCf42#vhUlS$0fFFxo#3%INY_z^$pH zur)&2Y3Mqh7_Lr0@?9`SL~$N0pC(G)uU`8Ew&q#bCAJ}x_eWd~fv>aq5?N0_;?Q`O zzisFW$)mRa7gZ;I-8{@|kK{iVqQ<89BSQg5mRo@3fB zdOInwkaSRjx+b^k$^H%i$+`USw?O4$9Um}|U5urKXYT*&(iX%pjg(pTaDYiJiDSTv z7l28^C4-}Gv;9!c4X0u>xv)mQfz^8`Hs`;ulIo~1T=DJwkq zq~Zj$k#j0SNwm3rlWevMgJ~h0xb~t6mzj9UezNdTw`cL#kCNbbSXe$`N)+edp0GCX z-hZA2%a26R%IGj@zn+Xc?CtXv2b{wL$(`)Vw&vqd+1bXGl^VxJ;99b~<9@qgZ(Vt6 zodI+=!3}??(-W;i*iVRtzfZc)QmA^^4Yar@|J~9NhzGt4hLgTFbdLsioM9Fz(-PKA z_i~*BntHqidnI~~tl_<3gh~)1x~aA3wW(-lgWa?fk7De&W%XffiCp7Ve%JLtu*U%A z?C8OHdFD7hnbHWgx@^1_i%-O>VxIOl_jn(D?O{igr{VFsp7ScTFg7b|;_3bps7n~u zf(U;%Cg9-a#z20G;>aB+y$15UQdrV2Ghc{W01N}~U^U^ngkdM=8~h<4;Aq_VpbGlV zeC}?ke@Fj!xaqB5tVz~WhdMe7=2SAaj^{?Qy$BIcCUo+ zldS0K$Abkha^}v#H9^m#;`H5E2ulxZ{Zmatv%J{!0(S0}H1kU#W>{$EnkGHXbV8qN zyoVOuB390p1@oFHH;vo{*dQT8kAR@%udi4aW_F5Uqvhg&!X^By;GROluPkdt#8o1^=a*FGKOHVnUUQxkDK z{t!3{OGpBeWcj|fF!+`Ht)h{>-)yIY)@-Lz53Sl+OU_S&@UuTs7O9CaEN$cJh?}r~ z$zOwDU1V%%4YVIpyK@Vvp6!wPlccU*P+k4=bzb=A&m4st7?`N=C1quKd3jVa>-Hdp z8DNls88CVN&U*`IW$wY%jEzE}mkEXkmgjEi^kYS)u}6}6-F`?GVdJ^*_tSqt<=$5e zB+yOf1K%$=PANJAk*@1w)of50mKGyJES&+5 zi4!u<{fvxwKB=AR;?5tl#ZV+Lo22C$5@HcMr>nB8 zwG-caPm5RbSgtRi{S~=ZLkl3-7<5XMcoG!M%rU8{M6$B7%5(oHWo&J2Nd!F;q^UVw zbr4_Ma{%ps5|We6cjO7e3N4Gn19-#f^?KZD$1Sh40+XK8;z#yAXQkak-MBMN=i>Q( zI~yR4`d&7sKR|SdpnF(yDDTt4!ZCS9QHq~djz9apL?boar!n3yWQk3YCQqKPKg?pM zq@eqGq%qtc3K$w@#i`(+Jn+`OSR}y6>W{dsj%;F z_poB~ofBYg#%H2xI@TJw#yoeQcJmV?){{2_i#>M8fm zA(+Y2acCnU~mZosqlPT`?IZq5YiKi5Pnq{g> zodM)x`926|y$Iq(mA>3?MzX^2IbLIYs$UDr;C)0fbj04so+@AM@2PO(remCYztyha z&X4U%UH?flx(c0Q3g1z-tCc;+h7xfcA5vpY{qi&B zMZdiha%U$IJ0*0IJY4-0upFcC3l{Vqn%ZEgD?t>-Q{+}V%VXw%^3JsPP58x5VYaFcwX!l=E`P+BP z6E!bIeoF3JvHA19ibe4wBeGUjRs(avN(-*hB)p<>awr3#Lv6rCnwuK{tZ&=el$uA~ zCv|lW1)wDnBu?Qya39n8kfr_2bDSXFugSR&+nl9;o}5D*#8n&%%sw1I0e3omB)#pF zU7pCK^Df3>Cql3!T(+u2fbd4pL;G6kTdsQ&&|&NR-LKz_ygccoc0PJonZH4DN^l9z z9H6QvS^SeLXP2?7Qiy$M*Z*f7gvo^PMVw*4LylW;!k*kaN&{ZxFnt(8lIv@Nq5{1_ z^h*uSe|D&vQ@!Y6<&hrNSEIT^`{_08L}Gg1r$}RdC2KkD zZG;o1MG#_rajOqly^zfVVY^~;D&Dg!-U{i$=e{hIYE(c${x8MQqu{Q2eMQNG2@pY8 zY-l%}s^Kf8PW!`h|DWH+Lfbb;C+5L1!RPx0j;P#RN}#iXxPhN0?hFU30uBY7>PX}- zP>oh3ECM>9=q7%4E~iI+`}QrPPQAsSdEc$>4Tne_zV`((08ZKROq&(!CB(<1|J z1VZF3jxLJ{QaHp99PV{N>7AI>no%jmg;`-wyx{98Alo~xY2FWmd15=UpX!i+mmNu8 zvj5GFzh;0UPlJaOkThhL#XY~IudHy{4nRS$il8E;yoZ0-;z^uOs@&yV3%#Kv7S*yu zYz+b=bbt{AFs9}6fIyyUFg957vooQQ3x(szROnH_qtX+55;fy!HLTPPyE?m;m<- z{*o5&3ZCR1U8d}_q!Qai{3ZQ~DJ5@mc9LF1i4p=ShF1BecL}QZ&C9&!F`bH*Fx>Av zz1_b4qI!v@RtVX!cq@dv@N=_c7R@trHCP^%a3XvnN+X^7t}&Lya%E1Y*x9>E3RJ3G zYjsIi(6`@-uCSVKYR}DEK!&_)n%rK}lp*c2Xq^k8V8@6k}j3gZk9Tc)Vvcjybf@v~q%;%5tVEr7z@Pt5Z^H1}x(fXM^uW8jX zgRw9tW~p}xFiV-0{w(|i{CeAUncq&|Z~Tg((u5ze&lViAU(aP(W8$VtagLHFm+RwjC3j(TRZO5(;V7w zBIAr^Xv`b2uqt%mtQ4rMrJp)Ef+DSbRiUZU|dS$|nYO0KD<7;9y)Blj=6LuW4l9r%~mXC)#`fi59 z(%xQ5Hte4}Eh-{{hPt}xDyva$r0bKi5`(9JmAUrVCxA%nA)u8lX(HLyui-OoT@2$| zI`x|i_e{kmoag}N{y2^^P(VxT_C3F#0JSsqPwQZ-j`JxQa!rQuf`^FHf|D6xd93g8 zs=4>bDefn4>&aH5$G-JkO(1hFVzqOVrp1&R>Iu3{}YqPFw@tXJs1 z-a|cv+h_f8)7wrEc18I17iQNZ{>Qz{MC&Ow9t0YPK1gr7*P5BT=3q9E*mcrGc7nyv zH*2Ww!bknxUB?g+L{hX&UQhfM(W8Zj=J=qxn&h2`*ab59faiCF)=(O^<`b7U;%|7mx^U0rYu@NLMmJyS>6O z^;hxN>1SE9fxzfqfz87CI&oiGYsLGJgm}AWu8aiVD^E|ur68>EX*Y-*F60{_?;i87 zE5`_kJ6HkY8+i5-oZyP(M`tQGv6b5SJKe9j;BqRxE|~)BaJXEPr509XqHdWbPfRgt zMJ{QJ-)}g(*eUy?e75Zq`xLT0`l)UcW8OJAJ8Gt7@m3l?cU@Jh3Cpt|Podqk%7R>X z0LkaUeL~5el8w@jayqnVIPDBSjbp-c@OA+DfO!rE1o#QCPAuzQxm9f^z%KM zLD>!};jJn|F1*(I!Ki0{#t+ba$M{9v_+iM)nSKc4_H=zA=TwB(|h&X zDYp$1lD}}@VDYo!s=Q;88sRF!vC2#ynOG5B%_P^YSYP84xn*Cb_nn}L7jm2OV#+!2 zBd1zRSh}ZNocoi6&^fGN*n**+=6&lQ78e?fcx^Tb`pH%T_kL|j?4QTcI%K?*QV(U+ zF#!*hErs^Rb_<`4(b22P`dn6zY9z$PvNoq6tm<^7FL+)On=lYL{w8c6Zkfu24+O%@ zC)lY`Q)5<5^*VRLjlnVLYrYbX1k7D#3Xk0bW|fYT^aux2f557u>M81#_fnbK? zb&6PTof;;W2K;x+eQL@aLZYa#&sYaXJ<_O1`AF*Bt!~C6Zh}ho{fL9`P>pt`KrW$G zq?w=M&yG4bu65879GmAc3b&VYde@`016$PG!AO~duaWuNCk&3vC2(NY04yE9D-N@- z7hfDsmYjgxhA`h+fsW=CzQXNq0q-K>(WS@?1d#Y3YD(Jb(ep?k%Ah}Bci-K)|hI2kRl?PvF zN2w6@e-J|P7t0KGbUWmo{;#&SKUx7@^Lbac0@B^C=1Nc5pz!Q}R11^mtqXBMmk%=H zVgNg8u$$$~$=k0U&%XeN7CWP(zXut61FAzqevIjh&h z4Eg!38<;i9dVSNzM1i>OYLUqJ-t76VH>F{Axido3y4jE8=r$i6uY0`|mEwO|b` zxRIeznS4C6`d85Mp?nGYb*iIe2oUSP{Lq2%h0ZE{tpjrB?EDqYke{m9DC6UcHnfTY zE6Lj|ntD2_r|#d^qfPpVf(w*UShdJAf!%h+1z(~I>C4$8=4oii$P9k30?=z_Mnu!o z*c>#4cGwt4aQu|@r2jS+j}%BebNqMP^>M-lFp9RtG&gj#&_#{lsQ~ZVHP{mAAHO>m zV9QE(>{xi^(lT4^r}uu*hgs~}ko0fKI;{chuWYG7VsMy-z-P)yuMa$S-GeA{3{9n0 zLdZt4!`2FDPg}bSi?d7O{xcGZ+#w(A(GxRCR;=96r?uXGMbCyn{3h3bgo4e0Oa7!&DbJ9>6Q>{XpB2wG^jeyvE^j(|M<>q36Z@ zRy~5wwK5D7<@Qr>ar>qdbr?u8hKp|O*DQ&9T!&Z_KOa+8n3_tn*YJmtm3dz3>fG!U zX!})5{xN0C_!eFCSsiR^s99`I$zW}3QvK5`Dwv8zklpq+7g-sPB@S}r(S+f5gml56 zI%)~Pv^ZWWHc()$*%jj6_Ri+;M3DiqZytky2{CjgAT>EeTBaEfEC@KE0q-leg!B7g ziQxjk-VyIEfbN0;3KgeR)1k}wSXvr=0Jm=|#3hyoY{s7U<6%OOoq3!{`WjCD?VO;@ z0W^F;VmwH0hODZJ^H0q}ss;ZdJH?hC&vXJ1k4gN>eiiCx5S)jRR&75g=k7j&r0cJz z?}r9tqO89I1tw|_Ib}1}Fd%@`|9ifOVOH=?fM|cXNpb9!QZK1cDz}cgAQEt}=6MA3 z`m?ol$d`X$+%3FM`eBh>VVKAqvh!Br2UH++z&_?=iw?nUigc6>$~kIW+-yk#>ZbQp zVrhkqguTrT#|T#Vf9vgDKl4g^Lu+f$BB7d|OhkhJCW!aNJj z!5kNRNarzi+@C4P`h1=^I$a~uZ209FW0fMWhSHD33&;Wynfu6{1$8dGLMtC{bAhd> z;@gRfZvd%yi_%BvB3Mfyb7j|q_-StMv`atD08ud&KE%N1XW_HjmIDDsm<9coy9&cd z|J3RBlvetC5!Kf$Il@K4D|nHq=Bh|*qJTIwIrk4g>89LpOWA}zxhnZ(nm9fDY{;aW4Fkd&St zA0;b-Qmrl+tk~DS@`+((B8k_y#uedna&yzbiXa*{76XjRFAcai5`?Mwvfv_dSR;=< z2*((B_yYlRcy1)vY>~SsrJna036LzMe&axda7LuU zs*?r5W4^haF8yROfhfN3^G$R5@-uzFQ`rJFQkKMjq=e+GaC_GCcbx2Nk=rJARKQ3^ zz%dYDTJ1m+(Z9}V$S8``Uf~dA zWDENxWlhwe0Dmra75Dk~Qa|IE5?81K`0v`(NYR4u#DcqBKA0r^XWFE0N~%>BI?Z6l zQtmWI5$%x)oK@z+G>kOP9ZJ3V9qMa*Nx?=CF@H7yOlKLhw%*9Vq0!~D|A3yxNz-5s z6q4lY)8(l=Rkd;CZGEH5^=J!?6)X!9iCOk}_Zo6ALPjGFrK439B-&pbx;smz@lp_( z7WQ$@EV4tis@=`tP1r4goJ%GvSs4xxseTN|Lx>-x>uDF_%H9fHR0>$1+0XpsI60)A zOZFD)M2x4%O^?8u&ly`iyM(?adc|H7DyOI*RQZw#>9DrsC_4q^EdaM9n!TL~0dHClm3uE0T7y^?}yqq~@&B z;Ae~Sh)Nv`@W{a_F8JSnQ!RhnB__`S1kEEkI^2u0WcmKZ+QMurR$wn#t~{EJ@L3YL z2;cK0`du~_H_L-w=bT=yL z!N|koMfp^V?7>65*%${4kA+$Em(|M^w@B&QwZPUw#*(*2w_!ppM~?%xdyFFt>nPSt zU3&zA6|KxaPP5#A6zS41^D&HYx8cCv}qzAz_b18(Ogc#Aq0$`Kc zU+TMftFX+Rfpf`rk{1%t-SiZ3z2%T-9LxmE2W)ej{k9+vQ3RYfNGsU;ES;UZ%xbfa!W#%mx1!T5fV&va0y);s%+X!TIQ|^^%t?t zCII2;C?kJgBc`mrmacUyMtt3w%GAoT3^Wkw$f`dQpi{&8bT5rc37ms?;s1+lxKg>r z#r?E~SB7+4DMdtU#v^$u*IM`oCc7!)v){op+?tSRTMfG_ESR@>%M`u+MrwY=dmI;Q&lP9Nz6)-1+Tk$Js0uKMKgs?nF{~Um0&G zA^b#o`Bqvh*DGYN#_9U{DyR-mD8hDWYlmv2h~@SQH~o>{?<3-)6^yRmEBhd;`vw!` z1~UfkKZh!TyhRen`Bo?OJhT^Fc1LxZa-|F1y;j|h%a^Y$4vZ>nY0jwB2Y3j zhPwf)q7sv$5;|b7)QPb%IU?_nka{+d7i><_P{H-?D2cfl(?I&Zd`t!mA$0@&p#f^4 zX-r2;cu%^o9NNobw0tf|BprE&I?ayPA^u4${=d)N7fQ}{&pLue0U*xWyZ$SfucMj0 z4#x73mejQ!mvLygMMw}ZP5`2J1VIk{G|Pdn70MjcgRa!x#WuobnAZ;pFRLw94SGG8 zV8Qa8EXL_<2T}u+4zG`YOpS9c6eE%}i-J>-xaH+(fYN(Pk)IcSMi9C3unzx}rh7tD zH7$1=cJ(VEIZ?BwvMsex`8)Xuq`L~^w7@HzE%9@R=y04j?#I>%ZHo_U4GrSaTo|S% z(uk*mtTaHaG3C3Wx<=v>K(V><07}*3fl>!GUxQa#3n*I!YC--59A%TS6Nl_Sjfiz#@( zxpPC{yT9ojx(&P0cMPR)VoMQ*=VerabL1onA%}9f0DsH!9~4sE-)jV`GonQhu3nxK zjkh?zrv&oMcM@dFS@RV}C;UX^g=rDsO`+ld zxFmp-1F+}{P-7Pg?-dH~iRj)qy|5;-2=VW{=ZWo5d{`m^WSp>|C!lI2-%@L!8S@8x z4Cb2|4$RQ6JD%Y9<>nhG)5DPto`i%7r*$*t{*=R#4fA)vcYeV(`!*&d))X#G{7o;5 zr0pm-DNlwXt^}dPB1@ff*Di;cEI@V29;#G5tRRMP;zf{H_8`(OT=SPj47a=NtV1$f zeIxI9@}Qy)qX81-ALPmLY|-B$pbb^-@3#Qc)DdIX2zU$rj-6gqX+=4I0>}PH;?b?I z4C)&AfxF^x(qte;8(64%eb0}0Qk?fV{1I>-E*-gH{j*8cKoFsPCz<%cQuvp}8{bC4 z`IbT)6a{v`K;@pp8$#uT_YA9y+!Af4y26BAblPMhJR(yr42DNcON+B z@o^jptw6m);d`&R8fSh#zt7oo7bOab0gm5^rINOtiAnsQCK6CEnfW8tZSB1~R6-!V zqf#mr-ql{9C-q239w_WkV;izj{v6fN=p7fFQI$KWYF}A%;})Lf6L0aIJ7uqRqy_InV*SgsO;^RiXWgCm>k7t7^hYsm8Me%wYFDR4 zrQna`i)#tIIa=Q&E+eraZa>Jhg zlY1`(P*fP$qC2&va-~HuGZ|a>YYJ1zE3-Sx@%5}7(HYo zT}%VB6DubaE80s-^Q?#nJW1FiZU@Y6kX*Pn?8)Yll@Qa_B`kuHLLIl2so0FED<*)_ zj%e)BoPt4AfZRHjRN1_7SlCQ#lJsW&|H&WEpg_fFWaF)Qov-A>t3>1m&d4;T85iOi%V6y`t_D+V^c1@kPy zTkjUwMiuQmVvf&(zjX_c(?nbXfGyr7B0Vd9)PGH}9DJZL< zjxsPR!1)n)_JoeVudTo!^2%<IS}Grn==Am^O#-p_j0`Yf5gQ!y^cn~o1L5(BQVJ_Q!$ zsbVBC5`Xm+3)PNWP4D0@%m&|(ke7>M`5!*!^k_wQgyVJ{X5H`#pBG8kgljN9Ix*s^ zs}5~HjGg6~=#Ii)6U`34y|O8!#_pZ-iAVP%Sa%SL_d1c9J9bBCVh>=Eam$#O14E0M z(`kJqGcIsNQMYXCrriYV7Zj{Xrzm}7ncPsP&vP5x#MgYbH7zvfK(Qf`*QJL6){+S} zH=i78m2_R9dP*_A2dQ2g#|1od*`BPD+j|jOtK@gH2u`y+2YYH4qvXBAJBUlpvmo|OXaSMzO+;&Ue06qD0=4I;%Z&gu;Tt&cdaaY(tVck^q9^XUR@ay zrRm*Oe@c?La&kExO#k6pyUMYOfZZLlbZQ~zIz1KZu*w8}RVLIObyp*g7xO^P*|N*z z##n8lG{3(+x7a^9|7X=(k+zg-3%teW%g?XpqK@(kcg?qunHQ|nRa5#gz~o@bP`om8 zN724G(v#XpE0YkaqF?+${X%6T{yoF>KWv^Ssdq3Y}Ns#Pw3oN)}lXi>=VZ= zmvHzqRKb`4(Jq8hme~l?tHF+g=^C0H{S({6*0SKg>@N^v1r=m!^3;xh1HjpdD@2vZ z7lq0JE=l784k(saMJVq-e7b-c8v$#NfE+x61k!VDs$oSBWEs8uYIQgUw*akq&@5d$hx+?$Ri<+cGK2b}s})qX!q zwsIjs@r_FhtuD?jgKZh~h-u&t^y2F*N<#8fQSCkr1CM3suEWm11>N=#h^bEDy6-gg zV=t>R3G9x3OVoPiEm}-R@z)!jW{>iR4R-2MJ+9sJa8YpL=v@CHI@ycjd$EF)9#;re zGC{;h3kN!K`e}-nN2H3Z_OxS5Py)jyMF+U+$gF&O996uIoI#LZ?4Wa|t-V;xsb0fw zFjKMlq7jF`E@DuJ6ue8|*Gk=#2!)E6C+RY_t{B;POZA=pkCQ^L1qD^HuI%PN=_2p@ zKj4nE=6#5Te)yH+AC2i388fC=8pRUy>c~&3svu7SoEgGVxt6mvu|WUrdqo`z>&|i3 zP5R~GP#wY9bcg>DMG#)_^Jik~>j|pw9SVn!KCKz?=tg1VU#s$qir`!LtFrcdawlbs z8p%nNb>4C-WhqW7U}9xkaxCZt*nVv7_Z_aNOpf+pl!Jf&prVWE7R{+pz5=ur zS@k}LFESjp>vU%q#6+lY{dzz2VZI{(PcFZ7xT0tigl*$fk*t#|v*Zs0+3nAhygTN- zZ*1hgxW|aFd}op%wvFG4`lG3rAJwIdT@Xk?Lb%Vxs#|dq6++sxNP7Q`H}($Lx56^CBf5_o0^_G`)$Ja z0?Ugk|9BW~T#)O_m4uB+-N#{h0LnQwukaA=s)8K@E?N<`nbuWFyP5OnY*^H3?SeU!Ro0RqL#{BV=8HNq83uc2S zA}s7+sY=AWHc5uumzQm)B8WOxGw$B~0vG@iO-@LTFCngTQzFTiHNASjMx^rWOJDbK zQz(&#bo}Kr+U?;iXh_!8170spg`{>cIR-)l;kS^rw;rfnChiE7et7i~2|8uT8%WXu zm9x7A(Rjbp_a=(6l3mWDdi4%GASVA?GC%0?! z#%#5LsIE29N0vrYH$ukX2sK>i^o_eePgH$M@L0_Bi3L zSfisQ+7u|{FUT(B*spHKul5S`W=8M9H|;I?L%8srCa(4x>(7b!Md<<6|5ki?2T_3D z8TpF} zj*X*|y^}H4CCuT%vX|nCrynf*Me#n^%yOADyBJf)8|6hE+K|9_n!ZM#aSSPcUu#q? zqi9A&+*_g1%US0g^zucro_)Y^xtN(5F!_BqRR^M~6^F)WLfO$SZLAymUd-w8Pl!zn zm(pKxx#0A_`P9^TT8h062B!+`pM%9O?mOPflN$MXHXT0#T#iY$bH0O%hZ!PL;0A^2 zZ-p3h0RdnQUUd@A+Ag?Ss#Q`td;_lgeL(33vzg(#%~MCoC6W4s{+m$8k0mCh+>42A zhi5)pkh=GngG>W$_B;QcGNJD|bf(-~P}&c7m!}crk}j1UGU+a5iNpq?ZftK?0UHr$eWXvaQt@rp(PMXUv55z6?>&YhA2kNLO0^MbX& z+l!ij;JebO_Wu#Ghtqa|kbUdB_;AoFm)g;0R~PND8QGF2e=I8-IPLD|xbSSLh63pz z%DGP|tA1P)ef8t*XECO_X4zUNzu`wPCCIqXz@EI!z$3;g z8{WR3cO$-`Sc9Ojf?P)5Dp^84-c>?8N;~Xc2#qS$Z%fH;o3oGFDNcQpv~Yr%SbWI! zl=f#g@Zg8ZFT&{GVjV8KIzFU8pi#D0B_si+%53>5g}Gd(x7(DW_aBr(-Kpdn=^ngl zMbrXPdADxWn`u9jo%`Cn#X6vfg6oOV(p-x2(tDxLn@&Wg%imjwV+*c-qQQ?`-sILo z8{l*J?5p&dgoVocpx}RcJ&fxIYa!c;Wm6I-9wjaz0$8U=X3nkPi2)y~kqjbbz*6^| zy*Vr=lJ-@C-X1SQPS~*z z0U%qEtol<;)}qXtA^=qq7XqoAxtSO)46D?F%IK4@um?t}(m$6N4s7j|sOA<-8?UsZ zY#*xl^Y^|Vi*{dlQiQuh-`e3IvFz?@~V0P12iMc%3;+$H%6pfAw(KHr4x5 zN(t0sbeq+iD#{20Twf(+;CQ3I$oQc3*=@Af+!OD`N4hO+gT_1Mha(+sA#jz()m~oc z^)DgwVXL&Z@6B~}#EvQh8q;K=Di3`;LKrck=nA#%DmC~PH~NTZ6mF|Tm#=#UaFLo5 zMm&5Z%9lPnefL+9)nv*Lgw@k7x*H+(yes%y(I*^j`GS%t)t7CdL-CJb`n5nY1#>&H z(GrdkF@K?RONM=i6N5&O*(zc{j*`c^Zou0^fdWO3C~-f0z2}i|E=Vgnw2tl7B)-@B z>O^p$1yB!2RUQwQ*Qw~zN9_h#Sqy4Oz9oI6#z23)6L%6+{A(2T4ZCRd!$W+n;n>Kb z95AF{h)TKrh^)MI(SNblS~$s$tvPnYR-vgkzyG(*e2TODW&raN=8GHbpUX|fh-ct8 zRjbg8unLl@8#93nBtk=4cO9Ea2_O=Z``LA&H-Asw4GeBO6(pK5m5<+@zu)=x8LqtQ zok4f76TE^**w^(F^Ll#%vYcGJBXaAvL>kqvwttoSM`x@=^`M-lFKQPd-NfN*uM5KW zQsa0V>r#wGak@1oWNgz&-Qv4$B!b#61&L4&36Bks#3x9(h|JrMQ4ZSm)l47nfnD#uyC8r zmn}glDiYN3f}`b~zq@y&K=Y>?CS~X6x14)PoBmWQOZ~J?R9bZ3d-h6qx=eEWO6>*MEV!XsgaBePRYezO7+16AO!qpQ_qherOF4!R@Vu68&mu1CYfzs-4FL` z>&@(j4j6PzE;Qzq6U&Ww`42UG!U*4Sq;2jIQD`m<(-1-pu2_p37kb2 z1rc~baE%0Ym1&CLy=b_P8Bl}KQ&r_5l)pPig4)&z_g)cvOpKq_%03BwGUoZ5 zFZ4F|7e!KJ?o)W#%^7Is{?aGanChPlkeYo!Cbwd~Z{UXIhLQ;ap}mZUQ!9vsQRl#C zgYpmLF{H9-4`&s$>#q1l6D46(6{T4w)2kD|;i)Ya&SBdk#SlxX-s7Z68D=k)*5XA$ zR?#d{5dJDs^mn*nFMM=?u3mepQyxel{xv*t#n)>md-akUHNCW zGVCluWC#16e&!*W0{m5&UfvHhYR6 z717S#)Mal_d^E5f7w@>ytMOmQpu)JvtU?0rXt#w)OmV#ED=wMQ{hH~`ZL0C%aiS#} zx*b0dMsBQnun`!3L4S<>+e&1SbPE)#!ofbKe0eK-GJ>Rp@LY5IN*>4cPC}qw1=oIUnnrCnfW;c}>oN zVY@%O&TJIG@JZ&4PFjcqesKv7*s6_w$>OLP;A=fniRl$|WIpsN$TsTCW&X%y4PZ@- zd$d~{kxAfQOiqW9D%O@zM=B=cvw)Ns>u{BS(R}$OEJ)v%ZIFqiu*ERK?PPaKv9&E(49dFW98AE^H3VuX2_ zw*H`YD!y6FiAEKMS_@oi0y3~azl({Dyldile0~Z)QVv$0Pb?6Ir=k)~^Cr--U5t)> z-i?eO84dAxs#D11O#MlPn2?Y_tKaO=O?ud&sLGoErj=!IAn1l={nT}5S8`m~J!{IF z9u0|9{}$um8-}4Gtl5>2#wM*r_!Kyef(XSYO?!x0CIS761OF&n;-bWW&!T$+NCsHO2$m`7GIPLrg)Xy7JUNC8(YSL8AA%&dr<s42Rtmxm{D+DyX~5)fyWEy zRL$q~XY?&hj z2QZ93P<+C1;e`h12!0%~ZG3O>&;1*V>=}%dm|>KFn)gQxV1a-juz{&QIHOSb0|32( znYJ)})fD}fcrg2p=IxU+l3WxW&tFU4Q>O||&XNbX8(!4B!AX(L{=h%m5;udXpIaiF zeV2xA-%s(`JfdDqweW}N`{njIZGOql<-Y#qXRAmXH@6}qzk+rrnrt4V)16d9X;!`n z=no@H0`7}PnsyMf9L)fQfL6s%@1^PrAfkd)Qf|6G>OXTr*^e7zAdHzo%u%>sqAY&K zX`d%;JTnwrq{o=Di#F2Szln9I&`=SZ4T^TAMi#o>z8w5_!JPXAQNykb%bV|)+J6L^ zbk5@`Po<1f=|=GiwaM0fm^gK>9h3+-X>Nk4vnH-L;?CCP4y|J`g7r^6brsvx&g(p= z=w}w50@bMtBDVKz4TPS_YuAlYdkoLa<*9RaQ5Ljmt)gKYxRZHV%U=^=4b|8p zEH4WSix|J}V}(+x^LsEqf4-7%|P|2hK_=MLOm+m)ecpQqP&v7mgo@ zZi!h6xq{(rxzcGhe+Ph~BpmhGVU~;-Ie6;L(s&3Y>xX*RNZ7)2lA98Pebv?Ene2IbJ%^}Xv%-a2~)aG(2sLiRZ1i8 z6S6zvp{z+#vHjwSv1%>-otOog&IQoOD;q+7Zw69ZJgggbrYR`0hKsz(rrJ4$Prtm6 zWzMo+{8dWw*XS>Sf^KqcHQ~}f0EI}onW2=RU0^Rb%3pI{@d)OC1PGI_eH61kEaDlF}JJf%9z=`D*cQaRCgSaVWOb86;&7S{(>%l!Zf0EN1_+Qzd zp}p66`(E!Y^Nf_!zY`Y{@5@ZaFzllKO~c!>O%TM*d(`O4)M9Y!3$4MP3b4 zerPWA_4mG4o_abvMkSpq+Km6E;rNaG5>E@kNj73+*%tT_y7ox6(zX5el66+Zoc>7I z%S(iAIp$i*p~rj}jTsvG7ML>Mo^PZ`4YJUaSxm_Ne++P$mzeo>&j)>M&CR=ZOP846 zb{!tru%~VjS|*QR_{F9jL={?^$w)>=49dg-lI*lI6YC?9?VxtJ)zVJ)Uz^K7jxd@K z)=UY__Xn1%H;HDP*SwkOHzAZh5H&p~%0ULckd&>UG;^!`?b*WOn*sg8#pRQGgfB}H z*UIv!3(XNbmAT2Qj7stqOWarz9a+a^?6WR|o912Qy&|u&OW;+z6cv5VcA4zy8AU{k(RN}BY2w-je*7c#sc+xraC&rk&gPz~?6KRws`1t253MNt!>YfKnMQ}0vAn}x zlou;i+RT;E)$UU&^t%dC9ZyP?@WV77x?OA0r#q`$+-JZ-OQ2C#Qc|+l!g)4`_*$=Z zTOPZI} zYLk*JaVprlvOxdAk=crLqhuH&_aC)*%Xw?0#p&^9A3bTCe}~1{)QwK#r!sNE2aD%h zenr6g8K#gPH$12E-9F1sqFA45&j6KlX43D|!yKxBsxLWq5?z14AXZjkGgMpo7DA+( zy)niESQDFz9%I7!ke-`;>{~!EFh=Tw3h0$%?*XjYz-knq?k1aj*SWv~McXbqBR7S~ zG!O8t^3&+=tIn8N|B?B5aL}7`otpc5qTWCOxf$BRUviUl8L}O>ov8T7t{%lkJw{n{ zfol)Ku`T%nQ91HB?~z%z{#ifY4?P)+Mx;=58?=J<3fn0EgDWfE4t4Q%Pq zR80BZ@`_fibjs^IK;(6fM%kkt3uZ&1J>O3%^Zb0QK{Jq-kISCB@w_osQ%G zyp6$6GO?Jd8x*aORsYJakIwdLlM&nq276+>AC`eO0a_@*6;aOabhp^=@SI#c*KAxc zMiWC&=$)ZamC(7}Mpa8t;@t-A>p>{13V{XNYlTjIDFs$zSR+}f^8tGZHdnzX9t!$4 zBHwSKwwH*m9)$_-kWnMY0pm{5_103SW|O+--~TPLw;_mPZK$~x^ZgBYuw334dkS9F z(~>kFK5c7J4QBr1sXgXm^=g}Ts8S22z=G1EGj}I+CcO4L^Cirw`JGxshhig0r&OgN zR0Xct5o5+=!wU%61l=cREcL2S%pPW#7%?YFrYK5`ugnDXD7z{AY;LUHd*1E+y8+vv z??v?$N8r3|&@q!sWH#1;l4Bz@W}hx=E9}~KrPbLuwfQeNl};NDzk?icAXN#kS8O?A-3#ARqIDo|;c;x{1GgoHq%^p>54{4u?06P5DFuOHZzmysO0+)ss zHvgE+Iy<4^=v)4{F)ScRIWRu^DIr?f@A6DZ%L_+qF#57o{nb+~6p{-K9?QQ4{=Bsl znX;h}5Hxc&?pRqCQiQ88nOv1|j+8?9x=u^rXi8X--SY|K6z4D*{pc$ty6kw35lm!` z&cuR?81mo5;~GcESl1ZYNa`x{ur2cZORx)rax)TV=SvO%tZxJgt>`+l4bhG(@Jdgw zP!*F+sbqp%Ad2lpS&!KQ6vTEY-^Ko7G!xb4E08Awgz%pSC_ni@h1vNxM6(_KTXJ&-vrbz|k*!KnGR;5F zan&uRLmyTM82{wf*~}ch6qO?J%=fXR!!1fa_&x(r|428r#~ZzK6H2(=fcu}GD_t0~ zFkTKC2Vz*CrbB2xbacU+l^yLU`!={Ilq3fJN2r3(voKMW40l$_%ohGKY=g!c@H~fb75U~*_Bp=RFU2-hUJM{!bs;%r7xeaEdt%w!mx1q$QJt>5D-i3F z-x@K46^4?M`YZ`Ka_l#F2D@??m_cUJMw8wAfWTGtqQf>>yEv103;BlOcIMZ$pPkc* zx9Sw+BHSKS0Dogi$!~+USD=+CoC!{A8d+o!E*k7H@dOITLvyHhhi!HW#Xo+b_47El zn|YG5*YrbGsD;>#X9^!mi4Ceq#AU;Q%v>7HBFb-pC~@Uuv`+*icKuMlvXU3@7;^XJ zhu`qYuOBu!@>i1gK^cf)P!Yx$i~N78+jDW^Afbr1r+~y^k?KF29Jcqt1j76ICs_`Q zbVn0q+9<+@{LT>!^Pv=n(p0fmoKsCg@Dq(G{Zh$at34P>rXyB|QG?FRsHz4}nq1uM z6}In~$Gc4vCF~&JU^Wa((xsuIIQIKl#b2V(`}c;+P0tgPnNV8u(1urVe)TIY$8m$* zTURD3q18q!exdcd9>#W~X;WDkVoXfcjs)$Hx~zDhlf~)(iT>SXrd;_TwD3n%@(wPY z^6Gpp>N(M18#81=Lek)vknMR}{m+9qf7WnP2VOzDm@uPq;G3bduqVC>VfF(NmiXF( zc5o~aFU+^q@;|FZ#53PLp1e4!k%OjdX+BPri!8Xk z#WWe6q7(jMkWB^d-VXQ{sl_!TcAmW`YB1fL3H2F$L}SKGO4k)cn}Ss$lbWCGTOQGTs zk4|t+%ZH?X*=|QLQ@8gjZ4xPTibzT`DI|!a@hKY&ybxNAKappf+vAPrm(-jYsE8<0 zZ{@wH^S@6i)o$SbOG=ZrJG2B!S!t0=kfgTwqV-G51=!h}{EL^#w>X<4jZ*TO-Gt$P z>XWRy!eg)$6#VyFghhr2y;ZzKA{+QW3c;GG@zh&C2f&vHzToSoLR+ells`gL=+>%B z<};|=Ufs?KkJyqOTB@oR7Z+z26qpWSKSQU{<3N<8Hq590eJN;jW+TqL#Y*FePiwE> zTrHJGo%ZgvcQp|X8o;qg4dIMtjinPKN!-JpmJQikseJQ6Gb5O$j!`n66JX7nC^^he zegeqS(ujq=I6Vti*M9pOO}Vre%iF~Fx-87eT{*1e$Xx(~A~%^$&zj8qq?8sSDj_1w zVfHJcB)g|)NCRz+fD{6swRt~s)@V$-5lW#n{%GXph72J-*+|b=s|0X8-#i>)_eFXk z-9C$xuzuN9r(?WlUnHqs-fjCFN|Zv`I{un_r}icSS2K;Bf2)0~3qSZJ=Y8q0XKUAS zeYYIf`xvqfZvz2B9_M^YPxAAnQd%>QNYGA~1Ta^hJ=7g%AI=ONutH!da*VzZAqU=! zH!U08s!p%#ak$NQ7k4Y_WvrbrMu=RM4Fmcm{OS_QONk8tR%3wI)I1>0%7U?*J+vK4 za8O_LCM)puxRgJ+lFmbFqYi5aG%n3*^U*Yk`xjE0fNC7&PSq*|JUsEaJg^d$F z2$vlP&^->!WxUup7j63nfd-NPm;G&V{K%(BnL!!EGYx|Gztexn zVvUXTWXxS6@iZ$dG}3pPr=lQ`PC}uE3eyf=>;1!-cKTJx87FZ^!FITuOfs`Yt1oc$ zCrN&1C2+bq>FE7)2%1z==yeVIh`lZI3q~T_NIod={o+8ZwDmghP#P0_Q)i|8V{)G2 zeKq+)=Emx0Xw}xRt|7*u1ina{hqcUZaQG(SvDQJF{z790=IkF=cVhk6-<`k170Y5j z=ZwTJI5gwW-ukgjczr|RpIgea$(j-*$C50oRUI+78coaHf z1W7@vItw>0Bqjc7k+0KKCBEUb?K1O)^IkZ>}6n9`heN5@yY-j0ll&}yda!1XZ3&y zvcRmJ$~eyvCjMy-L(I*HA6ZDzE)yZ&0bs&JenT4^YfHW*%eKS>y8x}H&ap2%-__Pj z^dxS2Gvb}xc>V#8%Rm^kfI|ayxn|&=huc7JHQ;Tho|l_-)wn2oPMNr#NmDpkD}#?3 z(bEr%dA)%z4@kadr=sz#G8w zhKtC<=XcbizggEm$%t<}=C{S|kz*{O@^M>!5rg5xsHX^k4FljY z3XjNB^c@bs7kPqq`jPSP<+sS*5^#R}z^$hHjAk!YQb6ljx;gR=faC866orXTX!*Xs z)_*`5y%Ppy#e%6+dPM&iPX(4W-z(l$GT808>vs>FaIGDFt$x_K2N2RIGg>a=pz*v_ zNefWUw#5SK!Tx64Q3GHqj%OL=?|%HTv|p~sYjc0Iz7U#;l<{k@&WSsd=s%J=vg9us zyKZbkn9Bij-DUsp^T4ztx!7O139HZ1`j`v?*Z71v4i zjr$$H*4jgf^d+Bp(S*_zbs^|627CK4WH=FW>L{jiUI4Tq)!BPZ)#2{kuu3Hp+3J$X z6O!Gd$V!>9kh2j%jvAuX0I&7#yh5>a6Q6!Ke2Cfi!Y_f8i!8gbssRE4=>Ov54oCw+ z44Q*c2U+^MJXP2WB`XES9|iLBarxJsH}Ods$6NKw4a?qh`ufx>{zrJynWW$4b+_2M z$Z*A4HP%Yoz3;sH?erd&<;Q>R*HkYV5MwrqZt6Uk>A&oZq?GnEKVI>iBl#_+%p*5! zGh}ZO9d+scO`BSNWH4=yl^(T6965^}Pe&|o@Ncfu&o+CE+qEeukenuct9Z3dL<;OV zg%?i7LIuT&2@(JWZz@M3>#yv-WYB-yBDj8Zsd+_weC*I=3Fl*x3YfJD>gwWDn7EHROFH!@x*`2GnSTaiMc&-rk^VZ|+{ieD zrz#d&xU(zLWu*g@e-Q|&r|z!$8;2=wO!g+y21|HZ(bK`f#0E>j-_TigH5npgA*tE{ zbw#g;#@qx(pZ zwV$r{SXfyJ6UiRDh3diz**^|4+1C1cW$G7AblZNs()tct?=L-Okyc3kW1K>>!P$1- z3L{3C5J{>gSayP}hjjXP2~L?)ofYfvx#D8x{O}4q4ps5y#?{;l(}FJJXeDYoeigrX zoPz5=VPD_>-AWO=q~gZ7O6kMB_uq15xQnAi-njP~cdpO`w&<<#E@ zaz7h~Xql)od^K{wH6Sy@_dHi*dpxoNYDk*yVUa>s9(v`G8V!Makh`2b1A4pQ#{S!m zX4yX8rQqz6j{v@$S6k~WH~kRRx$CI4yRAf$pJor&>NBFRt4q*kV0%&2qLAfGzLppz zEIAn;X{6Gi6)pexN{&F%dc4r`*8xxGAW>j3`i9cDw~NfUM5Y?AIA}XnR?ZHTU@{iX zC35IY6XhT=m?k^s2H zdGapat}pm0MV0;GHoO{x6I>s|-^7`ztJ0CXczB!;V|^_ZMv-w2@Ut}y3oF80z7c$| z;SG+-9$JEjF~8r9>ic_uh&g<5g~;abTm6+tStR_+7dhrU7O)3PKyJBp0bjZplYJ>D zLg0s(k-E)Xtu0^{8hQJ7N?YoA^g;@dS}V;*NdN>;N#%T-%eu~e1nNUTvJ#w6`92E^ zi|yG)-)P`dD^vJ%X%)$Hvr6Tp#rh6C5b&f_?_3&I+woAaOe4akgo(B^w|3>-jE{nFNO91|~r%0w+zt`MI$tPJl9Fyy5N1A<_Ko{wglS{o92 zp_w06vWh*+`BK?s#o{B3N7#X#s6`ndG&JWFC-+R$%Ii&i^jaDLtW`SwyduB4dd%TN z;Ph{KUn-I$9OdCX0yi&0J(?SR&y;rU!n%FG_%XNMh@FJ=5SYTtMmDy%wq>p1 ztKgRYHg%!_zD0e9_6L*1RsFyATS}E?xbi4t%dc_58Neh|R>n&j;r$CA0xdz^!!}Ws zS5Xl~Djqp94r+?Zeddw2_1Q1Gugp7hdy(ba<{2VwA1Z05wQQ-I5 z1ZtQ7F3!<6(*4JX3l#Oq)!)*fvNL+N1*VQ2P4=U9 z4C&pHl!a2uQ6&g}phORq#1R2)!ODelc6$@lhLx2{b8z8@Z(B8! zjAf!S?H|&6x4ocyuE>%+;9qai2{!JNGTQW2r!|yhxI2#rIMIZGvoS5 z2CNtFokGKZS|@N z&ks`S9fE#?my$>XAiBOSf7ro$9J{Z0J~j>SoXOQ$yjUT>dkd+3I@P|>me=rx7qp)B z*eYv^{mK8x&usYsEdsr$CZ&O!ws~%+I!Qr?r%6wYky{wLnc?1*8t6#1mx zcJm9kW8{sVpb9ByY5iO4`aWEg`W$1Pnn-4j{k@Aq1@=RJ-p`-k)45%g)YaK*Pb;`+ z1^fQCm^;NK7x!7OD{f>S1(I^dMR*TF{bUgHSUDdzeP9sqU%zKEUFjT-e&eI@i_()* zIMN+-YZJn+y=9@Sc>ikpA=a)Y5<@7BZ4m=tGN=#lE+j&h3XC{-_*kw(rcDYy!HG5+&d@yAWSJ-Tp$Af05YHUIr*sbtgZ=v4ldeHpC3^l7En(qSD zayX@R_VtbN^g11?nRip+6ZcV|jUyI_0FAlTh$fx>!{K5<)&WX}#xpLU1Y8N6$M8q9 z;VGYEjm6rlm#?MIDjHzBxOr=oONNuKwlYG3VDBI2W(FvDBg9?r-#!nU{rBN1EUKiLe67He$kx14^(0EHlz$E)r-QKcz!hDUy=O=iH+02tJ8( zbB8C5zNQ`Lcyc6dg3l^3#Lab*1}PyNrjkCnaKFm)DxBD*iTrEh<&h>*n?P9Vy5RT(a^L|Nb2($R|3 z40XXHPwq>P=iN`N`nJTnNMI)-K4?_mDPJGX@OOpH!P@$O|B)#s|CBJa@DO*!_Wpn#Y+K?4_N_VMjcG0ayuMM(V5g(( z=X58~5=00cN{n|pRUo?8f1pdCVGxr6W{DYz`%TkfyoXxrm!VQm`kR>M zp*KrXzPVJF-3(&!RM>5N;S#7$@dvN7IjR1hkHa`OB`6{Z9G{P$2E+CKxrtRA`$Zub zq|#yjD;N-;T&zbYGJ!q~`m?t|W@+}W#|CT_)-Dv@-1j`m{Uh~ff&=kfX7ZzMMdGCKRCKXR=;Ule?V{RT3JXB7$<7|nhv`=UW9a{ zp9^Vdy|44Ye^;K7;lNHP?78`Nr!dmcbSIiQ&6Uh2CS=%E{<@fAWHx~FcT-W0!0xYm z8Ae3XaI)-=_3!0mT?%Gf6=Q<|xw|qbvdsitQvUoCD53T={?U_+!b@C)PfA+zhduXq zBajDE`^s@1^LA9JULF6trJ{%HW`y&>2}3Wl`WDOyVfi48pkmm0O@b+pH=@K!2|_Yo z7XcClJcN2NHCzr6tJ`-iOg6!q%j}T+qVdYLq`iWLvtiY4$ou&{#n&Pt*IO46w=%eJ zRK#y4NcB6z%{;|Ma*3`~5i4&KTO-xd+}5YhkmBN_BWxK(H5dq%RJYf)zw!AJ*yJq> zNV#Qt``#?GuW3b7oEyYa$(c!A!)H^e$?I&H&gBf^i%N0HAG}1|?Oh**g@xjC$818% z8nEq7WfheAxE~v^mKNd<$f-AHMh}=nR?8s1pY;DOA8((mc7Wo#2oZZCopG?dtay%b8+7CDvR8^$Teez7BrNs`@aefNMgKE3MyeXkMP-^ z+KlVF!#FlvqQ~dMiciJE#;jMTv2q05Xh;}HmqTILQG?u33NAFDpOoL3|Is_3LF+fh zjl&WikOjm-%_`pbrb98J?jwJAJE% zMFA{LmbqB}4&p{DM1r1Hqtsmd5?|UA3Pu#|cr#fm5caBh%^^w}a#Ae}?_u>`>+mGV zgdKG60gl9;nYF|YzVOW_>_7!mL0RwsMv)&f7ULy0Bn?o`e-*#Arl%sz#oYQhZL%Hc!vqRo*1)VwT5zaHp2D~rYJk6PPq3Jl z9A8JJNX#ed==1}DHx2c^P0 zB)Iv4o^A2@8N5Km=Le+z6-_skwZ}~1#_MPQ*Zsq&>qG(?6(&p=`D_0smd=$jM*lsN zf>KBS$idYVNXIAHxS$|d6ha;jaLp%a~Aw&%FLQa2fl4a8u0v>!UmY)i;)sf(uwGqV1kPfjBEiXftx zMhEvMsHygtu`@eE01*G56(!q< zB>0~ht}bl^%0)yIb~KfKy!E0bUwx$f)L~Qu!z+xMYpfut#NQu~ks3e=lhL@gi=7UT z-$8kQf2nEsxwnn&@_zP0178H8jU7GX)Kpgh;{%CRUy#Ib73w**;{6<20USU~3Q8tf_3;rw^=^$W#Iw<`zD{R-U4_4K`s%cG%hqa+t1J&BHlcLe+Zr$}s;%h0~kPjzYNixp8>l9=2 zf>azNE2|0?9FF0l@9A>apQd92AnSvRWL)>QbjcZK^Wz9z+pybICOnmB&VnlPKcEbd zAagS^k=Zucs5fe9!FxZvPP6@f9%b#%!u{t3Gb1xR@VrU_WNd$olFK+Ht=(s#UpK#9 zz;f1)8vAqu$5tv_M2b1zl(1Fv6H(V@o5TfDVoI!Qe@L~UMb7mlDqq9ED{S0z{palj8MF-_06goS6pK3YS*p#I~nK!^^_k)0h?9B2fXWq&! zpKsD=?-${vr-q<9yf^l7K6^+bc{Gr#p zpizVi`z>$PxZYN7DgC6-bAybdvGX|!&8ui@5pU%$rCB4@qJ zBcw}L6#0h4TWRPP1>*UYQW$Brn>*E=9r1Pw3@<)0yiI6!CaI+U5>U_xg( z^2GSW^izT{{#8cbHnwD5ywsZ>RwR?)QwdbhyqPbNNIJN`vmmPzvD{FOuXjA{;5IYs z?SjO)&P5s?u}>8PT*Xa=^7@WYg(~|c!DqW~<&8+uN6u{>HquujL%%KU8R~;(lw0Ju zHEk;9MdwEeRLbkk7kcQ6v}6P)4KG{+X!vgjsj&|yz=Qg$h@Lr$NNT(oT?>5IVHf<} z{WW_?#xJeS9Z%kXw_kGa^Mm0JmO^k4^U9L&J$YbxL4ES38GlrAJKs{+e8iqK)JprXbIFVg_Dn6@wfFOIGPsLgJP26rhGcQ0Ptic{R(wOH{2#oY_V zp|}+dUR(nd8r&&X+}&OBzW>c+CX>u$GLxIVyL-;rJ-(L5XFU>1kgM9TxgVp(K{pCK zj=Pmmnpu>SvVt4>UoWZF_fZJASO#}QGj+E~jP_4V*3woKzc%VhK#+a5{vy6XQL?Hl zD{IiFWptNz67_8=y9rxjSVa{2FWJY~6Pk=BxktP9RsOg zbCM>y@RRVOXV`IPc1A5jMtpymf@3`>E*t^#Tjohq*XSs3Lj!`y=*-OMX6V(^mi$UGc((dfSMB%ygTHTknIc%YXxeA(DgeS+ zs4}8DL2Zi*+eFThVS&9+GnhQJY-AvX6e4bB_FHr9{1)tmOx_r1#LDmq|CNh1n4%1Dn%IugGu zCw~@8ej$?}vI=q#3ObT#)>OlYFd4$;c_W=o<35CLi#?PC7vKGg`jq!d!sPWqiCb9k zVow6K+(9u(l3z~~a4oOBZf~2N ztbq4}^SjJWU))=E@~k@77O8rg`to0l{`znWx{~Yv#z~#z8Ylfu4(ygPj#W5=E7-Jw7xk^9zH%_O-gj&AFE8)O9kC&Ez`Cf(g_n}uUnB`ItZ9R7IJrHk( z?!PlkoEX)s%64Ofva8;!U0RCqosaLQR=?7!q)=&jGcFOT;IEwf%x^oadOr5OKX2h+ zaC6<;Z{EH@$a&lq-?`b!!@ixQChEFob6Ni7>>>1e0@ zfdDrMdP|Mdi3Ys`(+6kT9KyZ4JrIL?YVKRq123IxK)ogZrC;CSyyxZ~ot(t2*T)?B zB^&k{V~CXgE7@q&^~}T4fN*8y;imKC05y{(^! z-H5u`s|@c~o|S_}CaJ9&8t&n{RMcCF>k;2KHZ9Q?6gHg$|vC@=HZ(OK%_D&p9*m zBLN2`dCCtT&%$b_9%A1Ok>(e&lxud4uV(Y7Uhj z0IRjtM~$Km(~pG1-N8wD_~bq(7UO|XCe`H(8@#b|A3u;jp=#gq4u9hpgx{Q^!tI+I z)>-;*X(pxL_^UY4tC0hfy-o(5UnWF5&sxO97*miYK1oyrDFIu(H#thAg~j5w7VNa~ zKm>|(Vv_^oQIN4bz+4^{6Y%py6e?FPd^=}bar;^6?_NxFl19I|4o^$i+nYCt>HLKeyZYxx@>L=4Zsl`4prDJE_>={|ctUQ@Amzuh+K)qwnt}OQ+4ikghkUEd zW1meq$My}nk4y+xG3rh{Qg+S<`5tw#^q}WhUFBC9sRFsr6_8ia#=!kM1@geypFvN{ zg7#;GJT6^rVP)}o1wM1&DLfGM+1W7|BBSu)J_lNaG%Pc`|I~p1&um>frnXaLVqsb3 zk^D08oKp?Crxlg^nl5&5hhi~D*k9tJck-;-GPMNEn+P`IKU_+?9=@mFQfAyIl8XD= z$G07}rh%L&PY}o3NaOs9S|uK2(;0|;qE3PA+ZmR{CN)$4&7aB|E!^P) zfqT}5bpBOo?m*0Ka|-#X*I7Bn4io6n>J(AJeJnI797V5%243t_$VRqps9sU%vn3_y z_rNmAlavl3ST$(5zu(m?a25A6d5=1XtvRxX861fXkd@AG^Jwnj9r^(DB3& zm-T*weWaq@mAz>`=cC zVR`=_Ojv26`>`>h`kxFbku@Xl!*Yp}MM7%;E~wZ#y&FCq?fIytJ+0L1g|RS=Ov28t z_r0uDrEM~jd0l0!i}))qJEYBq8^_jIH~{W$TSZZ}T}^u%M!;Bn)WdK3yt?M9bKa_Eo};wPY4-OhI5R?h$tpGI}7&b=w{VJ2T5$yhDxd$&ma1oi?a$1KW= zEi!iAQ(s0rQ4^qqbKe=Ypk~LWV_Pk$mp0DOjwn9KXLuhmMUV7r#a({8fBk%#P(G>)n zW(=(~i~%Baed%NPU-mzcaya&Fx!P)$Hm71`qXjy&WBp_gKdJ+*fa2vRdGf>l<9#O2 zUqpWI;ul5YQk1voQ-yw#L&Mz^w<#ySet_5%Z;c)uNkpK#!y!;Ne#ZA5J0{+pGkL&% zq7bGqO6Se zuV@4-U6)8cct+2kJqp~4J>VxZpy0uOCi}0EB3h{`hPmg;yuEdicJ@OTjt3J4Fi?k6 zqc6H$z0oXYf&bgw(WeVs5-q70xV^)%zH;ce#aofyhoBx z!ho=UX9M7o>*CjlJjjON?b^9&2|zDQx0XcBQ8S2D%&3}vKWm`Qyt%Jw$C*+Xk7(6~ z+lnA>(<`iiXZ~Q(siGet;Kyb+VZ~AMn2qjdDBfjT3Nbt?N~ui`GlS}Ize1)yg2tvu z!gLU<5>;C6ifn(*;#wZpOo`ps1kuc$-H<-o)Ch@GUS&Ola917EPf+zyTl`kyZHFH* zQ`cf< zDgJs(--ZEBod$>J%}zB_;Gl!}i(aH~ytm*dqF$n4o$NRBT~tfD>1#usRr199ktUpU zYraUnOLL!35K(E)pI!iY^Yp)e%F@yZz20x?K;!xM`=;dJ+ z@BLAKi|eXoAAo{Av@QF%b>JD&5P2&8B&jlcMtD#!S5T9ASX=+h+>S^j@#(}JG&bl= zJ>;Jr^7&xe8$*>vNK?~F!EaptN0h8TG6^Tph=hMB-B+3KdvvxKrV&Rj$q*#<{Ljq! z_C%6fOQl!ySdm_Af6OXG%5H!F6ajbrq3EfH?36IL9hntF@guq+h*tZlGeTxAeGXHs z!#&gii3OEhjnMSo`1`7!I>fQdmowK!bE=mME?8m+Mn@SFy?0y5fxOeB!GQ2}sV^S9 zNAI?tx+L&;y@So7S7?w&ql{c=t!Rlck!$o=gf=ma!%Sh_sQ=%8!D?S;PTTCgAD$mO zap~@_?3k{mxtVR};yuJD)6O3lt51st#3m1@^L+SKHC|?o#q>TNRMYh)EGT9(v`b>Q z(46}_7@zKMPQajO6t4?P+4^E+^H0$QlUEi*fO=*x%o2%no>uVbzl2{+agH zUk{!6z_0k5$E;T__zB>iP?V}7Q|EfJp=o;|~3JlYb+$TSyC z)812Jf{vdFTE&DM-^|ua@JIwt1{{iMf}x43;AN~^>L=lXqYT3xel6Up(9(hEu^&B; z7_%_jSQ%cX`xYK{XgaO)i;8}|+wOSB(Z_9kah-4J@R+{~0OE1O;_Rzg{Eun+yVsCJ zTRa0)EGMyh-?tW%BcM7)MAk3Ht14t%siU#6;iYf%-5yJhc@%DT$ljQBc|CK>DFoVw z6TPfbD#A;yY+k1Et44nEcrRY(RP%LWrigiA?EpI%i8!eaTykd@BEaxzgR0?Y&NL>4 znV|e9H=cNFF4sZn7sGOIEbkyl*@xtK0v7Fe6u;*?GuDSma_CbfPOJrHt}0=Fj!!&J z+#O?ICSH;oTwYFmCDLSMDA+CPr^C{db8@RS4(g0 z*Srdgh@l=6wh@>{T<)Afw!>J_Dspeuh<9;1F{<7r9vp7M$=ni@ur6My zfAQd-*PFYwfFFqSmh~ZJI&*%FhE9~}9~l|lWUAHXU!F}c8c|?a&$N(#S2ld%ReHtT z%EglXH~6bgsYv|O?h6u+{QN&V*`-3mLn*|E4E)nv&5|?bNl?!#`2?1mcM^7_vE$3Z zoNvv~2bqg<1N1Mp!xb8YKgAtl)UvZsh@0FKRTn&VR_+IF66K|A|5tl?A6ciHxo)w=)qJpPmjs{= z!OKUw8mJxCO_H!@KMx!=Z*F9^xP<@2Ukw5Y@XPxPQKOg~zmN8N*obH@f@Wz3-bgg@ zzXwM+RbC|tdLbT}Wh}k?T8gCe!V#y)1cZn&8)9F@?$?g}_l+={sn|45IjPA*ZiuKk zx(((ECv?p1xLb3^5HupKW3JezLnblj0~xvysKzM547#o@EfOYY)kXt)*AM9nQ&ma zpXb$Yro1>+`yJOLmQwdb;C(D-1*EnAlEmEH_w;1%3SC8wU&I4=0{Ce^MlFCYCnEw4 zpT^u;GU3jf)E+3U8w;pl(+`Un-Ne2}{f8expZ_Rq-o#1tbmqup1Ixs?J?p?&zF18U2xR#z1PYaab7U`6gHFg0#EeL>IdM4Q`oA%rUx%sA;0 z;1RJ#g!blh6`ej2jlP+5K`FY%c`hTx!J)^-gN^~&r7~u5QCQyaB02#}YNSTMt4@O` za=5d4(99FD|AvJcj|2~i)t%Lv?V`{8*b&ZCk;;!EvcMLcV0%3Ay0Af^AAYN~kI$7C zO>8utm+wKUwBzeMcNNP&-eJ_V?F;*!z3{6JcbIYnfGPa!P`kc9;y?Ec>+I|3=pQ_D z+Cb}-ygX~`3S8Mze%6W)D~aEW2;X|&2YAzjgHhJJX*Qpabk@c7b|4jkoe!N~uBnd4 zXMZQCHFpOA6Gs1e?ty{nu$H0Y!$SZUv;qz<4*4dlRKv(PzZPq+=&~jhax?c}bl@zsc%{eec%XS$3EfVJF6hMf>g-0Q7!g{gQ@;d zi-bEx-(=#Jf$-br3D#n!!}K`Ya%V%Sy4`Cgm3S9F_{>osbuomj52+eae*b{ohlu|7 zm2*~vQtOrI8b$;~Uq6xoKm_)8+%rn*RT23jFI=9ae#O!>6p6MJD=BJq1vDMTlJ;RB z6lLE@)NzDDO20`sA({zBzd8Sj@?yvEZk~MPRu}X(i{Yl7t11zV!RdOU-Pyb!>`r zgF4J)odfWY+y5x<{-HVAg#dZ=zxWh1Hf8qV&irqZV|-k7^WxraoeI!n+5x^y)!zYL zPc1E^)E;nF-Mk0|sVEX3vmXhLOpxF`BXqh#zO1#V+$`$*act)1hk~;*MfQ46>~I2z z{8~9(8gN6+-^&9g;X}g&tS0@f>|t+EeVW7F9cP@11k`}KATh#9fY&Sd=*`{>!HL873x(x>alk-dh&D{2LBV3@dpq=d=`M9zR>quPC9-g4fdlAD(W+t=DIJ zV@f)t-o-o!>Cucs2uX=1#wOZ5%nQgKcHQfa7>JLVhZZ==-dQ8S)a`O&PUzjpqq@7r zRC9#~*4Nj|>+^9#)!)fDIIt@H;9x(<0WRh4?q_No<|H}OH`Xx2*>1yF<^8QB7M(kyJoyQz82yG2K+HA^!7YhUNZm?>_>ZnVaO_Rch0 zKAjv;8Iz#O9NL-sQBpjay4&(Rbwr(HE;XQ5i#ll2Y-HFYQ$k|f9h^Da zBCK3*7-gHq&sI#Clc+r}5tEcSa-R`cB`}aLTG;!V{<2y#SouZ1pLNUFgS`AW4!W(G zHgjk0rIKmn&^nnLeS42Enwe&@tQr5hoEI6kZ1G8*0fA9sh8MV;^*g<}fG9(R-#Jne z<>r?1@)8gg745g=Vn|B=()j;tys@!S`4BkR{)QkCF0cGM^s-F4j>H6*49T0rNW+Kx zq4l+Tiz9sEiIp&+WU59CnsP9>sJ(ru!K0*{+yHe4i3ht|-E(R?L`&*vv@JR_t{p!s z@HYkd<8^1VmlP^^+*IfaA;&@!+J109@wET4=w7_^^dSwKgOqOd@_`MZo_0kjjBSbc zrET=n;#Ggb?BVOaghy-tPe*V-kM(uNfEkZygF_*jzmB9oVxe=hib1RuQ`asM9xYNH zj>;T3uBhOp%qlbPIwxZuypB#_lOmJ`nyfr3hur4UW1n6+q7hT=TSDp9cC*$VqlT!A z6{lm~X$sp#;;@qFRYw;0yE#5@9L^-On+6P3ANw4m)EIi(8)0@mzw?}jjLhsw>?417 zd7`d2seMNRN$tlur3D(FLOvr!VK46$Aykz+f9H~|#Dx~G&p5*fu+4$tR2DgOrV=d$ z5SuIc^5*_g!sWFdEaMgbiP2?YZY}0{PS1RbrC;YPyu@LS#g@C&ur0HqKK+ZP4Zy{w zE&EXy!(Y0R=&0zDMn10h!)?d!6d0p=ypP>JV8VmIQ?x%FZu!T3!3cVqz6KWgaI9|)2c5>|Df1t zssIC((3l8wEbX2A+w|>G{9mlEqH96P-5qAMp2Zjj#WrgZpuf$n3>!X7=c`q#RspLb zw$XNL%-Ak|?}L<+Kvj48g{PDk&)9FWWb3|0icPqq9tf_oWAkBwG4Mc5G&hv*-upF< z@gkg{*!(|49e1_ifi2O8Mh5}wPsK|Kt+xu$-La_GY%GB3_^V^bkyXYoQA!I!5rYM=9 z?HAaqqX;78hac9KKby}ovf)s9Ig)G zL+~&`l4~yvw7tV{I)*K-&D}(&Wv8tLi@zsq{L2sSpDWBAT;c4!cwyP+)Yv_{dI0jd z`F|th^YeR|$0`I+H)3Mqf`*2a*VjPJ3hmNTtvF<>>gwwMZi;2Cp0hhY6we`I^0lZD z$J&B)?LPfbMuCbf71?b{qnTL>S+lZHhcp%P{Y;s#Bn4VTsl%5yHIe27w8War0h#oP zr&*l+UQEd5jEkHe%6dN3O{*PJmw5bo~?vPE`yWoQyH zqrLsNl7Zw5Yp>BVGE#ak7#RC8$sS>MDL!Kop~r07ZPCVeH885!>NiV$ddHMY>vGOV z_(LZAb9rdtk@y)Dj8=DhxBA_UbbE2;;}Ukg0QS@c?9}$WUO?UpJGFT;Keq69I+~$D z^IT6iwjOOy^K|F!6=&V`KZgq>Rkeu?q*1h5xQGe9zji)GQ?cNI7-!8FtV@5d8;p z$zQ+3c}7$HFf8#TX&F!!eVa?08x1%GFi}u3=0<}ZpFE5;c>d5y+5Kc!_AmpQjYF^T z?Z3^_EHu>CtFPuN2(PYKI#^D#%{9gCoBzq@#zG-e0);?R0pJ?MPXFpNUrhvPrWV@HqNZ> zw~O5F**QdZ!?ijwL&z$0TK|O8KTUc78`pN22!AR3bWE0-*SVy|)dnXYBFlJoJ&I_T z+&=Kk^R5Vd6@zN5X)Vgg-e%bnTUiY;ig`${n9k^MGaIcYt0-DScR$73X0l8b9swLS zwh46ZWLdXdH)sw^D?kbgt+!ve;igM??ICz3izuQ+wsKja~+#lP5#bwv>VHq1{ouh|AkAmOza(6yg@)IT6 z#}<}-bv+b6v)bP2(tnw%nCO)X^os45`rFdyj`B}7SaZ7mi2?h=of7}rwS$%jjJ~d# z_bKnmp!*@ewAW`OEy;o--UF%Or`H9^JspNPoJ74dWn>h50ZT#hJ0^5(Y0c>%e@GzfL5#bkc$~eH%Ljh|HBbSk4rswqH^z|| zxcL{NX2~(<P1D^?lK}Sm;^rGNKwR5Vy zUPK%?AYl@7qs{8%hk{bj!uhLOtvN}-W$9jAHR&KAplj5YO@jCZy9vSXGQHv`*O ztLQ!_RXYndZd6AU)qIYfJUr;7B;inlr#6zyX;9%t2>8r8$`Vs(N_h`W_~LP7g^zO6 z4rgmp6WV(#ZS~~^OiCbQqld_=vclgXe0as$_VsHTFB!-Nq2&1FS4es4A$fBN{IfQm zCR!6XUp3wbZ6RH_%qhNlTUCnhEh?gZ+ZP6)=s^tYwMTkwcAA-=mjkXC1O$YS46i_9 zvOb|)kC@o|{T@@ffm&!2hj#T&yrA1)=!Aypej;YNq1KAKG#Pa9W{!i4GNK=qn;-td zLSAOhAt`|3lPq5%R&fIwa*(SdfoQhAJj1M-u#hez?aqRl>2!g9Xm4!JB_8O2pgd2O zU}iAbcWs&QUOL)H41G+=cGp;!^kI~f%PfdoBon^?HAEc$S*basN31THpquKOk1M9?u(y|@~ zKvy?=t{|ILEdiY$wpgV0y65ZgkzQq4Km3D9njC!_T>D-?RgKADRhAuz#VUC5j|`LI z=+nM8QTs%JuL8!HZ8hUR<1lJKuDg?871V??ztQ~ThT#*h1=3jPw-!}6x#-b2Bi!>)M`E?6y>*Fzr6$M73P`~*o zi=xvKs$Ck_Uk^>j{27DHv}V-TJbU7(grpyzt3WT*iih&EE!>zYe2AWyQQ@}hGchpp zUUyM+nU6r7D+APR{mo^U)^{_guX8J1sr2}MAXJqrWxHiv-#%=3cLE*)ue5=U8J+J9 zM7Ep}BAl{M2f8_@l0Ob*7^h|O{bKhq{3fLWpPg>*iTH3=f8|U@mW1=OF|@X0nYo`d z;vwj+p-!Dd#dHS-?~nM!Ks-Y|_CTuK-+7%T+GI4IGw;-WLy^ey;cQjZVK|EHZgav5 z%V?BCD6F}kCbq;sZQW4yohO_ba8djRn2qLK9g#YkZl}W#Fd3*1azT^+vP0~kJ-i!- z^y!v6;`$n~L_85EZtvpULMsZy&){;ihSb4io6sA+eK7=)%v=62wp$;&3XNLNUM0se zt(=7mwh_)&HycX*2y~1Lp-d16wR%zcH@nyLO9LlewNLAk_?s<%d&A!^9?CQ-kt>j9-f|jfe@XR3R z@^cRvEWO-mm;IynBE>0I!-)&gimt_4iQniNb}(#wN5j9&#!ZuIW$FVP9@0{@gL7o0 zR1c(e+jbYMhLF{vqw!oFifmW-7?xAR7kL8}Dc|U*UT8WWjPrhR964y1nThXc;!hw3 zr{dRalD)2%C)GH#UNusbq{VeW?jkxM5AUMdH}V7$yMAw>EXMc8l*m&a()*OBY!I!& z(@Z{#$@D~NkjY{oy(g`ls)Q&0&PU8=5w~j?w^RoX|HkNwXMYStLtY={Hw&x*t`3y? z$DjIP8pZZ%0TF9167GCe&brl4w_e}#5zDbz2xORZS}--tB92LbGQJJw`IvB4Xnpda zMSS0%5%cZ@#dLIy_`SJ%5~3{c&Mn*EI>v-?vL)l2tUusv4vV$gyuP>uhP&deL~`;% zt0T((+kksI3)S+l~*Us3s9Tt5E#>bV6L+Cuq4c_4)oE5!>CzQL^z zbn0hQ==L?5GpH{spAc$GOBw&xnyJajp{%T|0RI$nD2Wr~2i9uH6urbLtN~(GufHm$ z&Uz9E$p;wBsRD@c34GZ%+`i=v6JggZBZo8;u5|Tt+rXpDJ?|VsUxp+0UnTEsc9-l+ zwKu=c=F67%m_;kYOYYKRjVi;_mMD_qX?$|juh6=Mz%9->k4T+*Q`R3OUpBwQV$)LH zrwi-FM@ju%E~=?DQTPscNR4#vp;uu0wgfnqvmD+`y6>wXFX2o#{%UmA z2kKw-(M3d1FlB}Hl*=p0^sAqxh?!*iz_xv$IaJmT_Ghi((FGEwafH2e zQN~Te>!&&eTbQtWIfs$owHm%3_5L#VrGlLovbOWL%3c`gq*zBTSaWQOw-=hBKD$Rj ziW3_Y_(6mS7Om^P{Y_^YK@&{0P1}KDTwPUkd>|Ia~^KMPdHhOyi%7xar2 z*7o%H^u&dUU@{M|hlpX40B}=YSxJW%GwM5OCJ(GO(c#*r#h;onsNxbI1ZVw%JL1(V zlA-dcP$LD54 zFO?R?f)dhOht;#a0;(GQpU!$L^Hp+wk0902T=L}jv0kFHjpw`P*SL4MD5{h_LpMrq z%5TFrXy-$cv^D0UHmrjM5>Wr@G@Qme2;I&)^tYpY^YbQ9dglGE1^!j^M)kI1wG2=^ z1E?r|&Eps(`>}^rTM!|01`QN^{^+iMivp9nQ-h37#^P9g$84JgWr+t9g8SwhQ8QI7 zusy7xMO$FxaEpTZ*>?h#Mhbk^!qtWx2APH$G>OfkjMfubTGqE?jVV5sFl^!OD^9kD z(Y2z7D~K|@lCFl|&ho*_oPds!#7NOuQoef7O9z0Va{4Mebq-ZH(T#C4gJ|FA!Bgwc z$p60lZY>lTF%xEGBoEykID5eop}a?qDO`;?)1C=#eMNA+O?&ut`XHsUwSdl{_zUv& zWXXPRUHtB!J0L#!<+dg3PZBoT7S{qVAvJ1bV^pZ@K`AYEO<|pEu)xSV;boo$ASC|V zD3fl2Fh6Bfcx&|6xs6V!Ln%IfV9=we;j-dEVpiE%uewu;-)bT^jRlL3J4Y?mn83H0 z@Iz)QwDSCcCPl?n(W)gRY};hGfnhWo^rVBs&6zYmKj}2^d<{mgchfQ5N8MheI%_V+ zjN*#rU&uP#bx+}Yry(gQ#0O%MDo5XjC|VGG6Hatx!KiIylAg>HMBl%(TvKFwXVKn#231WHEPD%23e||N zagHRyX|EyE6z;qMR?mDHsnvpfvZSg4=*xGz1IHigycv|lpC)+!J;NdAwf*Q1$;||H zhx8=t+%Kmm3HxLuRZk=y3JPzqG@EKkA2Er2JaP#_x6fknb66G}q6hY>(x)uc6LV+w zE0iUbCOavP^v4g>MMc*A)Hv>it9lL0(MET0m7t(YlPaB2M9)zMui20?B}o|m-CqbV zrGba@XRp2|&5k}>ib)y%p`rk_S-uxHo_%@V|*}vt6`8(sPSNwfe@t__El|idk zJ+L@Dz$hu<1sBSwBh9@d*jg7s=qRZ^_?omr8WU_%UhWGn{B+7kbAyjoei`1E9yvf9=a(?%($hrNId;V;kq3EOU$)W!@pmjp z(dsbENkZ)DiFtV-<5g+lJ$HUF>C}+O)=C{m~wq0B=uSFo7Lj5ze}S-h2r|) z?q|bvj#AGN(rfNUnzvc9Ba069`W$Mm6c`MgxX5YXE&*IF|llfN=d)X^b-6sMp0N6JXf5a)arlrX4 zwLSfLu`7aB!}4pI!+Of}WV)H@c*2;)@ZGi|qg}fOFPDbJDRqf7?4s-W{Ekyg)1(YB zvw{<3YZBkOp&C7#o9;GF9HP#6t`2MBo_?(mezhHepFZF%wZ}speGyhW1F3`m1~l`D zr%|{QKqCe6WW_x7WG!gY+BP(I0F=D z^Pk+RsL$DNQ1yz;yL~~twPGkHcNTkJiR;PxYrbwr2!$M&OI084X%Y~-PjbrOe-6?C zrO$@#NN`u{E=Hx`sxha9?8Gr3{i;0s`C0alZWUi)GYLshXmFwK9UUBl1J09-JVr-X)$N1XI7O!M+9c}d#u&*7>xAB91vCeoZX z@d8LyGT-RbIu(m-am!py1+uh8pQnUmVCKo@I?<7y0|dn>1E@&)Uk(f!P3n?*f1hBI z3#wY06N{WoInS3Rz&hDo01B_Ja-~U0F-n?q1sbv!H1Ow0OuzX;ue>fX)2l(S5|X{4 zmuG(dfD&`&hamFCU5UXRw>`eLE#iD}Z_;Bgs=V#g_gN{Oc35!eO?y101`!9{gy?cd zUG0;HjRVD3CZNmF_ME_kyxL zOtHRjA&mX%Bz8;LB*Y^7e3))B2)MG9_D;*I(6#RmuuKceZO!Z5{3-KqhgVTlp-S;N%D?MDWd;_R;$C=RzTP=!aws z%`cl{w(^*_xEw!esx!agQ+&vTOu0y`2zCoxq?WCP;=*^unZEDxOGqaD2HtB5Pu5}0 zEOI&HI-F;dR%UA33OVHgAga(mSgg@s@7&Lmh|*vmIV9KaC$iP_4igbfPM?b6KAJtQ zuKoHnlBVrf;RNW&6QP|MLkdvFZWj?I*jm2(dC=O>5bO~HDBZC|_%|tlO@X>PA8&T_ z>W*Dr^Tu$*$4mXFiXZ01Rm~s1{X&NA^g%u`vqH|P!gXD4jTwH}WC2mVb#4YDoI2r* zYj)uNO!i;tCxKBu6z1U=Y3iG*{q$zyN-)M-0|}^3KEwDvqL+;83JYy@qY;$GTN(W) zOKW}NT$3(BZ1GZ=x7cCqM}eVobo*E!!a$J;QusXwsc`z~d`o=PlwKXg6Cb}JhaSkt zrkh$U6{ieBgF^jZP38Zx)>itky>WSnTJKySc4P770p}w@s?+_E?9&>Q`sVGN$!gy>Ng^Q|-E$o=rFlA)%rX zGhUDVY45!C+c-JBEG^1rfB*H80c1FC_!~^dX)&krW4h6eA!G@439;7xi);8N*l3pH zSE>)E&T!1?tj}8vAfUnruhM+6L z5QSyNjTxcrMZ{;uT|7=edS=XC%otX>+ncfXzAe9@dY0Dj^o^AB%9^_Va1&STO|6CA z?3A!7o|aXB{UyHOsNG9KzZ9>KCvTtnGOxDRApy)ET!3fyV+~#JU+F6qDxp8lC&l-S zVQK#{Zr(c<@5 ziaXB$U!9=vdl}{OXbTB9!^lLcbimx083+C1U{Y}G0jNDcz93X-haya@iFR!fnU`+r z+w}5@noHQ_%vWZ>OUw+nU)DTef|p1uOqcqmStqPlDCX-fBV#K}+K7Kf&j;u7lL)UG zG;_U8CMTxxEgE^b=ffH|J6(Z)|3e;kH6Y|O^FfG*<5%%V6XDxV%{(~ZP7Tg<*Qob! zOoraN9~!~>d&Rp_Qp{covYeC|FN+j z4V$nhJRKvB+thOeed&Pf)hj3Lmx0=uyB?fGn`B^wgV0c72(iRz3f9jlpDTRWUW-pg z!sMg*9pXeva{uqDShI#cGFMf#^-yc$dQ+c~Y1Kb6|K$Sf!_#;1?DHF7Fp(g8oYwb< z2^WxnhhClW@Y7_eo?5(Re6YXP?!%MhzZClDq=YZo9>FXU;Hs z7+R=n0`+!ZP-wNyqpg=oN+e={Fq8{ItQ7;#A_$?<0Be8&m}Wx%T%58wvp65wma#hSz_LeRjnIFTSf*)tic)ST-k!9lCbQ ztRJ*tSMX~+dNwq#Ayn%VL4!6qRAWV=m_9Ry>hhro9R&S~ltRGR8}l+q6+{_-LUt75 ze7{jd2I%`aVF5(oVKsORIEIHWpbEsxq%*oz?_q2546N6q&kkkmPgM7F70fFW^`mGa zefwgNYDEv(GR)xOQX}Wny4ZD5JYiyob zU|5ns-f!|_iuqX_6=|F(G^c+uD`PG{H%!iG{Kwnef7Jwm_t|N{9RK<+#h++R)?j#D zcL6wM(brm8_Ld!{H1$CuvR15y)X^z%mGPhnaT&W6AzsLpK%HzPap1ulyeBVjN)(!M z;XUKVb6!Yr!$NZE5P6}@x?W?h;z(_L_tub1L9;Q z+HnA(nA;1AHZAoJ2}$n1ZiyF3mOHgp|Bs~Tg^N&DxDTYy^K&XsqJrm@TC)(_i~Tde z`S?$rCYpcYX0po)kpz$bLK~h#($P0T*d&(tkHCA_WYd{vJGSGVv%NJ-vRoioXMLu1Ww*SJ(d;!{BGaKF+ z6vxiG3dR`$P+!+nf~STjUdplhww#5LS-*0Xmat7+PNAyaZ2NrLy`E%@UOidVop)C<0U3UL+aq5 z3%tWMH!`fVK+X;K@qzM6k!mZkTw>Nrw8tO6?o)zfT^C8)1FspHe9g9nzDzSuQ$Vla ziJ0E(%+A9i^E$dCQ&cSzFypgO2x)o0gReN>nPrR77*`53TU)9&M>a_rBu*DPz;v6} zo(OI~T>N}ixbeOZbNx#Y?Ev@@?$~Dw?E&e^Gb|M^DVjE}Vy?^vFdFGp0D)GgU70P4 zVb?e|Vp|J!FC=rjoWg?5CU?CN^K{Oy(IWRKH;XgZTqPjoW4J_BTB--JrT9HJbiLsS zYGO=HBO{~qyEW~wAP`jXN2QZx>@skK__yusQLC%6eNh+g^2d+|b6il~j06<#Nf{FaITw9@gWUYfF~t=`{6+9SN)q|INrx6bcwLKKx>iZo&!Ed^!_it;gopFAopDsvk zK(S;e9{}ms;M5|6WWRqx7f2g66w=T_O=+-P%tou9r#+fZ``G?P?VTaqeOAb48l}Ds zDPm;1iDp2=D|^= zg-U>2wXX0I=B)!cJ`DrfBAxs?zl5`1;Gky>OGOZa}Ya0cLH{Dl3Pnte5BmMlmkoxawOC#AvR zDrclYKlZN4d%s1EXf87i!}2Vk$sBYYEK9&c@^@ak*rqQTWcGfT= zGk!kky_#U78S&~9?lds%v6NWDM1SRk)vr@w(G?Rc&t_%^GT>KeZc2X?QY#CH1Ae1& zgWKs%PHEakK&rv?x2YSU)))_Z?#2_S5(#MZ<+g?(iU;cNcnr#UG3=QUEOG7#S19`o4A2Mfa+Y^IQc|8j<1eI}8VJjGh6k+?GyiV>no0)ey!GzQ zy_<#+G4V*(t=yG`;)hL(XS2YMwZ8&(G%E?TP;N`sY$PJ=l$on)EFMOq|IC8?y(Pklg$45Zf-6GWe?H!`k4ox#3iBlGqo z@D~a1(~yv88gLe* zWOgp17k*)LEgtqXmT;6KhzwZue$=;87za5=ZW5XOt}*B5Mp^+2zBxY|5;Z@yF%%lQ zO|FJPw_F?su+R$E%_s%IN4#cetqoV--%mYAB-mv2A|f3pbwja-&oY3-sH`KU)v znFoKf6q67G(1-{p{sPdTK~w_b+5m zLr_+kt_>}YkR%O|3v7qw98yTzJC=v284NIWnZ*yNvxz>Bg{C3{=4M)TP6T?I?BT-I znC=&5rcaV;Tcgq^2czw8`ZllMEw89w?;7^IoHP5waPG-8%;kJaix&UPJyvMVU*tjg z?hl4;*8N17czT1RB#ZSBwwaNdE%A#gG9#AH3wF2j3y!+4$EBsYiRG|5dCo3_&l>Zq z5Sf)wmmX$BzFfc5@qv%r4m}`+Z;ts}j`|L_1giefSr*fMITzWusP%7Z!3IS4LQa3w zt6arJfUe%*I}@DDP=I&>n|X5r4rTI_;EUhMj96)Ji63UXH){;9lU0Iww<07qx3aI0 z?k~$Lf9$&2WmaPMUsOPkv7iI3Y-R(H_S$f&K)6i(oJvj%;u}>ltE-CNBru&2_@$e( z+CySY=oyv|2S{+WAARc61B4-1na1$>(PB1W0|&R~O7;cn?Gw!2@NtOa1E(HBhOn~) z>XW$DJfqKuSbeRX$5EsdjazZMl8VMLCXfmv_#?RMGO~(5j_q6{OHdzikkc`_RZ;`Q z(xi(B(r)UGU2DVb8Us!EB-z4wds%eVK*PSMzfe{rkJKwgND15;1OCalw?>eG z2UE9ww8PF_nw!_=tpz5~Nz&du`z)D9KFW0{!W>OmR8K?f0VhxLQd>HBg#9FupxA}U zU;6nQDS>HMTxYgx;p14{T|2h;Cl$PHFINb6w%%U8)No0KHa7CutRCJWV-!GCt${MVO+*;9Ey z{b%WkJkkXw0(;u4-6_Wq=(;gv^63j~-$GXW0i*o#l~6g4J_2Zs1H^!QxOD%Kcd`nZ z(bHn2b%x9+I%x*HyPoir5gv3QDot=r5bW_BU$SC`$NX{mSjoykg6mUQ)grqCyz}Co zEf_U*DbBg1R%&a?x82w=JWGo*r$yy+mH({2 zSJ$;X>kk}yMj&!n{CY`_S2&=%|3Ep%pO_Xc^gHseC#GjTQd1j}lMl%61HJ^Uq2|rD zhHZF18TQ=LzcIT{UEx*I7YGqNy(wj(3mNK@H%)Tx&6Wupl?%v*HKmt5=V%G&f@#tbD0oP&{^+G|OYwK*=LjSPmkz3~D0Y+j8L$Sw@3f!F1 z9}NdQ7zjy}&(Fi;S1ao6qpKi2v1M+&8d@f>VEj4MSI<}d*6fG?XG|S zdx=%*4{A#5e|c><$D14Fv6G6%i4lZJg2}5HcsMPqTD(m!sB!zLO<<~Yj^h2_{PJE? z^sv&x@F6{JlZg&8i_ztE*z2jzD@+}37fJfhcM_@W+m7<+F^_-WFDx(-+#H3FYW+vF zv64`k@ZM%O1!u-IoM+}=AH~Bn0Y;OR0ZOL59M*nw>bBNW-*4N@LPRCE2E`G*FWn!5 zh=oE%yD5z=a3UJ~y6YDc-2=J!#8hE=IBvr}Gj9_f*0nq3^X_3Vq2Ko*)l*U*np5%S2z zxbHF$^8L3wjmoi7uj<0>_RS3cMB-OEGS4-Ky=?o92$ymnkn3^Ig%fkeFChDh84k0l z*uE1$Ler`|GZiaV+RA8Pei_~A#X^hE8B`@9H^J0P2plTSG)9p8L?PKsv~|WGlq`|2 zdp%X@2&M**qhPCph1(^j&<&%?l4zh!{;3E7qEDh2fV9c&@Xqr`>WbSByOmaV8nu5W zDu~O}14FHf;6QM1rMBX;i%SuAc_YBr6+g{*rRDmqp5Qp=0rc3J{HLoqC>+4V>4wxh z%|3nG!W%68wonP#{!T%)ox>&^)(y{g>zBB%tF5&kPvf@3dTFvp=SsLNEY`cyzF+7jGf;oddjM@_oKTGqt6RZ6J zuTK^D_sTVo`b>VZb)m*V9zmN6W8=^#A219S9iHjGTfJZ+EqGz3q4VMdfpMOjn)v-q z%5er1I2TmmzkiEl5QY5vtFK_V)Ek`zvafCfU66ds&zXjK;RTMn z#?+AQ=mxxE?oBry)1!Bcb|j9FSdP?OZn@Z&4-s*>QxMu3+pjmu&i|b^jnSX)I_Ml+ z$k)6JB??J2LJrHJgW!qR=^}DA$Xl-0+r2iQ_?7|_{@jvj!tMbZ0B=lF-j}@LHM6&E zE-aUSTy;*_8Xh0rl2mNPg-TKBbr)%(H$FKrHCr)lHcb~E~*S{Olh{b zi}0x;v3w$atF#42Jl$fsFw7;WT(J=i*iTz?2i>eje>YV zhp#}a?@{R!jZo%Zn}$h$iQ>(oUf)f)dcXEANQi+2%40gf&HSD= zfm$XwMd5=uRb5x|Li(yStigAt*;*jW{uLZmdohGbYON$rVFzA`?0skM0QpRAp^q++ z3(rvQCh#NQbjjslllYd%HDGH|(Us59uCU!Ylgs z{7O?O*wMxef|TF-$pR3fg$Q$uavsV59kWdES^ z=DyBId0(mn%BcX;>Ndh_Z5{?7^Nho%`9UNe&eCMTKw>7L7*# zSH*Iyfi=y8Z=`r>Y{B@H-$X2alJs3J-aC{#smvcW@gZ(LlaA!d^X7n`K%#RbIhF2T zjk%p>15ksrIFgVaq(O6u!+xl2pSd_wf4&xs!+G2|y0w{uEWfLT$$YV!KyMVm9H&f8 zfmAx1AjkKmu3Hld#f#r>^_bqj$h=MY7hWP4juNN#o)t_})X-2YA%fX%<}Iq0jBcqs zXw&-BDb6xbu(Ogk`9Mf0Z{UyeCy6*aj?gkp1&TB2DJPi+GP^60H`6DQ&u{*-5|2q( z&W_D3$8qcVF6-C-2^_tK9b7*cb$M}5PEOK#zvj8tK=_Z*S-%Gpeh<|axt{);Qv}1m zTB~#L$LFsr!JlcQb|3jPlq57S(d#l0x1XrBibAXT5Uk9d?fU$RtOt%)VI~;J7BQ%Q zXMNuDtM)*v4J#-G*Pi3d{g@9Mg1#zmWu49L%z4`JNm}6d=ifD7(aGnN6{bZ#4vmp* zDDzMs(=5(H7zI}?|ATE{=K+aFdydGdL^F+hl5)VoixIS0tb;(@+DWzlPiA>A%}tkP zXhgznF$ydg*PNR~f@vVRduLsa^F~*ZAgeW{VjD@~YJv8WxZBsq7b8et>L*xN?LNWa6*szFeU9P6mxI95ER6C4q;%W&YlsRURxz% z!QJBoUVDnWT)8+@04E>!_$Kw?k#J*;o{{?CY7{sATe5cc!UH4LjZ z(rt1WDn=ywMHU6#!Pz4AA?xQ@DyMOW$8HSjp-FJZy#FC0=;KL*p&!R`$Y$@nOu^rs zC39dK4jwT783Msct5xXU7vNoe*%}HM1;BZDA0|C@^*~ zM?5VBPN)C9?1u=<`85Jxs4RIa(Dc1^C$PNQm%!Aee<3pyyBGslV$-opC*oFH^xSx`h+Ug#P}iBA8w)PpZbuxmJ|Fr;`Ist?z?+Cczmdt@I`@ljs0Ae? zRTck~^T4VO13h=s&LaX)_nwJwHkUJ}0XeO%e-Y3)9}p}`FIOKZfB;wW(w`BZ-4XaiMVmRt>V~R_ zJw^I(^uv06ZI*u!Cg#ZFZH$ z)IIEinOM4S`LAZ_2HiCM*ctRL;Ca)s8jg8lc}A?xeuNLt0r)i9`}5#`t)>GKFBesk zFLZtB5I=_ciD)DT(6Xvcx{p+*3Q7>0P7)=BtoU8~K$gr-8jD10$3_fDw}HVrfaZZJ zT$?q>K|Gc~pESXXEg-DaeRt#~%k%vHh6~#{CQWF6w(-m#h}S(>@)NQfwD~mfQ{jOF z6b~{A5SegIGJ-w<(G3~1gT_6~LZ-gso3bt>S+zC}vG>fC)FFsSZyEGF=%EkwG(bKZ z?zfLt?`UGconAj4C4k7ej8>fg<3mJKfyc=HzunsPz0olaQbXuWD4|-!&uY=iECkWW zi~*=!3i$)*L-ftAFlRiQDMGq~t&UGGVtCKnN@5+`f9F_Rxb(p{nOU8>(>aJ>-7 zAqY6rfDby>f2)l5dkCbEs=DJucxaEpMLT7M}@p9yKO-B$_sND z`PquG>3H-?knh->FROOD4zr_GO(}_*%UQ{U8px6=FWm6t#0r6E^dGT$kIRe)_s%8b zD2Vw*?_WE!e!rppD+b#JbOxYTlwu$H|7PvXM-2VV8kTE;K2?@9EEBxW#0RB?M=8vr zJ_0`k4H>GSw>BSOu&7Oe8Nnm$k@7n4Pbq`~o5GG_y7iO9e)*3WG7m+^KCpebqK9ok zvqS`=+W<~f{L82I8yEbX2zA%O&83SyPr&S%NLRfJ#0QP1^ElidEtCZ`ruy|hl!Gq( zd#&XYUJcp0)L#AJtYnn|p0NhHQ6njk_@r)j$0I2E1^=OmXE1xZA`VkIY|~+=g>K`I z4D9u0r#-es&`}y95g$XnPRZdJ_43=aSE*2wS)TpV9_1eq%Dd8SwD&^`Q6X|fBdw;V zIQtTS(B0@)2y#$izA+8xD+6-#Z=bAvgd6HLF}6LTs@Gp6YK~uz^6Y74UX0hwBx4i^ z0YxEhIqIj+Cxq2icY;jH@x!W`*2jT#8dQegNgORID8xTkk5?iJZ<@OAiBbZY5&DPs(LU4Xnmw>Wn;`(X(t6>VR@a6`ibC(S+yQ3G>i2^=qgilm zvx{?;AWBfGYJ0bVy$*u*N#Ktp7XuDHD>l&N1p}5nO{-nfSDxx0VZ++_UCZCrwN-*J zE5lc;Cg&wxfv{-+KAEfc-v)LC>{YUjx%g)>0FzIQa|kiQ$RQ7+>qhSL%;i9t5lVkU zGZA}^0@2Vn?=VRN3kK)%^M#aTUN6)dy@h3!U{i}n+f+=q1GP!|38j-&jw{WWoSCKT z3=qC|s#<7pm8S3SKGPn&x=>3@Dx6|kvi~@c83oWW#-R}AZ{d{K> z$Cos05V<< zeVFM+c7Ld;f6%$`%WlY>wRLKygYymd7~+D>92P3|%O)>AG42h%{lpKXF@eoSxSY>z zOMf5T>xFK1MA-}-YXN!Q;Xndfx9Y;Pi2Cg5dc!7=o0{F{L#&N)DEk>HJ@7*7ztJl} zgq;N48p-TSBz~4bBf!VJRTCbUI5h)-ICJR%3p;I@Ux@2HK~y($5ZKw!aoC@YZ{_&T z37uTQuxDDdG8F%c*9&RFHs?%!*+B{cRR8=;W#l_0L>;(0&~Wat;2khf*MXq6q@%wK$v1|F&^~yjkQR(CgpK=QA`PQJ9$}w?9ZaVa>JH z>^$55+uqWj0p@3U&Nsd+2+WM)Au+_>BqS9n9PGT4>KFN*nWG)`m?Lj|YmT%qFyi#= zuzoQ&a)fYg1(_9!wjJY=IVRDvi+1{_?z^bw4o`Ulmfrt#tbg+1RAy-FuV%YdWs6Dw zQ&n7YYfJsI{hJwLGxT4E`m^Kg#yT&yL5+#fcFoJrqKj@%1`Iw5;05hpC#sXG_tKzY z+KpdRj6|f<$w_N z!55t;=x>U@9F~t&%7qJ1$Pa(N7aH&z3Y_{QTmivO`i zXI_%uTa1cY31%q%;gEQEyK?>V;HU=)y|`AMzWj_;U?hRi%J}H+?~`orsO)?{oviO| zO&(V!GfG@2w&BMbHLGu|9UAH6%I5CK)Bh-8P9p7}Ua>?o)C~rO142-7=%?M={D8cn zI%z%M*zYpv$J2&qFD{%8;FxPl;G--{P4UgyfJ|lyaRJU!gV3{wJP>4S#w(x#h}s`eM}KIQR$*I(Bb zUTMS7kq-&$Sb&QqX4+Q(@x|OLqkKkEP;;eT&bsge1T^S{47*1C%=j#9Z}zY%4hp)E z3xIU|{GS{<6>p$k?1xj#i!Jl-OvDks30wT^@b=F#ggj3_?21PVO13P%2bdxgt9(0f zJ|2WQjDqvS>WT7P?HOoZ{sU#!nY)W}mZg?1OS+(x8U}I*Fhd!P1#s$tVp=G>yT-Jd zYOR>Qa9dq!uj9MV=d>%x5)_v*h3O&mB|qS}M)#7#B4^(QC!mib2-GBD>V&v3c@SbW zUbz0{3fh+C51zrX9X+)dCwVIwar0D2trCu++7q-4juevfP{pKe5QM>mNOj$g3{Fl01MuU2KYofydip z<3`(G)o3f>2s6&{4jewMh+*M{pn7$nsx>T~?)8Nr{!FL(uNUB1E|W?^@UDKK@elOL zUO)WxSuuj*Ec&l7Pi)>h%}B7bryX zl)_(?-K^@YDcJr1xw0zi^Dd_jU|po-Xqx@bif{gFpt&T8MQC1o{p?V?1Q`Vw8@QyN zjwQ(NS*04=CC#4~@eTi;p5HSt+t~Yc?D_uX z@hFU~JsV$X1m)zAKtS2uq5HXNF)WCU+=`6>3NBa#@j%wLgDOfrO~PFUMZuYTnq^~= zf|A>jhwake;>L>cy3O(Lf&ow&#Cn?KlL+bBV%gkmhI-h$u^spR9~a zs_Oi$rB`xw0Ft(YYz6}V#|~c;{*;@qS91;*wNS>s_00wukTdO|^_^vc0PE&F@K@%j zfRL-G+Rx@KHeUMR_yB>vdwyerpWyME>r*R+(854ra;-aS^EKvGarEe-T4(aA(c#gA;>n7`Uh$-$%XMp9{gtU|L-+ z=r@+RZmSJ`ogxAXa;)SkC}rTrDs*c9>fKGMT{3}O%J^ydN*h0)^#Ut6I8+NAv9li^ zwNrbZ!7PY1Oh*(Q!10TSn3U}A9O8~`HESJ(J6#xD(AZhBKvD&7=)s`s((i-YizU@!-Wpz*JSiPuZ={CBUbIgkC zz&F4Z5N-XDXh>&$YzEhSIwNaPb5|sjyTzA*ejHX10amOUgJG%E)EL|}0k+S-t`6@8 z_^~6YY^c$Wk<^nvbFQAti&t+#J|I*~t5E_H<342hYh#sfz&rdrLLj_y`lfeMuHp-2bAKtJ^n9jma}a0AuHNmR4Y#5*+3oMV9fUGr{u5zM|8(tOf~? z3s3AnH8+dw_3te--N9Y$nqmzTG=3@Q$CYVzTHm!wB7$Dna5v^nP+pn)*ubf0Bumbe z7K_{61Tcyu%8V@Ql52(hHHdaGXfc7`nKPpc(vN%Hew-(G`l;3A6;Ty9Mo~NVJihlu zd+yku77I?`Pgc`jjvGlXQi+6^wqau&WLmCIVapR8x&QNUmP=hdSN_Z_Uk+>4(|skc z99C}G2bLOi(33LV83s}kSY;>@r=D$5NDcO&{2Gl${ZV0F><81fGS9Nw za=Zf48bR2G?-u`!SuQS-6>KjRTVwSPtElSlLl*no?{}X@OyDu!g0xATyOn>zveWdA zW>p!S?x8nk3U>PuR`sr{;%TqV#2rE)8Tp(q;Rl^(9yUrL>MVO@@4f)W)|*I<`)O_^ zLdxo;@(_4MCv2JZ>d<1aD!$wx8pyugY>a<_m_7%~j>GO|C<9`i{Almga}S zljnJI>h``fC5*e+1lGp2ur18#Fu{thxcr!S{;ai^(Rs$9F-qH*AS2@zvTn3eZE(O+ zTrv#S+sIABO=IFK8BVlhYA9wt9i7#OyjLYDQ0w0x;=`t9Zy2?3p?l9xwxpx5o;DG+ zgA{1{s+u)iX90Y~2(hDE;k@T>;-*@1NJTK#mlaN!UU0TeMlhJtb^WeKXfWlaCiis~ z@teEFq^a=7r1zgy$d~reX`6&l+1i^f6hs}?32ed05F4V-+D#=EK1~J;tSArxLW}Yi zx|cXd18=&Dch2{18F*;kc>q;YTU)a@!~$gCqG%dO#kHnuRgO9T8fd5xRj>1RLw28?nhW~)y^h) zm)GFubi$?yRzKYz7BoN69~*QZ7QIHA8?06Q3;Df%Ug>~D33YT|A-FM1)+6D9yP%Gg zLf*acFgl5BB%o(gzRbq6lXCSM*u=V6Aa)Jh8 zf(l~!=ZEJfA8h&4xGOm!irunn9ILw zT0~%aQ+lUoJ&37mbpRfLMbH*V&G=bYqSa>T8pdTe zM=wlW85~ppp?{IvRFlC3Kv2)WzzDBOK^X^GbyR_Zt8QOF%AR?CT08U_L~I5#3;lJR z7#89Fm58bon~5OhwtVJ>{Ti_}5RlV)s+-Ty?u!!|FRA`qvx+SPjd7B8VVTe47S|Fn zX6l!lmlLYL_uxOnMWNv2KAS3(SrUQ**MA?NJH8m5k?RW>8UGzatYeV8J|sH>_^dY9)f~R(YiouK(4)Wf`S8BKhYj6<;u?%Z)SceKtn_JPr3$~z zpIDW5?|PU&Q42VPGizBKe@G;(#+N6@_z1JFN5+vuJjY(zH>nYCy^ zDN|wjC>kQB9H@t)SYv4Yw6s6t$e2Y!rArd36SVrZr9td%Xfer}L;<^YAM5(uNl}HR z2hYo1{w!DY-FiI+zUV5N`;Z#X98IY+C$<*tK%3O-8zMIE&YnAZm{dl_%5>^=S?vdl zP6jwjGosdBJ>Te)d&O&!qjLA%F6-rbo)IgV6dKDSUCdA$?iplID_)Z1tZc|Qc-~?8 z{r+na7hq{-vDdgU#d1Q$!H= zwAn#Jj%V!(au}CSwOq(9Jr@=j=@809O~tif+<#3lY{RjMkIf*YfpSwoi2S!PT3wb$ zF_|n`c4-WqGgmHRu=xReHpB8QtoS`9Z6nIaR6kx{vMS%r5>w9eiVPvdQ$jOwk&O-^ z(PYV=eXFr;2)U-0gLw1c{peF4#=B2_==swl|5}{(!54T~L&!7x*AVPwqZRs|o;)9=_!c{iJVjYj&ke)F zL!s(}zY?DuWCXTV*QLnyF%y!RFFLKFIf9a=v!&X=pQhnOf?*&tdJiI&w12e*r)s%^ zq$y&yM98h-%3{hIbWYjmM~ttu>%zC`i;E`6lU0gqL;2R2-7v{ICo;me-9ptpI|%ya zE}Dxjzj9|#-3gZAIU2vU@Jqdeyr4zoLr5h|tnpT4(GBv$+aFhFS{tuQ1oSm&^$?aV zaX(c7<@nnowcgQm=tUUIReyw!~HM1X8gW(AG@_vx-Up3 zXsdZ~ZUb95-|d|CFWgypv|ciJQ^3ih7+oV*qc^x}Qv0^s;2A5k77>);R-%PI`5vJ@ zNEMWXhDXdS$Vvgqbre+CP_8TcE%D_5zap6po$I|c77{0pg&LNL{%|U&exB89Y#4pw z#fi?fRtfr4j~Rn8&lJu0Eg-oJzJXC}csz7O&sD*BN*^uwyuq?`==vVrQc$Sjr0#{( z!fE*i)elt{rFwCL?9X#{n;tpc(CwS4z;-317k1SBa8-mvF)suCjk&Gy@9;?`r}_LR zD>~Tw_H&6(*Z%GuEjGV3`oyhSTxL{)_@Lt2OKKv zSXI-tBx=sZx7|@n?H$5#~)gOX7H7g`_K<+~x7$%}@&uiozS4t?tNomT&bcv3E~3R9o++9p!E zJmo1eV-WkXq9>|Op@SD_v)C4aPWlXPq}qt|y`D-hLT^BYS7J3hqDO<3#QjAy1%Fo+ zZWavpz=-`HKC;$3Q`mb4I>|aNXEY!OXC1QOj0ejQ4x&GrVFdklBOdW8t$vA62`TpIlJ`@MYL;lFGO_S-Kc zJbd54eQbL`Hz5U|f1B3#7kmPf&-GNwtX+%=-#F*Okw5ve6O=~(79w#Y(`0S(ZWFMN zlp|g#o!Ovw@SN3EOF%SCN6vhQMVu!I70rsf*=OTA5**;xM-YC*`98N9Y?LJeY@`~A zo$DxH|1PL3w!LKMod;&`G!(g(ne&99w!}tpx<-1|{r4i$flR|xIO9NH?QykvkFPlj z{Mr~{Llk!;j%NE{w&+aXu#llrEu(xAH4djvCXX+E!pxCqEPXp}JwpF|sdt=6UT`{H zNOMxd$IgMQao7DcnMV`j5QrNmh`Dhm32{W&f$KhUjs&y;%OgW~b_$U-N7ov;3D-kR zkaVMm^L_k2hhQm*P4jUm4+G@59MgRv8GHWJ&xKtrc%vNFeFWi(Ynj z$)K(W(zfPbV84W0&D?CpH|{{-bYXQ#>e;yI2T2ajn;+2)tzL?1`QrpWM?dRbaxoqO-mO^rcBA{|eEUY
h}Se1glj6jVqtTRtAN2|-(u4!43>I+H4ns1Ny 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

+5wYA`U2h{ej&t|ugMM%C;EEU+5SI9( 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 O;HRmkt6HNBiTodF$1I!x literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/async/parallel-burgers/parallel-burgers-05.png b/docs/en/docs/img/async/parallel-burgers/parallel-burgers-05.png new file mode 100644 index 0000000000000000000000000000000000000000..45aca8e217fee2975f9b79c58a6fd2133a9e9bff GIT binary patch literal 185116 zcmeFZWmr{R)Hb>{-Q8W%B`w_`B_Q31lz@aZ(hUM4jdY2dl-M90A|l=Brcvqc{uU2D z&-`8XS+g)pI}pZV5E87bqkbG#)FxYI_}0;kP^c zh_OHC(Xh|ZVH7OK_Bqug9m982LmUlRUd;HSH7F6_gM{#>Xl@|_lSI5a443&T=|mjSWG!6PGj81olZLDS zjTwX4M@Jy(V^lXIocY&oH0mIpk}uA;Y6DS-93#U&kAn45X_~NiyE1tA|2#&2Sph*& z|2*t)EdK8$D2e|M6Qyi;WoVjfICW2D2nX3B#-A!c^p2pEXpm9pffa zW}`0-!($Nqq)+rfa5N2u-M^E2q>2uQ)mBnpwPK)2Pn&|{j<{2s<>H!|L&PO%F)%Qe z-24`g&-0p1s+}k^OAxiY>eNbOFzCZx@#v}}6APF|_WzE6uLSpR2lsAV|Q$ zLNV;&A6I~y04a+i2gQr)4vDcqDG?RO8yCmLhq6&RnzCpQy?N;A#&Y%1l#OMzkN^mq z6Pg0KH$+pA>sTPBKvt#s)+c%5@FNrhAny$AlE%kCP+;Zek~91*PL!6KUs@zUtPD=q^RTd}oXRYhRx%KB6&}8Kan*Z-s(deW0g|5`@_Jgjjjd*lv)pL;(pp3_*M#P} z9?V~4(ZIdRKdQ>(zzEaKLU+HdO11v*=Z|NgZ+lo0BrmG-9ipr&^AgNSEP<>$NV2qhIn|&%!F@YTkS&V@EXfZX2py-3 z-jr+(O=qH{^<&Td4q@Rc%WJH|3J+!g4NFs#ENIvqNb5yR=*2Xbf#~Rl?zqY5>KddX zo6^pVEk1S%A2H=#YwG`Orln<|lXdZm`99EPs2XePdpRbky(cR9FyJ>HAXObm1I(H^ zPjMa9E%-gWe!7UdLCi#`QO!UT+r_6&*2nZN8p*L=mVrcxfkcik(Io`c-+yg;L*Chz zl%W&v*%IFJC>iH?kY&2YVqUvhh+4la+1aqb8~k3Tu^OD3cyx#Y{#ycaLj+kZIw!IE zdO22=7yV(-7kIMLUd{Oq6tiZuCND{6&3IY@r0%qwt4!Lb=QR;=8l9#ILT)<%t{2pS zu2r6(l1)~2HtS%;`-RxcQ(hBL(1itQ>UzJH1cDkYUdv#P^IBvw7v$qTOGIPvmkA4% z3G=(e+t^kpZ6IBmmkbN_kLrCErBuNC?s0kqgfSy5S;(>96Q*!9ElT#*OqIShB4nG1 zxr1+TC188Ko-*jh2}f4Mm@*8(bUOb9g^qgnyWeGdiNk@&w&vb_t`a==U_l*ge_V<7 zU7@++)4q>#7WDYB^!N#k8On4sb8+5GF`CY_&qjk6WoQXLi9B24CD?CFipG1=QKLqN z&f;QWOrka(R=2IA)f>E;t|p6?oUFfSEW8kd$?ZR0BuRRIhq!ohVEio}0<@%4-vG_r zk{+;_?+Ry>w8`6%VIV?Og@9X*e2(4lW2bYnNJ?UYdbn z^JmC&5(Fm-yp8I`!&G{!#$>@nP_0IoYA+g2=0url*(H#*2`+AV)Qr+8-F8~r zu(SOOw^)ahM|tQenv3=rY?^sBIe0Ua^C~_+Y!CfsAO;2$z;3xm9XW^)Qq4)2?No72 z{nsBn1f&UvVFNm!r27R%;d3dg_d>+SM+5|xu)4{vWYNMdd=G_4?gw(?1wa2XkC=Y3 zj^$05oDUWD{pTTAzz9-oge>nk9{`3SHU5OZV38pR0%TewTA=lLGaT0^6GS8qto)4# zXdqT(HUNd?WcaZv91jm8aNz!(gE)TvOwG>uoHty6wh9{kfym!gAdmrh=p+g9mT=Dv z2YMtEZZ8_8zabL<=i7PF)I>Lq004O}jB*s^?%x5CK^{8s)=qFT-%9$9+i!b;zwZ8S zVw(PQbB4XMFQ3pT%HF@c{?EtJQTV}i6m)D`{wv>wyVn_ua+TBgc(y?EWFL6Mmgq?(30-Szxeu2m|@XMg`gN$(n~h?NJ;+K|6QiQ-K=>#b7;~d5?$5CJV_<)lC<6b0BL zCXv=cSwYuU`NDuNtzu9T{+tgo&Wnc~Zb|X@3xgK;Di~$n?SoD`#M)4!Qv@Q7p#2lX zJ}~P?*T0=8_q`EPtvi)P+KoywGQEvT(DPY&u?b~?Y9};%MBrTnIvWz*10D59yddc2 z$c1_6j;G9|K;!Jo(oB?uVa$eU%s{%uD!Eg{W zMoaZpQ;-;DErW)fYF)y7qR4>G_TY^?0$^cC0!aT9DoXyiN_OFJ3k2O*!JKS!-qU@~ z30!Q{Bm%)2T`;t3<*<9@G{3F@;Xz7@fu?@sdA%U(dnw%To*qpd57}NJLU89hVcT!4 zVn9S;l9da&*IO+VE{bRys<+x*FsJnEvfHQD7P3-D(>FX3Qo?=1JjF;CEHK0jlqJaz zbEYf7p&CKbXrMIC5Frzs<`KG-Q&v5XS@NIB9B0`FlWwiM)(DKT#{55Y-`%JUm3RCx z2wmmHN#ww%m!BA{^^8E}d;h2B+Z0)^&aF$0TQQ(Q&Fzz|*`|iQ0_vgddA$FlQZ}dM z0=PxVZ&Sl7VM6W=4J#u$BQ%u2xOTicSv2PTx0v@_^MX){~m0H&SZwM;l=6+X-womaOl2xA>6#YyQPt(29=H zh#{Rz3H9+jb_6K2W4&k9#LiGCjKLx!^|0(W(buFg!*vqCku4u>=_+%Ipt?OuN=Koi z=7GTAqYoe1b<~5QtRo(_!m=_l^g6nc{S|UrM0QY&Y3XDC21;3gI}H=iCiGE%x^#HH z=-RBQxdDr#khho&-5WBKHQ0tGdOb1~kh0Sfz zj}XgrUM$SV-C_1dA2&^#P>Q9Zc=6EiVuGNocnB`d9(I`+3C5AVD)g zKY&TpS`$vbNFrhb9J{|J-#&>yqmGkM3CBss)(_-LG>oOO`}ML>RX4yqX>P7{ZGBtr z)hn@+tw%xIVxE@0JBVvrTM>!MHgkm^3aR()-1SfZFm>a%e@=bs-LiLa{lE6hM_g1W z%Oq{rHIG*_KE+`uyx>ozs)%7yAln1qCW_HX*XJ`1VVar^j)*G#e~ROEbqD(!OFp{_ z!*=Epl+4z@Y)jpszjQCNq6qt6GYE@^)*dML6hy(*H$Nb+ z{29JVzeCBDPjTJo&l=p$`6>KLg3=0O+%$(XvlFhkW_%ov;xaIkpX!d z)l*|ui-!;%T%e_@nSGAx3voumKHm6pt-;r%OC>bB#;Om$6v%k>P`(!tBII7HJ zlzH-;>J8B-0$l~Vd3oma<^0%KT8|?K-M%oHM>@d!=;zh7m~@QUel1^m$n04=rFMUv{@QzC(-YoG8P~A=3Y- znt+p6!)aDS&%<@=2$y};t?|XG)3R#R1NTK*d8DYUO943cmxAsEIeb3bEoqeu ze)RyMsBlKm<(eu%`2SKx1#HM$Vb2L0TR!weI@9G5(_&Xe zf<#-=#{F&hxk@v7L$hG|Zt!O>1C`&fv5IHaHNpApB>J!V*OzbtizjEAtuE?jMmIfH z^a9j#50`1K-i^J`888ON?B{bm>nOp8qaXwo*yDY7F$QeRQKZF5LKD@VQ+s`$MI%c4 zWqCiCd~$y>G5{s54DOdW<^aQ0ez867GsdEu=2=NjlRBIjK9l2s=s#agqk3y%=d2}i zt!3%Nlr4;fu&mBtq5Qf`Wb)z^{gU)ZvoD7yaQ}LLx6F4y(6oM7O^m|ReI3Jp4gXM< z)j+eBBnMynws4frDDvZlRB~^H+|%0j3R$$762TI`0j7OBBE9&KsFw+-Z6P7zZ~h%H z;|K^U`CR>a@)2Ws<&zwFhLArIrq$a^Eb~1NbXhs7s83p)j7z1e?FW#r-aY2;y^sP^ zwrdyS%km(VXF25$aA~VCQ8$J z^5-cQ2i1nG?d1}FRfc&zg5>R9>qb%^Stf;N{H71F!Y@vDJ7bb^!zSboSdDpFW%Ht2 z^C9nFoBVD)h5*nw1DK@Q>1oj0RC`cmhqC;}QO__{v>pbsy{)yU1xc`1-P~k-XN|e$ zUZ24B9 z7%HHmLYnt%{#|t2cDh2Lf0V*ygv9PXkww6<3Yzs<8Vwe#u7fQ)B$?KbCbh7WCFi&9lWrub{ zub|LEo6+bI(;WOAIxvDGeX?^wP-M)HqQVa{6gfwI%MS<9ym#w)8M4g%w*QFd05l&3 zvc;%EnO|`Bet~fb2=7pwy!NrC#)xGH1uRaY=(pL4PM#1nkWI?-AlYyRWn^gIs5sfh zuk}AKj=h?0TlBJl7W2iEAiVSJiIZa(1JTxi&(I2+^khg1W4xA1bo@!TG7|$K9}PN$ z^~DJ~eY~Gvr+ac)X#t2M&oK{pDgJJ`#_fRj)%vdH>mDu_i) zYdt4mVWY~pqCaueXVQ_F!s=?CD5L8R;*+I^7uDsjpO>NkyD*}>z)4@KjnwQ2>HU_+ zm2D-`bpx?yy38ssT$gn#Pku@-zvwXG4Ed4cBrkoHQz-|QQHve zdlB*Xfcy5q8dJ;2jNcxSqUKXW^6XW8T8qdotL3IQkh|U@X|Crd2Z4-do$Yem<8A6@S8HHmsv>flG<^aRr%+u* zt*+$tOPrJyD(~}qx%{y`bM($oAsRBA#Wv_sTduZB>CdfxVDHQy;5!vEfQ5sp>whVb z1_of~cI57q+m1w>;*lhoH)@a?T^QK_MsLtScuWx0U&VPRJZFfQ`s1IZmtsDiTn>am zp_(>JZD%0}fkP|%WdGSoGzx%lQcB8gRs(^i?oI^ewH^ZniSt>j?5jKqGI@?)dO<(l zdQ7+cNpmCpfxwJren-eNv5%X*DHS}{dM;pwW=Ia~U1W0s>s8Y*r>TEIpUv0gBr~(m zWH()jEK^VE@o@|HzrnIrU$-&n=y8SO{UAOY@3FFUufv61*M>!;qe){RH&KUD{@GW* zfv{#~3bN_xAphi?MbISsmJPv+RV?vly5C-bcUlernnGvDPeR2XPo>U_>PB6j8xPg_ z(<4D0rn-I~#ChX_iZWJ5smPZjYSR%{v>ZNnE0La_##Zx{%?UlRP-0Q-L-Xv99_(WI zEWv+KM2c2g5$RNAC_LLQ#fv@sLUd&A$J;}bJa5Su6{upX64PwbAx+a8Ic0R}=oK0k zfHme@A|y}teYg`KVdgdUknKUi(iRT({T!2}7>AUvk&ExV1oSH(Cy01&7C9ab+h<~8 zzL=8MRmw3jP7MY_f3*|GN@F~d4m=D26PlGi<)O_WhA92>+a$F`2_eY%qtPA0W4t*t z|7Yv36@XXz-BlIFQX5}<4<6Um(@2Zw_$hpDaeH`$TwFN+Uo4S;pQsMKGBbbpot-4^XEt5NoBVno=ONf?&L(z zAuOO)y{QmHldm_-Kr7kM5J_{!bLb}u%=&j@=KZV=6d*{P zOF9?kEn9UhA8uF(@V6F@IsY*-VuhX-= zn=QbUgwB){Esk|RR+O8Tdm1hQ#*0l2MHx#&GgK=>W{LzyTG($|04atYpEXBnqm^Kv z_s-$|+WR{2{nJGSlwHT;1Q|d|&LRivd~3cpo67eLTcRA>`*xJzu4Q}8WAlQtR+>3e z>~6FCQi?yiRYA}fj0=xd+&w246Qk6Q=z6~h43t~RYUQyqHgIG`6Cpq12S6^o6YFoZ zw3p+kGKwuIcn7XfiP7Kg%*-poVB2D|B%C~@^0(QJw zUVu&qnWo2w1*9FFB)}5pWMUNMI}aZ6vqw0x_wNqQQfA2YEB0|7)X;0WkBeP!>nxrY45j*RqB8OP-eEW6K}%q zd+zvUGe)&+>5w$Rw_UjrHJkdFkiXH2D1j2FVF62fbO{+GNeU_%ht{8GY3!OLj}h-f zYeCil3CQzd4q}VpZ)}q7i|S5-aOSuaP6TF4#{B{5iUFtDlo1)ac(?%PcvcF>GK?#+ ztS%S|^_esJ%O+=lncrLOg63LFHNRukeut%FWW`qQndyj}%CG2TbNi$2#47R8Rd}_v zC_#0voE<=vfc2jUR4Uz%`$*8e3PB2Brdy2$wGA`D_TE4D7^C^HXc z`CU7~BOX!m14Yu6M1cC_{|ccDB_eIN+nXm*{sV72)F3 z6Ifp1_DFiAsevDYfd4Pa128oMhalMBlgDzFZJZOGFd+wG5Y%)1?e`-R!v;PfcYu7g z<-fFj78@q;#Y#qi<_AhYg+KA^CT3ns&C7{R-{$wDlRoYY5yF=5&>2_5W`wvvjyw`y zotrV^Ix`G!?_6=d=`Y%O>Y#!Eq*~{Fv?P5tx>!1mkq^=%*q~yc_OlRbfsiglA|j|u z=ip*?q;BgI2NXm1y+1ji(ZP6_p8SsdUyM6YgbEZTui1&c*?Cwwk(SCMZ<*(o?Dx~o z{%5FNkr#rZ$R`2az6^*(#-H*5p!$vc)I$X=1H_1O8Cc;y{&OKu3kz42H zc|npb@Z$E&!dwib2cAsev1H}&BT8qakOL9+jQWtTAzd94xPOU>2*Hn#ATU`hJT|rj zk$4rctcA>=QR6IQ6%|X;Lr^%9+uFBER)%0!HiIm!1z_1f0E=>W1qgE7dr0(K$Q}ap z^Nr3{6E10vm9baP0ao@fzG8LKk}y1s2pa$_ zD1#M%e+C(?E5I^s%|_Gyxn?I3HFU!GErmuh8NSlnrLd{Ubu~Q=kAbCLTQ%8AvZl>1*rg>{>+>3BNJ|k&Gfa~1;;{m23vpBwBDqDOBSOp%;Jc26x-dS-Nu%GN^x>k z!Hh0Bb^48WTrz<=w%7;%jaxvyU*kBpf*inO;6Rl2`n+gO`mYPMmK83hRHVW_uA(Q| zrW`+(415SBsiYA+mYSH3AA+qL9|y8lO=lM-1$R7uZv5+gT}3$-&|hO?-Foja3NjqK zZ0t2g+fCOt?a9ZFp(R#J1%ykUmr5-|0!bonXOrahzB`9aHOxKQD$0IGT=@KP_L+vA zI8G!mF{Xb^By*ja37{*Ei-(1RdL>Y@8Lr9O?)xAnW;M>!7BoKcSBjnPTAGV}nM9cM zHou|X3?z|GwkTqEEha;4sgd{IoBszA=cJ0emmtXC3_YwGBPAa0ZaMG2F#d+kL$D)b zeAu=^yG+Evxo1P2q#gf}by>~IPyqL!NRWmDjP`mqWyXF=3cak*h}bLl96S?E5tn zBRw{2!IWet?nVYT^FikkKPU5a_9LdQ;EL)k086Q!w+ktwokzFL`cnd zAA=@0LA`Pb{IVw|3H6fE=XRLlwQ)O1v>@b^WBgdWHvZs4|sJm(X-ldGm_Gs#WJ zvR2i$omiA|<7Ikp>Jyp-OgYkyY^_p5Q|(cbA-K6Uv)-hxD4qxTfm4x2V$-$)ufbnF15 z{Lwj)iO1Zf%g!cGc!_Pp@;@7%xhcR%9^5N-%FFY| z+LO(+`=R1&%}1Y1*?_fw)~c+8@Ypnqb*NxzrJ=yGZQXN*;FDBlcUXPF=Owfd|JJv# zxDaXlscbh<5+37+;Z+e)YiD*rU+f4E^064>!F-5eV*25q-m|M7Z~#OF>POgSD$$IQ zX4HqNA?wQ1I#}wE^tiHHzyj{6hZ0l(H3`lRZxRz5aXF*&bo>!E$RinB7>SctywA$8 zq^U>|C@DxK8FhPkL8|q%foN0Xk;FnbJww6D$mD!PJjih};JbU&<=3`iQ&$Y4MTUUA z3ur{dh2?HELRmiCQ;4*Upm$j}o^V_W!hPKg8PUte!iX_L`V(wZk>Qz&hpqujvWtD!k z;${mIuU|z)#g8@$7vH585vFU~FDVYKbaZ<|GV5LZFce#14bA8hrg1COQ+3O3_L>n$ z0FiEXiv517y|pzklPbtzOf#e#cO2<*Ywq0rG(j@x;@S^Cp_f^(+bS>MYsdWUkuQZ6 zdQrWg@banDwbbj?)%M8VB-6J^QBfryKqYka-C+|y-jk}{tmi^IIcA{10h7atFYpa# zDMz>v2=DL^+CpR}rnq{8PM zzG?M7&j?(4YJN^Kf%K)8-?Zfoth3*XxAm!MET-O6*5G zs3pOkwJOq{o*q_KR_Vu&(XRre*xA|f(2y40KI0|yj?T@+{rO{42ZI%S{!A_*ArYlm zXqbVjtX?7*{Qqm>6OQ?1O@+2=#l=nw55& z2?K9_56(2df;TeX$f7lxaGzZVB&Vi6>kL6QXmp`};ozWUWR#gXf{FkkA|}o+Dnd$0 zNpWeOV`XCtPA0fug)4FP%li3&C9xl&wmCj1Pb7|sV2(dREO^tKn5nRy?G8EJ0n$Mr z@^rVyt8MS`)kA;78iO3FLs%B^5rvfEjTA3e~3VjyJk^P5p8fVUn5^$MBdsjq>sy>|5-`kV$$}4XnST%Jcmpu5)!*8H2|5Syt;wCqlz?51Eq*N$5 z9bU6_fUND;p~}TBcN-(22ozl+-Hep*{V_3WEE+59pp1gr3gTh$6TF-A z)8=Zce&(xLx&GXP^{jg1pk|$LvyBPNofrlY6BCTtZKkObL-8DChFdr1|8LKqkEBO z1EL4kPviHlySagp+r~ow7|l@Y+1RJJ(5N+$xJEUOV)-Zfi1Aa-)Lyy)2@QB)#uvwS z++Odq(D)Nw!>jLke9Rg_ZH=H=<*BiPmYWUA$Pw5q)fwtF7w`A-68p|6F=kMC{#MW; zA|fSSDTw9n@|ZS_jOA`ZQ&3QND!!cf*@PQheQ{f=HbVy0N~9nt z%4~z?@nN|bfNoDmdp34?#Sm5Zg;7TtVuQ1L4aV0#m=kC))KV9J(yHAjDRj8XsZS-M zqP{wW0YUQSPcY-De2GgQw&9M+YH8M65R1=`mfa|I&Dl9i@K#}t zmxqV;q6uK*LXFewKaZ(qYWWZIg80vGm#wAuS&fy$0d|bw&0$0%8X(i}m3c!olK0`o zKwmT9;>P7N>AMIcjm+&xBSpwAk(S7N8oM>yy*f1>MzBdF@aaeBs8^B;fpaX=&-V zZoA&$n({X!A>5>xAhWBk4|7`i%tF)ma`XfH(Dz%rq+9nM$;B+|j=A>`zi)o;p#W(m z0z%Ggf`r#+{$S^|A<5OLAg?T;t6#J(Fy?Yopzg$al2KXWwu>O|3+@-}JdUlaG$D=t zn!R7$rz_dhfmmRakGt`Wj*1Uuj=YgWrz6{o&!5q{~juew3B1 zCr0HU4Ba8u%Mc;LC=Q{}Ryo7OJQN3W0SXOgeU_7*nN;dG+$WF^oHV!CPYkidYjN!` zZD^{vkTuFEZ(JowU4BP*?3x45>@l^W2w6>JB34+)7ebtFIbs=BWvd519r~3nJ1k4$ zy;ZpoA)4=KWOYJ<7OPW(pi>_$8vpXv(*xv%j#14Y{gF6NIZW#Ui!&P@eb2GL>V5B| zwo6G=FZPTa0tZw`^=Xs*PJuJV)0qwVbNh}Y^B)Ism#28REP_9J%KHIn}_)?EV)uEP53!tl?kOlnBd&5^e35Zot!th7IyUrU(OU5rTzn$+zWefa_2@%`LtA!5-3 z;%ta}&_Ro4Bs--VK z+HZkp(Vf$*Vd>crVTJ*g9g) zhXQ+_&B=aX?D*=N$Ff{>llz&;YdaW0SaSnaRF*t4(ld5r$4<#hN$g~IJ~(pcoI}!e zMc8FOMSu7^7uag)Az%!4-+?R*OOsdv&0!n{A~7pBwLkGFObx8hW_pC*j)iw;+_9?O z*Eiw`FWjQ2AV5BA*6ZI|e2E>`f!B-yH`mVjt;KA_Z?tuZIrPdq;)Lg;8rrY7K~@2G zYLJM>$rt(0+mxb^F6n<7M%OvVzf7s~ESuxrP&b>wL%@Uw%z4AQ^S5FzpA7}Bupx+0 zvp(YuqI>8@63UQO0a!h!K*8CChu)qqe;9$!g?st0t@KKmFl06hNauaFBnA0tIXBS{ALkO z-gD!ghN*AvLELtGy=ygEU}Zp_@h4}lk!@u+t7NmZ-0W@dU3$A%m(zNzw9wN(e1!>u ztVd!a&(V5M=ALy1`qNx` z?UMN~OLbHSmC#ri{{Dr-z0l&jlQ54ERQYJvJ~exCsv1D7o5^GL_4cjCuJJ z)4Pah1J39oOM08;5vu*XTK{hjLOG%*E> z?^L-v)rIHW2a*E&Yeq&!g03%5t>7>x@AKbju6v6zVDiC0h11Q8Wnh0P;&FFu0}Tmb z@BB3@JA26VCYaI7(cvj0qk?IRrisa$ni{@|iHSTAp%?e=o&G8wd_M}G%y2D#7C=nG zWA39?l?LXQ3ni;LUBWNVSTp0N5_W^N5nQdbzY0{8LlMkAqK1SXJ7|YvE;LSk46bfQ z_ICVG`=9^>lvQn42zS{@Est;aM8f+h8*!%FSaiSd>jEn#0ua8g!1$@m*{Ult1P;Oa2<_KVcbJ}#Ar3qVaxt<0PRvL9bk zvYy|XUCqzWPl$}^UyU`(gDr!)x?oeJrkPoMW+uhN)dN2iiBm<+vD>@mfP;)_B7&wo5{cqHXZLeO7Hc& z+U@)q)U0-d#sel6JV3-0{e^0<@eMK)O06p4K3UGqYN*I&m+IMRbktXMzyA007#-*W zBqSG_dfGs3V2#*bzNyN(+E6&ZA!%1eIb6GIWgl@lbpm@_>X#THvMnEefIJQ3C)lvY=|F!fGE3K$eg|*?~7>Mnz1*fTt=`pm2^WGT3+UaeY~SJOdt0 z8;fRt|7X#G>cWU6vwvIG{x{jrkpby$b(Ba?nY_N!1O8iCzC>kzQ)A;;W9L@lSW+IE zLy4bo`2LJl!cvakPG{|~WbFLB8QItUiZ4aC)Rbo)i>uvRRo+xMk)!kT@utn5#ZAhM zb1y-NZ8_KE(Ri^@=K3eY_Syb&536$7heTzjJnO2Af=+F#b_fgXODC-e5Uujmg*OANX!2DT8``Kb6?}xcUm>EP*Uyomkr? z)s6;c7fu)wd;!FESNrc zgDbeR?dN=tD%){oqb9z$mDuCScKmje(INsD3!eDsuX(G+{1i;^31bS1LX;7Qh4H9B zx&eHGPVmc0f)q~sLIS90S>DUhjR)U3TyM@-SNd#oWS(BzAp3l@eYFwXey1{Fu2R*c7xUF&Kx-JLX8VlzSr)ik??IrI=qe$WX!w7vD~m0 zDY91IFO@J9UUE6fVXeF+-d&gz5gtXK`Y(f5ElUXSTc_3Z1?g7@XV4Z z4qT}HO%A<>A>HDZQ~QnhhYNqv1f24ZDA?Vp(&hpV&GMWSGWxb+-)-z8*znb9D#}}^ z!26PSPUW*T*G_i4_*lR?R0Z8|^cu-?UeTZ`8hKC!bn#a zO+iM>kX$dT4(Ef33*>tlb31|W{87~vj#KBK{lUc(3H}M6G?Vztn*&skAQrRtfN^0RsgFJpmSYNR1xfL+=+@EvVb>oO}dFV6reGVPY!c zs>KMfE_OX0CakS|ES7WQz`aiB1&8IAViaXdB6Xc$K|r^Cda0$S7m@TtlAP4k9bZH0+MUy^g{-)^IO&AGu(T9yHOG&8NaECF z)}Ur~Niy*8g-)#Cwk}q|;)`JMw0WI3;yy%%L(nc^@>nHPN3&HB#NgKG_mG z0s+*Pbnm{2Ix;1lU2_R9?Sc?;{$MDuyWRhapEXxFmNYBMuFGz2tZU=xlRh796?A)e zlu}_3p7NJxLGCxBpPaz17w~w=+czv(5^dMP5Vq{-YJQoWX{jF#oP=+c)khQpJ9*$W z7pLg&H4>%a5l---=L@i>9D|6+T_`jkSTVkId~Yd`Mi?;_y(`F{0ALh2lzFud9#nnn z(x#!lyx#chg62gNPQvv2$k72O@VuYNK5Vnv4_@}Mz3_?DwEzx{coc|q0br-?^5s=; z;1#NaxJu>r~}`pd6205ZKPHt@*V zkEs}`t1YFOtL56W@3;Xmf-G@nmyuC_VEAC05k1(S&~|e)|D!1(J3ia~`R;Lp6J@kF z6%9`VM6JHR@-xZl_3zV}T7M3(4!gDWMdEZel+w8c|8&laR&qc3NnUL&=#%sE@&JeF z%AS9i1n;GAgHO+pTjUUgFHd)SUkyt{UhcNjg0M}_)|TbjCxaIcB=u21vYaLXHCP$| z{ayE+q=eh{;~LirVk0V)cnK1ELu-dbG4J9<=KJ@q-Ppp5JI%X8zB^z+%zL;!Ff&+! zp+KCS73;YzAWxvt6S%N7{3gp~vPWY&3~{^L>=^}$8T^Q#ZGSys5#De`!ZDR2;<56c z2@yHubA7Pg@+UMpYSIrkGR(SKOExIdX34rk-FKw@>s{4c8F89WYs1Esc(H2GdEROH z+V(a`Zy|HHU%9n_i9#>AZ5E^fp6!tvN#l!lhmp+uwgufiJgnW3$u2ryT5PKO65lE_ zg@8}cF*`1_Q3qa(=T_T|GMM-uPkOJ4EksY2>B~*`wv$e59v9Tt3pyO^?tAYCKk=vW z=+}?G%mICpE0|y->W=fpLL!%?ZD-c)pR5$Jcfd7}15T+%Xa?9(M5I-sO{<(bPus2x zXOAx1^Gl!q38+GCqWDPhflWRsIIwoDk*sfvWw$l63!+US2-QqBI zSXL!^uGwoPDffy=oCf!fDS5pgYyJiFscw?6;(B7?g@acD9v)Vwqs&N2%(I6tG!697 zowXif%yJX46EKh~c|E}uZT7>93tM8(MqP62Mmy=)+72Ve?iNy4hJ$Clzz{W4Yl|DO z7fQP*+&wvD_L&QI$dhZ&}Vz_Wl zaRHLPkiBKdq6^CiDWY$oAsnG7dFEj3RpTXWVKFrKLN?&5FI7=8Sb~Xxy8X_zBZ$tG zycpTyK7Nw4ahZE15WfS@b#?p!tv`c5t*RDwqG>%kB0QT#tyFOKA_Xjd)L8!$R$tCF zO+*eb^v5ko8GpcLiL5yo6F)G>5J;hPFtd1$ez+`Z(hrcs9r-pG16E(B82c%uR=mP~Os zm2$OK&rAX>XeEzx%3NKBD+dU}X%;cl5c=5<`Ky0OpWK==LAG8(cKnD-cPX%tt3EaFW z#vX_0*Cyx~5(2NRW&a?2$Y=!;30Kf z9|#{a9J-G2;p0Z);YN;3%|_|x1X2Xv96$NA^ybyxe8NJ|@rT?&0a=DjPb8q5SGy<9 zwrMBcQ`~KL;h$Q-y0ZTX@{-8j8ji*2CXk)fV#RQATh)P>rH5Lcjyrqw#3Io6tF;rC zy7$(u*NVjL&Bqq3SK=sq7DNhthssfZa01v%tZa-)TFpR!NikZgcm_Xzp`0lL%hmFpS#r_Y?Q4n3j&qVMyB{ zfJg1v4hCuOt()fkorD0lpD1Z4HPav{?Un# z(8{k>0f%BOE16{@V7hV#E*cgB#5OiJ!Jnk)-Rv&dbpFzY%z_9zwB$P4sC5}ON7C2b z4ogtt;8xl{*DqMxQcbiI4fF1XkWmkg#Ply^HsDpB*K1xwSa-Del#1jF*6}8~p2?rK zMU>?F6Gu;>SSZV=fQ?pSKV5sB1B3m=U`w?NS2P-;+XLuodQKpa^p6h z8-b-&q*7;9!z+n?w|`nnObBlw2-3Rvi`0P-0}2__N7c`0_Kf5dgFEipG2H8DFi9t| zZ)e0a`=n0g`Kb{B_W}30aFg2rhb9~ze&ArIFHkbX*!hP3q6;59wnuxtrUZJtyHm2P zBA41^tLBIh=}czPoom4m%G>bbg;sy<-UkBMISL$$hIPwi&UA}rjYI&R$TdSFDYwix zPh0TFpAYndR0?eBgzwE?liZ#!dHo-j&N3>h=i%ea(%qmmh=BBuknTol328)1q$H)5 z?gl}SMg?gEY1pMjI+X6tU2@6Y=kh<#n|;AK)V*`(&V1uD8x?Zx*gNg0#Jg1m<&U;z z+A>rHP;1pv8DC$YRfyZ2$b%sz)PzX@;wS!03!>`#VeU2uTmP{7hZ1893m1esuv|?O zg&nBwf^FwuNgS)$LE3BW^_h~-&gX~kQ4S^s#O?oL&hg}d$&K!SR11KokZ+Mb{HuVU z#yj-qBY*Zwd8w%9ZeDlmCyyS~sPzYTnyrWhh}_;q#zcnya5gvmRpy6u$nChCR9#?{ zQ@PE9%CFsc))Sz#)KndKfE&i;N(wyhbyqLsR0cYr3IiQY-9(pfEd;(P+upMV-V&li z-B3`gwS+T)KRso!F$}Iv4*fG6A2;)*5WSd4K>rfOHjyRuo6>;ozk^j_SlDnLL5Jao z76pznV**lrXCQ00iYZI@`D-Bw62H7-0wYs`kPLMWifejG zaxJrZ_H&Kv!j=!E{J<(t%&cxp1&}hwpvaZ0`w?`sdo3wNh$$H$OnfW9eCA5Zy@INH zwbnVF=`eAPTv6oMa384|Y__^xf+`oE=#K9ERP5+UDmhqNAeU+nP_(23VTsA$I}y8z zO$_an?Cn@7ZRkM5ejktMwqutC?m!AJJ;k~V+Q1S91|X4t%-z0j?W}E*qT~})N$vb z>HXf&BC-<0(;*;0l#Fh2_%-pRj-&z8XZyMMP^0yuK^rD{WzWYC_Y^2EXJ+4TJ1sm< zU@sZHcAsGmPF!%if4pAD1(Np;V0p#&SBO**2Hh6PFh|gULh@oRYI2ZZkolJ3W2AvO zlgRyz*=xQiP~pyCfDP%{&S80_>JS`S;3*m`uBwAe=XU*0wGB2czR1LY>c5CNUdWU* zy66;OY2UD80AwMLt1rxo?ZEfVjI{{TD9 zv3NMD99`q$kAHECv}fYI#DGf^BU-=v2%dqp8NZZRz(#b$k51m>H(-Y@SYn~>(xyVK z%Wkx-H`XTL1tOtPC_&(M4Xo77_b2f>rIiTsv#1xQS<>4>G;T<@JUNg(*>ZTPEK&uK z83x`rudBor%lp=WR$nbVERipgI}g6z_q?|IU^vtD((H$E5O$g0fm)&8Jt$ST9(Kd{C3j8e+F1E;!eIF7V_>yOkzM=*0 zuJdw{$W8zz#`E2 zMfi2N2go33s1&EXGhSu%93Ybe7@|^NQVVNxev7bSFDhTe5i|t;_6Ps9xa60F_ps=g z#*U8Fy?{A{i9kn|(_0I%#c4fIa=$~3XKWw$-G!Z%@;|S|TY9Rzpl8$F>(ec;JzATU z(iN<#W(K<~sMLms|3_-7CR!PtPpL=-yOLAW~Gc*mQGrLoC&w1{!|Fo_^&;J4k>%_e$8-nb5ZSC z3!iPWrQat#Rxc02wf1?*xU6~e$g{=h=eq4A1X1A3`K`LrPhX7OA`$}TW_j(46N?WYbOVviwEJ? z)-lcgTgAdC9!$ved)7*DwIpvf{hBWeMCTu}B8wY{FfVLpIlBQo#8TSO5jBu7Ux%!X z_z}^ViBRW>PDAtBTe0_Ue@yQI;!&1`=TXASJ4NO6aEuTqE~dody>T05C06Ago8E46 zK=A=Su>$d`Z({#UDr!3t>)=$jEAW2^H~n5?|v16((IO;G?)arIFZh2DI0% z07Ne?KLRA7h}ORq9^Kgvg{~v-{Mt)&mZ?=3WijOo)~R3;$IQNW_-$4TY45te`rUKM zqBXr_?y{yu%jH7iD&(*x&<;;r2-l7T6i6Dz(kT>&?rK@mJ=BL3i?0R4YNR89@u8VF z75Asm|IYD#N()i7402oAPZ-$WIs;ZI3I7&?@z6O2!u5n-6Q8WyW6?k^jNJ`Mv|*9o zOAaRw#xHl6*APu|ar0yMc-RM{AW(A!ZOP_(Q_;q;Rq;mWiej1Mwgu8@7&7Ldi-s12 zFvi2;BF0bK;N1il*2`_Ok=V@ll9lSrHLis|bL4)buBtl);x{{sW&_!0>9fwQgHR|nD?cRyAD+}$TkcI>EGiP!)>6m|V zDCYgn%Yy{ZV{C6c=q1~mXk_7zm2b)22(xOm{^lPn1^7dAi9&5`H0z> z=m>d7r|4U9dc@olMhYR0jZ%tXUdlR|2mI3wbTh6S1g)lPYWW|^O`3e`Z8nEx@TJD$ zO$q$XPDKq11<6+67~OHuOU-P_9j_b>7Mc-l4<=FoU-Fg$KodPA|1z`kdzgd$DM$U5 zRGKftnR{YO^ZzR`JJtHNKKb)-jIAmNwnkD)2vh%ar%iA_hQbcqJFTO}efb$?E0s4W z2U1=i%|}nm5QZdM?H)$1{M$W@7vn44S_J|TKwxQJpHpeIn`Cu@F&rC&yN@m1zJ$-M zE)I{sOexR_?HUfv?5x9<;+S$KmJnu?W)fIN#*P(#2u@1EQ&zSn4sqIhIswZA8uUb?zo1Jx_e+ zZD4fcPlI2YhF)v9eaBtfAao{KKq&4jfv6)4t?9tO8Wn{lW?`azp6ePP-v#b*BN`1t zW~#ixFqH}h8uF+w&Ih4nqdwRW)579!q_Zo}|G+q3^ZG`jy-RaP|3cX40D{$2jLYlU zUrq-0I!n@m6!?b=H}hpP5?!&2k3k%A%39eF)P40U$?v+N8RmD-{oZ9NTu{s7?O^`$ z9ehWXf7+TbkS)U)et{8!qPiZ|d86v!y#IM_Ch_-Wme45a^y6AK1>oWxBy?O>{{i}4 zEKk?fkWYTzZ%s@1dNVc@>(35~OTy7XMtZQHJj;Z?3h^cz$>vW*TA86d-qq_P@Mxn~ z1nRNMXXFj{*J##Oh?M!?cwsPuy_J}0Fj9s+hvbR-KYG39wf4$E6*i`x?>hxdYi%!- zZ6~Jw##*5eQdbC3x887H6gEK0TBL*+dT>~7VFPIr*y|UeLpoU6W`|TC!o4`I6vfkj zu?f`H_U^$uztfqBu6$gKL!mQ`!0|(&N?MY+vkd<4CC?KeOJ6?6Hw&=QHvVgG-1|0dL{wEq^{obq16uuIM$83{J67vk zKry~k(CfFYpFhHX`sqJG7?DruPw-bew)T6wjLx5qLIp0eT^vA>fNW0>a%;gi2c zP2z^~Xd=F2NRfLIfb%^(-sR@!d{b`}g&n%84Fo~E!XRk-{Z3%qAo%4liblq|wK>Kw zU@ly;$PbQvTjQGCy6%&6yC3(1<>z+;*2uA}J`xzQ#}pMrQShlv1n-RLdG;XnJpNwb zxdo6Ly+y!3yDZ$*>Uc4{U+90BJCi%FpB~P)est&2mt+kAHwmX&HIbPZU=Is?;Baas zScFphwNJEEH^A^ocx$kXj3k^E`eb(`VXyNHsU0GXVAtNOP{+wc-|c6v1N-D|U{F349)E&? zWeW>m2-zVOkGxrS2a~Cr$yixn^KyHl0@pQGlLf>n52g-s!*HGJ3E$1-|os z0%S6C!7)+T?fURLY2S$4%23||iNL~jzp5JV&H<$`GN4KFctq_0=ni;&$j<^Y5WtI^ zJb8t*$w5%-_1{7jJC9o#_ycI6`S%z_3Gxz>7zpsL9{*LHt z1Nq`PJNL>Ze0-=vU&pu^B^}WujSHAmVob^_FOrK6Cw(zMabr-5pfNcjsqB!vm!KbE zuatt1zb1BOF)8&;+#?FyCGR+MLs$Dbg&HNj8X8%E7<_l}x?bmkat434`f3gIULqKB zhJJ`u-L3>-IIwxY%QL8~`p%C06R5)Ke*bawEwq=^?_xOCx;1nIlc%m6gtI>4Ix+}0 zF+YB}NHjE;)mef+yS)1~MU5hxdMO$xNCK-v=p7Vpy^xuB)eS*|ym^cg4{n8w+YfG% zZGpI=c)OdgJAR6uvTpCan_@N>E-!^tb(943=kU~&BM#3mG&Du$#50Oq+zHjs*>gy3mlxznd`E;F z*kmKtFEGnV9HK}{oqJRMj86W(o8R4wn42N)X!wa{Wizy}%HdEn%U9Tvl09Br*}$CC zKTWKD-n)-1c4q!yWzQg+@)qUEPjds;BjnMakCE59CJ9n$A!_=Ch- zXqw-J*N5ec$0==r>YQu7*!5UtW;&Y8^O5<$eqkDl)~t6L7JNay7<1sWhYmU{&Nuqe5nF}=K? z1(!>9ib;HvGmtkhEQMI43_XsHbN*$gti1p^S>09SNO>X^cEFYoR-zjWi)p6Lmi{VZsu~C@njpDz+ z4L?7Q$3-8?F-B!P?=~w47tx|}6(?QZS9!ccNPu|=mKPxb+HzMnrLqaoMgJFr;~hf z$P`!GU-0)JgD*I9z1N=p;O*t5(B6*x@9n}|pL1>jbttMJ6;;Ksmu_$9u`i$7LUlFv zA{3ds_a1+cHfx)Ty50I)eU(Z~S2c!`Q+axoU^Y#bM7F3C;Ccb3Eyc;KASqjM%LA}A zK~8X%$3lzm*1e= zKTuJ+FG3BI&@K{Yt?!b$x|gGorI%(!mJZ%QaObk-8~@>enNod9pP6<#E=%-kzTeX} zvb0C@BVCQw*a4w`B7Z3D{lXevc6|hlrKpf{2{SC5>h$MqZB1W0jR@l=g|OJyF9OD` zzIH!O8pt}ivoE7fl`&hj)Wz-FiYg{Ov!Mhq8Vi;NNqg8_m@4Vm@;?2Ia!2gs4boJ1+NY-M7OuMyA zcrYeVn$f-7lv~B-wZ1&4Cs^6F-(+qj~_! zumb>|{gCq={d3ep6CL+sR|gFc2}`7l(H?PWId?p9?w2T1jeF);D2p&cP#1I6giA4d zF~|)I6ceGO-RHt;&AHK|jlwlaB?FY-rH>GZ8U z7a$2{SfGv*6oVB13K?C0wPT$KY({?tt03Plj_vM+Q18ExDNw(4cTfqh-3WInp=#n3 z4u>_8Ni};S4%RNwgcyCFuG$4xCe13H^SOIoSo;yxnBF(SMyqO11+>xIk&RP z8EeaOXc8o9&sQ+bX(0qF7ytJFKd4&5Zu=!_szs!fq>Gl{TffosrR28^@mdGY&a;97 zWnZ=1>12;6fOWnG=!)5UJ3~7Xd#6g?Sb2Yg@g26H0^83qIP~x_l(ZO z>f-TdlVa2#0(;XN0fXM{U4B7~eeUDanuA3EW6q6NSiWDzgAA#MTg`Yomd{&-)Ib!) zlFo!&_iCB>75Ot6_#e)v<%mn3;hC>P?dJ&I3fE25$l7wF$;Bg6jf@s z0>5tSL6epSBK2-Rb%oM15{`XLYi z&6jN|d+~p>O`i0@_Yib?%Z?RHV+l1(ACms=yH4C?9k+n{PYixj(ODlspy*Ct5I`V? z9-ARcCZ5{Juk}3Go~GfEf7=LpNvku&_8)Jl$?D6$UoOvBuxj6-dTF_b`+nvfeqs;} zJS?gTj*YoFNr#K1$Mh9)5df4_U8-jDCpF*kSPakI3U4}LtD9Ctx6t9100kQx)h-KR zCl)5gYs@5u?=?SN7JkG&)5=%o-LCB2`rW8&?n+eFwYfhWC~iV zuiANZ1DB~0|Eg5yw%}2|bFGyDPJLTkZR;{zizyb^W!NZ+&A zBrOq&{*jeyx*YnXxA|NGShB%I^##F=h?WrK`3%nQI_d)o&%?0G{FI-C-nT94biihfa82qV=pOl`=G zspEV4CY7NoZxSvzd1yQNsx1rTdSr$XJqetWt`DWXjA>!flW_?!hkG}_V^3DU$u5LLp!N(m>-#`Y^- z-U_)6v^6eb+Okwux%I2WmHm@%XJ_Fb*#KDhXCHuV50IS~cEX1? zm^|33xHKXA(6qXnNLNqS+ zhsGQnfVg>g`dC-HFG`L3mlY4U$`J(hD7y2`RT=3LKDU7H^g8?p+EqujO{R&Ptf`*{ zth;FW>V9EIw;5$_vc?j~qZ0)PsKsrTq0}VZW=#a;GX>>REQ^+oD&Zyimo#!vG>9xpi-^a9w2x7O&%kV`Uw(4VdM>D8TV8X%Wly zK$D`dNa`T5zN&@%VVZCsIzp1Jpj>I_pOx%Cto1q|C`Q1q8^!1WqZ;x3sxVAU_ppFI zZW(+y(0V1M(fi9mDCecP8D6z1oQjg6CdlOq^|Jyo!Lx!3@(Wma){L03>Q0Tk>807N z;PGvQ5w-g=9fB?!#9ahA<1%1pxVhETd_mg(lu3XTc%pF!Ba<7`!c&TghUekSi90w` zBCJ%{%V4>XHNq==)cxS|%QMT3Q#{Ov zf^s>MQeUnEqT?U|%rR7dIAF}7-e}U$iuIlA6-L9Pruvk+;l7MRbg#H{_{xnZv9eHc*)3g=1IQglo7T;*0oR5h0KxAA-&ikv>@mDS5q1WlhakJ^|@? zpxiA?ia7+x;$JgLTuV(M+Y%;_{<-X#r!TDop17~j#6VpmE|ZVAkkq91#_Hl-Y&{>0 z8sLH0FHjzjtRO?Ux(3Xc!S@OEcGl7_=|3ei^_ubf+j84_oU~(K#kesdl4qjwkV`0DQzO!)RgW0o|>mwmvt_97rj61;rMW3(|c)9L#rKRw3&DKJF|V}3#!8iubN zjJz&@NYwH^707VW9o$kbOW@xs`Rj-_J@>XLn!P=>+?Zj1_)*1p*};Kc9^?`L|*2DyA)k z1OpF9&He6<-`Id`Vou)+T@#TTPYS*7c{QD3oXnka!WS!55*2mSkv(#?MGG2QOk~<1 zMw`NNzsY_{r+-FLM~i?a@U^lucEI*(X+*B~VA&5F-1qb&-Rd;m&~59$M&E^4Ao;ED z(eFz73G{Mt89}xGLI6Ol3x&iUIeCM4Im;R<9xs=Ytdb(-VmwDBa35049;is#)n979 zAY->jx7G(z{|S7z59CPIXZuGF&Y#E3ZF|2S%&~)=`xiBGF?x^znh%G$<$M2R23orM za<#6k?%QdHhnPKJ9}x_xFQFwC+Gff%LvwHH%Lzjq!NU|lt9VcK%LW~nrJG$B)C-!M(D`(e$H%LKwt zmBn+gsJ!cscwYIs0aT5;b4AM;_6xgtc;U#NCBolWUUX0I zV3c;$#SuH?$LrkBH0AMu_g=rJ&`#O1#?Ac8+Z{zr4;+rN0$0;0#Pd5@y)v3YHoTd+ zWc0gQiv}D^fRO+M_nCsPY*-%QTDm;C?NHsA2yM#Sr|N$Rk%`*61Hud5{phT$&;efu z*-LpTM2}i%0gBM?Z5t)t?85&0l8>vRtDMQ0D?z{UD-$1xHKdTs7tO1aVu^l?i_Sw=1L#D`J3I3P2M&#qL$?jadHO9=%HS>02rmoPheRw;#Ll3iEkXXc z)%EWSU=<;*!<1t2d|Nndr5jE_Bg-b~sz%BPt5258ZMlJaO9Wg;^X$?lU_t{%H7>Nr z*eTxFH{p3X!_0IrNW8_(=qA}I@$asU#WslDf_aQBrho@^XyrzGn%6RHW$8|dVX>YN zhzE#Y^JH#X8mA&R zV3|(l-NG>z+|u85e*{>al@)gU>^!x^hO^(_xLj4Mo;JPX0T@SxOiD9_{nl!ew^F zW-Z?8?#+I)uIR9{w8-`?u#rnhvcE$ip#!2b=HdJM?;i53>W$Ck&*2h4EI70Ne(esM zP!9lhN@^=_oLylei<_kQ60JbuKkToH?elCL#?Fp25AO#lMk5?K%uK-M4S(hc1UD}p zusXQ#PDc1WQfJ4>eQKuFKzqP9_)-eMNO18#d%;HraMZFb*#7%W!yE^+83XqX3c4A6 z7egOPEV!ghUX8dSUWqcGajRDdSSaSzCZ}9>P^Zl1G&MrYT6Gan@Y@wvyIf!uYI%jT z<51GZqsr}-j=t+(ZE|GA+#xGR@2Sb0Ht3bwG^T{e<44DN>Dh8V5CQL+OeY0R{2`Gw zq#X^D{*3|JP(_Qn;c@T`di;qJH1n~82p@n)!^7~*-~)kJqAK^*pJk)uuubXc^)Wzw z8$b|d1_biYzX>Q826XPigEhI5D{td&gSI!515|<6b-5L%la1D1B8k_ssa0c2rW`=m z&EvRb2S9}bi}=2SXlfsV*vB%AuahYi3%|QYOuuELO9voGfN98YVn3hn3mtIU$aH=x zm+57M@)aem_9roap7O4-Z7v&+*jU`1quEgu@rS&$&y4O#b}u9JTB6!-!JCVZd1KRy z8q2v_FiXGC8>cbquVb`^?N(M@^UCuy$%+B6A;rFmbDc4g5-^*#8 zw;+)FK0)BB`RY#=#9}aKqMQt#?k+~~`De<)S{D$Q>r@isyJwcv1=f3fh9exNxwUEP zN8dojPLHkzfSS;^8T0s$hJ+lK7i1Ha!r?2i4h1IS;Acm#igLP(yE4TTV^En8ZYn$L zQ#p5`d$FGPr(s!n5M5oEil77M+(AVF`}L%(Th&Y3e&jbx+l7CzcjX#ZVC%5uoEj{n%|96>UZuVddtzz`dd@fH20jz$4Ce}muh=R8zhVFAJ%vwEpbvcbGp_bX}xt(pXYpJDa8=$~zv28gtX^eIP0hcQUC49)1O5B+ zv_s9!&b-j`KtXw!;BZ3b2W{ezi6#)2QW#ELu{$A*Jw4?IOFS7t`-^{HM?XQg!pC`N zKyMIZEOY-tcJl`<_=Id;VMq$ffTA~G8AD)CvTZK zi~j>8$Np{Fd`6;80`c3GEaGSGSt5Bf4hwX78XaV?;t#7+w3u+|9;bjzS-niq(Kg_c{Vz6Og1}OVT4yzDE6qv^^ZBPE|%_`l>(&suWRUp@x>J1 z;Jszq`b6&Z?ck=T9nR=`iAQ9JT^J`nn0r7MNUMF*R)yi==L0Mf1gfL9Kh;A`*^|=Y z-BgU7_}-{?E0hqZ$iyTXwg!AYF94n7S~+xO4-XiiD5ArSbm4&M!3hJzm;};X`C88{QD&Q_D079 z6u73pGZTtGb~E4Q0s_j-pyXqB(~^<5&K04CbFV>lj#v-SlHo9V6|)zVIY|to(q$Fl zk3pC!hz}>E`0WYPmZx8HQrmPgu7n}7oJv3Bd{Y$gtb5oBTe)t{`T2>(?M`BtkAW5B z&1_V5jOzC%zd)bRUJEq z8%y?x)#w5Z`yADB+i)`QpOke?Q$75M%u7~{c|7-LP4M)uE(ctu>JBbLkrj-cy{iPiZFN3i@Mp`&PcN{<6+>(07SY? zCu?D)vk8A?MWwjw>!ndh2&MU;A+Nq+$h;6|`ew2K$f{*vQ2DX83P3q|Y4%11G{l1| zMfx&xUT8J#SN7PG2m|KTpOmUrRT{QmOrt_!Tz6z7+u-?lBKE^E3|$&B}kDAQ6-E0u2AG4VUH8DxNG)H5C@9FK4cr2s;+m=f_I9IIf4D(F44N!MWGCS70i{lcA4Qz02+bkFVO+hcSMKbYV~_=98*OvWs?5rVn1;vP2D_+Gqp#?Ht) z%Y7g8KDRw&GXr2_4K=dz`~=*HA8M%3{OVC@;`cWPjxK|Xa$FGxL<094y|-h7@5f5< zICmSGyb(J!)q&@YX>PCKSX7hnD(=zzn@?xS$ZrXW<>HT@0AYN<;0w_)-<|&ZNI>gx zopaZup>yr!ra`i8p|XgvAV@G#Ijrl|360jw+N2j(+^$tNv-&-=^B3p|eXHBBP&S63 zMV{Y&_QP3YFWItfp9bw>y*8yu8&Tu%etDeptqe5_=>bfGUAwc@a?^;@ikfKOJ`IT) z0m&U9EGvJa=*}yWbLn8(Lq8Gk>ea8FER#mp)DYG?XB|vXS*Kzdb_-IAY4#T;(CwBE zt%-9yez2RYbdakUDud?V0v+kc=XYMNV>qDC%k?<);{W7|?0QA*or}Q}w}Xe@?&rW< z6deRSjk2S~n;mEyJGvp!QB%v487+po)N?8vSw`a6r!-Wg{|eM|!sP;Fv(ioF=n4p% zPd=0eC@G+yhu3FG7+zdxETe%_v{-8cmsgFVPxO0hMTr-If??m9+rVO0k3kIhyeIfV1 zm`gaTj@ut*rovEnK79ugA1f-{ZRe@m#PzkE<4d+4L@y_uHU*0ygM&b(8rX585K17; zGk#ZJvyRBBQG%x7GYS<^ZpDz=Rin8!S-cDVbLW|nX zqs4r5T26G$7mTrSEB#-mde?XbUr&Txl6lp~T|b;g5~qB$>Dc@M z_^7y5;PNN;G2hE&0+mSLK{~2RIbY3w4iRG0LMs2QFdI%Z=ARmrAc3|d{iHfuqZ@rr z@P{WPLC16tWA0DQwP0o-+|49-sDz9DXCjOHKDO@pA*s8*ru6Ns5@4PzL_qnA=lJRH zdtQ>>_ElcE?|88r<5*f~Y7By{Tqhz+SB^w)z#k zNq|<`&5g#ry2_0O+sB!IpDLvfiIdUC+U%RM^Q9OKw;1G1(A&U;MJmsA1W zB2f(B#y>Tt@sayAEVBXZjpCH&_gHcRn0|f(y^GF$fu*|ZWWYJ4 zfStF~D?PwF3Df|s-AtEgF$!D7J@uKiBwaX2z3p{Sv>70{RKRqYU%kGwl`5f@){Mt} zDVh>}(PLKoX5>5o!dbi*|F8x>6xasNMg0Eb_aaiRd(%o&JUuA&(yAX@BFS#38~|;X z0MPzlhcuEvVWWd4N7}){{XYOPs1Kq5ypoZoU@32HN6EOcHgJ6B73Fl`O#qkp$?)KU z)Q8-2K!(5hBcL065nYru60Iw$J`_QHtkfP%dXpc32MGTE(LvlXNg&rtj#tZ^HeQV^ z?%RE2n#Zmn9l28wg~z{Q7P@5Ma36`a)BVxlt)4u29WTM}xgx+iRE?RJOgv{Q3v@v9 zwv$=|)MggKc}cV!!2Jpb$SZ*bUdqp;gm5}2Gyz0UJ^@+ZJ7j^~1Mj#)#v&Wn`Gv4? zW5FzOyFa^BUu`=X;NWXp#J21@ilt;)?D+(^@6YX&2&S}gkdRNf1BQp{WW|ni&N5oT zNjkmpTx_T)6qLPT!r`s{&1a}Vl^E3Ak)K;}uLM`29NY5Qcv5gHL;&s!$=D6L#PC`S z;Q5*d^x(O|DIw3uYgXbB&H}I`Eo;_F{K6{&LG(R3wiUjd^v5@dVA<}V@CNe-9~Q8K z=G+jJG#kGi#(a>-xn$v*Wh!$=*cgQ!zaHcI=!_FEPcYa#()fLjObsQf!93Dl>%^#1 zD15Tn;7WhsBha+{>aD;z$w&R6Im_CPlPerKrd9$kwGcTd)}fOG>b9zCv|Lr-TkmMq z7lX*4!Md$)J3{p@MW{_aU&8$Y$o%Y?tx2w1xGykg3U&f(cG!k|P)y(NFT7fo@Ti!T z#4y-$&ct7QQ5}9O^hvgci%ry!IcUviHSGLl$hoJ`(cQRhxR;El=*myvAkkRzf;nJc zK`r@iPhP*84! zCj34&P7(J9jxeJ=bSDGC!8I!z4LIhlI7PrVfv^nN9cFy-l9Ge;s~0Y+FEGX>cBz+R{R%Sf03wte4?EqiM6iMtJcYXIjZIBkTg0|J4;Q0BXEWj1uhBmIcgQ8xZ z;i)naBLvMCMqeO5_|#2pPsLd2dPb0F6}!v`Pv|L+z>XmZJM}I_w9Y?40ihtFR~*$zfm13| zqfw)t88yv9hJyNB+}8gKc^)uBLV^m~!2z;f81wKO>`HfLmZwx%^w^yF*e;*XuVX2e zny7*C6-JHUP2^JoD7`6;(jq8&eC)UdLO`t7M#$sIYd*@WayRCS`dxR!^(|3X4RdoG z27raQYzg()F(THrZ@jC;o=ky(#1J!c>=<_Y?aM+h^)+ey()C+`0&pwGM-Aas??w!D zaWk^~S((|jRTHY)4>)`jsj>~si~fhmJUccEzL@A%VNxS%2O3bqm+Sy^MSPw%q32wu zQK`L(DUY(sU)_MyNb~os#kSqRQvXfqg_CiwAIh;m{h8q1uh3%F$hYid@d?tYNNOs1 zt~p!7iw*VCuO9frW>_kJLC*l8x_ZA)*NgV6Q}^ys#KaVaQw~`i0Z_Q3o(X(p$b7}7 zoCHpy)WE|Qs)~;Q%1wu|mzh>R(l_WisRMC|{j+4K!$03KQ3ns-7&6`ztgLliQ?5wG zI_7c35Je}#zC3VW-uiJ0h#bs%?3$I^Rt5hJPlhiq9UtXylYq0uYEl*t4~Gwr7Z0V! z&OXYLn!<2f`KO3Jz7c!LxO~3k>PB!o^xMBIQ1nb8SbcIL9(B zWn$F^>OT}jg9)7x4jjr&8COc(_m=mco_Z2itR30aI)3pBI;tzB$$!nY9F0dIboxl- z;!0Ll9TPL$ibs_YCaYYZV424Lm1Jk(x$(X2(*42=rk5-}ON$oJLpNL_Si%_X?R`{g zDN4n%>$xGA29m@7f!$LlPN5qXe6AbQi%HUxDU~wsw){l5osw^LAU7?^d752F2Z}Yb ztHHPJzx4E#L8#jq_bVvw%;#q&x4#62GV)^e43+pE!Jq+zu?7y8jw_)*mmFnvt~eP- z;C;{l9)E^(zPYy?4%3}d*D#p1m9oJnM{Idr@}HMZ;j9AKKCe`);NHC(`SQqsUDC&N z)$d&sNshf3|50_tuf%u4du3C^7lu>FJla{QY2YE*Pc}FuTf0+U7IEV>aGZKul)Rs! zILS$HCb#>NrAjR+I4D?~xTEBgcJ+S08_D$T!hL~L5noUo6UQztGZc${mUq40K(y>E z#@kt@d%;GXhC%wPvn@B|9TlR{E{OBZwLMT=7yxe)>kq` z>pPbL^BTr@(_g!Aqujx4@ibwC=&OI?d2!mcP{Zzf6n-Ov$2< zr3rfm-tXRswS;i*t=DYJ*=v-d<*zBSiP}4ceQfv9O>~9CEIMR^4!sYwH); zpXsdsi&akSV-JHEQpRR&djdcHbrFL0vo6A*v$tArjw_97WT3NxApgpUy^n(`X2a?y zrO`1)vfTDREyeEq&)gXh;Hu-uz?%IJZ5uv=`cwZRD^FK0ejSoHy!xa#BO;y17yvS1 zto!>|beD72)7Ja4E;Sv4C2VQ9YvTvsJh(M!{*}(&nePPTQi<#0?=FU%Rt0UU&+weg zvf5rc`m5`)6w8qKz{{?T6y;MJPOmElXCtO0n+<#;s$Z{&!>LscQOLg8Qyg1nX@Y@q z4-+BJ|0F0F2)TR{Vk$($pE(x?Xh~1ZLTF`C1fcd=4BNe?=Ml zI!c%W9~*Y6sJ9mV&nzp28`bb%Z~RgbxGTsDtAsc7={gfoqrXS|#aUA;I#|DR+wsww z5p5iXe3?+ob0`B55fS-Eq=IIn$EWeI<=gl`N5-f&Tii#~*e{IwVALU4k{#+9^f2G(_S!LlMy+Hciyf5ezMUs*&||M2@K@Z^(D6dg2L#q&*mC$bA$0?)01}ZV$+o8@u34vwlZBmm)5_l&P)U{1xo$fSg6atag!t4 zj^yxkKGm3+P*Na|X|P>)&>jIOrZF_<1Jd?7FYWF$vgo$ead4K{H<1xq&!5md_v?dK zPjTm-7UVEL^;DZGvJtqsh<@7*Ckfl>j6b?H7Fi2+O)@2;#NB_IAf zv3|NnJHO|Z0QQr zCyF5Vq4GuVocM$CXmk@wUY~T5Bw4vNY|744p@lY98=!_RE-6WU$jc_>7TKFO*MFCO zopg@QlsPkLj^{C<-b+F@Si*I=l$xLZiS7YJ7`%CrTn?S1s6TJRL#Wls!{JFIU6)(& zAc&OVBEWmNUzlk=;HH3~* zV}9c%LBl0w-nn$A>&5D|#b5%tL`usR(jXX(lm_+(g)e`Lwqskw=1S^47V3@YU2yDZ zW*ON^X{)#O5LJ1VQdjNM|J&W2GlYla=Z&z5_0jeNOCx8MMd#JM%G)|Gkd~gl8p3V; zLIcL^5gy`paVdtbr@H&9rh2zU{U>KR#Rio+^Ab0?)0y-vO^NMMF{`k%Pd{)68KMvnQMpDVWHx^N8NHR^ z1Rtc-Z4vx2^a9TQEcmE=1dX&ZAb44P{dFap#RUE3a;jt5)^pY)4+~jo6av!8NKXxC zvG9}j;pd?_l9Zq4B%nu{c19`HR?Y`X0`hyi3ofItyn1vww=*O?(0E&aG2gd<0w=Rb zrQ9F3#;os)%`~|1BMNh+_9H(bBq{ji6tkW^!Fdn0V7-Y?lmu!#_83tb?J2Hw}&{Vayd^Cj8oK;OcR*3cSk?z2R_>B3;R!x6rTYGLn~#)Hy(m@Z%pLgQ{*)Rn)<)U%tmlWTbET8J)Ma-n2xj zu+gWQhq6&JE%57*d%Gq=UqGn?T`3qTrx?+%Li|#&5e(u;I`3Uq31u&QX42@w;dizf zlt?ofmOgocqwF`+L*FM_)mFppA^aM&E>TYjBqaUSVW|~{+31KC62B2hr9=k(rzG*; z!G(`uf=B{!lUkTs23JXL4#o>19^MTC#fO^M=?V}F=e>Bdmypx<$8!ruKayH+;LiC~ z5~d!N01*yV8}1MKwOZQ@Akhv547LND1Z#4~VVIcASEA!C4>04b_GDa<>N-Xlb6-OI zOyz0opsdAXGpFmP-|B1N=??YH_0FtSS6#`oT?n;7FPVJmm|KAJ?K*bI*5612iXX7 zz^(MPh#VH8U)Ep#zm+XqM^@YiHk2Es>(yAGwWmJ^7>RY1f`Qpsz6fi2AJ%PgDoMwi zHR~8BU?qY%2fPzOaa$iv9TGNIxad3lpt7qljzYOoDuu3}&GhvzA_t^FCt?R_F;&Op zA3lXX7ib5(ovFR0A9@7(F{amOP! z+)JXn-UkuD76T}vO4p(6_J4i|e1U_8hOYUNXjrvR{sFzpT%CL1?d&j&)n`>=Zeu8u zJg3_7$ivq+pL9)&{`(8OiBqSKr?dg$Q`6OftcOL10&jy=|1#ohg2i(O;SJZcLQ~{y zyMchvnjX*F;F1~y7kvbCd6I{@r9u}^-nJawG;w@r_A{d7O(3lH0I$~fHrK(pQ;bAF zB}vo+_zR0wgrU?288&?Ap_?;Sd_24pZoH*j?=b0mFUQCV2Ck{_cBbiV#z>zAt2tkR z7sxOU2t5&zqw)19X3loVI{Fop8xth-`E4-m-w$1;vnPVJN-Ns-Z8l56XHu=2efsEr zI`#wCOQw{>zX)?`N>}|%N&HXTP=#$@d>zoHv4fX>pXWRp6~-l48}`SW0sHZ^{*@5# zWvTWHX56wDd=5ofei|y(?PZsei&`YTkGdk~w9)GyH1dtJs^5Ah6zxxe3TP)NymYlb zP@HES?3`or(X3FjoG5nQCX>yN4Bgu57@7P^jppz#*%8!T=2X95mE&71QQwHwcno1y zoXC-*q!~G5iBD`2h-$$_!}MKzpz|v9EW96gcFxpnZcE`EzTW|;`3xHZrB5oF9Q>k} zU{ay>-zxs&yd#!y$L(jG*!ZW=fZP%cUz_QVu%+WaEXzc@b=J2j*cLoj<_ei?*Xrg% z{#oyp9K$d~(z)-gKe$-uti#r`(iGpDlhR4OzzG}_pFFlHq#u<&KM|*?F;hNHZG#1l zWKRMmZ~u*c=+DUsQ?mzH{;vE^#JZZu zSkM3EcTQ;NP|`zAVVB1B>`xIFobCEsogV8E=Y^fonRH zi1e1Xgiz zQ&jfUCV3%H59IU;Y3pCdf7xw(UK3f{ZQeXEsLArUSv*LS9Qb;l%kdAhj4^jFAGZ{8 z2YnJKmB@35kYQA$KR$sQ9)h~kcB}8F|40&`T3B)hrxf!bH@l4z*(vNigeXiI{)T0C zGJmB79k|w=92c=@NpoOx?YV~-vGx!{zfB_KRrjSS^fyx(Ty|yjYBD3Dawlad%S^qt zIH;kG`psNWHPDW$_kuBnqlV%0=j15yniMhz%V;6Wz54rEYvysPf>Zy4tkc1WRe|NT zY8W{y2&XV1W`igj=rbyz+NWLg>@8*gz&OacSp`YhlhCMzFXnpB&p)38pq zhz@NmS87L2bmm0m0@pSxwM9Lc6DMD+Dzt`xI-3*u{(KR@3HfrM<*BAmm~@cXY?v^4 z?Zd8zy6L_l^p2{cQgdQF{*gMOe9)9Lik_Hr+a&9OCxvH;IhLnOPlly-(UxxY!N0tD4RUB-A*5|!wDH%S6^J*QM*3v=NBDCG8!S+a?b zm5SO3d>%^0j=OjUqbaG5%~1_Mek^HGfo=3bOaxeh1lNJn>1vvdVxOL&X=)_{Mi*S8 zm^ZQebxJ^_pp`ZKT2Y%)C6)>Q4`+yz+^RI^xZ8DkqaTqtz3sHhUx0IZa`IcqVc%ws z2vFNUG<;mUkD+&SF^H-q`Gg65UK#WOKW=*9_fb6M;y_sQFic=kq_-UY!t1F&p0-(Y z!xQzVPAtOm2#=-?J;a!uD04KBPAE#DeHno^id`&eQzTc?4UBzIpq{UE`}~<7!ULxc z%;g$Lg+ufw-%4*@c>m!wL^xOkQ}lq!!6T#|?0kd64J_$=J#`OxqjE4lZRypH9hSsJITBo5d*c18o z&?{nKX7&C26JrLAo7gtFf901V33a-dC1HYuElLVce!?Rpol@@y5VBBFI(ud}t%&Tr zuCEplL->yNIwQ?)?}IG=OwXaWmQ5SVltzYNfs^AaxGH#|rFplw3rM_i_L4Nz?F2p+ z90l%cdn1#xq*g}pKA~w436#9ydF0%K}?_<@6LyBUOd4i_VX4?;j?=@TV9Qnu@Ej)9RT&CjLKunB)j^sA&JU*-hk^Lyk6OanIrzOiyk7xJ!FHHZj zd#1W4KBMQQ*mS7D!dwuIGy63b=q6Xp9oX7F_xsjgP1z?f;5UeVfGmxO0sM1;Ydfgd z60G2i??D@8T&q7aJiuzq5o3T=gGLZOGUwCV1Mrr$%JOZ&4=!meOMmp*4)$Ss@^@Bthkg&sDca11b-fj5^>C6nX znhVnW^GBNRu8c1&Vk$R$qKd(a8zxdnkIw4gn!ex4o6~lAJGXuaP(oTLeRVUvJhy~grvFbG=X$2+@tOg%xT@fihoO(#OgPW z^2K{C~uQR>bV4BpT--NkzFZf_#kuo=3yXk{aTwD>= z3emsDO$iz7B~&8hBh=qZ7b*8q4Z8j7@i=yQ0JYlVTmvAJcUJ#XPpiUZyo~kN2?ZcF z^H#}k+zcQUIVyDIJX$2vdMcR@pVUq(U_VXZ3A+kdvY@;CnV(Tm#KU$wC==bTdxR-}lRcB*PULVlos1=;``JGtmB8~joNV*84p~~~ zj$lePxGYvr^tN@oGZ_%PaJvjG^e?bxR8+@et5-+%-0dP*OESU59${5hA1kw@rj~=| z79)GWDGdP_2C$<2y?&U_9+diSO)RI-cyY{cAg?ZBRAK^OctkE9E1CNMh^gdmcU1hv zVVZ{P%@v%RBmQ^Ze9uY{e)#m!e;_@CvQ+_jkTGSyweqFsx!u0jI~^)bJb+(YD-gw@pikJ&|#i?oO9&$uTq#GH&w2 zZeHms#9>108qnT=U6ED>bf(*p^A>;LPcdutS6GXtImr;N=A)ga%{kZV)?;ygzQvaw zg|N81Fz-ghF%DI#k5Sv*ME>IjvF=#}NIB=hl07+g_=2y7#|6h@zl{)(f4rEP=?z zYyk#-t|!_DyP{4f__?-!wgqS73lphXGXi30s7!aJu0 zbxjY$cV|^z1e|GZ@I#80qb^ZjCd23yan9Dwl#KCnl+_Ilm<^hSG?1Mtvf$(ck8(YD zc$huF7pNS9egET$VQapc2`nQH{SG28HFPWLt5oS)0$zFqwg89{l?fB3omJZXwY#7! zs%(@wPr;f#&Pwl}(;ObxT>9UT@rC2G?-ug6mP21-lvKR?ABD9sSHI59CZT;T=KZQa z1H9)bR#)62YMX`(jGW3(636byjNAA6rON-YCEgeRjnWf4sE=kh~gXZZutg zs{A^@iWQ$RxYvDH;HhK+q(ox zy{b?htABjqm1nm<+oWF1f4Qn&ywy_-z7cX5%bunFkW+^AL2n3Kzg$4=Lync@$%-aG{$RYAV`P=y0n7zaTO5}Q4Xt871V857x>n7cYOyZOeH1c6xl>}(*^%ciG`mGEu z$eC4<$pKkv1;ojC%6#BtR%h4VKh<%H^*%#2ecvKE(e6SwPIoW1C(dkY{y{@B;az=@ zTzzoPw$Jk{D@$;)bU_l%)6EL*rG_%yYa4fwhab_r=p;3p@{#JO3RrHNx&PYr?{q9@ z0~=F>o8>I)9^*w&FA_>Pp`b(%;wA`MC4QvI%4?Q}%-LU5H)YFnBtC+_;_qwn+!vJZ?%MAV91$Z((l!=_67j%(GfKcKKYy z=x(oR{pO`xNmUg!9&+CC7wW|DogHH(B_&!$#weL7Nz9A8@e);V0>0-;u=UfPGXaCk zklu)84A=9rDvJCV78q2HIOz1FLOT`>6C=llE|k=zpMbWC8Pf(qD*DVz8}bSEso0J?N?kozrxMbnzd=Te75?F@rHd3bW-G;|*)VrTSnyEU1Sz5-|8UkC<8 zcR>~Qnj4rGBxVD)RiR}Lw8(1%5eMI?()AP(QAoxX^T$2cq$m>lA z(5GNS4C?&-d*mYkY$r9v$@C@cOdna_EjWcraoNgU+U}9pVMp)N%KbV{BJWd@g6hwl zu5B3MdZ{=inMC&XFj?6-)7F)W=j8WlO>$jS^poC&uDiv*sy~AIhw71LQ z?r?MvHy3N72C4LhD$`ObT?R>>r5KxZ)ZFtSsr*FJ&N1Z2s6r(P#101rCiw_S-kqxw$!F zf|@5ns~`gnQoPEdJ(TSnN629BzAa`JoDEnWJ%7O|=*QBi5W>C}7<2ny>-NE#G9xv{ z&S^zJUC6-@P$XK_++cT#oZ2%f;VGAL>c$;>5!(IL68p~dlZ~L{7mWvvqwJtRvDW*g zs=tL!o(skv^`cCpc?4c;(#OL*yf(B@VCUnde9*+mCla%?5UgkY z3XBLjSf)(}5F|X7hyH8{^gyE_!N3ua7VqRYmpZ&tU$yP0Tot7%h6nuJuu1~@iU?w` zRsv|8`SlV&YsJ5hVo}qWWg%(hayGTg#H>ADQ|q-E6UO1UAca7mmrIXklsmc=&O+;q zYniV(HsHw~RyU65uU#VQMp)uy#o<>FfgrnOEeGZ|zNQRadp*gUXZVPwnT8|26)W6r z-L8*~rGjUznqt&!_E)SOb$ZZ&F&Er(b%*Kmn$M4pvnOi{XBu65W!^qMf~l#gDZNJD zsldI5)+mCax#&PhH4w?Xb?K&6_FTu9n2^w0qL?o&jehkwQB*BHu6zy+)SJYg=RySO zpg2lkP6D%G#~f_O6`?OB3NyM-GV4C|PurNL#||xkWdPXpfgpJeE0?C>>^8NJPVh)!NLh}u}StmuvYjkw9 za9zl^<>yqDKy>CGT{5gCA9ZL@QB8Pyft$;*an)o=90DqeP6fNtQ3>Mmba) z30`WKw(QDG`ZClt?g##x#kLVq z6=U5mpae5vL)zQ*hPzeQG!ViA z!c*Fl^{l1$Va!4|a=%7HuI9xt=vP1cG{`Et3c zmaB0v`~tx6M1bpxeQwV*2+cMfenUY<_ThT(yrT6pKG$-3j`DYFQ7=|pFY|p>kg~E( zG&n^*mVNg0@nLE#OP-5FWBjh-ZC#)G0nBm`0_<~z)LhPS5R|Uq)BItQjTV%!p{bzq z;1j5Bj|Vm8+_D=n>Q6v-JCEsLlnH<1e{KZ1ihxQ9yg4 zc#PYY&UB{-M&t1mP?!QA#eZ<;zKzu04-lRw;h%9+c4QCXwAu8&$WT80fnJo9-+s`KtR6cNzKCrW~4Np$e(9zMI-F!r!zXnp)Tuv55 zl$DhmHp?hC9e(4n86#+FYSv1YWm18>Y?JiQqYk2^7K=!?X?{Ex_Y5kLIsS7}6ajRj z>!MeVzmTRBV~iFX=D-N9b>260FA`hWgW+ib-KpXuPu^tawSBXwL7UMM&8~dIV+S?g zE0vS;^q7NuUpM9q2fvb?q2~%&Hd#W(D7tkmI0hs#^!RqQ#yg>}ZLp?MAH5+uC~t(X zqTm|+NNE=>Q3*Ei%0LyFY_K@%2v>Y>f8Lp&YrlkvVv3UPQ(EsRDd|VHcrbgxK(oc)KuWCd7k^WrJ|L zVk?&bVsFPCPATF#b(_@;8m^O$691Gc8v^wS72tLa*S5J;#(cq}oe_2wiyK&TudAH) z6G?X{Pi$D5R}`Dir@j7NgR4JO1*cJIqC&9-otYl&uKKo!MQ#3u_vldqIu$Dat0gM` zsijzlslI-Vygnu94uet6DiWN;ILd136|mRc^&0QH=^H#Baqz4z3L|#;{6@j59R&5M zPH8R`F=yO177>+Gj%uUy{K6g1JG84bS^mu%wN2fdLXZ$_`4|%0SB6M;Fo=H*)7iPjTrXGmI0Pw0p-IT@LMMg~z`eSPiX7p|@wj}9ORU0F>HXSw?3nWnyikiaAE^iI=hWlZ{booK|tGxbWT)q1cq| zj|&Bj7L=x>0hveN1?D0gW10cqs=NW8N=uJPZIF`y^~FaLA)!^gIX%pYB*vJxV`VRA zNcKYg(elH&vM`3ybEc~OmRrgfX_fP3!vupTd%R>6V3>^E7AzH9)jXltloss1Q3~Jv z5D>G+@E?J<;Zi@)hl-0v!^r^K7hoxPhhq5rr~S+V?*jHlaW}xXC^Rb6ICQLj1pdqG z1f{QnT1D&Mx(2REsrav|J31lKV>n;O6Zy<+ zjq>I{i?Q9aEbXnB%hQ$gsq;x@bH}bOj>7~FzgYL9t0x$qIN0cbhko5Xq1BxDM<9RB*DWYGqDg478pmy zp)}vSfd3p#vd#tOt}$vFp>XVQ z6Uk-k%wba(^xVYyzkU)wD{s(MdHIFwqIt9)#m<>tJM_KV-|V}X=#%_Oq`^CRLwpBa zWEyKlF zsq}Hvgv3NTTG~$(6#1)fZ>B1$g$)f20~^(+jWzYUkUN%CSz_HsPSr!@{5iZpHpFSj zc|%0b@l-C0ZzMtt!J_k{h3+#P=c%xsO1dtDX9PD{|l=ub+5_ANI8 zVR+2ZiC92aN_lg%4lO1o;zGi)1l2^q2tqNL>Nf9BmjhbTKN01eqd`_aSQ{o_ditOs4q^%{1!NE0p-ou+a29DfuiIL>dk_sU8zf7(eKS4T z#IQa-vYMhfXqDW(n@g&Aj;R|T1vHh_GFVk%3C5c1LF=;y96lV3W6tHV(Y zrjhKo9u~a&%THvSj*ixtsWoMx!i1CM28WxO3nDBdPeWfB1vC56(&_Qbp?;PJqE8z_u=STfz*Yo%0y19JQBAD;4xn)qc@a0y@ zT#Kt(C$YOlnhk)Q0<75TmR#;eXjHB@Jv%XiDFr^z(GlSy)mZlP?`rY_%RP3$T2aZ6 zu_iSgUHAuRxVZTE>D^;JAYTyp4t@LA1C>uSl$0#?8^S=7X-kqdk6g4KoaL%r3Q3u^ z=#Unb)V2OkR3~Wb)5r&0V$@h~ihyFK-HYMx)7svD#PNQ0-NV=ZCR)=);w$Ta;Hn41 zI8!;3AMlyUoH$DEbzfL(XcKj`FaD?hWM3OCy`}opxFGn(jJ3}v@6}950@CT(lGZ=6 zv!nEUpu`b?xY9RQpFMO~p&mtA4v>~AFxuG;#8G0raDTSCb5Tfx_U`a5mE(>##sX;D z&BWll7wHLM`@4fHYtElb*q4M3QF4u#EcKY>; zw6wGvjq|$OcguGBmqVmOr#K=(RkR#T=|dB~I?_Fy_gh43mvT`|zAUehCy-ouSMgih z)rV_azC8%=Z49~_PMHBza~K#d)O+sE zD;f<_f_pdZS@gR>4h{}qPA}~w=beH0!i7$6&!{%vYc5Ae$C1g&A(^3@H_0NOV6c|1UWgTWkcvqW}$W+;wtw?CxCX#AEd9wRdcpnB!Z;=M%QA+k=BIRUAf2(SXp> zDfjv42Kf$-*QH>cHEzf1q+b z%nWWhYtsI(=3bX7e4}dvVaXE*iNN#b@TP#0-P7;;vYzqX=2&=TGPEue9U{e)Cb#=7 z$Ju88=3*1mxGSh~U+N7Lg4czYaX$8U{LH;!VPRRAXZ;AUdtFznoE37K=wasnIE8t} z{y95=MPn1H!Z-A8Ug3>wzb)qt9je1>$s$nH@_xxuwrLz9fDec(nSnkm7hz~EQqpb+ zj@IiM1XjMF%2+#;JD$2e^~d%gtMP@t;NwmrPI%YQ_GWG<$rcc*Oa7%xm?(Waaa3(IW{?qz3 z@I1z7YVZ8AI?s%wT)x8}RaI(U77sc=q>hZP9eRs@sST|?+NN3HKbLEF{+UT#;)xB$ zRE7>Qj=%^KtJ~XI=K@MX^-~Au(@BR;%@^Na9x4xe~I!I`5iNaDh29net z8tgCDi3|_@ijHB#L;`XZVwV>=94>Zwf0EfG+hAH@+LY8JLymR7r=w(( z1WH!NJ=rzyd!4tZ`7Sosv+qzJe)Lth5|%t&xbqVxCtMeIC9<65e2ZO9n;}(JqL(qM z;Lx0RPEtTe`+ao~=@HnB2Wsm(It~b2b?kf7t5+Nhwlf*Fx zFW0vTgi^zmC3uG1I=Us-Zqtx;*`UB)CPe?Z7Ha8D-{zqYubbD-y*o?^IplYof?cXb zq;`DN;FC5Q^V&6TvMyXbG0yN**i-(I9QOCATz~et(aI0pT7!wvcYb$k!lxMM-&xxF z)o+ebg$5oKedaUL-+uS$1KjY^rG=&-fD2A>&Pf{rF3^+u|1EO;z+WZ*zZ^gHgRXrX zH`W;$X5-^1byg^X3n5J2kRJUCGCghtI4DI5Xev!=R?fn{eGSHc$v7d>9IB5adjk+H z1dsHE$L->Zr-Lq))9(z_bWV39GMIF)p%;I#Vt@Rtl`>*Wl?Hk-9JN?`0R?&@wG#{e0;ByxoL40SLVYB7_CA zV+RKZlh>&UzyjRXXiB39@~BIl?M$r7H(zHq z*L?#)r3ly!QC46lS0Z(SQLf@K>-B9!s;CG(C|Z1tjxs{(>< zRF)L4?z!bkpxd+TT!(NtOL!5HFE zAmAC0^?`u}GO1vOX1!^x)1gvCWTc{Mo z4FmOf*ZF-o%Y}UE)DvUUiWD_97ai6l3E=nk_FOy-Wn2%fpR-yWl5)BxRn+>5vwM!@ zv-*{labCN#v{+qED@IsNU3Gd%15SUk`2JTx2W9u^pDYug5Xt#%4@#zAo|af^r;c=-*v7Y*e1rqNzJ2v<`Z5gA8Rw5#FcC_V^r9Y)Z2_F3U z4^3T52=1F8oSbUO=f!U$A*8hH?MV?f3>W{@F>bhW=|!eER5*huN6gCts>&<(GhhIR zGfZRv2VN#cz*`WimM|qETv1<0LW<+OwjOw(=_inR0NBQ||7TGahnr3-q8PzEz>oxe z`83UjEu2iLLg^$&0hl35Eh8gxif2_+r5ZsRHe7$}>%Vx#ShE?_4zAH!l##Pt!kxZL z;@k7pwR?%hC7)L@R_&G%lcR_nm|L>?q0!9lJJf40mhE^B3ldu(Z_m8jQS3ax!t(CK zi9I|WS8Goo%FR3+ z&gTXPq?~?xKQpIXU;8^jdh956e9jg7qH3eE+VC*_{?6lKJ*p@iC&1M8UH-Su#LK_K z$viD)pw96Wd-9t`&Tv8|-0Q@AT`MCPTW2)B$!|Syt2#9FT94(ExX}6ciT=&z-BDrs z8*=t3WkKkvb}#kxQ@}~UY|MS?FLLA$xHi$Ke^pRA*HLFe!cH4iLDIsKhLZ_|;{`nV zUS3W*#Z_+Flg(yRWOXC5T9EjHThfu1ZDz`k7kMzR0E|U=6rdFm0&GPJ-Cs|$#(`dT z_D{TxvS+g|Xfo(Gp=FxlZx&ahCias9}1-hSP(8A1rGA*ZTpWk;`1 zSN8K+8(RAKAaJ80)_mp4uEUSqENUZmZj`C-}a5T@Em6 z)cE1*y7>qHhpVKnjvL>iP`cC@^BYZX@W*_Y@L7u3-QFse z{p`u2c(;x`?WeNh)-v+oYEo)|2N)0T$&j&@`vo9MvF&Umju5d4{^RY8ol*%V7V-j& zK~wQ(DH2&y}Nn?1nHbl z8loFFOzCk+DK-mycN5A5T~QVWL8jK)!W`vM?ZXOUtc)+IEE{C1Kij9X}gf68NE!uH5(D+J5hTR{t^1)Ol+?eU#8TOLW3c$=z>vn1Qjqj9Wf@d39*KISH_U9i!9peGY z-g}kf0r(0paQUcatsyM;suLFQ;X21Zj=wYxXg@wpGw?oU+W}+>Ij}=)p7SW8x%O=o zck%ZQJK%G`x^mZ&j2Nt_4%^8o3!Cg;SCJBXWCnn9_fc&2C*Kz*>bE_It0DRG8F9br z+7PPh)q&Q}Su`6K2QP>Q{c%{l{93Bt3H_66%TIG@k+89`0i>FknAiv0w&|N7?zd`t zt}s^^0_A8j4T!)V=BN(G3%t!THgrqRrxj)9%>q5jHr2^%!P=ohpu+&Pbp>n?9V9n zO=f@jEbK+4HZzP42Ke2Vvq_sW+pOMlaUR)`v@gU3hAQylMsaJQfvN6UVRD zjC;Yq&Q^N}-beQz-c@A7v01u`w>S89IXT)~^s+p0*lkaW({seSk^U4q<(h>rUDI;X zGBE;@;JmrtBRQ#M#8wY`wdExYhZ?As&C4U>##VxD3^?A=kSXv-@mKze8>)kSg)-D7%nTh8FTaO%yf;AG+MDc-vAUNGq+ql$h2AO{?EBb=N?+EQ&xV$a6#;^P`C5E;u@B4%mO5#C#czCF| z7sr(z4Xhoys;c|ZVKLyoE4$C`5!B}?W&cKXD-Y?0Z0G@wrk$d6+pWsxwXGg_(t?VL zD4?}SHoCzCitLT0_NAI-jQ|(q_5hKWAGi5+wY2^SK_2Dl>FKqS!3ja^y!h2^;eXBE zqWA8@6@Mo5*I-b%Uly9tt+s|kIv)4|qGGe$RO|ih(ynVa%SAq!IuMD=?s;dO<$3ly z8N=Fj&7Gz%1Z5le26?Tm1n8>Ad2`pAFGpReKOV&fo%~1?nl7a}JvYw*BOtRD(lYA! zel{cczz`Mu^_KtWUU&WyFs|IT-P5hNM}-&bj`Syf?mIdWFI$ypcgAjay$)Bg)?UnY zkc3J|{4OhG&VdWLGZ&5M-Bgy8sC3-lK6e;+9BOmcnPeH{(1Fnq#2BL}HQh;-{yJvs3cmG~jcz8d? zhozvmIbA~j4L>mt<(Ul=7{Z7*t`ei~`HsW6uOhZM>G!DB5)%XIF^p};lcYbPQOnIq z!^G$Mxt6!;L`)6mn@M9|RC1Y1EG;z)f}jBP?RPxrAr&}@ur>P3oL5?jpRakgBA+J! zSMcBGC@MaqBue0*LdyM4Ba!d+sAS#JJC^?65e#6gSTvC~GONjc`CEWi2Ek4l>nuc) zKRS9=Rb5$IS47SoRt^Q_f!`KNmlSA%L89cWITo91K4fNzCRks-0QtoKwN;b@DE_cA z97YG1`*u@Nsj5I+QZ{3FY(}1fqS#7FLfz0XgHRQG6G;flqcYrmh8Rp9xwG~}Qi6fL zc9FjR67uzPN76_ljjP6My;ddv%SE4xirVGd^LZ}p%SWL%S8MmcAK$V>t`yC-AqNtB z6PcrC+1`WUG+Dy{FI)d>Mw$vU<(!E^$Yp;yfa`cWr|t;oXbHgKsH+8vSpMD!Y=%5R zZ2)q?cJcjs$3%YftA9n66coB=Dwh_&gJR&>ruR zzaMZzA4hcannxrwDvw{+%kF1Cyg(P=a}wj@dw~14W#!~hK9w;L?sv+Z9R%WY9=m!% zHcx2G%X*9Kbobl#?FMy}3An?OO%UrBgH|ocD44>NC~qVy5)|y(M` zsCitgiNpKn#ddcfupl8leVGOf0$g;Aql#VC@dvHnx>s*;!a58tdT3Z*F4z}X_IDTH z10zz9a*pkv3h>~)h}14=FkM^rTkHP_br&g3f6F-}@3CK~sS>EhC$4E?SIn)X2es~f zp2Da5W?+gJ4-%eV?AWsMLYV_@^0>2ohSQOpSBX%YG+RrYSL$F;tCG*txw*cFDaVf? zfj*WAiWT1RagGLdx5o3~46?upho}!4bv<_CKmTfHf<{D7A57)QGX9)Q~Y(K0Q)e8RiWmE{Y88ck^1@}XH<PnyW_;USzooBU$_hLIP zqr9DRG0=>s^-Gvd8Urt^#u#m~^@#{g$kBiD)~>#&Z9h{(qqgI~6e;!g zC=mBcb}n0{2>f~iWVppnZ$3b$92;A!zHsJ(Q&1LlJEH&cy?8!mzano<>}!1-sc%EGml>6pV!OBuWRiz zE*Vr|jz@lPy<^qI$dfqxml-HI;(8RoQzg~?9c}V|EL~+(6->9L1VIp`JEXh2LAp!n z?(T+zgh-ckDIh5+-60^-(wzd*-JElW_q#v%!CEdP_RQ@4)IJsl`k(~mp2@-7^p8|D z$w9rei982zZZaw~G|mK8_9z7?K%<84he`#1@5ZGf0*?u%zNBZUd?D@fu zu?A{3qTk*%H`9v((2w|=#1Vvb)hF|vHGuzJNO{PzKuw#JQL$oNt(qH#ICp~l`L9Ux zZS7%W9fsYBDDlrsPNP`R8Gm6#hD^P!Y~A8xHf{`Mf`~=?8{MShme15HHH>@4F66TW zJmxW-Or^SZmBMBZ&5iE|C%-=)%zK5xC=uP74j0_hT~BnFw`aciiU-kRuAIp_xb}ax z`sN)pUAOrCVSAT>#G{Ly%onP;@?i0r%-JJ_?bY*Y%ON}iR(uuf>Ttr31}9Ev%bh5WeDHZbi3+;#`0xZPJXZl~E91!3gk7H>?< zOP@od_kWJDmfLXcDnNpysRF7sd~j}BO+V8QnRWO(v$JA~#yE+nrBD!e`o;qL(@zKY zi_su-v*))<05D}4TJdFfxWgY{dn;LIMS6I#TGv@|jFsO&kF zcJu{gJ#nb_WrQ(_q%CovOajlvQw36T!RWHe%z)4f0)P{Azh@CcxRb?Yj1rPi{*?U3 zgeT=Scy>7WMu3b^_+ng21Pz)+2$1b~vAgH^Mjo#fD8wiC$V+_oU27r!xREF$`9a-u z!WX|F{F@h(Pg)cvrf69*-!Sl|pI1R1qmz=7O2$+sx!+Y8jW|Q@_4oGo%|+jX7a4Sm zVbt;@E?w|#r~Sl-WQ&FJ$<;c{NC@zmnVIx}5M@-Q@?c+ptB*E_Jt zGoq-hvgTb(Hkvn@+m{0KNfl>*D`QuV1TF(nR2SQfot=G4B4I=lre-Wd9ub2G0QaC>N~Y#I|bF% z3F-whmv&6S({_hy&%nZEfQ$e1f54{cLRJ>Z1!Tv>r58GZwsO<{Y*RRvNkGuWX6A|> z&Gc)osaf0?Y0#Lr1gX#dp{i-1>??L-ligc$bbnW8uSp)8#D0Yu&NdruA?5w(Hq42> zN5dogF3Crv{JQQcVqegb7*gnPu7O5NG9h%&0T(vaX*FiW8+cBnzul>HFMmF*0H*?aWk>5Lit=)+P5XEoI$w2uo@P=SzK_{cN*J-nVNOd#ApEaKN!8>AXr($i9d`6n( z-sb2}NnXUu;qJpr_aSX-@V^C@h=b*gj7&@gO052f0>a1*Q~+j)u89?UErofD!TX+H zfLi_(rF|E~6J7rlzVS1!V-}=>w~E7IK^PfxR6+VYh5DoO`aPN^k2n7f-=L%lEGu$j z_4GM?wGYbDqd;5D+l!U!pUktCp-V=8rsjv)1NqYnI*CJ9Xn98i``8B$Sv+z;(9^L0 znczzEx=m__FF6=07BHcfs375qX42Hd>A_R5VDtrf)yGBAkJlilB}Jt_(hJp4Ry%}S z8@C4hy?tQY$?32uPw?sCW5d%6*h7E{7i!dPR^P7`#Y2PBIO_v39}4j{Y`{aW-Yjxw@V)ZFq9a45<*I7G#UpNb8&8(a@A}5M z^N|;J4T0g~YI)b?ag7Nl$HY`-;45cxqef7@+ZGJCabfLza1-}=mhH7*%Yj;0R>t^r zg#>i${yqjT;GnuJ=`>dGzZRAGe)5uee()!&6RSBmL^0cGjF1NxNTd+k-`o32izzkr zZ={?QS?W?R`2X(i{v&@^PFP;g^QvB#g`f1)zio&y+~B@z0?}u`J35_XQ%&jhG?Q~6 z?h8;zz@H^35(GTI{_n=(ZE1dwjch#@SM9Jt1S7ZJ()lZtr3VfSwVlsns_(PhxJoS@ zj@~*cPmQ6(k@gH7j|`C8U=YNKIw0^ZJ)(6dJpYq~MPhJuZ+tMMmO0qU_RHAk+SYf*&k z+(LeKaCP)DdTvCtg#BAR(h+?`s3CH!-&sCJYRhgG0!&|!4AYJSAo%Mo=B5_2N^u1?7rprVYgejk zIe+t~sdE!82n4!S&Js7PEoi-szhUQ=)q+;w_F!~X^M}9rb^I)#eJEZ6_UFs{>IKL2 zg?DAts5AuD{bxa8;Iulfw&QF51D3Yzz^nNfLaw6OtCXCSK5+PA`v|>GFrV4#H75Y{ zbLJp`$lm^DmI@YIid*#Un=9y*&wvEwr`EU=; z?KCQc!cHQ@5;BQcdZ#>d048w`FxNhZ>$4%Y0bEnz`Kh_CB204Xt>ev%-A)RxhZ}}y zqb8c8k&`v(d;s*%s=#G>X*yL%qh?(0?}4$^pI?((c^g~m@>#9s?a5gSTN+*m_tTQY znk?|>NQrZhO4t?$ALy!j7b8F9v=)617V;yvEoBf6+{w$}Rv{1z5_NuNZQvyq8ToQl zux2{c?C)Jp+cGWX`0zvDenHStT85;wwuo~;U5TZxgcGuWZt27RS3dJxF%J_lXuuH% z$DmMW=XI4gufC9#-^jf~S@b`)BgJ7}lmhEVvgAHSgv$hkXSOvT0=Oie;Wi-oAp zekud_*H7r1=dEk62qsjuRxITo5?_6QZ4k{=GzT z{kMyM)i*l+3YTBw>C|9}V^pj=Y}WsN9+P3YDLvjIX%Jn{Qq4f5=j;pyf5c7vEJ++N~kVXeL5RaES+y>u)6*9nyP89tlaP0 z9r@sE6xGf2$uT>_FZ(`?6_ln++zHg%kXyo(6m5>NA3DqiZi6(!fc4v|JxL)qwcAoq&fs?>es|krMH)p84LiFEtec zo|{%hv#~^6ToWgabT{?c&m&B2p+<1rR4%{2boc3|S?6N0f2T-@I&=Lp|O? zOC7C6+(p;0FOgE|oTl_}d4a7V5YY=fm5A7K7{TEl?8H4!O-ql|yLC07GqvU8uEhkX zP~ppk_bh10!9!z0sl=>8JV(3-yf2D)`%QDS_x(dA{~e6KU)xDY#1y&E`g0eL1^**o zA{756>CA_7F}3{e9L;y=)P1yw?>K9z=2FzK0ovu7mk@Qch8saeHD zCf#%{Ge0edZHMXy_2y2WZzw)yCCeoiHm`k-Tvk2dzIi(i`zbu9H&NR2^Ro%vVIf0| z&nEJnq3fI45y?LnHRzDPdNL4K;zw8@8G|f&Fbx;syw$%58cCY>9UYN& zK9PsSF-XA@RVKCRiJR%#x63uSJ&%zhP6iT$%04}0@_DqC}Za;>Yt@Eu9d>BAS z#RpkQr25Cz^nSJ;w~=1^CQ-X?XIQ%pkb3{DLzGMU32vgMwU8Grfx@%K5E0nz7^LZ- zcE|^SMz@>fFaoOUMo?maqjOz+1tj6W!BvA_&kSq~^FOSUb5|HEFpxu;?{^8`wOz|; zsckuz_30Z+gX8j8L}Pj0##b7>`BKbGj;u=T>b=_!P1KJ#9Y%GjFRaBze_sma&-m{r zADid4wpVADW$wO>88KUjkTb{Vd9BTLQ`7J_?HP}0uI^HHTkfnO%AVw9jap2lPq6T{ zf?$mEMrW^d>N>i@w;9HRwQub->}h=FGY@rGXvx|SRSRhw-(6gN)~az*yyqLd6(Ce6 zLxHo-^LhT`C8=tDdA((y(~kxv$xnrCWx4pPw|F%92sWG<`C1I^;nNE>B!-%jV!nTd za?^iralxYch=Qv20(;O8AEd&NP4H;Z!RABLrjMK;tNBoa(~O7j2?`<6$+E6+WJ2KA-`rUzxu z@AnVQ>BO7xz8P&7U9iNo92v*DKs+JRYC1FLK($p}c73kIyf9wm%Ts%0JxiY2G~Y zgx`q7e%aS@FdWT}VCyq;hrD>A*)l|u1kNGJymZ_YC*N)_u8i09+Uhpf?*+}Knlv z#jJ5Lk(y!qZXu=!dVvZT{8~15fq!*jdhkg-gHm|w9ChQcleYe{s&L9HhS2-I2m#-Fo!01{VOqt_dv?68gr2H@Uv}6 ztWn#(x90skC!$TG@W3_avPHSpSq6-mZO_|)xhj>2MqN@*6 z*Vf{wH#+z8KbRpq5=-@#X5Y1lb(^ODZuFr*tE8`B{~CvU_a~1D)AI&J_;T?rPp%hG zUThp$gNA6!;v1*18OhowN?!nh*Vt`VmLQ3PZpc~q>b*gnjr>F1DdMk@L9-n%^>&Sr zAl@_rEcm(-Ic|_AsxycR&rneh2qirGI4zG zsuok(MP607`s3Q3?#Uq0ja}gVSpey4iwYLJ=?5G-3)(0&N4XC~@oKmpe7h*F&_Y4e zg;)GuF>_8_1@1r7yBdylLzI>1cA5SdGqObN%L=erqE4kr6R(uGxyKH{)hYeXUdsQlf#x^!(o>ux}zi!;U+c zfP(4m$~guPjBgUDf78{qS}OYpd=6E$4t1oyFx*`#CjA7zmAgWXIVHdeb6JxRPVyOa z?z%Jg(ypD}J*f)>mp)G0SpqR-w&#qlQWkH;SSZX20FNb2jG;wEMNL-0VAg*E;DJ24 zupXv2i`uj=y)V6(6<}EGaE{f$atFtQ(`Bf67fnvy1jYuPiw!Z5t`%Aows94PNiGmp zLv`2q5U^XAnC<$&eU$zvgnS*Z5(Xm2RkC>)tcX8|e`@(ShZmJb-V)^wan~6>o7xI1 z`H9eVw86M^=~z}ZlBd4)R2Y?|9C@bfB`0%3#?@|*qv?TH&qN^!&K<@>e<6^MFRJ`w zzS%d(K7-XVo7QBEd*lH1C9mT`SlLx>;i3hn=PvvgMv1yOtDI}fM;Rs4;Trt923u~Q z@cqupUczKR{!!31pO@Lo;Y!T*Yv`!cCn3;y{-;?<3q}$m;ngBQhaP>|T_ZQ~5tDOr zf8-gIq#2(~bTPE-|I~yQ3Od~R+O?dH;|DvYCgl!^ zL6kMwCl@IV6|8bOJ^mE??PQm)+TuNs)I0o02wI?wfnFFF<_OmhbVKx+GX~K+VP+aI zza%x5pWXWJyYum#Zsjq=!MGkI^1pE%2q~TPFO#SxJ053K+Dpp_v}0egj#zP_kb0=) z9p3{H5)}=L-^J%WC%AG_=~>_X!^YBzeA^NMVTbAGkPkZU661e%`4XkkDy)VO1o-HXW%P%Xlr;Re~cHs3+558}v=>pL) ze6=5y(93yWncs-&Gi_Z*ybLdo4?%sDCJ!<;+fuaIBtP!`-nKe$wBu#z4K^^PpH`f+|~c9E96fWB%*r?^a2uz>|46 z4V~HReLcTr2vts=33q z#W}xUjEUgax7qd~C@~N^EH>oCi{5>D!PZti7XM8MG`~;yuIR&w6u@{3EY^j*4onUg z>PftG?CYBW0rtf73b=2-l`nMUB~CLA5-5<3w*t=s2iCt0ZFf{;1OVm*&Z{WQK*kbm z={PpeB3!;*Lq`mZzm%eZ(fXO{7QKjkPxj}J4&GR0tWuce@Bmo7nOx0XI|-F>y=@-g zv^_{j-wUu#_a`86WMkfG=Ih+9P{m(AZMZ5^Oj>f{VI!mc5kus)I^&rGz}JVz53{$& zOvaptUF#&GmN--&w-R$RKTSGG&r%8uMwW)k6N^G#H4dmc24V|ceg5Gq%MktE$<*5w zH-!fS3E?Y2DTJ$*IT0ex$YmY>hO$?ttAhPFail~dHYo`U6tbV??}&MaBlbZMFn6Hy zz~D)<$gxP0u7s!bj@T+Ioy?@BHDLtLBKcYJd`RWKj-kAc!>Cu|JSm6@d}4h?+cymW z*qm>7cS%c-$5wWT_-DN=<4u#_;Xph2^ekSqJi&bq`^%R^OEp7cpX`5MtXFrujln`1 z@9AFsDs?Za?)uZ^#)|5*bmPD@uA^yhc3MWjjAvjg%$NYkajK!^X&ZM2>66%lg3idk zvw0tQ*hIEZV>FA@PlwZNj$Ee2O1ixDO(lao%)Sh=q0{a-|69`0NOg%N?Fw#XrTCkU zn7|z9-I0FqPG-rQ!yeyRnyEbA*8ufS_wh6tM6o&?MA`;=kpPQ-r|AEttop_3pBe8T z71Q1yV`LK9RYZk@#c6@tQ1VFTAp`em?T6#~S=75WmZ8uTnqTjT_2_?%Kwv_T_t!fo zy_m;SwhzS?{V{|}!fSAN!+GSmO`z%h&*X$8&Q7oeu$jO6Rh@BYP*N2-?-f@RM3k3X zPaU;7hFsEHCzY4DDJec8WFVhPlusY8Efnj*eToN0L)jkxt*gCjCj3~V%}PS++wbhp zApG3uXRcU!%PqP3?TmSt9pg9;Y5d|?+G?fQdQvP2hb8(>j$lr2QB05N@$#6FIuBYj zqi&nq3u6|YzD(uT(lW=Gf2L_lv=)PZED>87b}-Fu_rvKw%oOUem#FC&3?3yht~?%~ zkf;#9izX~y{*G4d|KTJp}|F7wfyURDLO{J0kBL&k7^_`lt&4Gy! z37fO-^5}4Mu|9^oJL{ORDn3bd7K#t@dmh<(5p%=&zacj+>6~hEu-4!3kJ5de!X#mQ zO!zY?U4`%>Vnvl}wUMYOY=PvLciJ?+$}nL=pQy%YIer{>QUx(Mz}C$(%c2VK6|myI z3ZMt9sTSt4Hyi$I-|za`MEkPHc0#5`TvqPkh5gUwg8iPqu*1LA-`WBvf3w&2D%|~x zU|)+nufce*6}B*tavK!habbj>+Zt#~0?zvCd*;B&KkEnT0do+9y$=lc1Q%#6Dob|n z>DQ#SL(+%uRf2CdO>6sC5`Nb%F==1jy0!5v!G;dq<;yQ)!%U z_4zhG=5Kbypxpp8>b~8{Qw8+f@Kv!=Mo$EJ(61jr67JZz{E;ZAqq6kCNHoGwvD0Mf z6tcI7-<$?=Wk5zPOBh1MAnBk5?PUp9jn$6kMIN!OUj1|38P_7c(`l3mEmDt@7Q}#h z6F?X_m`sf$sATd)mSTV8bWmd*j3j*@&ZAuXRsLY|@Kk+v_iN$zk0O@S4l71Ipjs7v z+m|?BY@FVqe%#RVrx8Cka#Q0~oWmYbf-Z~Cv~{8wo6|Ga6zvN}l}%1jkERjj%f8@k z-5sjMF%=j`+!RJVh{Ws9%^r}zNAJ(IkwCROFwz+LsH%9%7e4Mu8gFdYSB7B|MP3KD zi-v`kR7s&o^d*>;w)ib+JoVRoB8|^Ac`F%2x(+Wyj)X6a$O8#Y$DijvlSowt*?OFu znfw;7E!u04pdL*nW%nU()}~*NMEpa|U)XzrIxQ~C#u+gBc7lx%>V{73Np<4fZ-zAu z#e83n90=l08j$<{UbdsHRhzPV7Z5}eNPtZ(Pw`W|tStr3*RU6qyN+hpQOz82u()+! zbciD_YVt>~n*hy_obRh+UF{xBem@opCfJ4<$HWLgwvapD^B;q8K?BsN_C|uYd^yrr z(y~<7Zhz{y+@LNvJXNEzrIDl=Wk>-Ix!1J4FU2tAIa~ZdXVvmC5srNq_do^Zo&P~> zIuY8>2IK=e{$#F13xU1uzT<06W8+miO*o)nKkkHyVhW$Y?ax$5see`pC5a#C&2sJ- z93Y1%>18Io5+r$v{F3C?)?l*0-_M%qoR$hz}wwmFlzSi|%G^3Zo5X$SFW7u)$`%fcMd10C_lehyx6EBhGWDgI;NBiSir zH%eqp_4wZ^#v2SGcx}ziPFiJb8M{(eetXWQgZyhlNrHQA?gr$mMB~??oO-b^tiyZ_ zXP)TSeu+g6e=(|?(TiU0&F`m+lHfUF4@?Y?+D)Lb7ho2{2m6Q=lfbEt#oI*!xxA-w zN7t{6og@zSnLKkyK}~)FkSeGTG0!ug2VOB6U?Z#F<;9oYI}nx*D8IR0(A;#Qdb|iN z^*Lf%Vrqzf*7B0+`qNl@b&Do$lm$n!$D9rF(j^KOCeiC~68sM3ATOpuvIlgh|btMe@MEwKxgwD0-|FW1-|CPo5{z{s%%+KqnWQ zNacTxJbCElzUbI^y5v3c8*vH& zM>E!7^O~QBVS(#EKH#vi|H)ru$W2o0F2V&BOoAp)>fXMi#LXgBm(0g1-~UoZWaJE2 z#n*w;sf+O^IjY$qN$yu2my4dDx#}>LzXfMB*-C#Drnfh}ef+#ilUwKIB3jbjJYiq} z>pw(8EeEHchPYk&fZ^HK0dryE} zQ_~MAh3OWo9a>gMn5cbFo$U%kNc7oNEJ6?Csy+fVD)W)8M*&mRUpzM46L=R!kG_QqdiF(OId6c`f_+&y7*+$y$_zdu3Fk1AMLKb-=(_#=eT-G zPY3gT?@>$&W^|&(eK7(BB@0S&AKqV9a|32R^wkJ0!~ip0xtiiP5CZ!NErUmYH1d0E-Vat8tij~yw89OIx*puP)}JS z5OFTtA$QEb8-{FGy&?ESo)82SOY~8a^i!ES0zil2yAeqT9#wGB2`do7RiwZsRfIqg zflp}bQA_m4k2ct>!2dz@A%$EtuF@7g+s&b>vp+YVfl`5wbvq)>`J2sQ5B<3?cpp7y zGN{SjNc6fQ9K7GibK)&g|C4-XTKfw1ra#}& zg*dY!t*_{4RwJ?0bkBaP2;)jn>mn$b3_a6)Q$nWp_$9)I0d*)J-w$m?`B=YJY>`8f;eigF?q{ zq9E98uBjHn2)=>Rg^9OEb2xAzWZ}2*4*j#QCVo@sQ@CQ4I6a>Z-bP_TDp}urPKV|Z zOK<2O?eX5}QFm5PnOz^jp!8M>p8GT?L+K0sK{6@|>Fc)6PORGGZqt7c>}dyKz^ohg zX>Hc&LrmB%lDPY72OH8TnL;gS*(}4W+w(7*7>T6qyT~yeX9L^cvk$s#Q_mT)^iPIQ zc*Ytkidv7*sk*NHo4r_uLf{W{McC}Vmj;^f>h6qvxykjWhn%_B1)t2JF{bD?O5spR{h(a2c?DeCOv=X$+S=L% zu6?)x7ZbAT@Eo5a5rG7W`iJ${_9v>iFHKG9KnwW+46F2v>J;)=AfV;@cV+-bOmN2< zhPRfMS4d292Z)k8x1Az`G{Ysp>$O`E?4J4n;d>zv=Zxq#RPPRaV>WKhjf!&9sv>5= zfi&Faiuj-0>st$e`aj#wa^7&^zX9(h5Gwjl-2oO^_;N}!+ocEfNmD$WE0WdH(y|-- z5s?`L#|z;>B+Y#(N2eSBPOw|ovI|5srM1tam*vggL55x1$ge&E8{F--B{Sq6{x>g6 zVXPLsLX5TMdi)Z!!}=a)Hoa_1#y}!%~PI_2^RczS2o%Iq~@<2 z1*DnmOW97pB2DAv1y{Zq)R?nTk>MTU*Q_Yq5+d+%p_O_$L`^xVGTlJC7gbie^ygPn znpZwUW)n)pwGMOHR$9^n`uNnl>f>6ZKa~4ZoUmD;Wa0gs z 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Î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�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 Date: Wed, 17 Aug 2022 10:48:46 +0000 Subject: [PATCH 0320/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5f2f0e38c..6cd2a298c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsorship. PR [#5272](https://github.com/tiangolo/fastapi/pull/5272) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Striveworks badge. PR [#5179](https://github.com/tiangolo/fastapi/pull/5179) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). From 623b0f37de152a49fa27078c1e28fd1ee80e2587 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 18 Aug 2022 17:24:28 +0200 Subject: [PATCH 0321/1881] =?UTF-8?q?=F0=9F=93=9D=20Remove=20unneeded=20Dj?= =?UTF-8?q?ango/Flask=20references=20from=20async=20topic=20intro=20(#5280?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/async.md | 6 +----- docs/fr/docs/async.md | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 43473c822..95d2f755a 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -232,11 +232,7 @@ This "waiting" 🕙 is measured in microseconds, but still, summing it all, it's That's why it makes a lot of sense to use asynchronous ⏸🔀⏯ code for web APIs. -Most of the existing popular Python frameworks (including Flask and Django) were created before the new asynchronous features in Python existed. So, the ways they can be deployed support parallel execution and an older form of asynchronous execution that is not as powerful as the new capabilities. - -Even though the main specification for asynchronous web Python (ASGI) was developed at Django, to add support for WebSockets. - -That kind of asynchronicity is what made NodeJS popular (even though NodeJS is not parallel) and that's the strength of Go as a programming language. +This kind of asynchronicity is what made NodeJS popular (even though NodeJS is not parallel) and that's the strength of Go as a programming language. And that's the same level of performance you get with **FastAPI**. diff --git a/docs/fr/docs/async.md b/docs/fr/docs/async.md index 71c28b703..db88c4663 100644 --- a/docs/fr/docs/async.md +++ b/docs/fr/docs/async.md @@ -205,10 +205,6 @@ Cette "attente" 🕙 se mesure en microsecondes, mais tout de même, en cumulé C'est pourquoi il est logique d'utiliser du code asynchrone ⏸🔀⏯ pour des APIs web. -La plupart des frameworks Python existants (y compris Flask et Django) ont été créés avant que les nouvelles fonctionnalités asynchrones de Python n'existent. Donc, les façons dont ils peuvent être déployés supportent l'exécution parallèle et une ancienne forme d'exécution asynchrone qui n'est pas aussi puissante que les nouvelles fonctionnalités de Python. - -Et cela, bien que les spécifications principales du web asynchrone en Python (ou ASGI) ont été développées chez Django, pour ajouter le support des WebSockets. - Ce type d'asynchronicité est ce qui a rendu NodeJS populaire (bien que NodeJS ne soit pas parallèle) et c'est la force du Go en tant que langage de programmation. Et c'est le même niveau de performance que celui obtenu avec **FastAPI**. From a79228138bf2d3fa7b4d2cf4485b387cb833f910 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 15:26:58 +0000 Subject: [PATCH 0322/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6cd2a298c..1ad350acb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). * ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsorship. PR [#5272](https://github.com/tiangolo/fastapi/pull/5272) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Striveworks badge. PR [#5179](https://github.com/tiangolo/fastapi/pull/5179) by [@tiangolo](https://github.com/tiangolo). From a0d79b37068e21647b55cd2b69c2d1cb78c49638 Mon Sep 17 00:00:00 2001 From: Baskara Febrianto <41407847+bas-baskara@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:53:59 +0700 Subject: [PATCH 0323/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Indonesian=20tra?= =?UTF-8?q?nslation=20for=20`docs/id/docs/tutorial/index.md`=20(#4705)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Baskara Febrianto Co-authored-by: Sebastián Ramírez --- docs/id/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/id/docs/tutorial/index.md diff --git a/docs/id/docs/tutorial/index.md b/docs/id/docs/tutorial/index.md new file mode 100644 index 000000000..8fec3c087 --- /dev/null +++ b/docs/id/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutorial - Pedoman Pengguna - Pengenalan + +Tutorial ini menunjukan cara menggunakan ***FastAPI*** dengan semua fitur-fiturnya, tahap demi tahap. + +Setiap bagian dibangun secara bertahap dari bagian sebelumnya, tetapi terstruktur untuk memisahkan banyak topik, sehingga kamu bisa secara langsung menuju ke topik spesifik untuk menyelesaikan kebutuhan API tertentu. + +Ini juga dibangun untuk digunakan sebagai referensi yang akan datang. + +Sehingga kamu dapat kembali lagi dan mencari apa yang kamu butuhkan dengan tepat. + +## Jalankan kode + +Semua blok-blok kode dapat dicopy dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji). + +Untuk menjalankan setiap contoh, copy kode ke file `main.py`, dan jalankan `uvicorn` dengan: + +

+ +**SANGAT disarankan** agar kamu menulis atau meng-copy kode, meng-editnya dan menjalankannya secara lokal. + +Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari FastAPI, melihat bagaimana sedikitnya kode yang harus kamu tulis, semua pengecekan tipe, pelengkapan otomatis, dll. + +--- + +## Install FastAPI + +Langkah pertama adalah dengan meng-install FastAPI. + +Untuk tutorial, kamu mungkin hendak meng-instalnya dengan semua pilihan fitur dan dependensinya: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...yang juga termasuk `uvicorn`, yang dapat kamu gunakan sebagai server yang menjalankan kodemu. + +!!! catatan + Kamu juga dapat meng-instalnya bagian demi bagian. + + Hal ini mungkin yang akan kamu lakukan ketika kamu hendak men-deploy aplikasimu ke tahap produksi: + + ``` + pip install fastapi + ``` + + Juga install `uvicorn` untk menjalankan server" + + ``` + pip install "uvicorn[standard]" + ``` + + Dan demikian juga untuk pilihan dependensi yang hendak kamu gunakan. + +## Pedoman Pengguna Lanjutan + +Tersedia juga **Pedoman Pengguna Lanjutan** yang dapat kamu baca nanti setelah **Tutorial - Pedoman Pengguna** ini. + +**Pedoman Pengguna Lanjutan**, dibangun atas hal ini, menggunakan konsep yang sama, dan mengajarkan kepadamu beberapa fitur tambahan. + +Tetapi kamu harus membaca terlebih dahulu **Tutorial - Pedoman Pengguna** (apa yang sedang kamu baca sekarang). + +Hal ini didesain sehingga kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**. From e23fa40e6c99b0550586194d4ee2ef7cc61277ab Mon Sep 17 00:00:00 2001 From: sUeharaE4 <44468359+sUeharaE4@users.noreply.github.com> Date: Fri, 19 Aug 2022 00:54:22 +0900 Subject: [PATCH 0324/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/advanced/nosql-databases.md`=20(#4?= =?UTF-8?q?205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ja/docs/advanced/nosql-databases.md | 156 +++++++++++++++++++++++ docs/ja/mkdocs.yml | 1 + 2 files changed, 157 insertions(+) create mode 100644 docs/ja/docs/advanced/nosql-databases.md diff --git a/docs/ja/docs/advanced/nosql-databases.md b/docs/ja/docs/advanced/nosql-databases.md new file mode 100644 index 000000000..fbd76e96b --- /dev/null +++ b/docs/ja/docs/advanced/nosql-databases.md @@ -0,0 +1,156 @@ +# NoSQL (分散 / ビッグデータ) Databases + +**FastAPI** はあらゆる NoSQLと統合することもできます。 + +ここではドキュメントベースのNoSQLデータベースである**
Couchbase**を使用した例を見てみましょう。 + +他にもこれらのNoSQLデータベースを利用することが出来ます: + +* **MongoDB** +* **Cassandra** +* **CouchDB** +* **ArangoDB** +* **ElasticSearch** など。 + +!!! tip "豆知識" + **FastAPI**と**Couchbase**を使った公式プロジェクト・ジェネレータがあります。すべて**Docker**ベースで、フロントエンドやその他のツールも含まれています: https://github.com/tiangolo/full-stack-fastapi-couchbase + +## Couchbase コンポーネントの Import + +まずはImportしましょう。今はその他のソースコードに注意を払う必要はありません。 + +```Python hl_lines="3-5" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## "document type" として利用する定数の定義 + +documentで利用する固定の`type`フィールドを用意しておきます。 + +これはCouchbaseで必須ではありませんが、後々の助けになるベストプラクティスです。 + +```Python hl_lines="9" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## `Bucket` を取得する関数の追加 + +**Couchbase**では、bucketはdocumentのセットで、様々な種類のものがあります。 + +Bucketは通常、同一のアプリケーション内で互いに関係を持っています。 + +リレーショナルデータベースの世界でいう"database"(データベースサーバではなく特定のdatabase)と類似しています。 + +**MongoDB** で例えると"collection"と似た概念です。 + +次のコードでは主に `Bucket` を利用してCouchbaseを操作します。 + +この関数では以下の処理を行います: + +* **Couchbase** クラスタ(1台の場合もあるでしょう)に接続 + * タイムアウトのデフォルト値を設定 +* クラスタで認証を取得 +* `Bucket` インスタンスを取得 + * タイムアウトのデフォルト値を設定 +* 作成した`Bucket`インスタンスを返却 + +```Python hl_lines="12-21" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## Pydantic モデルの作成 + +**Couchbase**のdocumentは実際には単にJSONオブジェクトなのでPydanticを利用してモデルに出来ます。 + +### `User` モデル + +まずは`User`モデルを作成してみましょう: + +```Python hl_lines="24-28" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +このモデルは*path operation*に使用するので`hashed_password`は含めません。 + +### `UserInDB` モデル + +それでは`UserInDB`モデルを作成しましょう。 + +こちらは実際にデータベースに保存されるデータを保持します。 + +`User`モデルの持つ全ての属性に加えていくつかの属性を追加するのでPydanticの`BaseModel`を継承せずに`User`のサブクラスとして定義します: + +```Python hl_lines="31-33" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +!!! note "備考" + データベースに保存される`hashed_password`と`type`フィールドを`UserInDB`モデルに保持させていることに注意してください。 + + しかしこれらは(*path operation*で返却する)一般的な`User`モデルには含まれません + +## user の取得 + +それでは次の関数を作成しましょう: + +* username を取得する +* username を利用してdocumentのIDを生成する +* 作成したIDでdocumentを取得する +* documentの内容を`UserInDB`モデルに設定する + +*path operation関数*とは別に、`username`(またはその他のパラメータ)からuserを取得することだけに特化した関数を作成することで、より簡単に複数の部分で再利用したりユニットテストを追加することができます。 + +```Python hl_lines="36-42" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### f-strings + +`f"userprofile::{username}"` という記載に馴染みがありませんか?これは Python の"f-string"と呼ばれるものです。 + +f-stringの`{}`の中に入れられた変数は、文字列の中に展開/注入されます。 + +### `dict` アンパック + +`UserInDB(**result.value)`という記載に馴染みがありませんか?これは`dict`の"アンパック"と呼ばれるものです。 + +これは`result.value`の`dict`からそのキーと値をそれぞれ取りキーワード引数として`UserInDB`に渡します。 + +例えば`dict`が下記のようになっていた場合: + +```Python +{ + "username": "johndoe", + "hashed_password": "some_hash", +} +``` + +`UserInDB`には次のように渡されます: + +```Python +UserInDB(username="johndoe", hashed_password="some_hash") +``` + +## **FastAPI** コードの実装 + +### `FastAPI` app の作成 + +```Python hl_lines="46" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### *path operation関数*の作成 + +私たちのコードはCouchbaseを呼び出しており、実験的なPython awaitを使用していないので、私たちは`async def`ではなく通常の`def`で関数を宣言する必要があります。 + +また、Couchbaseは単一の`Bucket`オブジェクトを複数のスレッドで使用しないことを推奨していますので、単に直接Bucketを取得して関数に渡すことが出来ます。 + +```Python hl_lines="49-53" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## まとめ + +他のサードパーティ製のNoSQLデータベースを利用する場合でも、そのデータベースの標準ライブラリを利用するだけで利用できます。 + +他の外部ツール、システム、APIについても同じことが言えます。 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 985bb24d0..b3f18bbdd 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -85,6 +85,7 @@ nav: - advanced/additional-status-codes.md - advanced/response-directly.md - advanced/custom-response.md + - advanced/nosql-databases.md - advanced/conditional-openapi.md - async.md - デプロイ: From 7e3e4fa7eac3c6ec5fc085652fb633c2369076a2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 15:54:41 +0000 Subject: [PATCH 0325/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1ad350acb..88becd991 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Indonesian translation for `docs/id/docs/tutorial/index.md`. PR [#4705](https://github.com/tiangolo/fastapi/pull/4705) by [@bas-baskara](https://github.com/bas-baskara). * 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). * ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsorship. PR [#5272](https://github.com/tiangolo/fastapi/pull/5272) by [@tiangolo](https://github.com/tiangolo). From fc4ad71069b7a690944a7f17129ab0630173f57f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 15:56:11 +0000 Subject: [PATCH 0326/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 88becd991..abbb1584b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/advanced/nosql-databases.md`. PR [#4205](https://github.com/tiangolo/fastapi/pull/4205) by [@sUeharaE4](https://github.com/sUeharaE4). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/index.md`. PR [#4705](https://github.com/tiangolo/fastapi/pull/4705) by [@bas-baskara](https://github.com/bas-baskara). * 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). * ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). From dc7e13846c2ddddd9b8f56bb8c20dc9be61e24e5 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Thu, 18 Aug 2022 23:57:21 +0800 Subject: [PATCH 0327/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/security/first-steps.md`=20(#3841)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/security/first-steps.md | 189 ++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 190 insertions(+) create mode 100644 docs/zh/docs/tutorial/security/first-steps.md diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..116572411 --- /dev/null +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -0,0 +1,189 @@ +# 安全 - 第一步 + +假设**后端** API 在某个域。 + +**前端**在另一个域,或(移动应用中)在同一个域的不同路径下。 + +并且,前端要使用后端的 **username** 与 **password** 验证用户身份。 + +固然,**FastAPI** 支持 **OAuth2** 身份验证。 + +但为了节省开发者的时间,不要只为了查找很少的内容,不得不阅读冗长的规范文档。 + +我们建议使用 **FastAPI** 的安全工具。 + +## 概览 + +首先,看看下面的代码是怎么运行的,然后再回过头来了解其背后的原理。 + +## 创建 `main.py` + +把下面的示例代码复制到 `main.py`: + +```Python +{!../../../docs_src/security/tutorial001.py!} +``` + +## 运行 + +!!! info "说明" + + 先安装 `python-multipart`。 + + 安装命令: `pip install python-multipart`。 + + 这是因为 **OAuth2** 使用**表单数据**发送 `username` 与 `password`。 + +用下面的命令运行该示例: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## 查看文档 + +打开 API 文档: http://127.0.0.1:8000/docs。 + +界面如下图所示: + + + +!!! check "Authorize 按钮!" + + 页面右上角出现了一个「**Authorize**」按钮。 + + *路径操作*的右上角也出现了一个可以点击的小锁图标。 + +点击 **Authorize** 按钮,弹出授权表单,输入 `username` 与 `password` 及其它可选字段: + + + +!!! note "笔记" + + 目前,在表单中输入内容不会有任何反应,后文会介绍相关内容。 + +虽然此文档不是给前端最终用户使用的,但这个自动工具非常实用,可在文档中与所有 API 交互。 + +前端团队(可能就是开发者本人)可以使用本工具。 + +第三方应用与系统也可以调用本工具。 + +开发者也可以用它来调试、检查、测试应用。 + +## 密码流 + +现在,我们回过头来介绍这段代码的原理。 + +`Password` **流**是 OAuth2 定义的,用于处理安全与身份验证的方式(**流**)。 + +OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户身份。 + +但在本例中,**FastAPI** 应用会处理 API 与身份验证。 + +下面,我们来看一下简化的运行流程: + +- 用户在前端输入 `username` 与`password`,并点击**回车** +- (用户浏览器中运行的)前端把 `username` 与`password` 发送至 API 中指定的 URL(使用 `tokenUrl="token"` 声明) +- API 检查 `username` 与`password`,并用令牌(`Token`) 响应(暂未实现此功能): + - 令牌只是用于验证用户的字符串 + - 一般来说,令牌会在一段时间后过期 + - 过时后,用户要再次登录 + - 这样一来,就算令牌被人窃取,风险也较低。因为它与永久密钥不同,**在绝大多数情况下**不会长期有效 +- 前端临时将令牌存储在某个位置 +- 用户点击前端,前往前端应用的其它部件 +- 前端需要从 API 中提取更多数据: + - 为指定的端点(Endpoint)进行身份验证 + - 因此,用 API 验证身份时,要发送值为 `Bearer` + 令牌的请求头 `Authorization` + - 假如令牌为 `foobar`,`Authorization` 请求头就是: `Bearer foobar` + +## **FastAPI** 的 `OAuth2PasswordBearer` + +**FastAPI** 提供了不同抽象级别的安全工具。 + +本例使用 **OAuth2** 的 **Password** 流以及 **Bearer** 令牌(`Token`)。为此要使用 `OAuth2PasswordBearer` 类。 + +!!! info "说明" + + `Beare` 令牌不是唯一的选择。 + + 但它是最适合这个用例的方案。 + + 甚至可以说,它是适用于绝大多数用例的最佳方案,除非您是 OAuth2 的专家,知道为什么其它方案更合适。 + + 本例中,**FastAPI** 还提供了构建工具。 + +创建 `OAuth2PasswordBearer` 的类实例时,要传递 `tokenUrl` 参数。该参数包含客户端(用户浏览器中运行的前端) 的 URL,用于发送 `username` 与 `password`,并获取令牌。 + +```Python hl_lines="6" +{!../../../docs_src/security/tutorial001.py!} +``` + +!!! tip "提示" + + 在此,`tokenUrl="token"` 指向的是暂未创建的相对 URL `token`。这个相对 URL 相当于 `./token`。 + + 因为使用的是相对 URL,如果 API 位于 `https://example.com/`,则指向 `https://example.com/token`。但如果 API 位于 `https://example.com/api/v1/`,它指向的就是`https://example.com/api/v1/token`。 + + 使用相对 URL 非常重要,可以确保应用在遇到[使用代理](../../advanced/behind-a-proxy.md){.internal-link target=_blank}这样的高级用例时,也能正常运行。 + +该参数不会创建端点或*路径操作*,但会声明客户端用来获取令牌的 URL `/token` 。此信息用于 OpenAPI 及 API 文档。 + +接下来,学习如何创建实际的路径操作。 + +!!! info "说明" + + 严苛的 **Pythonista** 可能不喜欢用 `tokenUrl` 这种命名风格代替 `token_url`。 + + 这种命名方式是因为要使用与 OpenAPI 规范中相同的名字。以便在深入校验安全方案时,能通过复制粘贴查找更多相关信息。 + +`oauth2_scheme` 变量是 `OAuth2PasswordBearer` 的实例,也是**可调用项**。 + +以如下方式调用: + +```Python +oauth2_scheme(some, parameters) +``` + +因此,`Depends` 可以调用 `oauth2_scheme` 变量。 + +### 使用 + +接下来,使用 `Depends` 把 `oauth2_scheme` 传入依赖项。 + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +该依赖项使用字符串(`str`)接收*路径操作函数*的参数 `token` 。 + +**FastAPI** 使用依赖项在 OpenAPI 概图(及 API 文档)中定义**安全方案**。 + +!!! info "技术细节" + + **FastAPI** 使用(在依赖项中声明的)类 `OAuth2PasswordBearer` 在 OpenAPI 中定义安全方案,这是因为它继承自 `fastapi.security.oauth2.OAuth2`,而该类又是继承自`fastapi.security.base.SecurityBase`。 + + 所有与 OpenAPI(及 API 文档)集成的安全工具都继承自 `SecurityBase`, 这就是为什么 **FastAPI** 能把它们集成至 OpenAPI 的原因。 + +## 实现的操作 + +FastAPI 校验请求中的 `Authorization` 请求头,核对请求头的值是不是由 `Bearer ` + 令牌组成, 并返回令牌字符串(`str`)。 + +如果没有找到 `Authorization` 请求头,或请求头的值不是 `Bearer ` + 令牌。FastAPI 直接返回 401 错误状态码(`UNAUTHORIZED`)。 + +开发者不需要检查错误信息,查看令牌是否存在,只要该函数能够执行,函数中就会包含令牌字符串。 + +正如下图所示,API 文档已经包含了这项功能: + + + +目前,暂时还没有实现验证令牌是否有效的功能,不过后文很快就会介绍的。 + +## 小结 + +看到了吧,只要多写三四行代码,就可以添加基础的安全表单。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index e70ec7698..9e8813d6c 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -93,6 +93,7 @@ nav: - tutorial/dependencies/global-dependencies.md - 安全性: - tutorial/security/index.md + - tutorial/security/first-steps.md - tutorial/security/get-current-user.md - tutorial/security/simple-oauth2.md - tutorial/security/oauth2-jwt.md From 8fe80e81e11f3bc9a6be7ddfe63ded5e0160c916 Mon Sep 17 00:00:00 2001 From: AdmiralDesu <49198383+AdmiralDesu@users.noreply.github.com> Date: Thu, 18 Aug 2022 18:59:02 +0300 Subject: [PATCH 0328/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/background-tasks.md`=20(#4?= =?UTF-8?q?854)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ru/docs/tutorial/background-tasks.md | 102 ++++++++++++++++++++++ docs/ru/mkdocs.yml | 4 + 2 files changed, 106 insertions(+) create mode 100644 docs/ru/docs/tutorial/background-tasks.md diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..e608f6c8f --- /dev/null +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -0,0 +1,102 @@ +# Фоновые задачи + +Вы можете создавать фоновые задачи, которые будут выполнятся *после* возвращения ответа сервером. + +Это может быть полезно для функций, которые должны выполниться после получения запроса, но ожидание их выполнения необязательно для пользователя. + +К примеру: + +* Отправка писем на почту после выполнения каких-либо действий: + * Т.к. соединение с почтовым сервером и отправка письма идут достаточно "долго" (несколько секунд), вы можете отправить ответ пользователю, а отправку письма выполнить в фоне. +* Обработка данных: + * К примеру, если вы получаете файл, который должен пройти через медленный процесс, вы можете отправить ответ "Accepted" (HTTP 202) и отправить работу с файлом в фон. + +## Использование класса `BackgroundTasks` + +Сначала импортируйте `BackgroundTasks`, потом добавьте в функцию параметр с типом `BackgroundTasks`: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** создаст объект класса `BackgroundTasks` для вас и запишет его в параметр. + +## Создание функции для фоновой задачи + +Создайте функцию, которую хотите запустить в фоне. + +Это совершенно обычная функция, которая может принимать параметры. + +Она может быть как асинхронной `async def`, так и обычной `def` функцией, **FastAPI** знает, как правильно ее выполнить. + +В нашем примере фоновая задача будет вести запись в файл (симулируя отправку письма). + +Так как операция записи не использует `async` и `await`, мы определим ее как обычную `def`: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## Добавление фоновой задачи + +Внутри функции вызовите метод `.add_task()` у объекта *background tasks* и передайте ему функцию, которую хотите выполнить в фоне: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` принимает следующие аргументы: + +* Функцию, которая будет выполнена в фоне (`write_notification`). Обратите внимание, что передается объект функции, без скобок. +* Любое упорядоченное количество аргументов, которые принимает функция (`email`). +* Любое количество именованных аргументов, которые принимает функция (`message="some notification"`). + +## Встраивание зависимостей + +Класс `BackgroundTasks` также работает с системой встраивания зависимостей, вы можете определить `BackgroundTasks` на разных уровнях: как параметр функции, как завимость, как подзависимость и так далее. + +**FastAPI** знает, что нужно сделать в каждом случае и как переиспользовать тот же объект `BackgroundTasks`, так чтобы все фоновые задачи собрались и запустились вместе в фоне: + +=== "Python 3.6 и выше" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +В этом примере сообщения будут записаны в `log.txt` *после* того, как ответ сервера был отправлен. + +Если бы в запросе была очередь `q`, она бы первой записалась в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). + +После другая фоновая задача, которая была сгенерирована в функции, запишет сообщение из параметра `email`. + +## Технические детали + +Класс `BackgroundTasks` основан на `starlette.background`. + +Он интегрирован в FastAPI, так что вы можете импортировать его прямо из `fastapi` и избежать случайного импорта `BackgroundTask` (без `s` на конце) из `starlette.background`. + +При использовании `BackgroundTasks` (а не `BackgroundTask`), вам достаточно только определить параметр функции с типом `BackgroundTasks` и **FastAPI** сделает все за вас, также как при использовании объекта `Request`. + +Вы все равно можете использовать `BackgroundTask` из `starlette` в FastAPI, но вам придется самостоятельно создавать объект фоновой задачи и вручную обработать `Response` внутри него. + +Вы можете подробнее изучить его в Официальной документации Starlette для BackgroundTasks. + +## Предостережение + +Если вам нужно выполнить тяжелые вычисления в фоне, которым необязательно быть запущенными в одном процессе с приложением **FastAPI** (к примеру, вам не нужны обрабатываемые переменные или вы не хотите делиться памятью процесса и т.д.), вы можете использовать более серьезные инструменты, такие как Celery. + +Их тяжелее настраивать, также им нужен брокер сообщений наподобие RabbitMQ или Redis, но зато они позволяют вам запускать фоновые задачи в нескольких процессах и даже на нескольких серверах. + +Для примера, посмотрите [Project Generators](../project-generation.md){.internal-link target=_blank}, там есть проект с уже настроенным Celery. + +Но если вам нужен доступ к общим переменным и объектам вашего **FastAPI** приложения или вам нужно выполнять простые фоновые задачи (наподобие отправки письма из примера) вы можете просто использовать `BackgroundTasks`. + +## Резюме + +Для создания фоновых задач вам необходимо импортировать `BackgroundTasks` и добавить его в функцию, как параметр с типом `BackgroundTasks`. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 2150d5c19..f70b436d2 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -58,6 +58,10 @@ nav: - tr: /tr/ - uk: /uk/ - zh: /zh/ +- python-types.md +- Учебник - руководство пользователя: + - tutorial/background-tasks.md +- external-links.md - async.md markdown_extensions: - toc: From b862639ce9eebcd113d0d16806c3f63a036bdd34 Mon Sep 17 00:00:00 2001 From: Atiab Bin Zakaria <61742543+atiabbz@users.noreply.github.com> Date: Thu, 18 Aug 2022 23:59:50 +0800 Subject: [PATCH 0329/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/python-types.md`=20(#5007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/python-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 963fcaf1c..f170bb1ef 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -267,7 +267,7 @@ You can declare that a variable can be any of **several types**, for example, an In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. -In Python 3.10 there's also an **alternative syntax** were you can put the possible types separated by a vertical bar (`|`). +In Python 3.10 there's also an **alternative syntax** where you can put the possible types separated by a vertical bar (`|`). === "Python 3.6 and above" From 65e4286bc9d00ccd640383f0cd265c6c534a7b30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:00:22 +0000 Subject: [PATCH 0330/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 abbb1584b..976b96e22 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/tutorial/security/first-steps.md`. PR [#3841](https://github.com/tiangolo/fastapi/pull/3841) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/nosql-databases.md`. PR [#4205](https://github.com/tiangolo/fastapi/pull/4205) by [@sUeharaE4](https://github.com/sUeharaE4). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/index.md`. PR [#4705](https://github.com/tiangolo/fastapi/pull/4705) by [@bas-baskara](https://github.com/bas-baskara). * 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). From 48ca7a6368f7e5bf4d8a6eb3bf3cc321d6cf8b6d Mon Sep 17 00:00:00 2001 From: Ruidy Date: Thu, 18 Aug 2022 18:01:14 +0200 Subject: [PATCH 0331/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/history-design-future.md`=20(#3451)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sam Courtemanche Co-authored-by: Arthur Rio Co-authored-by: Ruidy Co-authored-by: Ruidy --- docs/fr/docs/history-design-future.md | 79 +++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 80 insertions(+) create mode 100644 docs/fr/docs/history-design-future.md diff --git a/docs/fr/docs/history-design-future.md b/docs/fr/docs/history-design-future.md new file mode 100644 index 000000000..b77664be6 --- /dev/null +++ b/docs/fr/docs/history-design-future.md @@ -0,0 +1,79 @@ +# Histoire, conception et avenir + +Il y a quelque temps, un utilisateur de **FastAPI** a demandé : + +> Quelle est l'histoire de ce projet ? Il semble être sorti de nulle part et est devenu génial en quelques semaines [...]. + +Voici un petit bout de cette histoire. + +## Alternatives + +Je crée des API avec des exigences complexes depuis plusieurs années (Machine Learning, systèmes distribués, jobs asynchrones, bases de données NoSQL, etc), en dirigeant plusieurs équipes de développeurs. + +Dans ce cadre, j'ai dû étudier, tester et utiliser de nombreuses alternatives. + +L'histoire de **FastAPI** est en grande partie l'histoire de ses prédécesseurs. + +Comme dit dans la section [Alternatives](alternatives.md){.internal-link target=\_blank} : + +
+ +**FastAPI** n'existerait pas sans le travail antérieur d'autres personnes. + +Il y a eu de nombreux outils créés auparavant qui ont contribué à inspirer sa création. + +J'ai évité la création d'un nouveau framework pendant plusieurs années. J'ai d'abord essayé de résoudre toutes les fonctionnalités couvertes par **FastAPI** en utilisant de nombreux frameworks, plug-ins et outils différents. + +Mais à un moment donné, il n'y avait pas d'autre option que de créer quelque chose qui offre toutes ces fonctionnalités, en prenant les meilleures idées des outils précédents, et en les combinant de la meilleure façon possible, en utilisant des fonctionnalités du langage qui n'étaient même pas disponibles auparavant (annotations de type pour Python 3.6+). + +
+ +## Recherche + +En utilisant toutes les alternatives précédentes, j'ai eu la chance d'apprendre de toutes, de prendre des idées, et de les combiner de la meilleure façon que j'ai pu trouver pour moi-même et les équipes de développeurs avec lesquelles j'ai travaillé. + +Par exemple, il était clair que l'idéal était de se baser sur les annotations de type Python standard. + +De plus, la meilleure approche était d'utiliser des normes déjà existantes. + +Ainsi, avant même de commencer à coder **FastAPI**, j'ai passé plusieurs mois à étudier les spécifications d'OpenAPI, JSON Schema, OAuth2, etc. Comprendre leurs relations, leurs similarités et leurs différences. + +## Conception + +Ensuite, j'ai passé du temps à concevoir l'"API" de développeur que je voulais avoir en tant qu'utilisateur (en tant que développeur utilisant FastAPI). + +J'ai testé plusieurs idées dans les éditeurs Python les plus populaires : PyCharm, VS Code, les éditeurs basés sur Jedi. + +D'après la dernière Enquête Développeurs Python, cela couvre environ 80% des utilisateurs. + +Cela signifie que **FastAPI** a été spécifiquement testé avec les éditeurs utilisés par 80% des développeurs Python. Et comme la plupart des autres éditeurs ont tendance à fonctionner de façon similaire, tous ses avantages devraient fonctionner pour pratiquement tous les éditeurs. + +Ainsi, j'ai pu trouver les meilleurs moyens de réduire autant que possible la duplication du code, d'avoir la complétion partout, les contrôles de type et d'erreur, etc. + +Le tout de manière à offrir la meilleure expérience de développement à tous les développeurs. + +## Exigences + +Après avoir testé plusieurs alternatives, j'ai décidé que j'allais utiliser **Pydantic** pour ses avantages. + +J'y ai ensuite contribué, pour le rendre entièrement compatible avec JSON Schema, pour supporter différentes manières de définir les déclarations de contraintes, et pour améliorer le support des éditeurs (vérifications de type, autocomplétion) sur la base des tests effectués dans plusieurs éditeurs. + +Pendant le développement, j'ai également contribué à **Starlette**, l'autre exigence clé. + +## Développement + +Au moment où j'ai commencé à créer **FastAPI** lui-même, la plupart des pièces étaient déjà en place, la conception était définie, les exigences et les outils étaient prêts, et la connaissance des normes et des spécifications était claire et fraîche. + +## Futur + +À ce stade, il est déjà clair que **FastAPI** et ses idées sont utiles pour de nombreuses personnes. + +Elle a été préférée aux solutions précédentes parce qu'elle convient mieux à de nombreux cas d'utilisation. + +De nombreux développeurs et équipes dépendent déjà de **FastAPI** pour leurs projets (y compris moi et mon équipe). + +Mais il y a encore de nombreuses améliorations et fonctionnalités à venir. + +**FastAPI** a un grand avenir devant lui. + +Et [votre aide](help-fastapi.md){.internal-link target=\_blank} est grandement appréciée. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 53d1b2c58..32ce30ef6 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -72,6 +72,7 @@ nav: - deployment/docker.md - project-generation.md - alternatives.md +- history-design-future.md - external-links.md markdown_extensions: - toc: From b489b327b8e753d8b17fc17dab86f5c78d17533b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:01:57 +0000 Subject: [PATCH 0332/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 976b96e22..bad8aca0d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/background-tasks.md`. PR [#4854](https://github.com/tiangolo/fastapi/pull/4854) by [@AdmiralDesu](https://github.com/AdmiralDesu). * 🌐 Add Chinese translation for `docs/tutorial/security/first-steps.md`. PR [#3841](https://github.com/tiangolo/fastapi/pull/3841) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/nosql-databases.md`. PR [#4205](https://github.com/tiangolo/fastapi/pull/4205) by [@sUeharaE4](https://github.com/sUeharaE4). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/index.md`. PR [#4705](https://github.com/tiangolo/fastapi/pull/4705) by [@bas-baskara](https://github.com/bas-baskara). From 5d0e2f58e711979175fa2793bbdf9d08b520730d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Sim=C3=B5es?= <66239468+frnsimoes@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:02:20 -0300 Subject: [PATCH 0333/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`tutorial/handling-errors.md`=20(#4769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lucas <61513630+lsglucas@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/pt/docs/tutorial/handling-errors.md | 251 +++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 252 insertions(+) create mode 100644 docs/pt/docs/tutorial/handling-errors.md diff --git a/docs/pt/docs/tutorial/handling-errors.md b/docs/pt/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..97a2e3eac --- /dev/null +++ b/docs/pt/docs/tutorial/handling-errors.md @@ -0,0 +1,251 @@ +# Manipulação de erros + +Há diversas situações em que você precisa notificar um erro a um cliente que está utilizando a sua API. + +Esse cliente pode ser um browser com um frontend, o código de outra pessoa, um dispositivo IoT, etc. + +Pode ser que você precise comunicar ao cliente que: + +* O cliente não tem direitos para realizar aquela operação. +* O cliente não tem acesso aquele recurso. +* O item que o cliente está tentando acessar não existe. +* etc. + + +Nesses casos, você normalmente retornaria um **HTTP status code** próximo ao status code na faixa do status code **400** (do 400 ao 499). + +Isso é bastante similar ao caso do HTTP status code 200 (do 200 ao 299). Esses "200" status codes significam que, de algum modo, houve sucesso na requisição. + +Os status codes na faixa dos 400 significam que houve um erro por parte do cliente. + +Você se lembra de todos aqueles erros (e piadas) a respeito do "**404 Not Found**"? + +## Use o `HTTPException` + +Para retornar ao cliente *responses* HTTP com erros, use o `HTTPException`. + +### Import `HTTPException` + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### Lance o `HTTPException` no seu código. + +`HTTPException`, ao fundo, nada mais é do que a conjunção entre uma exceção comum do Python e informações adicionais relevantes para APIs. + +E porque é uma exceção do Python, você não **retorna** (return) o `HTTPException`, você lança o (raise) no seu código. + +Isso também significa que, se você está escrevendo uma função de utilidade, a qual você está chamando dentro da sua função de operações de caminhos, e você lança o `HTTPException` dentro da função de utilidade, o resto do seu código não será executado dentro da função de operações de caminhos. Ao contrário, o `HTTPException` irá finalizar a requisição no mesmo instante e enviará o erro HTTP oriundo do `HTTPException` para o cliente. + +O benefício de lançar uma exceção em vez de retornar um valor ficará mais evidente na seção sobre Dependências e Segurança. + +Neste exemplo, quando o cliente pede, na requisição, por um item cujo ID não existe, a exceção com o status code `404` é lançada: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### A response resultante + + +Se o cliente faz uma requisição para `http://example.com/items/foo` (um `item_id` `"foo"`), esse cliente receberá um HTTP status code 200, e uma resposta JSON: + + +``` +{ + "item": "The Foo Wrestlers" +} +``` + +Mas se o cliente faz uma requisição para `http://example.com/items/bar` (ou seja, um não existente `item_id "bar"`), esse cliente receberá um HTTP status code 404 (o erro "não encontrado" — *not found error*), e uma resposta JSON: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip "Dica" + Quando você lançar um `HTTPException`, você pode passar qualquer valor convertível em JSON como parâmetro de `detail`, e não apenas `str`. + + Você pode passar um `dict` ou um `list`, etc. + Esses tipos de dados são manipulados automaticamente pelo **FastAPI** e convertidos em JSON. + + +## Adicione headers customizados + +Há certas situações em que é bastante útil poder adicionar headers customizados no HTTP error. Exemplo disso seria adicionar headers customizados para tipos de segurança. + +Você provavelmente não precisará utilizar esses headers diretamente no seu código. + +Mas caso você precise, para um cenário mais complexo, você pode adicionar headers customizados: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## Instalando manipuladores de exceções customizados + +Você pode adicionar manipuladores de exceção customizados com a mesma seção de utilidade de exceções presentes no Starlette + +Digamos que você tenha uma exceção customizada `UnicornException` que você (ou uma biblioteca que você use) precise lançar (`raise`). + +Nesse cenário, se você precisa manipular essa exceção de modo global com o FastAPI, você pode adicionar um manipulador de exceção customizada com `@app.exception_handler()`. + +```Python hl_lines="5-7 13-18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +Nesse cenário, se você fizer uma requisição para `/unicorns/yolo`, a *operação de caminho* vai lançar (`raise`) o `UnicornException`. + +Essa exceção será manipulada, contudo, pelo `unicorn_exception_handler`. + +Dessa forma você receberá um erro "limpo", com o HTTP status code `418` e um JSON com o conteúdo: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "Detalhes Técnicos" + Você também pode usar `from starlette.requests import Request` and `from starlette.responses import JSONResponse`. + + **FastAPI** disponibiliza o mesmo `starlette.responses` através do `fastapi.responses` por conveniência ao desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette. O mesmo acontece com o `Request`. + +## Sobrescreva o manipulador padrão de exceções + +**FastAPI** tem alguns manipuladores padrão de exceções. + +Esses manipuladores são os responsáveis por retornar o JSON padrão de respostas quando você lança (`raise`) o `HTTPException` e quando a requisição tem dados invalidos. + +Você pode sobrescrever esses manipuladores de exceção com os seus próprios manipuladores. + +## Sobrescreva exceções de validação da requisição + +Quando a requisição contém dados inválidos, **FastAPI** internamente lança para o `RequestValidationError`. + +Para sobrescrevê-lo, importe o `RequestValidationError` e use-o com o `@app.exception_handler(RequestValidationError)` para decorar o manipulador de exceções. + +```Python hl_lines="2 14-16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +Se você for ao `/items/foo`, em vez de receber o JSON padrão com o erro: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +você receberá a versão em texto: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +### `RequestValidationError` vs `ValidationError` + +!!! warning "Aviso" + Você pode pular estes detalhes técnicos caso eles não sejam importantes para você neste momento. + +`RequestValidationError` é uma subclasse do `ValidationError` existente no Pydantic. + +**FastAPI** faz uso dele para que você veja o erro no seu log, caso você utilize um modelo de Pydantic em `response_model`, e seus dados tenham erro. + +Contudo, o cliente ou usuário não terão acesso a ele. Ao contrário, o cliente receberá um "Internal Server Error" com o HTTP status code `500`. + +E assim deve ser porque seria um bug no seu código ter o `ValidationError` do Pydantic na sua *response*, ou em qualquer outro lugar do seu código (que não na requisição do cliente). + +E enquanto você conserta o bug, os clientes / usuários não deveriam ter acesso às informações internas do erro, porque, desse modo, haveria exposição de uma vulnerabilidade de segurança. + +Do mesmo modo, você pode sobreescrever o `HTTPException`. + +Por exemplo, você pode querer retornar uma *response* em *plain text* ao invés de um JSON para os seguintes erros: + +```Python hl_lines="3-4 9-11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "Detalhes Técnicos" + Você pode usar `from starlette.responses import PlainTextResponse`. + + **FastAPI** disponibiliza o mesmo `starlette.responses` como `fastapi.responses`, como conveniência a você, desenvolvedor. Contudo, a maior parte das respostas disponíveis vem diretamente do Starlette. + + +### Use o body do `RequestValidationError`. + +O `RequestValidationError` contém o `body` que ele recebeu de dados inválidos. + +Você pode utilizá-lo enquanto desenvolve seu app para conectar o *body* e debugá-lo, e assim retorná-lo ao usuário, etc. + +Tente enviar um item inválido como este: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +Você receberá uma *response* informando-o de que a data é inválida, e contendo o *body* recebido: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### O `HTTPException` do FastAPI vs o `HTTPException` do Starlette. + +O **FastAPI** tem o seu próprio `HTTPException`. + +E a classe de erro `HTTPException` do **FastAPI** herda da classe de erro do `HTTPException` do Starlette. + +A diferença entre os dois é a de que o `HTTPException` do **FastAPI** permite que você adicione *headers* que serão incluídos nas *responses*. + +Esses *headers* são necessários/utilizados internamente pelo OAuth 2.0 e também por outras utilidades de segurança. + +Portanto, você pode continuar lançando o `HTTPException` do **FastAPI** normalmente no seu código. + +Porém, quando você registrar um manipulador de exceção, você deve registrá-lo através do `HTTPException` do Starlette. + +Dessa forma, se qualquer parte do código interno, extensão ou plug-in do Starlette lançar o `HTTPException`, o seu manipulador de exceção poderá capturar esse lançamento e tratá-lo. + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### Re-use os manipulares de exceção do **FastAPI** + +Se você quer usar a exceção em conjunto com o mesmo manipulador de exceção *default* do **FastAPI**, você pode importar e re-usar esses manipuladores de exceção do `fastapi.exception_handlers`: + +```Python hl_lines="2-5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +Nesse exemplo você apenas imprime (`print`) o erro com uma mensagem expressiva. Mesmo assim, dá para pegar a ideia. Você pode usar a exceção e então apenas re-usar o manipulador de exceção *default*. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 3fb834bed..5b1bdb45e 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -69,6 +69,7 @@ nav: - tutorial/extra-data-types.md - tutorial/query-params-str-validations.md - tutorial/cookie-params.md + - tutorial/handling-errors.md - Segurança: - tutorial/security/index.md - Guia de Usuário Avançado: From f9dcff1490bf3887d988589798ab42015d228d00 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:02:28 +0000 Subject: [PATCH 0334/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bad8aca0d..784917dc7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/background-tasks.md`. PR [#4854](https://github.com/tiangolo/fastapi/pull/4854) by [@AdmiralDesu](https://github.com/AdmiralDesu). * 🌐 Add Chinese translation for `docs/tutorial/security/first-steps.md`. PR [#3841](https://github.com/tiangolo/fastapi/pull/3841) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/nosql-databases.md`. PR [#4205](https://github.com/tiangolo/fastapi/pull/4205) by [@sUeharaE4](https://github.com/sUeharaE4). From e5eb56f0b536f96dd618d3dacac1772f0f53623c Mon Sep 17 00:00:00 2001 From: Ruidy Date: Thu, 18 Aug 2022 18:02:40 +0200 Subject: [PATCH 0335/1881] =?UTF-8?q?=F0=9F=8C=90=20=20Add=20French=20tran?= =?UTF-8?q?slation=20for=20`docs/fr/docs/deployment/index.md`=20(#3689)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Arthur Rio Co-authored-by: Ruidy Co-authored-by: Ruidy Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/fr/docs/deployment/index.md | 28 ++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 29 insertions(+) create mode 100644 docs/fr/docs/deployment/index.md diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md new file mode 100644 index 000000000..e855adfa3 --- /dev/null +++ b/docs/fr/docs/deployment/index.md @@ -0,0 +1,28 @@ +# Déploiement - Intro + +Le déploiement d'une application **FastAPI** est relativement simple. + +## Que signifie le déploiement + +**Déployer** une application signifie effectuer les étapes nécessaires pour la rendre **disponible pour les +utilisateurs**. + +Pour une **API Web**, cela implique normalement de la placer sur une **machine distante**, avec un **programme serveur** +qui offre de bonnes performances, une bonne stabilité, _etc._, afin que vos **utilisateurs** puissent **accéder** à +l'application efficacement et sans interruption ni problème. + +Ceci contraste avec les étapes de **développement**, où vous êtes constamment en train de modifier le code, de le casser +et de le réparer, d'arrêter et de redémarrer le serveur de développement, _etc._ + +## Stratégies de déploiement + +Il existe plusieurs façons de procéder, en fonction de votre cas d'utilisation spécifique et des outils que vous +utilisez. + +Vous pouvez **déployer un serveur** vous-même en utilisant une combinaison d'outils, vous pouvez utiliser un **service +cloud** qui fait une partie du travail pour vous, ou encore d'autres options possibles. + +Je vais vous montrer certains des principaux concepts que vous devriez probablement avoir à l'esprit lors du déploiement +d'une application **FastAPI** (bien que la plupart de ces concepts s'appliquent à tout autre type d'application web). + +Vous verrez plus de détails à avoir en tête et certaines des techniques pour le faire dans les sections suivantes. ✨ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 32ce30ef6..6bed7be73 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -69,6 +69,7 @@ nav: - tutorial/background-tasks.md - async.md - Déploiement: + - deployment/index.md - deployment/docker.md - project-generation.md - alternatives.md From d268eb201136d5e1ba188f5e9cff9b201aabb5cc Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:03:07 +0000 Subject: [PATCH 0336/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 784917dc7..be1641443 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/history-design-future.md`. PR [#3451](https://github.com/tiangolo/fastapi/pull/3451) by [@rjNemo](https://github.com/rjNemo). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/background-tasks.md`. PR [#4854](https://github.com/tiangolo/fastapi/pull/4854) by [@AdmiralDesu](https://github.com/AdmiralDesu). * 🌐 Add Chinese translation for `docs/tutorial/security/first-steps.md`. PR [#3841](https://github.com/tiangolo/fastapi/pull/3841) by [@jaystone776](https://github.com/jaystone776). From 46f5091c9f2e3c029d4aaecb43f95d5e01df6f30 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 18 Aug 2022 18:04:33 +0200 Subject: [PATCH 0337/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`python?= =?UTF-8?q?-types.md`=20(#5116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/python-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index f170bb1ef..3b0ee4cf6 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -326,7 +326,7 @@ If you are using a Python version below 3.10, here's a tip from my very **subjec Both are equivalent and underneath they are the same, but I would recommend `Union` instead of `Optional` because the word "**optional**" would seem to imply that the value is optional, and it actually means "it can be `None`", even if it's not optional and is still required. -I think `Union[str, SomeType]` is more explicit about what it means. +I think `Union[SomeType, None]` is more explicit about what it means. It's just about the words and names. But those words can affect how you and your teammates think about the code. From 441b72653757d5a4e3fb77ff2ed1f5e939b4c462 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:05:11 +0000 Subject: [PATCH 0338/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 be1641443..b57fa78bd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `tutorial/handling-errors.md`. PR [#4769](https://github.com/tiangolo/fastapi/pull/4769) by [@frnsimoes](https://github.com/frnsimoes). * 🌐 Add French translation for `docs/fr/docs/history-design-future.md`. PR [#3451](https://github.com/tiangolo/fastapi/pull/3451) by [@rjNemo](https://github.com/rjNemo). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/background-tasks.md`. PR [#4854](https://github.com/tiangolo/fastapi/pull/4854) by [@AdmiralDesu](https://github.com/AdmiralDesu). From 8afd291572c72d54e9daa353d0ccd0fc87cbb42f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:05:42 +0000 Subject: [PATCH 0339/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b57fa78bd..8dfa6d146 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/deployment/index.md`. PR [#3689](https://github.com/tiangolo/fastapi/pull/3689) by [@rjNemo](https://github.com/rjNemo). * 🌐 Add Portuguese translation for `tutorial/handling-errors.md`. PR [#4769](https://github.com/tiangolo/fastapi/pull/4769) by [@frnsimoes](https://github.com/frnsimoes). * 🌐 Add French translation for `docs/fr/docs/history-design-future.md`. PR [#3451](https://github.com/tiangolo/fastapi/pull/3451) by [@rjNemo](https://github.com/rjNemo). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). From e90d53f6f4c455a4f2787a02a8f08b94b2b80cc0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:06:52 +0000 Subject: [PATCH 0340/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8dfa6d146..aca99264b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). * 🌐 Add French translation for `docs/fr/docs/deployment/index.md`. PR [#3689](https://github.com/tiangolo/fastapi/pull/3689) by [@rjNemo](https://github.com/rjNemo). * 🌐 Add Portuguese translation for `tutorial/handling-errors.md`. PR [#4769](https://github.com/tiangolo/fastapi/pull/4769) by [@frnsimoes](https://github.com/frnsimoes). * 🌐 Add French translation for `docs/fr/docs/history-design-future.md`. PR [#3451](https://github.com/tiangolo/fastapi/pull/3451) by [@rjNemo](https://github.com/rjNemo). From fb849ee7ef623c4a6f581a78d46b4d4bc583d189 Mon Sep 17 00:00:00 2001 From: zhangbo Date: Fri, 19 Aug 2022 00:09:38 +0800 Subject: [PATCH 0341/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20translation=20fo?= =?UTF-8?q?r=20`docs/zh/docs/advanced/response-cookies.md`=20(#4638)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zhangbo --- docs/zh/docs/advanced/response-cookies.md | 47 +++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 48 insertions(+) create mode 100644 docs/zh/docs/advanced/response-cookies.md diff --git a/docs/zh/docs/advanced/response-cookies.md b/docs/zh/docs/advanced/response-cookies.md new file mode 100644 index 000000000..3e53c5319 --- /dev/null +++ b/docs/zh/docs/advanced/response-cookies.md @@ -0,0 +1,47 @@ +# 响应Cookies + +## 使用 `Response` 参数 + +你可以在 *路径函数* 中定义一个类型为 `Response`的参数,这样你就可以在这个临时响应对象中设置cookie了。 + +```Python hl_lines="1 8-9" +{!../../../docs_src/response_cookies/tutorial002.py!} +``` + +而且你还可以根据你的需要响应不同的对象,比如常用的 `dict`,数据库model等。 + +如果你定义了 `response_model`,程序会自动根据`response_model`来过滤和转换你响应的对象。 + +**FastAPI** 会使用这个 *临时* 响应对象去装在这些cookies信息 (同样还有headers和状态码等信息), 最终会将这些信息和通过`response_model`转化过的数据合并到最终的响应里。 + +你也可以在depend中定义`Response`参数,并设置cookie和header。 + +## 直接响应 `Response` + +你还可以在直接响应`Response`时直接创建cookies。 + +你可以参考[Return a Response Directly](response-directly.md){.internal-link target=_blank}来创建response + +然后设置Cookies,并返回: + +```Python hl_lines="10-12" +{!../../../docs_src/response_cookies/tutorial001.py!} +``` + +!!! tip + 需要注意,如果你直接反馈一个response对象,而不是使用`Response`入参,FastAPI则会直接反馈你封装的response对象。 + + 所以你需要确保你响应数据类型的正确性,如:你可以使用`JSONResponse`来兼容JSON的场景。 + + 同时,你也应当仅反馈通过`response_model`过滤过的数据。 + +### 更多信息 + +!!! note "技术细节" + 你也可以使用`from starlette.responses import Response` 或者 `from starlette.responses import JSONResponse`。 + + 为了方便开发者,**FastAPI** 封装了相同数据类型,如`starlette.responses` 和 `fastapi.responses`。不过大部分response对象都是直接引用自Starlette。 + + 因为`Response`对象可以非常便捷的设置headers和cookies,所以 **FastAPI** 同时也封装了`fastapi.Response`。 + +如果你想查看所有可用的参数和选项,可以参考 Starlette帮助文档 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 9e8813d6c..b60a0b7a1 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -107,6 +107,7 @@ nav: - advanced/additional-status-codes.md - advanced/response-directly.md - advanced/custom-response.md + - advanced/response-cookies.md - contributing.md - help-fastapi.md - benchmarks.md From f79e6a48a19ef79d1b2368acadecd07ea309dcc5 Mon Sep 17 00:00:00 2001 From: Vini Sousa <101301034+FLAIR7@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:10:25 -0300 Subject: [PATCH 0342/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/security/first-steps.md?= =?UTF-8?q?`=20(#4954)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/pt/docs/tutorial/security/first-steps.md | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 docs/pt/docs/tutorial/security/first-steps.md diff --git a/docs/pt/docs/tutorial/security/first-steps.md b/docs/pt/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..ed07d1c96 --- /dev/null +++ b/docs/pt/docs/tutorial/security/first-steps.md @@ -0,0 +1,181 @@ +# Segurança - Primeiros Passos + +Vamos imaginar que você tem a sua API **backend** em algum domínio. + +E você tem um **frontend** em outro domínio ou em um path diferente no mesmo domínio (ou em uma aplicação mobile). + +E você quer uma maneira de o frontend autenticar o backend, usando um **username** e **senha**. + +Nós podemos usar o **OAuth2** junto com o **FastAPI**. + +Mas, vamos poupar-lhe o tempo de ler toda a especificação apenas para achar as pequenas informações que você precisa. + +Vamos usar as ferramentas fornecidas pela **FastAPI** para lidar com segurança. + +## Como Parece + +Vamos primeiro usar o código e ver como funciona, e depois voltaremos para entender o que está acontecendo. + +## Crie um `main.py` +Copie o exemplo em um arquivo `main.py`: + +```Python +{!../../../docs_src/security/tutorial001.py!} +``` + +## Execute-o + +!!! informação + Primeiro, instale `python-multipart`. + + Ex: `pip install python-multipart`. + + Isso ocorre porque o **OAuth2** usa "dados de um formulário" para mandar o **username** e **senha**. + +Execute esse exemplo com: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## Verifique-o + +Vá até a documentação interativa em: http://127.0.0.1:8000/docs. + +Você verá algo deste tipo: + + + +!!! marque o "botão de Autorizar!" + Você já tem um novo "botão de autorizar!". + + E seu *path operation* tem um pequeno cadeado no canto superior direito que você pode clicar. + +E se você clicar, você terá um pequeno formulário de autorização para digitar o `username` e `senha` (e outros campos opcionais): + + + +!!! nota + Não importa o que você digita no formulário, não vai funcionar ainda. Mas nós vamos chegar lá. + +Claro que este não é o frontend para os usuários finais, mas é uma ótima ferramenta automática para documentar interativamente toda sua API. + +Pode ser usado pelo time de frontend (que pode ser você no caso). + +Pode ser usado por aplicações e sistemas third party (de terceiros). + +E também pode ser usada por você mesmo, para debugar, checar e testar a mesma aplicação. + +## O Fluxo da `senha` + +Agora vamos voltar um pouco e entender o que é isso tudo. + +O "fluxo" da `senha` é um dos caminhos ("fluxos") definidos no OAuth2, para lidar com a segurança e autenticação. + +OAuth2 foi projetado para que o backend ou a API pudesse ser independente do servidor que autentica o usuário. + +Mas nesse caso, a mesma aplicação **FastAPI** irá lidar com a API e a autenticação. + +Então, vamos rever de um ponto de vista simplificado: + +* O usuário digita o `username` e a `senha` no frontend e aperta `Enter`. +* O frontend (rodando no browser do usuário) manda o `username` e a `senha` para uma URL específica na sua API (declarada com `tokenUrl="token"`). +* A API checa aquele `username` e `senha`, e responde com um "token" (nós não implementamos nada disso ainda). + * Um "token" é apenas uma string com algum conteúdo que nós podemos utilizar mais tarde para verificar o usuário. + * Normalmente, um token é definido para expirar depois de um tempo. + * Então, o usuário terá que se logar de novo depois de um tempo. + * E se o token for roubado, o risco é menor. Não é como se fosse uma chave permanente que vai funcionar para sempre (na maioria dos casos). + * O frontend armazena aquele token temporariamente em algum lugar. + * O usuário clica no frontend para ir à outra seção daquele frontend do aplicativo web. + * O frontend precisa buscar mais dados daquela API. + * Mas precisa de autenticação para aquele endpoint em específico. + * Então, para autenticar com nossa API, ele manda um header de `Autorização` com o valor `Bearer` mais o token. + * Se o token contém `foobar`, o conteúdo do header de `Autorização` será: `Bearer foobar`. + +## **FastAPI**'s `OAuth2PasswordBearer` + +**FastAPI** fornece várias ferramentas, em diferentes níveis de abstração, para implementar esses recursos de segurança. + +Neste exemplo, nós vamos usar o **OAuth2** com o fluxo de **Senha**, usando um token **Bearer**. Fazemos isso usando a classe `OAuth2PasswordBearer`. + +!!! informação + Um token "bearer" não é a única opção. + + Mas é a melhor no nosso caso. + + E talvez seja a melhor para a maioria dos casos, a não ser que você seja um especialista em OAuth2 e saiba exatamente o porquê de existir outras opções que se adequam melhor às suas necessidades. + + Nesse caso, **FastAPI** também fornece as ferramentas para construir. + +Quando nós criamos uma instância da classe `OAuth2PasswordBearer`, nós passamos pelo parâmetro `tokenUrl` Esse parâmetro contém a URL que o client (o frontend rodando no browser do usuário) vai usar para mandar o `username` e `senha` para obter um token. + +```Python hl_lines="6" +{!../../../docs_src/security/tutorial001.py!} +``` + +!!! dica + Esse `tokenUrl="token"` se refere a uma URL relativa que nós não criamos ainda. Como é uma URL relativa, é equivalente a `./token`. + + Porque estamos usando uma URL relativa, se sua API estava localizada em `https://example.com/`, então irá referir-se à `https://example.com/token`. Mas se sua API estava localizada em `https://example.com/api/v1/`, então irá referir-se à `https://example.com/api/v1/token`. + + Usar uma URL relativa é importante para garantir que sua aplicação continue funcionando, mesmo em um uso avançado tipo [Atrás de um Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. + +Esse parâmetro não cria um endpoint / *path operation*, mas declara que a URL `/token` vai ser aquela que o client deve usar para obter o token. Essa informação é usada no OpenAPI, e depois na API Interativa de documentação de sistemas. + +Em breve também criaremos o atual path operation. + +!!! informação + Se você é um "Pythonista" muito rigoroso, você pode não gostar do estilo do nome do parâmetro `tokenUrl` em vez de `token_url`. + + Isso ocorre porque está utilizando o mesmo nome que está nas especificações do OpenAPI. Então, se você precisa investigar mais sobre qualquer um desses esquemas de segurança, você pode simplesmente copiar e colar para encontrar mais informações sobre isso. + +A variável `oauth2_scheme` é um instância de `OAuth2PasswordBearer`, mas também é um "callable". + +Pode ser chamada de: + +```Python +oauth2_scheme(some, parameters) +``` + +Então, pode ser usado com `Depends`. + +## Use-o + +Agora você pode passar aquele `oauth2_scheme` em uma dependência com `Depends`. + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +Esse dependência vai fornecer uma `str` que é atribuído ao parâmetro `token da *função do path operation* + +A **FastAPI** saberá que pode usar essa dependência para definir um "esquema de segurança" no esquema da OpenAPI (e na documentação da API automática). + +!!! informação "Detalhes técnicos" + **FastAPI** saberá que pode usar a classe `OAuth2PasswordBearer` (declarada na dependência) para definir o esquema de segurança na OpenAPI porque herda de `fastapi.security.oauth2.OAuth2`, que por sua vez herda de `fastapi.security.base.Securitybase`. + + Todos os utilitários de segurança que se integram com OpenAPI (e na documentação da API automática) herdam de `SecurityBase`, é assim que **FastAPI** pode saber como integrá-los no OpenAPI. + +## O que ele faz + +Ele irá e olhará na requisição para aquele header de `Autorização`, verificará se o valor é `Bearer` mais algum token, e vai retornar o token como uma `str` + +Se ele não ver o header de `Autorização` ou o valor não tem um token `Bearer`, vai responder com um código de erro 401 (`UNAUTHORIZED`) diretamente. + +Você nem precisa verificar se o token existe para retornar um erro. Você pode ter certeza de que se a sua função for executada, ela terá um `str` nesse token. + +Você já pode experimentar na documentação interativa: + + + +Não estamos verificando a validade do token ainda, mas isso já é um começo + +## Recapitulando + +Então, em apenas 3 ou 4 linhas extras, você já tem alguma forma primitiva de segurança. From a9c2b8336b980da91e5d2a754a00c6e2fffed4d0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:11:55 +0000 Subject: [PATCH 0343/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 aca99264b..0d2ed3d07 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add translation for `docs/zh/docs/advanced/response-cookies.md`. PR [#4638](https://github.com/tiangolo/fastapi/pull/4638) by [@zhangbo2012](https://github.com/zhangbo2012). * ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). * 🌐 Add French translation for `docs/fr/docs/deployment/index.md`. PR [#3689](https://github.com/tiangolo/fastapi/pull/3689) by [@rjNemo](https://github.com/rjNemo). * 🌐 Add Portuguese translation for `tutorial/handling-errors.md`. PR [#4769](https://github.com/tiangolo/fastapi/pull/4769) by [@frnsimoes](https://github.com/frnsimoes). From 30e7b6a34cf41c927979c595e90329acf9ecca7d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:12:24 +0000 Subject: [PATCH 0344/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0d2ed3d07..88ec5566b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/first-steps.md`. PR [#4954](https://github.com/tiangolo/fastapi/pull/4954) by [@FLAIR7](https://github.com/FLAIR7). * 🌐 Add translation for `docs/zh/docs/advanced/response-cookies.md`. PR [#4638](https://github.com/tiangolo/fastapi/pull/4638) by [@zhangbo2012](https://github.com/zhangbo2012). * ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). * 🌐 Add French translation for `docs/fr/docs/deployment/index.md`. PR [#3689](https://github.com/tiangolo/fastapi/pull/3689) by [@rjNemo](https://github.com/rjNemo). From 73c6011ec47ebc2b08fd2b05c3a4acf03155d818 Mon Sep 17 00:00:00 2001 From: Bruno Artur Torres Lopes Pereira <33462923+batlopes@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:13:12 -0300 Subject: [PATCH 0345/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/query-params.md`=20(#47?= =?UTF-8?q?75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/pt/docs/tutorial/query-params.md | 224 ++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 225 insertions(+) create mode 100644 docs/pt/docs/tutorial/query-params.md diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md new file mode 100644 index 000000000..189724396 --- /dev/null +++ b/docs/pt/docs/tutorial/query-params.md @@ -0,0 +1,224 @@ +# Parâmetros de Consulta + +Quando você declara outros parâmetros na função que não fazem parte dos parâmetros da rota, esses parâmetros são automaticamente interpretados como parâmetros de "consulta". + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +A consulta é o conjunto de pares chave-valor que vai depois de `?` na URL, separado pelo caractere `&`. + +Por exemplo, na URL: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...os parâmetros da consulta são: + +* `skip`: com o valor `0` +* `limit`: com o valor `10` + +Como eles são parte da URL, eles são "naturalmente" strings. + +Mas quando você declara eles com os tipos do Python (no exemplo acima, como `int`), eles são convertidos para aquele tipo e validados em relação a ele. + +Todo o processo que era aplicado para parâmetros de rota também é aplicado para parâmetros de consulta: + +* Suporte do editor (obviamente) +* "Parsing" de dados +* Validação de dados +* Documentação automática + +## Valores padrão + +Como os parâmetros de consulta não são uma parte fixa da rota, eles podem ser opcionais e podem ter valores padrão. + +No exemplo acima eles tem valores padrão de `skip=0` e `limit=10`. + +Então, se você for até a URL: + +``` +http://127.0.0.1:8000/items/ +``` + +Seria o mesmo que ir para: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +Mas, se por exemplo você for para: + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +Os valores dos parâmetros na sua função serão: + +* `skip=20`: Por que você definiu isso na URL +* `limit=10`: Por que esse era o valor padrão + +## Parâmetros opcionais + +Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo o valor padrão para `None`: + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrão. + +!!! check "Verificar" + Você também pode notar que o **FastAPI** é esperto o suficiente para perceber que o parâmetro da rota `item_id` é um parâmetro da rota, e `q` não é, portanto, `q` é o parâmetro de consulta. + + +## Conversão dos tipos de parâmetros de consulta + +Você também pode declarar tipos `bool`, e eles serão convertidos: + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` + +Nesse caso, se você for para: + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +ou + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +ou qualquer outra variação (tudo em maiúscula, primeira letra em maiúscula, etc), a sua função vai ver o parâmetro `short` com um valor `bool` de `True`. Caso contrário `False`. + +## Múltiplos parâmetros de rota e consulta + +Você pode declarar múltiplos parâmetros de rota e parâmetros de consulta ao mesmo tempo, o **FastAPI** vai saber o quê é o quê. + +E você não precisa declarar eles em nenhuma ordem específica. + +Eles serão detectados pelo nome: + +=== "Python 3.6 and above" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +## Parâmetros de consulta obrigatórios + +Quando você declara um valor padrão para parâmetros que não são de rota (até agora, nós vimos apenas parâmetros de consulta), então eles não são obrigatórios. + +Caso você não queira adicionar um valor específico mas queira apenas torná-lo opcional, defina o valor padrão como `None`. + +Porém, quando você quiser fazer com que o parâmetro de consulta seja obrigatório, você pode simplesmente não declarar nenhum valor como padrão. + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +Aqui o parâmetro de consulta `needy` é um valor obrigatório, do tipo `str`. + +Se você abrir no seu navegador a URL: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +... sem adicionar o parâmetro obrigatório `needy`, você verá um erro como: + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +Como `needy` é um parâmetro obrigatório, você precisaria defini-lo na URL: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...isso deve funcionar: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +E claro, você pode definir alguns parâmetros como obrigatórios, alguns possuindo um valor padrão, e outros sendo totalmente opcionais: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` +Nesse caso, existem 3 parâmetros de consulta: + +* `needy`, um `str` obrigatório. +* `skip`, um `int` com o valor padrão `0`. +* `limit`, um `int` opcional. + +!!! tip "Dica" + Você também poderia usar `Enum` da mesma forma que com [Path Parameters](path-params.md#predefined-values){.internal-link target=_blank}. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 5b1bdb45e..c37b855d8 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -64,6 +64,7 @@ nav: - tutorial/index.md - tutorial/first-steps.md - tutorial/path-params.md + - tutorial/query-params.md - tutorial/body.md - tutorial/body-fields.md - tutorial/extra-data-types.md From 58848897f49ac61d0ac7fdabc473e1c93eb13950 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 16:15:37 +0000 Subject: [PATCH 0346/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 88ec5566b..0be6db642 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/query-params.md`. PR [#4775](https://github.com/tiangolo/fastapi/pull/4775) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/first-steps.md`. PR [#4954](https://github.com/tiangolo/fastapi/pull/4954) by [@FLAIR7](https://github.com/FLAIR7). * 🌐 Add translation for `docs/zh/docs/advanced/response-cookies.md`. PR [#4638](https://github.com/tiangolo/fastapi/pull/4638) by [@zhangbo2012](https://github.com/zhangbo2012). * ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). From 38802eefeb7c1ef8c80373a7a07a7cb402946228 Mon Sep 17 00:00:00 2001 From: Yumihiki <41376154+Yumihiki@users.noreply.github.com> Date: Fri, 19 Aug 2022 01:16:54 +0900 Subject: [PATCH 0347/1881] =?UTF-8?q?=F0=9F=93=9D=20Quote=20`pip=20install?= =?UTF-8?q?`=20command=20for=20Zsh=20compatibility=20in=20multiple=20langu?= =?UTF-8?q?ages=20(#5214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/de/docs/index.md | 2 +- docs/es/docs/index.md | 2 +- docs/es/docs/tutorial/index.md | 4 ++-- docs/fr/docs/index.md | 2 +- docs/id/docs/index.md | 2 +- docs/it/docs/index.md | 2 +- docs/ja/docs/deployment/manually.md | 2 +- docs/ja/docs/index.md | 2 +- docs/ja/docs/tutorial/index.md | 4 ++-- docs/ko/docs/index.md | 2 +- docs/ko/docs/tutorial/index.md | 2 +- docs/pl/docs/index.md | 2 +- docs/pt/docs/deployment.md | 2 +- docs/pt/docs/index.md | 2 +- docs/pt/docs/tutorial/index.md | 4 ++-- docs/ru/docs/index.md | 2 +- docs/sq/docs/index.md | 2 +- docs/tr/docs/index.md | 2 +- docs/uk/docs/index.md | 2 +- docs/zh/docs/index.md | 2 +- docs/zh/docs/tutorial/index.md | 4 ++-- 21 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 929754462..287c79cff 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index ef4850b56..44c59202a 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -130,7 +130,7 @@ También vas a necesitar un servidor ASGI para producción cómo ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md index 9cbdb2727..e3671f381 100644 --- a/docs/es/docs/tutorial/index.md +++ b/docs/es/docs/tutorial/index.md @@ -41,7 +41,7 @@ Para el tutorial, es posible que quieras instalarlo con todas las dependencias y
```console -$ pip install fastapi[all] +$ pip install "fastapi[all]" ---> 100% ``` @@ -62,7 +62,7 @@ $ pip install fastapi[all] También debes instalar `uvicorn` para que funcione como tu servidor: ``` - pip install uvicorn[standard] + pip install "uvicorn[standard]" ``` Y lo mismo para cada una de las dependencias opcionales que quieras utilizar. diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index f713ee96b..1307c063b 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md index 0bb7b55e3..041e0b754 100644 --- a/docs/id/docs/index.md +++ b/docs/id/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md index 6acf92552..5cbe78a71 100644 --- a/docs/it/docs/index.md +++ b/docs/it/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/ja/docs/deployment/manually.md b/docs/ja/docs/deployment/manually.md index dd4b568bd..67010a66f 100644 --- a/docs/ja/docs/deployment/manually.md +++ b/docs/ja/docs/deployment/manually.md @@ -11,7 +11,7 @@
```console - $ pip install uvicorn[standard] + $ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index 5fca78a83..51977037c 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -131,7 +131,7 @@ $ pip install fastapi
```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index 29fc86f94..a2dd59c9b 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -43,7 +43,7 @@ $ uvicorn main:app --reload
```console -$ pip install fastapi[all] +$ pip install "fastapi[all]" ---> 100% ``` @@ -64,7 +64,7 @@ $ pip install fastapi[all] また、サーバーとして動作するように`uvicorn` をインストールします: ``` - pip install uvicorn[standard] + pip install "uvicorn[standard]" ``` そして、使用したい依存関係をそれぞれ同様にインストールします。 diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index ec4422994..5a258f47b 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -131,7 +131,7 @@ $ pip install fastapi
```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index 622aad1aa..d6db525e8 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -43,7 +43,7 @@ $ uvicorn main:app --reload
```console -$ pip install fastapi[all] +$ pip install "fastapi[all]" ---> 100% ``` diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index bbe1b1ad1..9cc99ba72 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -130,7 +130,7 @@ Na serwerze produkcyjnym będziesz także potrzebował serwera ASGI, np. ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/pt/docs/deployment.md b/docs/pt/docs/deployment.md index cd820cbd3..2272467fd 100644 --- a/docs/pt/docs/deployment.md +++ b/docs/pt/docs/deployment.md @@ -336,7 +336,7 @@ Você apenas precisa instalar um servidor ASGI compatível como:
```console - $ pip install uvicorn[standard] + $ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index b1d0c89f2..51af486b9 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -124,7 +124,7 @@ Você também precisará de um servidor ASGI para produção, tal como ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index f93fd8d75..b1abd32bc 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -43,7 +43,7 @@ Para o tutorial, você deve querer instalá-lo com todas as dependências e recu
```console -$ pip install fastapi[all] +$ pip install "fastapi[all]" ---> 100% ``` @@ -64,7 +64,7 @@ $ pip install fastapi[all] Também instale o `uvicorn` para funcionar como servidor: ``` - pip install uvicorn[standard] + pip install "uvicorn[standard]" ``` E o mesmo para cada dependência opcional que você quiser usar. diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 9a3957d5f..932bb2139 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md index 29f92e020..2b64003fe 100644 --- a/docs/sq/docs/index.md +++ b/docs/sq/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 5693029b5..0d5337c87 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -143,7 +143,7 @@ Uygulamanı kullanılabilir hale getirmek için ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 29f92e020..2b64003fe 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -135,7 +135,7 @@ You will also need an ASGI server, for production such as ```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 797032c86..3898beaee 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -131,7 +131,7 @@ $ pip install fastapi
```console -$ pip install uvicorn[standard] +$ pip install "uvicorn[standard]" ---> 100% ``` diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md index 36495ec0b..6093caeb6 100644 --- a/docs/zh/docs/tutorial/index.md +++ b/docs/zh/docs/tutorial/index.md @@ -43,7 +43,7 @@ $ uvicorn main:app --reload
```console -$ pip install fastapi[all] +$ pip install "fastapi[all]" ---> 100% ``` @@ -64,7 +64,7 @@ $ pip install fastapi[all] 并且安装`uvicorn`来作为服务器: ``` - pip install uvicorn[standard] + pip install "uvicorn[standard]" ``` 然后对你想使用的每个可选依赖项也执行相同的操作。 From ea017c99198c73f0e4a67a758bc5113e2cca6102 Mon Sep 17 00:00:00 2001 From: Teofilo Zosa Date: Fri, 19 Aug 2022 05:12:34 +0900 Subject: [PATCH 0348/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20misc=20dependenc?= =?UTF-8?q?y=20installs=20to=20tutorial=20docs=20(#2126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/websockets.md | 14 ++++++++++++++ docs/en/docs/tutorial/response-model.md | 6 ++++++ docs/en/docs/tutorial/sql-databases.md | 14 ++++++++++++++ docs/en/docs/tutorial/testing.md | 5 +++++ 4 files changed, 39 insertions(+) diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 878ad37dd..0e9bc5b06 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -2,6 +2,20 @@ You can use WebSockets with **FastAPI**. +## Install `WebSockets` + +First you need to install `WebSockets`: + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ ## WebSockets client ### In production diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index e371e86e4..2bbd4d4fd 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -61,6 +61,12 @@ Here we are declaring a `UserIn` model, it will contain a plaintext password: {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` +!!! info + To use `EmailStr`, first install `email_validator`. + + E.g. `pip install email-validator` + or `pip install pydantic[email]`. + And we are using this model to declare our input and the same model to declare our output: === "Python 3.6 and above" diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 3436543a5..5ccaf05ec 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -80,6 +80,20 @@ The file `__init__.py` is just an empty file, but it tells Python that `sql_app` Now let's see what each file/module does. +## Install `SQLAlchemy` + +First you need to install `SQLAlchemy`: + +
+ +```console +$ pip install sqlalchemy + +---> 100% +``` + +
+ ## Create the SQLAlchemy parts Let's refer to the file `sql_app/database.py`. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index fea5a54f5..79ea2b1ab 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -8,6 +8,11 @@ With it, you can use `requests`. + + E.g. `pip install requests`. + Import `TestClient`. Create a `TestClient` by passing your **FastAPI** application to it. From 3a3b61dc6e26717f824ffa4c7ace999ad96c07f0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 20:13:14 +0000 Subject: [PATCH 0349/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0be6db642..b2e73d4a3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add misc dependency installs to tutorial docs. PR [#2126](https://github.com/tiangolo/fastapi/pull/2126) by [@TeoZosa](https://github.com/TeoZosa). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/query-params.md`. PR [#4775](https://github.com/tiangolo/fastapi/pull/4775) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/first-steps.md`. PR [#4954](https://github.com/tiangolo/fastapi/pull/4954) by [@FLAIR7](https://github.com/FLAIR7). * 🌐 Add translation for `docs/zh/docs/advanced/response-cookies.md`. PR [#4638](https://github.com/tiangolo/fastapi/pull/4638) by [@zhangbo2012](https://github.com/zhangbo2012). From e88089ec210dadb06311377ab203eabfbda3d830 Mon Sep 17 00:00:00 2001 From: Luca Repetti <39653693+klaa97@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:31:19 +0200 Subject: [PATCH 0350/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20edge=20case=20wi?= =?UTF-8?q?th=20repeated=20aliases=20names=20not=20shown=20in=20OpenAPI=20?= =?UTF-8?q?(#2351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/openapi/utils.py | 4 +- tests/test_repeated_parameter_alias.py | 100 +++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 tests/test_repeated_parameter_alias.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 5d3d95c24..4d5741f30 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -223,7 +223,9 @@ def get_openapi_path( parameters.extend(operation_parameters) if parameters: operation["parameters"] = list( - {param["name"]: param for param in parameters}.values() + { + (param["in"], param["name"]): param for param in parameters + }.values() ) if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py new file mode 100644 index 000000000..823f53a95 --- /dev/null +++ b/tests/test_repeated_parameter_alias.py @@ -0,0 +1,100 @@ +from fastapi import FastAPI, Path, Query, status +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get("/{repeated_alias}") +def get_parameters_with_repeated_aliases( + path: str = Path(..., alias="repeated_alias"), + query: str = Query(..., alias="repeated_alias"), +): + return {"path": path, "query": query} + + +client = TestClient(app) + +openapi_schema = { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.0.2", + "paths": { + "/{repeated_alias}": { + "get": { + "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", + "parameters": [ + { + "in": "path", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + { + "in": "query", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Parameters With Repeated Aliases", + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == status.HTTP_200_OK + actual_schema = response.json() + assert actual_schema == openapi_schema + + +def test_get_parameters(): + response = client.get("/test_path", params={"repeated_alias": "test_query"}) + assert response.status_code == 200, response.text + assert response.json() == {"path": "test_path", "query": "test_query"} From 8a9a117ec7f58711dc68d6d6633dd02d4289cd1d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 20:31:57 +0000 Subject: [PATCH 0351/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b2e73d4a3..cbe368772 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix edge case with repeated aliases names not shown in OpenAPI. PR [#2351](https://github.com/tiangolo/fastapi/pull/2351) by [@klaa97](https://github.com/klaa97). * 📝 Add misc dependency installs to tutorial docs. PR [#2126](https://github.com/tiangolo/fastapi/pull/2126) by [@TeoZosa](https://github.com/TeoZosa). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/query-params.md`. PR [#4775](https://github.com/tiangolo/fastapi/pull/4775) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/first-steps.md`. PR [#4954](https://github.com/tiangolo/fastapi/pull/4954) by [@FLAIR7](https://github.com/FLAIR7). From eb2e1833612e67c9b9ffd2bb446a672d3dffccf4 Mon Sep 17 00:00:00 2001 From: Xavi Moreno Date: Fri, 19 Aug 2022 06:48:21 +1000 Subject: [PATCH 0352/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`jsonable=5Fenco?= =?UTF-8?q?der`=20using=20`include`=20and=20`exclude`=20parameters=20for?= =?UTF-8?q?=20non-Pydantic=20objects=20(#2606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/encoders.py | 9 ++++++++- tests/test_jsonable_encoder.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 4b7ffe313..b1fde73ce 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -80,6 +80,11 @@ def jsonable_encoder( return obj if isinstance(obj, dict): encoded_dict = {} + allowed_keys = set(obj.keys()) + if include is not None: + allowed_keys &= set(include) + if exclude is not None: + allowed_keys -= set(exclude) for key, value in obj.items(): if ( ( @@ -88,7 +93,7 @@ def jsonable_encoder( or (not key.startswith("_sa")) ) and (value is not None or not exclude_none) - and ((include and key in include) or not exclude or key not in exclude) + and key in allowed_keys ): encoded_key = jsonable_encoder( key, @@ -144,6 +149,8 @@ def jsonable_encoder( raise ValueError(errors) return jsonable_encoder( data, + include=include, + exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index ed35fd32e..5e55f2f91 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -93,16 +93,42 @@ def fixture_model_with_path(request): return ModelWithPath(path=request.param("/foo", "bar")) +def test_encode_dict(): + pet = {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, include={}) == {} + assert jsonable_encoder(pet, exclude={}) == { + "name": "Firulais", + "owner": {"name": "Foo"}, + } + + def test_encode_class(): person = Person(name="Foo") pet = Pet(owner=person, name="Firulais") assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, include={}) == {} + assert jsonable_encoder(pet, exclude={}) == { + "name": "Firulais", + "owner": {"name": "Foo"}, + } def test_encode_dictable(): person = DictablePerson(name="Foo") pet = DictablePet(owner=person, name="Firulais") assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} + assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} + assert jsonable_encoder(pet, include={}) == {} + assert jsonable_encoder(pet, exclude={}) == { + "name": "Firulais", + "owner": {"name": "Foo"}, + } def test_encode_unsupported(): @@ -144,6 +170,14 @@ def test_encode_model_with_default(): assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { "foo": "foo" } + assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} + assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"} + assert jsonable_encoder(model, include={}) == {} + assert jsonable_encoder(model, exclude={}) == { + "foo": "foo", + "bar": "bar", + "bla": "bla", + } def test_custom_encoders(): From 74638fcf7cbdc836d46cfee398b781276860cd7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 20:49:01 +0000 Subject: [PATCH 0353/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cbe368772..855bd7ec6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). * 🐛 Fix edge case with repeated aliases names not shown in OpenAPI. PR [#2351](https://github.com/tiangolo/fastapi/pull/2351) by [@klaa97](https://github.com/klaa97). * 📝 Add misc dependency installs to tutorial docs. PR [#2126](https://github.com/tiangolo/fastapi/pull/2126) by [@TeoZosa](https://github.com/TeoZosa). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/query-params.md`. PR [#4775](https://github.com/tiangolo/fastapi/pull/4775) by [@batlopes](https://github.com/batlopes). From 2f9c6ab36abff2f7c80b7c33cab92171c66a3cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 18 Aug 2022 22:57:02 +0200 Subject: [PATCH 0354/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20Jina=20sponso?= =?UTF-8?q?rship=20(#5283)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a5e8cae22..326ad2279 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,10 @@ The key features are: + - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 7fbb933e0..af1f97818 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -2,6 +2,9 @@ gold: - url: https://bit.ly/3dmXC5S title: The data structure for unstructured multimodal data img: https://fastapi.tiangolo.com/img/sponsors/docarray.svg + - url: https://bit.ly/3JJ7y5C + title: Build cross-modal and multimodal applications on the cloud + img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg @@ -11,9 +14,6 @@ gold: - url: https://doist.com/careers/9B437B1615-wa-senior-backend-engineer-python title: Help us migrate doist to FastAPI img: https://fastapi.tiangolo.com/img/sponsors/doist.svg - - url: https://bit.ly/3JJ7y5C - title: Build cross-modal and multimodal applications on the cloud - img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas From 94bbb91ec2912dfbcababc77386abee6e3c02c1d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 20:57:50 +0000 Subject: [PATCH 0355/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 855bd7ec6..fe3aebbfc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Jina sponsorship. PR [#5283](https://github.com/tiangolo/fastapi/pull/5283) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). * 🐛 Fix edge case with repeated aliases names not shown in OpenAPI. PR [#2351](https://github.com/tiangolo/fastapi/pull/2351) by [@klaa97](https://github.com/klaa97). * 📝 Add misc dependency installs to tutorial docs. PR [#2126](https://github.com/tiangolo/fastapi/pull/2126) by [@TeoZosa](https://github.com/TeoZosa). From 7f0e2f6cfa44d6710725a7950e8acd3235ad64af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 18 Aug 2022 23:04:30 +0200 Subject: [PATCH 0356/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fe3aebbfc..1d267a97e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,27 +2,38 @@ ## Latest Changes -* 🔧 Update Jina sponsorship. PR [#5283](https://github.com/tiangolo/fastapi/pull/5283) by [@tiangolo](https://github.com/tiangolo). +### Fixes + * 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). * 🐛 Fix edge case with repeated aliases names not shown in OpenAPI. PR [#2351](https://github.com/tiangolo/fastapi/pull/2351) by [@klaa97](https://github.com/klaa97). * 📝 Add misc dependency installs to tutorial docs. PR [#2126](https://github.com/tiangolo/fastapi/pull/2126) by [@TeoZosa](https://github.com/TeoZosa). + +### Docs + +* ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). +* ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). +* 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). +* ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). Updated docs at: [Concurrency and Burgers](https://fastapi.tiangolo.com/async/#concurrency-and-burgers). + +### Translations + * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/query-params.md`. PR [#4775](https://github.com/tiangolo/fastapi/pull/4775) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/security/first-steps.md`. PR [#4954](https://github.com/tiangolo/fastapi/pull/4954) by [@FLAIR7](https://github.com/FLAIR7). * 🌐 Add translation for `docs/zh/docs/advanced/response-cookies.md`. PR [#4638](https://github.com/tiangolo/fastapi/pull/4638) by [@zhangbo2012](https://github.com/zhangbo2012). -* ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). * 🌐 Add French translation for `docs/fr/docs/deployment/index.md`. PR [#3689](https://github.com/tiangolo/fastapi/pull/3689) by [@rjNemo](https://github.com/rjNemo). * 🌐 Add Portuguese translation for `tutorial/handling-errors.md`. PR [#4769](https://github.com/tiangolo/fastapi/pull/4769) by [@frnsimoes](https://github.com/frnsimoes). * 🌐 Add French translation for `docs/fr/docs/history-design-future.md`. PR [#3451](https://github.com/tiangolo/fastapi/pull/3451) by [@rjNemo](https://github.com/rjNemo). -* ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/background-tasks.md`. PR [#4854](https://github.com/tiangolo/fastapi/pull/4854) by [@AdmiralDesu](https://github.com/AdmiralDesu). * 🌐 Add Chinese translation for `docs/tutorial/security/first-steps.md`. PR [#3841](https://github.com/tiangolo/fastapi/pull/3841) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/nosql-databases.md`. PR [#4205](https://github.com/tiangolo/fastapi/pull/4205) by [@sUeharaE4](https://github.com/sUeharaE4). * 🌐 Add Indonesian translation for `docs/id/docs/tutorial/index.md`. PR [#4705](https://github.com/tiangolo/fastapi/pull/4705) by [@bas-baskara](https://github.com/bas-baskara). -* 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). -* ✨ Add illustrations for Concurrent burgers and Parallel burgers. PR [#5277](https://github.com/tiangolo/fastapi/pull/5277) by [@tiangolo](https://github.com/tiangolo). +* 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). + +### Internal + +* 🔧 Update Jina sponsorship. PR [#5283](https://github.com/tiangolo/fastapi/pull/5283) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Jina sponsorship. PR [#5272](https://github.com/tiangolo/fastapi/pull/5272) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Striveworks badge. PR [#5179](https://github.com/tiangolo/fastapi/pull/5179) by [@tiangolo](https://github.com/tiangolo). -* 🌐 Add Persian translation for `docs/fa/docs/index.md` and tweak right-to-left CSS. PR [#2395](https://github.com/tiangolo/fastapi/pull/2395) by [@mohsen-mahmoodi](https://github.com/mohsen-mahmoodi). ## 0.79.0 From a21c31557464b477ea284f9bad635b5cbef19425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 18 Aug 2022 23:11:39 +0200 Subject: [PATCH 0357/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20note=20giving=20?= =?UTF-8?q?credit=20for=20illustrations=20to=20Ketrina=20Thompson=20(#5284?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/async.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 95d2f755a..dc8e10bb8 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -136,6 +136,9 @@ You and your crush eat the burgers and have a nice time. ✨ +!!! info + Beautiful illustrations by Ketrina Thompson. 🎨 + --- Imagine you are the computer / program 🤖 in that story. @@ -196,6 +199,9 @@ You just eat them, and you are done. ⏹ There was not much talk or flirting as most of the time was spent waiting 🕙 in front of the counter. 😞 +!!! info + Beautiful illustrations by Ketrina Thompson. 🎨 + --- In this scenario of the parallel burgers, you are a computer / program 🤖 with two processors (you and your crush), both waiting 🕙 and dedicating their attention ⏯ to be "waiting on the counter" 🕙 for a long time. From 767df02814735b43898c05de016c976a144b38c5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 18 Aug 2022 21:12:18 +0000 Subject: [PATCH 0358/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1d267a97e..01e01d586 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add note giving credit for illustrations to Ketrina Thompson. PR [#5284](https://github.com/tiangolo/fastapi/pull/5284) by [@tiangolo](https://github.com/tiangolo). ### Fixes * 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). From 09381ba04256adbd86e440715dc80e79d90fe840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 18 Aug 2022 23:13:19 +0200 Subject: [PATCH 0359/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 01e01d586..54a4297d7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,6 @@ ## Latest Changes -* 📝 Add note giving credit for illustrations to Ketrina Thompson. PR [#5284](https://github.com/tiangolo/fastapi/pull/5284) by [@tiangolo](https://github.com/tiangolo). ### Fixes * 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). @@ -11,6 +10,7 @@ ### Docs +* 📝 Add note giving credit for illustrations to [Ketrina Thompson](https://www.instagram.com/ketrinadrawsalot/). PR [#5284](https://github.com/tiangolo/fastapi/pull/5284) by [@tiangolo](https://github.com/tiangolo). * ✏ Fix typo in `python-types.md`. PR [#5116](https://github.com/tiangolo/fastapi/pull/5116) by [@Kludex](https://github.com/Kludex). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#5007](https://github.com/tiangolo/fastapi/pull/5007) by [@atiabbz](https://github.com/atiabbz). * 📝 Remove unneeded Django/Flask references from async topic intro. PR [#5280](https://github.com/tiangolo/fastapi/pull/5280) by [@carltongibson](https://github.com/carltongibson). From ab8988ff7c5bb2ec25d41ba8c8687114962e0986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 18 Aug 2022 23:14:07 +0200 Subject: [PATCH 0360/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?79.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 54a4297d7..4b4ebee42 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.79.1 + ### Fixes * 🐛 Fix `jsonable_encoder` using `include` and `exclude` parameters for non-Pydantic objects. PR [#2606](https://github.com/tiangolo/fastapi/pull/2606) by [@xaviml](https://github.com/xaviml). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index e5cdbeb09..abcb219a9 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.79.0" +__version__ = "0.79.1" from starlette import status as status From eb80b79555d98bfbabe812676fae8296130874fd Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Aug 2022 12:44:46 +0000 Subject: [PATCH 0361/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4b4ebee42..8e1db01d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* Translated docs to ru `project_generator.md`, `features.md`, `help-fastapi.md`, upgrade `python-types.md`, `docs/ru/mkdocs.yml`.. PR [#4913](https://github.com/tiangolo/fastapi/pull/4913) by [@Xewus](https://github.com/Xewus). ## 0.79.1 From 6c16d6a4f9776fd61fbfe39d7365c611061c7213 Mon Sep 17 00:00:00 2001 From: Joona Yoon Date: Mon, 22 Aug 2022 23:52:50 +0900 Subject: [PATCH 0362/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20missing=20naviga?= =?UTF-8?q?tor=20for=20`encoder.md`=20in=20Korean=20translation=20(#5238)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 70cb419ba..50931e134 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -68,6 +68,7 @@ nav: - tutorial/response-status-code.md - tutorial/request-files.md - tutorial/request-forms-and-files.md + - tutorial/encoder.md markdown_extensions: - toc: permalink: true From 7953353a356bda1112fd60794365a32f72e9d4e3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Aug 2022 14:53:35 +0000 Subject: [PATCH 0363/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8e1db01d3..8ff0d3375 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add missing navigator for `encoder.md` in Korean translation. PR [#5238](https://github.com/tiangolo/fastapi/pull/5238) by [@joonas-yoon](https://github.com/joonas-yoon). * Translated docs to ru `project_generator.md`, `features.md`, `help-fastapi.md`, upgrade `python-types.md`, `docs/ru/mkdocs.yml`.. PR [#4913](https://github.com/tiangolo/fastapi/pull/4913) by [@Xewus](https://github.com/Xewus). ## 0.79.1 From 1ff8df6e7fd5c80193c5f7344b6c82c7fc951911 Mon Sep 17 00:00:00 2001 From: 0xflotus <0xflotus@gmail.com> Date: Mon, 22 Aug 2022 20:33:10 +0200 Subject: [PATCH 0364/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typos=20in=20Ger?= =?UTF-8?q?man=20translation=20for=20`docs/de/docs/features.md`=20(#4533)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/de/docs/features.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index d99ade402..f825472a9 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -97,7 +97,7 @@ Hierdurch werden Sie nie wieder einen falschen Schlüsselnamen benutzen und spar ### Kompakt -FastAPI nutzt für alles sinnvolle **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 brauchen. +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 brauchen. Aber standardmäßig, **"funktioniert einfach"** alles. @@ -119,9 +119,9 @@ 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. +Integrierte Sicherheit und Authentifizierung. Ohne Kompromisse bei Datenbanken oder Datenmodellen. -Unterstützt alle von OpenAPI definierten Sicherheitsschemata, hierzu gehören: +Unterstützt werden 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}. @@ -142,8 +142,8 @@ FastAPI enthält ein extrem einfaches, aber extrem mächtiges eines der schnellsten Python frameworks, auf Augenhöhe mit **NodeJS** und **Go**. +* Stark beeindruckende Performanz. Es ist eines der schnellsten Python Frameworks, auf Augenhöhe mit **NodeJS** und **Go**. * **WebSocket**-Unterstützung. * Hintergrundaufgaben im selben Prozess. * Ereignisse für das Starten und Herunterfahren. @@ -196,8 +196,8 @@ Mit **FastAPI** bekommen Sie alle Funktionen von **Pydantic** (da FastAPI für d * 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. + * Validierungen erlauben eine 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.. + * Pydantic erlaubt die Definition von eigenen Datentypen oder sie können die Validierung mit einer `validator` dekorierten Methode erweitern. * 100% Testabdeckung. From a6a39f300989f58062141d58c2581c55b0f0f9c3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Aug 2022 18:35:01 +0000 Subject: [PATCH 0365/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8ff0d3375..f57512cc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typos in German translation for `docs/de/docs/features.md`. PR [#4533](https://github.com/tiangolo/fastapi/pull/4533) by [@0xflotus](https://github.com/0xflotus). * 🌐 Add missing navigator for `encoder.md` in Korean translation. PR [#5238](https://github.com/tiangolo/fastapi/pull/5238) by [@joonas-yoon](https://github.com/joonas-yoon). * Translated docs to ru `project_generator.md`, `features.md`, `help-fastapi.md`, upgrade `python-types.md`, `docs/ru/mkdocs.yml`.. PR [#4913](https://github.com/tiangolo/fastapi/pull/4913) by [@Xewus](https://github.com/Xewus). From e7b1b96a54af8e6327be1e0cc2172c81d052edda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 22 Aug 2022 20:49:03 +0200 Subject: [PATCH 0366/1881] =?UTF-8?q?=F0=9F=8E=A8=20Update=20type=20annota?= =?UTF-8?q?tions=20for=20`response=5Fmodel`,=20allow=20things=20like=20`Un?= =?UTF-8?q?ion[str,=20None]`=20(#5294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 20 ++++++++++---------- fastapi/routing.py | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 7530ddb9b..ac6050ca4 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -273,7 +273,7 @@ class FastAPI(Starlette): path: str, endpoint: Callable[..., Coroutine[Any, Any, Response]], *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -331,7 +331,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -434,7 +434,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -489,7 +489,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -544,7 +544,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -599,7 +599,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -654,7 +654,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -709,7 +709,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -764,7 +764,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -819,7 +819,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 6f1a8e900..2e0d2e552 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -315,7 +315,7 @@ class APIRoute(routing.Route): path: str, endpoint: Callable[..., Any], *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -511,7 +511,7 @@ class APIRouter(routing.Router): path: str, endpoint: Callable[..., Any], *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -592,7 +592,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -787,7 +787,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -843,7 +843,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -899,7 +899,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -955,7 +955,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1011,7 +1011,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1067,7 +1067,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1123,7 +1123,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1179,7 +1179,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Optional[Type[Any]] = None, + response_model: Any = None, status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, From 12f60cac7a2262231374404c538f0b227f9b9496 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Aug 2022 18:49:42 +0000 Subject: [PATCH 0367/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f57512cc1..f5ccf9b26 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Update type annotations for `response_model`, allow things like `Union[str, None]`. PR [#5294](https://github.com/tiangolo/fastapi/pull/5294) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix typos in German translation for `docs/de/docs/features.md`. PR [#4533](https://github.com/tiangolo/fastapi/pull/4533) by [@0xflotus](https://github.com/0xflotus). * 🌐 Add missing navigator for `encoder.md` in Korean translation. PR [#5238](https://github.com/tiangolo/fastapi/pull/5238) by [@joonas-yoon](https://github.com/joonas-yoon). * Translated docs to ru `project_generator.md`, `features.md`, `help-fastapi.md`, upgrade `python-types.md`, `docs/ru/mkdocs.yml`.. PR [#4913](https://github.com/tiangolo/fastapi/pull/4913) by [@Xewus](https://github.com/Xewus). From 634cf22584fc4fd9ee53cfdf0ad6d48a2830ac34 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Mon, 22 Aug 2022 22:17:45 +0300 Subject: [PATCH 0368/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`response=5Fmode?= =?UTF-8?q?l`=20not=20invalidating=20`None`=20(#2725)?= 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 --- fastapi/utils.py | 2 +- tests/test_validate_response.py | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index 887d57c90..40750a5b3 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -50,7 +50,7 @@ def create_response_field( type_: Type[Any], class_validators: Optional[Dict[str, Validator]] = None, default: Optional[Any] = None, - required: Union[bool, UndefinedType] = False, + required: Union[bool, UndefinedType] = True, model_config: Type[BaseConfig] = BaseConfig, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index 45d303e20..62f51c960 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Union import pytest from fastapi import FastAPI @@ -19,6 +19,19 @@ def get_invalid(): return {"name": "invalid", "price": "foo"} +@app.get("/items/invalidnone", response_model=Item) +def get_invalid_none(): + return None + + +@app.get("/items/validnone", response_model=Union[Item, None]) +def get_valid_none(send_none: bool = False): + if send_none: + return None + else: + return {"name": "invalid", "price": 3.2} + + @app.get("/items/innerinvalid", response_model=Item) def get_innerinvalid(): return {"name": "double invalid", "price": "foo", "owner_ids": ["foo", "bar"]} @@ -41,6 +54,25 @@ def test_invalid(): client.get("/items/invalid") +def test_invalid_none(): + with pytest.raises(ValidationError): + client.get("/items/invalidnone") + + +def test_valid_none_data(): + response = client.get("/items/validnone") + data = response.json() + assert response.status_code == 200 + assert data == {"name": "invalid", "price": 3.2, "owner_ids": None} + + +def test_valid_none_none(): + response = client.get("/items/validnone", params={"send_none": "true"}) + data = response.json() + assert response.status_code == 200 + assert data is None + + def test_double_invalid(): with pytest.raises(ValidationError): client.get("/items/innerinvalid") From 982911f08f11ceb10ad8bba34ec22095d6018856 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 22 Aug 2022 19:18:24 +0000 Subject: [PATCH 0369/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f5ccf9b26..0cab8220a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). * 🎨 Update type annotations for `response_model`, allow things like `Union[str, None]`. PR [#5294](https://github.com/tiangolo/fastapi/pull/5294) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix typos in German translation for `docs/de/docs/features.md`. PR [#4533](https://github.com/tiangolo/fastapi/pull/4533) by [@0xflotus](https://github.com/0xflotus). * 🌐 Add missing navigator for `encoder.md` in Korean translation. PR [#5238](https://github.com/tiangolo/fastapi/pull/5238) by [@joonas-yoon](https://github.com/joonas-yoon). From b993b4af287e63904b675ba2ee1c232e86f2b072 Mon Sep 17 00:00:00 2001 From: laggardkernel Date: Tue, 23 Aug 2022 21:30:24 +0800 Subject: [PATCH 0370/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20cached=20depende?= =?UTF-8?q?ncies=20when=20using=20a=20dependency=20in=20`Security()`=20and?= =?UTF-8?q?=20other=20places=20(e.g.=20`Depends()`)=20with=20different=20O?= =?UTF-8?q?Auth2=20scopes=20(#2945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 10 +++++++--- tests/test_dependency_cache.py | 25 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f397e333c..f6151f6bd 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -161,7 +161,6 @@ def get_sub_dependant( ) if security_requirement: sub_dependant.security_requirements.append(security_requirement) - sub_dependant.security_scopes = security_scopes return sub_dependant @@ -278,7 +277,13 @@ def get_dependant( path_param_names = get_path_param_names(path) endpoint_signature = get_typed_signature(call) signature_params = endpoint_signature.parameters - dependant = Dependant(call=call, name=name, path=path, use_cache=use_cache) + dependant = Dependant( + call=call, + name=name, + path=path, + security_scopes=security_scopes, + use_cache=use_cache, + ) for param_name, param in signature_params.items(): if isinstance(param.default, params.Depends): sub_dependant = get_param_sub_dependant( @@ -495,7 +500,6 @@ async def solve_dependencies( name=sub_dependant.name, security_scopes=sub_dependant.security_scopes, ) - use_sub_dependant.security_scopes = sub_dependant.security_scopes solved_result = await solve_dependencies( request=request, diff --git a/tests/test_dependency_cache.py b/tests/test_dependency_cache.py index 65ed7f946..08fb9b74f 100644 --- a/tests/test_dependency_cache.py +++ b/tests/test_dependency_cache.py @@ -1,4 +1,4 @@ -from fastapi import Depends, FastAPI +from fastapi import Depends, FastAPI, Security from fastapi.testclient import TestClient app = FastAPI() @@ -35,6 +35,19 @@ async def get_sub_counter_no_cache( return {"counter": count, "subcounter": subcount} +@app.get("/scope-counter") +async def get_scope_counter( + count: int = Security(dep_counter), + scope_count_1: int = Security(dep_counter, scopes=["scope"]), + scope_count_2: int = Security(dep_counter, scopes=["scope"]), +): + return { + "counter": count, + "scope_counter_1": scope_count_1, + "scope_counter_2": scope_count_2, + } + + client = TestClient(app) @@ -66,3 +79,13 @@ def test_sub_counter_no_cache(): response = client.get("/sub-counter-no-cache/") assert response.status_code == 200, response.text assert response.json() == {"counter": 4, "subcounter": 3} + + +def test_security_cache(): + counter_holder["counter"] = 0 + response = client.get("/scope-counter/") + assert response.status_code == 200, response.text + assert response.json() == {"counter": 1, "scope_counter_1": 2, "scope_counter_2": 2} + response = client.get("/scope-counter/") + assert response.status_code == 200, response.text + assert response.json() == {"counter": 3, "scope_counter_1": 4, "scope_counter_2": 4} From f8f5281ef5efa25e780fec97701ed35eec3bcc71 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 23 Aug 2022 13:31:17 +0000 Subject: [PATCH 0371/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0cab8220a..f2c455e2f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix cached dependencies when using a dependency in `Security()` and other places (e.g. `Depends()`) with different OAuth2 scopes. PR [#2945](https://github.com/tiangolo/fastapi/pull/2945) by [@laggardkernel](https://github.com/laggardkernel). * 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). * 🎨 Update type annotations for `response_model`, allow things like `Union[str, None]`. PR [#5294](https://github.com/tiangolo/fastapi/pull/5294) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix typos in German translation for `docs/de/docs/features.md`. PR [#4533](https://github.com/tiangolo/fastapi/pull/4533) by [@0xflotus](https://github.com/0xflotus). From f6808e76dcb332aefe16abab37b0480b16e7cdb1 Mon Sep 17 00:00:00 2001 From: Andrey Semakin Date: Tue, 23 Aug 2022 18:47:19 +0500 Subject: [PATCH 0372/1881] =?UTF-8?q?=E2=99=BB=20Strip=20empty=20whitespac?= =?UTF-8?q?e=20from=20description=20extracted=20from=20docstrings=20(#2821?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/routing.py | 2 +- .../test_tutorial004.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 2e0d2e552..80bd53279 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -409,7 +409,7 @@ class APIRoute(routing.Route): self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") # if a "form feed" character (page break) is found in the description text, # truncate description text to the content preceding the first "form feed" - self.description = self.description.split("\f")[0] + self.description = self.description.split("\f")[0].strip() response_fields = {} for additional_status_code, response in self.responses.items(): assert isinstance(response, dict), "An additional response must be a dict" diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 456e509d5..3de19833b 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -31,7 +31,7 @@ openapi_schema = { }, }, "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item\n", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", "operationId": "create_item_items__post", "requestBody": { "content": { From 09acce4b2c7f50cdea7c487e9123b88c44a6724f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 23 Aug 2022 13:48:07 +0000 Subject: [PATCH 0373/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f2c455e2f..58ba2fb2c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Strip empty whitespace from description extracted from docstrings. PR [#2821](https://github.com/tiangolo/fastapi/pull/2821) by [@and-semakin](https://github.com/and-semakin). * 🐛 Fix cached dependencies when using a dependency in `Security()` and other places (e.g. `Depends()`) with different OAuth2 scopes. PR [#2945](https://github.com/tiangolo/fastapi/pull/2945) by [@laggardkernel](https://github.com/laggardkernel). * 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). * 🎨 Update type annotations for `response_model`, allow things like `Union[str, None]`. PR [#5294](https://github.com/tiangolo/fastapi/pull/5294) by [@tiangolo](https://github.com/tiangolo). From ec072d75fe0aae16caec3a8aa3f57ca8d6f05de5 Mon Sep 17 00:00:00 2001 From: Teo Koon Peng Date: Tue, 23 Aug 2022 21:57:25 +0800 Subject: [PATCH 0374/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20Swagger=20UI=20?= =?UTF-8?q?copy=20of=20`oauth2-redirect.html`=20to=20include=20fixes=20for?= =?UTF-8?q?=20flavors=20of=20authorization=20code=20flows=20in=20Swagger?= =?UTF-8?q?=20UI=20(#3439)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/openapi/docs.py | 43 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index d6af17a85..b7803b13b 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -115,12 +115,14 @@ def get_redoc_html( def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse: + # copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html html = """ - + - - - + + Swagger UI: OAuth2 Redirect + + + + """ return HTMLResponse(content=html) From ad65e72d901c12edb5c69c1cded8994fa094f92f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 23 Aug 2022 13:58:06 +0000 Subject: [PATCH 0375/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 58ba2fb2c..97748436f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade Swagger UI copy of `oauth2-redirect.html` to include fixes for flavors of authorization code flows in Swagger UI. PR [#3439](https://github.com/tiangolo/fastapi/pull/3439) by [@koonpeng](https://github.com/koonpeng). * ♻ Strip empty whitespace from description extracted from docstrings. PR [#2821](https://github.com/tiangolo/fastapi/pull/2821) by [@and-semakin](https://github.com/and-semakin). * 🐛 Fix cached dependencies when using a dependency in `Security()` and other places (e.g. `Depends()`) with different OAuth2 scopes. PR [#2945](https://github.com/tiangolo/fastapi/pull/2945) by [@laggardkernel](https://github.com/laggardkernel). * 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). From 18819f9850007633f728fcf457db5170f7922001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Aug 2022 16:13:37 +0200 Subject: [PATCH 0376/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 78 +++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 97748436f..524da6f42 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,14 +2,86 @@ ## Latest Changes -* ⬆ Upgrade Swagger UI copy of `oauth2-redirect.html` to include fixes for flavors of authorization code flows in Swagger UI. PR [#3439](https://github.com/tiangolo/fastapi/pull/3439) by [@koonpeng](https://github.com/koonpeng). +### Breaking Changes - Fixes + +* 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). + +If you are using `response_model` with some type that doesn't include `None` but the function is returning `None`, it will now raise an internal server error, because you are returning invalid data that violates the contract in `response_model`. Before this release it would allow breaking that contract returning `None`. + +For example, if you have an app like this: + +```Python +from fastapi import FastAPI +from pydantic import BaseModel + +class Item(BaseModel): + name: str + price: Optional[float] = None + owner_ids: Optional[List[int]] = None + +app = FastAPI() + +@app.get("/items/invalidnone", response_model=Item) +def get_invalid_none(): + return None +``` + +...calling the path `/items/invalidnone` will raise an error, because `None` is not a valid type for the `response_model` declared with `Item`. + +You could also be implicitly returning `None` without realizing, for example: + +```Python +from fastapi import FastAPI +from pydantic import BaseModel + +class Item(BaseModel): + name: str + price: Optional[float] = None + owner_ids: Optional[List[int]] = None + +app = FastAPI() + +@app.get("/items/invalidnone", response_model=Item) +def get_invalid_none(): + if flag: + return {"name": "foo"} + # if flag is False, at this point the function will implicitly return None +``` + +If you have *path operations* using `response_model` that need to be allowed to return `None`, make it explicit in `response_model` using `Union[Something, None]`: + +```Python +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +class Item(BaseModel): + name: str + price: Optional[float] = None + owner_ids: Optional[List[int]] = None + +app = FastAPI() + +@app.get("/items/invalidnone", response_model=Union[Item, None]) +def get_invalid_none(): + return None +``` + +This way the data will be correctly validated, you won't have an internal server error, and the documentation will also reflect that this *path operation* could return `None` (or `null` in JSON). + +### Fixes + +* ⬆ Upgrade Swagger UI copy of `oauth2-redirect.html` to include fixes for flavors of authorization code flows in Swagger UI. PR [#3439](https://github.com/tiangolo/fastapi/pull/3439) initial PR by [@koonpeng](https://github.com/koonpeng). * ♻ Strip empty whitespace from description extracted from docstrings. PR [#2821](https://github.com/tiangolo/fastapi/pull/2821) by [@and-semakin](https://github.com/and-semakin). * 🐛 Fix cached dependencies when using a dependency in `Security()` and other places (e.g. `Depends()`) with different OAuth2 scopes. PR [#2945](https://github.com/tiangolo/fastapi/pull/2945) by [@laggardkernel](https://github.com/laggardkernel). -* 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). * 🎨 Update type annotations for `response_model`, allow things like `Union[str, None]`. PR [#5294](https://github.com/tiangolo/fastapi/pull/5294) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Fix typos in German translation for `docs/de/docs/features.md`. PR [#4533](https://github.com/tiangolo/fastapi/pull/4533) by [@0xflotus](https://github.com/0xflotus). * 🌐 Add missing navigator for `encoder.md` in Korean translation. PR [#5238](https://github.com/tiangolo/fastapi/pull/5238) by [@joonas-yoon](https://github.com/joonas-yoon). -* Translated docs to ru `project_generator.md`, `features.md`, `help-fastapi.md`, upgrade `python-types.md`, `docs/ru/mkdocs.yml`.. PR [#4913](https://github.com/tiangolo/fastapi/pull/4913) by [@Xewus](https://github.com/Xewus). +* (Empty PR merge by accident) [#4913](https://github.com/tiangolo/fastapi/pull/4913). ## 0.79.1 From 7d6e70791d0d4c5cf271bcd231ba78d1a3c0f582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 23 Aug 2022 16:14:48 +0200 Subject: [PATCH 0377/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?80.0?= 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 524da6f42..fc48bf7dd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.80.0 + ### Breaking Changes - Fixes * 🐛 Fix `response_model` not invalidating `None`. PR [#2725](https://github.com/tiangolo/fastapi/pull/2725) by [@hukkin](https://github.com/hukkin). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index abcb219a9..3dc86cadf 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.79.1" +__version__ = "0.80.0" from starlette import status as status From 68f25120c51545d36832478e508d63fcacdb3903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 24 Aug 2022 10:43:44 +0200 Subject: [PATCH 0378/1881] =?UTF-8?q?=F0=9F=8D=B1=20Update=20Jina=20banner?= =?UTF-8?q?,=20fix=20typo=20(#5301)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/img/sponsors/docarray-top-banner.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/img/sponsors/docarray-top-banner.svg b/docs/en/docs/img/sponsors/docarray-top-banner.svg index c48a3312b..b2eca3544 100644 --- a/docs/en/docs/img/sponsors/docarray-top-banner.svg +++ b/docs/en/docs/img/sponsors/docarray-top-banner.svg @@ -1 +1 @@ - + From 2201f29be32fd6a584433bcfc7c6310ac043b7f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Aug 2022 08:44:22 +0000 Subject: [PATCH 0379/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fc48bf7dd..6e17be159 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🍱 Update Jina banner, fix typo. PR [#5301](https://github.com/tiangolo/fastapi/pull/5301) by [@tiangolo](https://github.com/tiangolo). ## 0.80.0 From 1835b3f76d6c72629500f971920730c645867168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 24 Aug 2022 16:18:11 +0200 Subject: [PATCH 0380/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20?= =?UTF-8?q?testing,=20fix=20examples=20with=20relative=20imports=20(#5302)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/async-tests.md | 14 ++++++++-- docs/en/docs/tutorial/testing.md | 38 +++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index d5116233f..e34f48f17 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -26,13 +26,23 @@ The important difference for us is that with HTTPX we are not limited to synchro ## Example -For a simple example, let's consider the following `main.py` module: +For a simple example, let's consider a file structure similar to the one described in [Bigger Applications](../tutorial/bigger-applications.md){.internal-link target=_blank} and [Testing](../tutorial/testing.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +The file `main.py` would have: ```Python {!../../../docs_src/async_tests/main.py!} ``` -The `test_main.py` module that contains the tests for `main.py` could look like this now: +The file `test_main.py` would have the tests for `main.py`, it could look like this now: ```Python {!../../../docs_src/async_tests/test_main.py!} diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 79ea2b1ab..d2ccd7dc7 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -50,7 +50,17 @@ And your **FastAPI** application might also be composed of several files/modules ### **FastAPI** app file -Let's say you have a file `main.py` with your **FastAPI** app: +Let's say you have a file structure as described in [Bigger Applications](./bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +In the file `main.py` you have your **FastAPI** app: + ```Python {!../../../docs_src/app_testing/main.py!} @@ -58,18 +68,40 @@ Let's say you have a file `main.py` with your **FastAPI** app: ### Testing file -Then you could have a file `test_main.py` with your tests, and import your `app` from the `main` module (`main.py`): +Then you could have a file `test_main.py` with your tests. It could live on the same Python package (the same directory with a `__init__.py` file): -```Python +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Because this file is in the same package, you can use relative imports to import the object `app` from the `main` module (`main.py`): + +```Python hl_lines="3" {!../../../docs_src/app_testing/test_main.py!} ``` +...and have the code for the tests just like before. + ## Testing: extended example Now let's extend this example and add more details to see how to test different parts. ### Extended **FastAPI** app file +Let's continue with the same file structure as before: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + Let's say that now the file `main.py` with your **FastAPI** app has some other **path operations**. It has a `GET` operation that could return an error. From f90465f5b4e83110722b096306057c54bb211ba7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Aug 2022 14:18:57 +0000 Subject: [PATCH 0381/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6e17be159..1b5806741 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs for testing, fix examples with relative imports. PR [#5302](https://github.com/tiangolo/fastapi/pull/5302) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update Jina banner, fix typo. PR [#5301](https://github.com/tiangolo/fastapi/pull/5301) by [@tiangolo](https://github.com/tiangolo). ## 0.80.0 From 8c6ad3561534d69a2ccb0d3d0e9e60afc5b388de Mon Sep 17 00:00:00 2001 From: Kevin Tewouda Date: Wed, 24 Aug 2022 16:53:43 +0200 Subject: [PATCH 0382/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20?= =?UTF-8?q?handling=20HTTP=20Basic=20Auth=20with=20`secrets.compare=5Fdige?= =?UTF-8?q?st()`=20to=20account=20for=20non-ASCII=20characters=20(#3536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: le_woudar Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/security/http-basic-auth.md | 14 ++++++++++---- docs_src/security/tutorial007.py | 14 +++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 6c589cd9a..90c516808 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -34,13 +34,19 @@ Here's a more complete example. Use a dependency to check if the username and password are correct. -For this, use the Python standard module `secrets` to check the username and password: +For this, use the Python standard module `secrets` to check the username and password. -```Python hl_lines="1 11-13" +`secrets.compare_digest()` needs to take `bytes` or a `str` that only contains ASCII characters (the ones in English), this means it wouldn't work with characters like `á`, as in `Sebastián`. + +To handle that, we first convert the `username` and `password` to `bytes` encoding them with UTF-8. + +Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. + +```Python hl_lines="1 11-21" {!../../../docs_src/security/tutorial007.py!} ``` -This will ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. This would be similar to: +This would be similar to: ```Python if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): @@ -102,6 +108,6 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -```Python hl_lines="15-19" +```Python hl_lines="23-27" {!../../../docs_src/security/tutorial007.py!} ``` diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007.py index 90b9ac054..790ee10bc 100644 --- a/docs_src/security/tutorial007.py +++ b/docs_src/security/tutorial007.py @@ -9,9 +9,17 @@ security = HTTPBasic() def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): - correct_username = secrets.compare_digest(credentials.username, "stanleyjobson") - correct_password = secrets.compare_digest(credentials.password, "swordfish") - if not (correct_username and correct_password): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", From 39319a7ede38a57d8b2dbd1c376850fcf8ae5a17 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Aug 2022 14:54:27 +0000 Subject: [PATCH 0383/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1b5806741..806127162 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs for handling HTTP Basic Auth with `secrets.compare_digest()` to account for non-ASCII characters. PR [#3536](https://github.com/tiangolo/fastapi/pull/3536) by [@lewoudar](https://github.com/lewoudar). * 📝 Update docs for testing, fix examples with relative imports. PR [#5302](https://github.com/tiangolo/fastapi/pull/5302) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update Jina banner, fix typo. PR [#5301](https://github.com/tiangolo/fastapi/pull/5301) by [@tiangolo](https://github.com/tiangolo). From 438656395afcc78e06ba2c7c0655e0156dfdc20d Mon Sep 17 00:00:00 2001 From: Caleb Renfroe <35737053+ccrenfroe@users.noreply.github.com> Date: Wed, 24 Aug 2022 17:35:30 -0400 Subject: [PATCH 0384/1881] =?UTF-8?q?=E2=9C=8F=20Reword=20confusing=20sent?= =?UTF-8?q?ence=20in=20docs=20file=20`typo-fix-path-params-numeric-validat?= =?UTF-8?q?ions.md`=20(#3219)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/path-params-numeric-validations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 29235c6e2..4b2e3f973 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -122,9 +122,9 @@ And you can also declare numeric validations: * `le`: `l`ess than or `e`qual !!! info - `Query`, `Path`, and others you will see later are subclasses of a common `Param` class (that you don't need to use). + `Query`, `Path`, and other classes you will see later are subclasses of a common `Param` class. - And all of them share the same all these same parameters of additional validation and metadata you have seen. + All of them share the same parameters for additional validation and metadata you have seen. !!! note "Technical Details" When you import `Query`, `Path` and others from `fastapi`, they are actually functions. From afaa0391a59e54ff59e1d3d1b49785aecd78ee11 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 24 Aug 2022 21:36:15 +0000 Subject: [PATCH 0385/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 806127162..7418fae6a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Reword confusing sentence in docs file `typo-fix-path-params-numeric-validations.md`. PR [#3219](https://github.com/tiangolo/fastapi/pull/3219) by [@ccrenfroe](https://github.com/ccrenfroe). * 📝 Update docs for handling HTTP Basic Auth with `secrets.compare_digest()` to account for non-ASCII characters. PR [#3536](https://github.com/tiangolo/fastapi/pull/3536) by [@lewoudar](https://github.com/lewoudar). * 📝 Update docs for testing, fix examples with relative imports. PR [#5302](https://github.com/tiangolo/fastapi/pull/5302) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update Jina banner, fix typo. PR [#5301](https://github.com/tiangolo/fastapi/pull/5301) by [@tiangolo](https://github.com/tiangolo). From 9359a8d65fdff4460d01d652ccafd31507c6c426 Mon Sep 17 00:00:00 2001 From: Sidharth Ajithkumar Date: Thu, 25 Aug 2022 15:25:53 +0530 Subject: [PATCH 0386/1881] =?UTF-8?q?=E2=9C=A8=20Preserve=20`json.JSONDeco?= =?UTF-8?q?deError`=20information=20when=20handling=20invalid=20JSON=20in?= =?UTF-8?q?=20request=20body,=20to=20support=20custom=20exception=20handle?= =?UTF-8?q?rs=20that=20use=20its=20information=20(#4057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/routing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 80bd53279..8f4d0fa7e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -209,7 +209,9 @@ def get_request_handler( else: body = body_bytes except json.JSONDecodeError as e: - raise RequestValidationError([ErrorWrapper(e, ("body", e.pos))], body=e.doc) + raise RequestValidationError( + [ErrorWrapper(e, ("body", e.pos))], body=e.doc + ) from e except Exception as e: raise HTTPException( status_code=400, detail="There was an error parsing the body" From 5215b39d5081dab2145fb682d832a93649d26063 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 25 Aug 2022 09:56:40 +0000 Subject: [PATCH 0387/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7418fae6a..337149972 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Preserve `json.JSONDecodeError` information when handling invalid JSON in request body, to support custom exception handlers that use its information. PR [#4057](https://github.com/tiangolo/fastapi/pull/4057) by [@UKnowWhoIm](https://github.com/UKnowWhoIm). * ✏ Reword confusing sentence in docs file `typo-fix-path-params-numeric-validations.md`. PR [#3219](https://github.com/tiangolo/fastapi/pull/3219) by [@ccrenfroe](https://github.com/ccrenfroe). * 📝 Update docs for handling HTTP Basic Auth with `secrets.compare_digest()` to account for non-ASCII characters. PR [#3536](https://github.com/tiangolo/fastapi/pull/3536) by [@lewoudar](https://github.com/lewoudar). * 📝 Update docs for testing, fix examples with relative imports. PR [#5302](https://github.com/tiangolo/fastapi/pull/5302) by [@tiangolo](https://github.com/tiangolo). From eb3ab337ab49d1904f027c0a523456cfe40d582d Mon Sep 17 00:00:00 2001 From: Andy Challis Date: Fri, 26 Aug 2022 07:44:40 +1000 Subject: [PATCH 0388/1881] =?UTF-8?q?=E2=9C=A8=20Allow=20custom=20middlewa?= =?UTF-8?q?res=20to=20raise=20`HTTPException`s=20and=20propagate=20them=20?= =?UTF-8?q?(#2036)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/routing.py | 2 + tests/test_custom_middleware_exception.py | 95 +++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/test_custom_middleware_exception.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 8f4d0fa7e..1ac4b3880 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -212,6 +212,8 @@ def get_request_handler( raise RequestValidationError( [ErrorWrapper(e, ("body", e.pos))], body=e.doc ) from e + except HTTPException: + raise except Exception as e: raise HTTPException( status_code=400, detail="There was an error parsing the body" diff --git a/tests/test_custom_middleware_exception.py b/tests/test_custom_middleware_exception.py new file mode 100644 index 000000000..d9b81e7c2 --- /dev/null +++ b/tests/test_custom_middleware_exception.py @@ -0,0 +1,95 @@ +from pathlib import Path +from typing import Optional + +from fastapi import APIRouter, FastAPI, File, UploadFile +from fastapi.exceptions import HTTPException +from fastapi.testclient import TestClient + +app = FastAPI() + +router = APIRouter() + + +class ContentSizeLimitMiddleware: + """Content size limiting middleware for ASGI applications + Args: + app (ASGI application): ASGI application + max_content_size (optional): the maximum content size allowed in bytes, None for no limit + """ + + def __init__(self, app: APIRouter, max_content_size: Optional[int] = None): + self.app = app + self.max_content_size = max_content_size + + def receive_wrapper(self, receive): + received = 0 + + async def inner(): + nonlocal received + message = await receive() + if message["type"] != "http.request": + return message # pragma: no cover + + body_len = len(message.get("body", b"")) + received += body_len + if received > self.max_content_size: + raise HTTPException( + 422, + detail={ + "name": "ContentSizeLimitExceeded", + "code": 999, + "message": "File limit exceeded", + }, + ) + return message + + return inner + + async def __call__(self, scope, receive, send): + if scope["type"] != "http" or self.max_content_size is None: + await self.app(scope, receive, send) + return + + wrapper = self.receive_wrapper(receive) + await self.app(scope, wrapper, send) + + +@router.post("/middleware") +def run_middleware(file: UploadFile = File(..., description="Big File")): + return {"message": "OK"} + + +app.include_router(router) +app.add_middleware(ContentSizeLimitMiddleware, max_content_size=2**8) + + +client = TestClient(app) + + +def test_custom_middleware_exception(tmp_path: Path): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + with client: + with open(path, "rb") as file: + response = client.post("/middleware", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": { + "name": "ContentSizeLimitExceeded", + "code": 999, + "message": "File limit exceeded", + } + } + + +def test_custom_middleware_exception_not_raised(tmp_path: Path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with client: + with open(path, "rb") as file: + response = client.post("/middleware", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"message": "OK"} From af3a6ef78b01fcef5060dad63da2f4ee22a0b618 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 25 Aug 2022 21:45:33 +0000 Subject: [PATCH 0389/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 337149972..0d179cb31 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Allow custom middlewares to raise `HTTPException`s and propagate them. PR [#2036](https://github.com/tiangolo/fastapi/pull/2036) by [@ghandic](https://github.com/ghandic). * ✨ Preserve `json.JSONDecodeError` information when handling invalid JSON in request body, to support custom exception handlers that use its information. PR [#4057](https://github.com/tiangolo/fastapi/pull/4057) by [@UKnowWhoIm](https://github.com/UKnowWhoIm). * ✏ Reword confusing sentence in docs file `typo-fix-path-params-numeric-validations.md`. PR [#3219](https://github.com/tiangolo/fastapi/pull/3219) by [@ccrenfroe](https://github.com/ccrenfroe). * 📝 Update docs for handling HTTP Basic Auth with `secrets.compare_digest()` to account for non-ASCII characters. PR [#3536](https://github.com/tiangolo/fastapi/pull/3536) by [@lewoudar](https://github.com/lewoudar). From ca2fae0588260f93f886aa59f689e3018e934a27 Mon Sep 17 00:00:00 2001 From: juntatalor Date: Fri, 26 Aug 2022 00:52:53 +0300 Subject: [PATCH 0390/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`Fr?= =?UTF-8?q?ozenSet`=20in=20parameters=20(e.g.=20query)=20(#2938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: saborisov Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 2 ++ tests/main.py | 7 ++++++- tests/test_application.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_query.py | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f6151f6bd..d781fdb62 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -34,6 +34,7 @@ from pydantic import BaseModel, create_model from pydantic.error_wrappers import ErrorWrapper from pydantic.errors import MissingError from pydantic.fields import ( + SHAPE_FROZENSET, SHAPE_LIST, SHAPE_SEQUENCE, SHAPE_SET, @@ -58,6 +59,7 @@ from starlette.websockets import WebSocket sequence_shapes = { SHAPE_LIST, SHAPE_SET, + SHAPE_FROZENSET, SHAPE_TUPLE, SHAPE_SEQUENCE, SHAPE_TUPLE_ELLIPSIS, diff --git a/tests/main.py b/tests/main.py index f70496db8..fce665704 100644 --- a/tests/main.py +++ b/tests/main.py @@ -1,5 +1,5 @@ import http -from typing import Optional +from typing import FrozenSet, Optional from fastapi import FastAPI, Path, Query @@ -192,3 +192,8 @@ def get_query_param_required_type(query: int = Query()): @app.get("/enum-status-code", status_code=http.HTTPStatus.CREATED) def get_enum_status_code(): return "foo bar" + + +@app.get("/query/frozenset") +def get_query_type_frozenset(query: FrozenSet[int] = Query(...)): + return ",".join(map(str, sorted(query))) diff --git a/tests/test_application.py b/tests/test_application.py index d9194c15c..b7d72f9ad 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1090,6 +1090,41 @@ openapi_schema = { "operationId": "get_enum_status_code_enum_status_code_get", } }, + "/query/frozenset": { + "get": { + "summary": "Get Query Type Frozenset", + "operationId": "get_query_type_frozenset_query_frozenset_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Query", + "uniqueItems": True, + "type": "array", + "items": {"type": "integer"}, + }, + "name": "query", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, }, "components": { "schemas": { diff --git a/tests/test_query.py b/tests/test_query.py index cdbdd1ccd..0c73eb665 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -53,6 +53,7 @@ response_not_valid_int = { ("/query/param-required/int", 422, response_missing), ("/query/param-required/int?query=50", 200, "foo bar 50"), ("/query/param-required/int?query=foo", 422, response_not_valid_int), + ("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"), ], ) def test_get_path(path, expected_status, expected_response): From 92181ef1822920c746009d035d53009be9bab65c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 25 Aug 2022 21:53:36 +0000 Subject: [PATCH 0391/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0d179cb31..56253c873 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `FrozenSet` in parameters (e.g. query). PR [#2938](https://github.com/tiangolo/fastapi/pull/2938) by [@juntatalor](https://github.com/juntatalor). * ✨ Allow custom middlewares to raise `HTTPException`s and propagate them. PR [#2036](https://github.com/tiangolo/fastapi/pull/2036) by [@ghandic](https://github.com/ghandic). * ✨ Preserve `json.JSONDecodeError` information when handling invalid JSON in request body, to support custom exception handlers that use its information. PR [#4057](https://github.com/tiangolo/fastapi/pull/4057) by [@UKnowWhoIm](https://github.com/UKnowWhoIm). * ✏ Reword confusing sentence in docs file `typo-fix-path-params-numeric-validations.md`. PR [#3219](https://github.com/tiangolo/fastapi/pull/3219) by [@ccrenfroe](https://github.com/ccrenfroe). From 880c8b37cf8ba88a7f4de40cf18dec49c9e71454 Mon Sep 17 00:00:00 2001 From: Ori Levari Date: Fri, 26 Aug 2022 02:26:20 -0700 Subject: [PATCH 0392/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20?= =?UTF-8?q?extending=20`openapi=5Fextras`=20with=20parameter=20lists=20(#4?= =?UTF-8?q?267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/utils.py | 6 + .../test_openapi_query_parameter_extension.py | 127 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tests/test_openapi_query_parameter_extension.py diff --git a/fastapi/utils.py b/fastapi/utils.py index 40750a5b3..0163fd95f 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -161,6 +161,12 @@ def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> and isinstance(value, dict) ): deep_dict_update(main_dict[key], value) + elif ( + key in main_dict + and isinstance(main_dict[key], list) + and isinstance(update_dict[key], list) + ): + main_dict[key] = main_dict[key] + update_dict[key] else: main_dict[key] = value diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py new file mode 100644 index 000000000..d3996f26e --- /dev/null +++ b/tests/test_openapi_query_parameter_extension.py @@ -0,0 +1,127 @@ +from typing import Optional + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.get( + "/", + openapi_extra={ + "parameters": [ + { + "required": False, + "schema": {"title": "Extra Param 1"}, + "name": "extra_param_1", + "in": "query", + }, + { + "required": True, + "schema": {"title": "Extra Param 2"}, + "name": "extra_param_2", + "in": "query", + }, + ] + }, +) +def route_with_extra_query_parameters(standard_query_param: Optional[int] = 50): + return {} + + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Route With Extra Query Parameters", + "operationId": "route_with_extra_query_parameters__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Standard Query Param", + "type": "integer", + "default": 50, + }, + "name": "standard_query_param", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Extra Param 1"}, + "name": "extra_param_1", + "in": "query", + }, + { + "required": True, + "schema": {"title": "Extra Param 2"}, + "name": "extra_param_2", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} From 0968329ed7aa07e66b6a55f1f5f71e4232085f4a Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 09:26:59 +0000 Subject: [PATCH 0393/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 56253c873..a30e397a5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix support for extending `openapi_extras` with parameter lists. PR [#4267](https://github.com/tiangolo/fastapi/pull/4267) by [@orilevari](https://github.com/orilevari). * ✨ Add support for `FrozenSet` in parameters (e.g. query). PR [#2938](https://github.com/tiangolo/fastapi/pull/2938) by [@juntatalor](https://github.com/juntatalor). * ✨ Allow custom middlewares to raise `HTTPException`s and propagate them. PR [#2036](https://github.com/tiangolo/fastapi/pull/2036) by [@ghandic](https://github.com/ghandic). * ✨ Preserve `json.JSONDecodeError` information when handling invalid JSON in request body, to support custom exception handlers that use its information. PR [#4057](https://github.com/tiangolo/fastapi/pull/4057) by [@UKnowWhoIm](https://github.com/UKnowWhoIm). From d5c84594cb816bc17b044fd1063a3bda3f728726 Mon Sep 17 00:00:00 2001 From: James Curtin Date: Fri, 26 Aug 2022 05:41:23 -0400 Subject: [PATCH 0394/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20version=20pin?= =?UTF-8?q?=20accepted=20for=20Flake8,=20for=20internal=20code,=20to=20`fl?= =?UTF-8?q?ake8=20>=3D3.8.3,<6.0.0`=20(#4097)?= 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 5ffdf93ad..9263985ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ test = [ "pytest >=6.2.4,<7.0.0", "pytest-cov >=2.12.0,<4.0.0", "mypy ==0.910", - "flake8 >=3.8.3,<4.0.0", + "flake8 >=3.8.3,<6.0.0", "black == 22.3.0", "isort >=5.0.6,<6.0.0", "requests >=2.24.0,<3.0.0", @@ -83,7 +83,7 @@ dev = [ "python-jose[cryptography] >=3.3.0,<4.0.0", "passlib[bcrypt] >=1.7.2,<2.0.0", "autoflake >=1.4.0,<2.0.0", - "flake8 >=3.8.3,<4.0.0", + "flake8 >=3.8.3,<6.0.0", "uvicorn[standard] >=0.12.0,<0.18.0", "pre-commit >=2.17.0,<3.0.0", ] From f928f3390ca3ff5fdb3575fb26f458cb93c7416c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 09:42:03 +0000 Subject: [PATCH 0395/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a30e397a5..a3186b150 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade version pin accepted for Flake8, for internal code, to `flake8 >=3.8.3,<6.0.0`. PR [#4097](https://github.com/tiangolo/fastapi/pull/4097) by [@jamescurtin](https://github.com/jamescurtin). * 🐛 Fix support for extending `openapi_extras` with parameter lists. PR [#4267](https://github.com/tiangolo/fastapi/pull/4267) by [@orilevari](https://github.com/orilevari). * ✨ Add support for `FrozenSet` in parameters (e.g. query). PR [#2938](https://github.com/tiangolo/fastapi/pull/2938) by [@juntatalor](https://github.com/juntatalor). * ✨ Allow custom middlewares to raise `HTTPException`s and propagate them. PR [#2036](https://github.com/tiangolo/fastapi/pull/2036) by [@ghandic](https://github.com/ghandic). From bb53d0b0eaad0b16a7defa640291c1ef35c95e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joon=20Hwan=20=EA=B9=80=EC=A4=80=ED=99=98?= Date: Fri, 26 Aug 2022 22:04:48 +0900 Subject: [PATCH 0396/1881] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20syntax=20highlig?= =?UTF-8?q?hting=20in=20docs=20for=20OpenAPI=20Callbacks=20(#4368)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/openapi-callbacks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 138c90dd7..656ddbb3f 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -31,7 +31,7 @@ It will have a *path operation* that will receive an `Invoice` body, and a query This part is pretty normal, most of the code is probably already familiar to you: -```Python hl_lines="10-14 37-54" +```Python hl_lines="9-13 36-53" {!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` @@ -83,7 +83,7 @@ So we are going to use that same knowledge to document how the *external API* sh First create a new `APIRouter` that will contain one or more callbacks. -```Python hl_lines="5 26" +```Python hl_lines="3 25" {!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` @@ -96,7 +96,7 @@ It should look just like a normal FastAPI *path operation*: * It should probably have a declaration of the body it should receive, e.g. `body: InvoiceEvent`. * And it could also have a declaration of the response it should return, e.g. `response_model=InvoiceEventReceived`. -```Python hl_lines="17-19 22-23 29-33" +```Python hl_lines="16-18 21-22 28-32" {!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` @@ -163,7 +163,7 @@ At this point you have the *callback path operation(s)* needed (the one(s) that Now use the parameter `callbacks` in *your API's path operation decorator* to pass the attribute `.routes` (that's actually just a `list` of routes/*path operations*) from that callback router: -```Python hl_lines="36" +```Python hl_lines="35" {!../../../docs_src/openapi_callbacks/tutorial001.py!} ``` From 75792eb82b47a3a6bb4a971d9694456cc2760865 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:05:26 +0000 Subject: [PATCH 0397/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a3186b150..bd4fa0ba4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Fix syntax highlighting in docs for OpenAPI Callbacks. PR [#4368](https://github.com/tiangolo/fastapi/pull/4368) by [@xncbf](https://github.com/xncbf). * ⬆ Upgrade version pin accepted for Flake8, for internal code, to `flake8 >=3.8.3,<6.0.0`. PR [#4097](https://github.com/tiangolo/fastapi/pull/4097) by [@jamescurtin](https://github.com/jamescurtin). * 🐛 Fix support for extending `openapi_extras` with parameter lists. PR [#4267](https://github.com/tiangolo/fastapi/pull/4267) by [@orilevari](https://github.com/orilevari). * ✨ Add support for `FrozenSet` in parameters (e.g. query). PR [#2938](https://github.com/tiangolo/fastapi/pull/2938) by [@juntatalor](https://github.com/juntatalor). From c8124496fc34c024e411f293d3893219005d409f Mon Sep 17 00:00:00 2001 From: Muzaffer Cikay Date: Fri, 26 Aug 2022 16:16:17 +0300 Subject: [PATCH 0398/1881] =?UTF-8?q?=E2=99=BB=20Simplify=20conditional=20?= =?UTF-8?q?assignment=20in=20`fastapi/dependencies/utils.py`=20(#4597)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/dependencies/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index d781fdb62..d098b65f1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -302,10 +302,7 @@ def get_dependant( assert is_scalar_field( field=param_field ), "Path params must be of one of the supported types" - if isinstance(param.default, params.Path): - ignore_default = False - else: - ignore_default = True + ignore_default = not isinstance(param.default, params.Path) param_field = get_param_field( param=param, param_name=param_name, From a64387c3fc03120e3424fb12bfa64df8d949da43 Mon Sep 17 00:00:00 2001 From: Guillermo Quintana Pelayo <36505071+GuilleQP@users.noreply.github.com> Date: Fri, 26 Aug 2022 15:16:44 +0200 Subject: [PATCH 0399/1881] =?UTF-8?q?=E2=99=BB=20Move=20internal=20variabl?= =?UTF-8?q?e=20for=20errors=20in=20`jsonable=5Fencoder`=20to=20put=20relat?= =?UTF-8?q?ed=20code=20closer=20(#4560)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/encoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index b1fde73ce..6f571edb2 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -137,10 +137,10 @@ def jsonable_encoder( if isinstance(obj, classes_tuple): return encoder(obj) - errors: List[Exception] = [] try: data = dict(obj) except Exception as e: + errors: List[Exception] = [] errors.append(e) try: data = vars(obj) From 923e0ac0c16b782f41bc906f4e883fa0b687fa06 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:17:04 +0000 Subject: [PATCH 0400/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bd4fa0ba4..ed67fab04 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Simplify conditional assignment in `fastapi/dependencies/utils.py`. PR [#4597](https://github.com/tiangolo/fastapi/pull/4597) by [@cikay](https://github.com/cikay). * 🎨 Fix syntax highlighting in docs for OpenAPI Callbacks. PR [#4368](https://github.com/tiangolo/fastapi/pull/4368) by [@xncbf](https://github.com/xncbf). * ⬆ Upgrade version pin accepted for Flake8, for internal code, to `flake8 >=3.8.3,<6.0.0`. PR [#4097](https://github.com/tiangolo/fastapi/pull/4097) by [@jamescurtin](https://github.com/jamescurtin). * 🐛 Fix support for extending `openapi_extras` with parameter lists. PR [#4267](https://github.com/tiangolo/fastapi/pull/4267) by [@orilevari](https://github.com/orilevari). From ae56590c515e0ece83de951c4656c5c7d7e523c2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:17:25 +0000 Subject: [PATCH 0401/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed67fab04..74efd542f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Move internal variable for errors in `jsonable_encoder` to put related code closer. PR [#4560](https://github.com/tiangolo/fastapi/pull/4560) by [@GuilleQP](https://github.com/GuilleQP). * ♻ Simplify conditional assignment in `fastapi/dependencies/utils.py`. PR [#4597](https://github.com/tiangolo/fastapi/pull/4597) by [@cikay](https://github.com/cikay). * 🎨 Fix syntax highlighting in docs for OpenAPI Callbacks. PR [#4368](https://github.com/tiangolo/fastapi/pull/4368) by [@xncbf](https://github.com/xncbf). * ⬆ Upgrade version pin accepted for Flake8, for internal code, to `flake8 >=3.8.3,<6.0.0`. PR [#4097](https://github.com/tiangolo/fastapi/pull/4097) by [@jamescurtin](https://github.com/jamescurtin). From 00bdf533ef387b85a85df298de8537992c7d7c45 Mon Sep 17 00:00:00 2001 From: Shahriyar Rzayev Date: Fri, 26 Aug 2022 17:23:25 +0400 Subject: [PATCH 0402/1881] =?UTF-8?q?=E2=99=BB=20Change=20a=20`dict()`=20f?= =?UTF-8?q?or=20`{}`=20in=20`fastapi/utils.py`=20(#3138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index 0163fd95f..b7da3e484 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -87,7 +87,7 @@ def create_cloned_field( ) -> ModelField: # _cloned_types has already cloned types, to support recursive models if cloned_types is None: - cloned_types = dict() + cloned_types = {} original_type = field.type_ if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ From f3e9dcd891342c3818781ebccb72f43edd71d9a6 Mon Sep 17 00:00:00 2001 From: Micael Jarniac Date: Fri, 26 Aug 2022 10:24:04 -0300 Subject: [PATCH 0403/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/python-types.md`=20(#4886)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/python-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 3b0ee4cf6..87fdd6d8d 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -158,7 +158,7 @@ The syntax using `typing` is **compatible** with all versions, from Python 3.6 t As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. -If you can chose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. +If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. #### List From 3df68694c01b4f897519c63dddf2cd230fc17e40 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:24:10 +0000 Subject: [PATCH 0404/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 74efd542f..9e3dba5e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Change a `dict()` for `{}` in `fastapi/utils.py`. PR [#3138](https://github.com/tiangolo/fastapi/pull/3138) by [@ShahriyarR](https://github.com/ShahriyarR). * ♻ Move internal variable for errors in `jsonable_encoder` to put related code closer. PR [#4560](https://github.com/tiangolo/fastapi/pull/4560) by [@GuilleQP](https://github.com/GuilleQP). * ♻ Simplify conditional assignment in `fastapi/dependencies/utils.py`. PR [#4597](https://github.com/tiangolo/fastapi/pull/4597) by [@cikay](https://github.com/cikay). * 🎨 Fix syntax highlighting in docs for OpenAPI Callbacks. PR [#4368](https://github.com/tiangolo/fastapi/pull/4368) by [@xncbf](https://github.com/xncbf). From 26097421d3aeef032cbc97b9117594fdd31af6a5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:24:42 +0000 Subject: [PATCH 0405/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9e3dba5e8..fc4523537 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#4886](https://github.com/tiangolo/fastapi/pull/4886) by [@MicaelJarniac](https://github.com/MicaelJarniac). * ♻ Change a `dict()` for `{}` in `fastapi/utils.py`. PR [#3138](https://github.com/tiangolo/fastapi/pull/3138) by [@ShahriyarR](https://github.com/ShahriyarR). * ♻ Move internal variable for errors in `jsonable_encoder` to put related code closer. PR [#4560](https://github.com/tiangolo/fastapi/pull/4560) by [@GuilleQP](https://github.com/GuilleQP). * ♻ Simplify conditional assignment in `fastapi/dependencies/utils.py`. PR [#4597](https://github.com/tiangolo/fastapi/pull/4597) by [@cikay](https://github.com/cikay). From 8cd8aa4b67f06255bb0016cb478b7541766a8e31 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Fri, 26 Aug 2022 14:25:02 +0100 Subject: [PATCH 0406/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20mypy=20config?= =?UTF-8?q?,=20use=20`strict=20=3D=20true`=20instead=20of=20manual=20confi?= =?UTF-8?q?gs=20(#4605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- pyproject.toml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9263985ce..3b77b113b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,21 +104,7 @@ 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 +strict = true [[tool.mypy.overrides]] module = "fastapi.concurrency" From afe44f0b258d44822e77f9a9db965933b38add05 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:25:48 +0000 Subject: [PATCH 0407/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fc4523537..ff2a7120a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#4605](https://github.com/tiangolo/fastapi/pull/4605) by [@michaeloliverx](https://github.com/michaeloliverx). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#4886](https://github.com/tiangolo/fastapi/pull/4886) by [@MicaelJarniac](https://github.com/MicaelJarniac). * ♻ Change a `dict()` for `{}` in `fastapi/utils.py`. PR [#3138](https://github.com/tiangolo/fastapi/pull/3138) by [@ShahriyarR](https://github.com/ShahriyarR). * ♻ Move internal variable for errors in `jsonable_encoder` to put related code closer. PR [#4560](https://github.com/tiangolo/fastapi/pull/4560) by [@GuilleQP](https://github.com/GuilleQP). From e3c055ba8f1339faa1b0d931069c028b4e36fa17 Mon Sep 17 00:00:00 2001 From: Micael Jarniac Date: Fri, 26 Aug 2022 10:26:03 -0300 Subject: [PATCH 0408/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs,=20compa?= =?UTF-8?q?re=20enums=20with=20identity=20instead=20of=20equality=20(#4905?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/path_params/tutorial005.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/path_params/tutorial005.py b/docs_src/path_params/tutorial005.py index 8e3767744..9a24a4963 100644 --- a/docs_src/path_params/tutorial005.py +++ b/docs_src/path_params/tutorial005.py @@ -14,7 +14,7 @@ app = FastAPI() @app.get("/models/{model_name}") async def get_model(model_name: ModelName): - if model_name == ModelName.alexnet: + if model_name is ModelName.alexnet: return {"model_name": model_name, "message": "Deep Learning FTW!"} if model_name.value == "lenet": From aaf5a380df524a3254f7acb70dd17e160220b5b4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:27:07 +0000 Subject: [PATCH 0409/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ff2a7120a..cab55390e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs, compare enums with identity instead of equality. PR [#4905](https://github.com/tiangolo/fastapi/pull/4905) by [@MicaelJarniac](https://github.com/MicaelJarniac). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#4605](https://github.com/tiangolo/fastapi/pull/4605) by [@michaeloliverx](https://github.com/michaeloliverx). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#4886](https://github.com/tiangolo/fastapi/pull/4886) by [@MicaelJarniac](https://github.com/MicaelJarniac). * ♻ Change a `dict()` for `{}` in `fastapi/utils.py`. PR [#3138](https://github.com/tiangolo/fastapi/pull/3138) by [@ShahriyarR](https://github.com/ShahriyarR). From 0539dd9cd35b7eb3b19945c19c1dc569440c0ec1 Mon Sep 17 00:00:00 2001 From: David Kim Date: Fri, 26 Aug 2022 22:29:50 +0900 Subject: [PATCH 0410/1881] =?UTF-8?q?=F0=9F=94=A7=20Fix=20Type=20hint=20of?= =?UTF-8?q?=20`auto=5Ferror`=20which=20does=20not=20need=20to=20be=20`Opti?= =?UTF-8?q?onal[bool]`=20(#4933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/security/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 888208c15..653c3010e 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -119,7 +119,7 @@ class OAuth2(SecurityBase): flows: Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]] = OAuthFlowsModel(), scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: Optional[bool] = True + auto_error: bool = True ): self.model = OAuth2Model(flows=flows, description=description) self.scheme_name = scheme_name or self.__class__.__name__ From 96c3f44a0e2063199d798c685f562b15422f61e2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:30:28 +0000 Subject: [PATCH 0411/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cab55390e..e90ff2463 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Fix Type hint of `auto_error` which does not need to be `Optional[bool]`. PR [#4933](https://github.com/tiangolo/fastapi/pull/4933) by [@DavidKimDY](https://github.com/DavidKimDY). * 📝 Update docs, compare enums with identity instead of equality. PR [#4905](https://github.com/tiangolo/fastapi/pull/4905) by [@MicaelJarniac](https://github.com/MicaelJarniac). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#4605](https://github.com/tiangolo/fastapi/pull/4605) by [@michaeloliverx](https://github.com/michaeloliverx). * ✏ Fix typo in `docs/en/docs/python-types.md`. PR [#4886](https://github.com/tiangolo/fastapi/pull/4886) by [@MicaelJarniac](https://github.com/MicaelJarniac). From dde140d8e35e82d56f5aa0064942da1e6c9cf238 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 26 Aug 2022 08:32:30 -0500 Subject: [PATCH 0412/1881] =?UTF-8?q?=F0=9F=93=9D=20Simplify=20example=20f?= =?UTF-8?q?or=20docs=20for=20Additional=20Responses,=20remove=20unnecessar?= =?UTF-8?q?y=20`else`=20(#4693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/additional-responses.md | 2 +- docs_src/additional_responses/tutorial001.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index 5d136da41..dca5f6a98 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -23,7 +23,7 @@ Each of those response `dict`s can have a key `model`, containing a Pydantic mod For example, to declare another response with a status code `404` and a Pydantic model `Message`, you can write: -```Python hl_lines="18 23" +```Python hl_lines="18 22" {!../../../docs_src/additional_responses/tutorial001.py!} ``` diff --git a/docs_src/additional_responses/tutorial001.py b/docs_src/additional_responses/tutorial001.py index 79dcc2efe..ffa821b91 100644 --- a/docs_src/additional_responses/tutorial001.py +++ b/docs_src/additional_responses/tutorial001.py @@ -19,5 +19,4 @@ app = FastAPI() async def read_item(item_id: str): if item_id == "foo": return {"id": "foo", "value": "there goes my hero"} - else: - return JSONResponse(status_code=404, content={"message": "Item not found"}) + return JSONResponse(status_code=404, content={"message": "Item not found"}) From d80b065b5e696d65fbd139c539266276e0d0817d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:33:08 +0000 Subject: [PATCH 0413/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e90ff2463..9c61b19d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Simplify example for docs for Additional Responses, remove unnecessary `else`. PR [#4693](https://github.com/tiangolo/fastapi/pull/4693) by [@adriangb](https://github.com/adriangb). * 🔧 Fix Type hint of `auto_error` which does not need to be `Optional[bool]`. PR [#4933](https://github.com/tiangolo/fastapi/pull/4933) by [@DavidKimDY](https://github.com/DavidKimDY). * 📝 Update docs, compare enums with identity instead of equality. PR [#4905](https://github.com/tiangolo/fastapi/pull/4905) by [@MicaelJarniac](https://github.com/MicaelJarniac). * 🔧 Update mypy config, use `strict = true` instead of manual configs. PR [#4605](https://github.com/tiangolo/fastapi/pull/4605) by [@michaeloliverx](https://github.com/michaeloliverx). From 1f2070af20f889c956b13aa79313532cf0956b71 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:44:50 +0000 Subject: [PATCH 0415/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9c61b19d8..fe3a0961b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update `ko/mkdocs.yml` for a missing link. PR [#5020](https://github.com/tiangolo/fastapi/pull/5020) by [@dalinaum](https://github.com/dalinaum). * 📝 Simplify example for docs for Additional Responses, remove unnecessary `else`. PR [#4693](https://github.com/tiangolo/fastapi/pull/4693) by [@adriangb](https://github.com/adriangb). * 🔧 Fix Type hint of `auto_error` which does not need to be `Optional[bool]`. PR [#4933](https://github.com/tiangolo/fastapi/pull/4933) by [@DavidKimDY](https://github.com/DavidKimDY). * 📝 Update docs, compare enums with identity instead of equality. PR [#4905](https://github.com/tiangolo/fastapi/pull/4905) by [@MicaelJarniac](https://github.com/MicaelJarniac). From dc10b81d05fc4438038d74543333661073bf7c8a Mon Sep 17 00:00:00 2001 From: pylounge <78209508+pylounge@users.noreply.github.com> Date: Fri, 26 Aug 2022 16:46:22 +0300 Subject: [PATCH 0416/1881] =?UTF-8?q?=E2=99=BB=20Simplify=20internal=20Reg?= =?UTF-8?q?Ex=20in=20`fastapi/utils.py`=20(#5057)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index b7da3e484..0ced01252 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -140,14 +140,14 @@ def generate_operation_id_for_path( stacklevel=2, ) operation_id = name + path - operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id) + operation_id = re.sub(r"\W", "_", operation_id) operation_id = operation_id + "_" + method.lower() return operation_id def generate_unique_id(route: "APIRoute") -> str: operation_id = route.name + route.path_format - operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id) + operation_id = re.sub(r"\W", "_", operation_id) assert route.methods operation_id = operation_id + "_" + list(route.methods)[0].lower() return operation_id From 26c68c6e0d95ff7b915365c9e78c2da93538f011 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 26 Aug 2022 13:47:11 +0000 Subject: [PATCH 0417/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fe3a0961b..bd2d78fa1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Simplify internal RegEx in `fastapi/utils.py`. PR [#5057](https://github.com/tiangolo/fastapi/pull/5057) by [@pylounge](https://github.com/pylounge). * 🌐 Update `ko/mkdocs.yml` for a missing link. PR [#5020](https://github.com/tiangolo/fastapi/pull/5020) by [@dalinaum](https://github.com/dalinaum). * 📝 Simplify example for docs for Additional Responses, remove unnecessary `else`. PR [#4693](https://github.com/tiangolo/fastapi/pull/4693) by [@adriangb](https://github.com/adriangb). * 🔧 Fix Type hint of `auto_error` which does not need to be `Optional[bool]`. PR [#4933](https://github.com/tiangolo/fastapi/pull/4933) by [@DavidKimDY](https://github.com/DavidKimDY). From de6ccd77544b0c6e5bdcf2f470e6fd1a068344ef Mon Sep 17 00:00:00 2001 From: Erik Vroon Date: Fri, 26 Aug 2022 06:56:07 -0700 Subject: [PATCH 0418/1881] =?UTF-8?q?=E2=9C=A8=20Add=20ReDoc=20`
-
+
From 173b8916034d15bd96084e60b05f7713f3e92713 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 Sep 2022 09:53:06 +0000 Subject: [PATCH 0461/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1e5dd927f..242327787 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for `ORJSONResponse` with details about improving performance. PR [#2615](https://github.com/tiangolo/fastapi/pull/2615) by [@falkben](https://github.com/falkben). * 📝 Add docs for creating a custom Response class. PR [#5331](https://github.com/tiangolo/fastapi/pull/5331) by [@tiangolo](https://github.com/tiangolo). * 📝 Add tip about using alias for form data fields. PR [#5329](https://github.com/tiangolo/fastapi/pull/5329) by [@tiangolo](https://github.com/tiangolo). From 7250c194da6fae6155817c4b30bcdd112acce655 Mon Sep 17 00:00:00 2001 From: "abc.zxy" Date: Fri, 2 Sep 2022 18:17:31 +0800 Subject: [PATCH 0462/1881] =?UTF-8?q?=E2=9C=A8=20Update=20`ORJSONResponse`?= =?UTF-8?q?=20to=20support=20non=20`str`=20keys=20and=20serializing=20Nump?= =?UTF-8?q?y=20arrays=20(#3892)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/responses.py | 4 +++- tests/test_orjson_response_class.py | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/test_orjson_response_class.py diff --git a/fastapi/responses.py b/fastapi/responses.py index 6cd793157..88dba96e8 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -31,4 +31,6 @@ class ORJSONResponse(JSONResponse): def render(self, content: Any) -> bytes: assert orjson is not None, "orjson must be installed to use ORJSONResponse" - return orjson.dumps(content) + return orjson.dumps( + content, option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY + ) diff --git a/tests/test_orjson_response_class.py b/tests/test_orjson_response_class.py new file mode 100644 index 000000000..6fe62daf9 --- /dev/null +++ b/tests/test_orjson_response_class.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from fastapi.responses import ORJSONResponse +from fastapi.testclient import TestClient +from sqlalchemy.sql.elements import quoted_name + +app = FastAPI(default_response_class=ORJSONResponse) + + +@app.get("/orjson_non_str_keys") +def get_orjson_non_str_keys(): + key = quoted_name(value="msg", quote=False) + return {key: "Hello World", 1: 1} + + +client = TestClient(app) + + +def test_orjson_non_str_keys(): + with client: + response = client.get("/orjson_non_str_keys") + assert response.json() == {"msg": "Hello World", "1": 1} From b1d0f1e9703b8f163896864e1eef7faf4a7ce661 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 Sep 2022 10:18:09 +0000 Subject: [PATCH 0463/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 242327787..7ce9a74e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Update `ORJSONResponse` to support non `str` keys and serializing Numpy arrays. PR [#3892](https://github.com/tiangolo/fastapi/pull/3892) by [@baby5](https://github.com/baby5). * 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for `ORJSONResponse` with details about improving performance. PR [#2615](https://github.com/tiangolo/fastapi/pull/2615) by [@falkben](https://github.com/falkben). * 📝 Add docs for creating a custom Response class. PR [#5331](https://github.com/tiangolo/fastapi/pull/5331) by [@tiangolo](https://github.com/tiangolo). From 30b3905ef38532c867036c1b650a9ceef5bfb1cc Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Fri, 2 Sep 2022 14:43:21 +0200 Subject: [PATCH 0464/1881] =?UTF-8?q?=E2=9C=A8=20Support=20Python=20intern?= =?UTF-8?q?al=20description=20on=20Pydantic=20model's=20docstring=20(#3032?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fastapi/utils.py b/fastapi/utils.py index 0ced01252..89f54453b 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -37,6 +37,8 @@ def get_model_definitions( ) definitions.update(m_definitions) model_name = model_name_map[model] + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] definitions[model_name] = m_schema return definitions From e7e6404faf41836bc74978714381c2587b53c0c3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 Sep 2022 12:44:09 +0000 Subject: [PATCH 0465/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7ce9a74e9..7f2cc6df2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Support Python internal description on Pydantic model's docstring. PR [#3032](https://github.com/tiangolo/fastapi/pull/3032) by [@Kludex](https://github.com/Kludex). * ✨ Update `ORJSONResponse` to support non `str` keys and serializing Numpy arrays. PR [#3892](https://github.com/tiangolo/fastapi/pull/3892) by [@baby5](https://github.com/baby5). * 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for `ORJSONResponse` with details about improving performance. PR [#2615](https://github.com/tiangolo/fastapi/pull/2615) by [@falkben](https://github.com/falkben). From 52b5b08910356b22cb9837bda88f73f517dd8652 Mon Sep 17 00:00:00 2001 From: Junghoon Yang Date: Fri, 2 Sep 2022 22:36:00 +0900 Subject: [PATCH 0466/1881] =?UTF-8?q?=E2=99=BB=20Internal=20small=20refact?= =?UTF-8?q?or,=20move=20`operation=5Fid`=20parameter=20position=20in=20del?= =?UTF-8?q?ete=20method=20for=20consistency=20with=20the=20code=20(#4474)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index ac6050ca4..a242c504c 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -635,10 +635,10 @@ class FastAPI(Starlette): response_description=response_description, responses=responses, deprecated=deprecated, + operation_id=operation_id, response_model_include=response_model_include, response_model_exclude=response_model_exclude, response_model_by_alias=response_model_by_alias, - operation_id=operation_id, response_model_exclude_unset=response_model_exclude_unset, response_model_exclude_defaults=response_model_exclude_defaults, response_model_exclude_none=response_model_exclude_none, From fbe1a803fc98901a273177d06c4fa8ccb2bcd635 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 Sep 2022 13:36:48 +0000 Subject: [PATCH 0467/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7f2cc6df2..7b956f3f7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). * ✨ Support Python internal description on Pydantic model's docstring. PR [#3032](https://github.com/tiangolo/fastapi/pull/3032) by [@Kludex](https://github.com/Kludex). * ✨ Update `ORJSONResponse` to support non `str` keys and serializing Numpy arrays. PR [#3892](https://github.com/tiangolo/fastapi/pull/3892) by [@baby5](https://github.com/baby5). * 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). From 56f887de15f25157114820d0d5c5ed5e606d53f0 Mon Sep 17 00:00:00 2001 From: Charlie DiGiovanna Date: Sat, 3 Sep 2022 13:12:41 -0400 Subject: [PATCH 0468/1881] =?UTF-8?q?=F0=9F=90=9B=20Make=20sure=20a=20para?= =?UTF-8?q?meter=20defined=20as=20required=20is=20kept=20required=20in=20O?= =?UTF-8?q?penAPI=20even=20if=20defined=20as=20optional=20in=20another=20d?= =?UTF-8?q?ependency=20(#4319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/openapi/utils.py | 17 ++- tests/test_enforce_once_required_parameter.py | 111 ++++++++++++++++++ 2 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 tests/test_enforce_once_required_parameter.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 4d5741f30..86e15b46d 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -222,11 +222,18 @@ def get_openapi_path( ) parameters.extend(operation_parameters) if parameters: - operation["parameters"] = list( - { - (param["in"], param["name"]): param for param in parameters - }.values() - ) + all_parameters = { + (param["in"], param["name"]): param for param in parameters + } + required_parameters = { + (param["in"], param["name"]): param + for param in parameters + if param.get("required") + } + # Make sure required definitions of the same parameter take precedence + # over non-required definitions + all_parameters.update(required_parameters) + operation["parameters"] = list(all_parameters.values()) if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( body_field=route.body_field, model_name_map=model_name_map diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py new file mode 100644 index 000000000..ba8c7353f --- /dev/null +++ b/tests/test_enforce_once_required_parameter.py @@ -0,0 +1,111 @@ +from typing import Optional + +from fastapi import Depends, FastAPI, Query, status +from fastapi.testclient import TestClient + +app = FastAPI() + + +def _get_client_key(client_id: str = Query(...)) -> str: + return f"{client_id}_key" + + +def _get_client_tag(client_id: Optional[str] = Query(None)) -> Optional[str]: + if client_id is None: + return None + return f"{client_id}_tag" + + +@app.get("/foo") +def foo_handler( + client_key: str = Depends(_get_client_key), + client_tag: Optional[str] = Depends(_get_client_tag), +): + return {"client_id": client_key, "client_tag": client_tag} + + +client = TestClient(app) + +expected_schema = { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error " "Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.0.2", + "paths": { + "/foo": { + "get": { + "operationId": "foo_handler_foo_get", + "parameters": [ + { + "in": "query", + "name": "client_id", + "required": True, + "schema": {"title": "Client Id", "type": "string"}, + }, + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful " "Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation " "Error", + }, + }, + "summary": "Foo Handler", + } + } + }, +} + + +def test_schema(): + response = client.get("/openapi.json") + assert response.status_code == status.HTTP_200_OK + actual_schema = response.json() + assert actual_schema == expected_schema + + +def test_get_invalid(): + response = client.get("/foo", params={"client_id": None}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + + +def test_get_valid(): + response = client.get("/foo", params={"client_id": "bar"}) + assert response.status_code == 200 + assert response.json() == {"client_id": "bar_key", "client_tag": "bar_tag"} From dbb43da9c00380d4cfee6e6fb01853eb2dfb0189 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Sep 2022 17:13:24 +0000 Subject: [PATCH 0469/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7b956f3f7..8acf8d004 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). * ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). * ✨ Support Python internal description on Pydantic model's docstring. PR [#3032](https://github.com/tiangolo/fastapi/pull/3032) by [@Kludex](https://github.com/Kludex). * ✨ Update `ORJSONResponse` to support non `str` keys and serializing Numpy arrays. PR [#3892](https://github.com/tiangolo/fastapi/pull/3892) by [@baby5](https://github.com/baby5). From 0e7478d39c503a6ed472503a24a06d7ec9bbf013 Mon Sep 17 00:00:00 2001 From: sjyothi54 <99384747+sjyothi54@users.noreply.github.com> Date: Sun, 4 Sep 2022 18:31:08 +0530 Subject: [PATCH 0470/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20link=20to=20New?= =?UTF-8?q?=20Relic=20article:=20"How=20to=20monitor=20FastAPI=20applicati?= =?UTF-8?q?on=20performance=20using=20Python=20agent"=20(#5260)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index e7bd0530b..e9c4ef2f2 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ articles: english: + - author: New Relic + author_link: https://newrelic.com + link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 + title: How to monitor FastAPI application performance using Python agent - author: Jean-Baptiste Rocher author_link: https://hashnode.com/@jibrocher link: https://dev.indooroutdoor.io/series/fastapi-react-poll-app From fae28a9bd7f806d1bd2c01f053ea6c9a297fd4a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:01:40 +0000 Subject: [PATCH 0471/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8acf8d004..f9b90f05c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). * 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). * ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). * ✨ Support Python internal description on Pydantic model's docstring. PR [#3032](https://github.com/tiangolo/fastapi/pull/3032) by [@Kludex](https://github.com/Kludex). From d3ff7c620aa7b07acec0eedd1139f24ffa86b97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Sun, 4 Sep 2022 21:18:08 +0800 Subject: [PATCH 0472/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20a=20small=20code=20?= =?UTF-8?q?highlight=20line=20error=20(#5256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index c5fc35b88..060e1d58a 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -216,7 +216,7 @@ To do that, you can declare that `None` is a valid type but still use `default=. === "Python 3.6 and above" - ```Python hl_lines="8" + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} ``` From c00e1ec6bff30c98201d823467c67db51d1479ea Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:18:44 +0000 Subject: [PATCH 0473/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f9b90f05c..224ac9a52 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). * 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). * 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). * ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). From 68e04f653143ea4836a2b536cd74d0b36af36693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Sun, 4 Sep 2022 10:23:34 -0300 Subject: [PATCH 0474/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20MkDocs=20file=20?= =?UTF-8?q?line=20for=20Portuguese=20translation=20of=20`background-task.m?= =?UTF-8?q?d`=20(#5242)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/pt/mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 5e4ceb66f..59ee3cc88 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/handling-errors.md - Segurança: - tutorial/security/index.md + - tutorial/background-tasks.md - Guia de Usuário Avançado: - advanced/index.md - Implantação: From 198b7ef2cb9bf6e5ab0b13819197b39aa80229ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:24:23 +0000 Subject: [PATCH 0475/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 224ac9a52..5d359dddc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). * ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). * 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). * 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). From ae369d879af021fa3477a39155981f346578a078 Mon Sep 17 00:00:00 2001 From: MendyLanda <54242706+MendyLanda@users.noreply.github.com> Date: Sun, 4 Sep 2022 09:29:06 -0400 Subject: [PATCH 0476/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20note=20about=20P?= =?UTF-8?q?ython=203.10=20`X=20|=20Y`=20operator=20in=20explanation=20abou?= =?UTF-8?q?t=20Response=20Models=20(#5307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/response-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 2bbd4d4fd..ab68314e8 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -168,7 +168,7 @@ Your response model could have default values, like: {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -* `description: Union[str, None] = None` has a default of `None`. +* `description: Union[str, None] = None` (or `str | None = None` in Python 3.10) has a default of `None`. * `tax: float = 10.5` has a default of `10.5`. * `tags: List[str] = []` as a default of an empty list: `[]`. From 697ec94a7e90a0798cdcd3c83fa230aba0b2a3a2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:29:46 +0000 Subject: [PATCH 0477/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5d359dddc..ab431c379 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). * 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). * ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). * 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). From f266dc17d7a3e6e9d2de0ff3d5b54bea8b7874ba Mon Sep 17 00:00:00 2001 From: Peter Fackeldey Date: Sun, 4 Sep 2022 15:37:47 +0200 Subject: [PATCH 0478/1881] =?UTF-8?q?=E2=9C=8F=20Tweak=20wording=20in=20`d?= =?UTF-8?q?ocs/en/docs/advanced/dataclasses.md`=20(#3698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/dataclasses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index 80a063bb8..72daca06a 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -8,7 +8,7 @@ But FastAPI also supports using internal support for `dataclasses`. +This is still supported thanks to **Pydantic**, as it has internal support for `dataclasses`. So, even with the code above that doesn't use Pydantic explicitly, FastAPI is using Pydantic to convert those standard dataclasses to Pydantic's own flavor of dataclasses. From 89839eb83470b42b96cd285c2764577b9409aade Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:38:49 +0000 Subject: [PATCH 0479/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ab431c379..9f3e2f7b5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). * 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). * 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). * ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). From 0d43b6552b30bb81d2f62536cc4faa5aa4251e64 Mon Sep 17 00:00:00 2001 From: Zssaer <45691504+Zssaer@users.noreply.github.com> Date: Sun, 4 Sep 2022 21:39:06 +0800 Subject: [PATCH 0480/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/encoder.md`=20(#4969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/tutorial/encoder.md | 42 ++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 43 insertions(+) create mode 100644 docs/zh/docs/tutorial/encoder.md diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md new file mode 100644 index 000000000..cb813940c --- /dev/null +++ b/docs/zh/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# JSON 兼容编码器 + +在某些情况下,您可能需要将数据类型(如Pydantic模型)转换为与JSON兼容的数据类型(如`dict`、`list`等)。 + +比如,如果您需要将其存储在数据库中。 + +对于这种要求, **FastAPI**提供了`jsonable_encoder()`函数。 + +## 使用`jsonable_encoder` + +让我们假设你有一个数据库名为`fake_db`,它只能接收与JSON兼容的数据。 + +例如,它不接收`datetime`这类的对象,因为这些对象与JSON不兼容。 + +因此,`datetime`对象必须将转换为包含ISO格式化的`str`类型对象。 + +同样,这个数据库也不会接收Pydantic模型(带有属性的对象),而只接收`dict`。 + +对此你可以使用`jsonable_encoder`。 + +它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本: + +=== "Python 3.6 and above" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +在这个例子中,它将Pydantic模型转换为`dict`,并将`datetime`转换为`str`。 + +调用它的结果后就可以使用Python标准编码中的`json.dumps()`。 + +这个操作不会返回一个包含JSON格式(作为字符串)数据的庞大的`str`。它将返回一个Python标准数据结构(例如`dict`),其值和子值都与JSON兼容。 + +!!! note + `jsonable_encoder`实际上是FastAPI内部用来转换数据的。但是它在许多其他场景中也很有用。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index b60a0b7a1..ac8679dc0 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -85,6 +85,7 @@ nav: - tutorial/request-forms-and-files.md - tutorial/handling-errors.md - tutorial/path-operation-configuration.md + - tutorial/encoder.md - tutorial/body-updates.md - 依赖项: - tutorial/dependencies/index.md From fc559b5fcdfb0676e4534fb47a319f015db3cb43 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:39:50 +0000 Subject: [PATCH 0481/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9f3e2f7b5..755f315dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). * ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). * 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). * 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). From dc1534736d37a03b352eedd22e5d76e55209fb49 Mon Sep 17 00:00:00 2001 From: ASpathfinder <31813636+ASpathfinder@users.noreply.github.com> Date: Sun, 4 Sep 2022 21:40:14 +0800 Subject: [PATCH 0482/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/zh/docs/tutorial/request-files.md`=20(#4?= =?UTF-8?q?529)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: KellyAlsa-massive-win Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/request-files.md | 67 +++++++++++++++++++++----- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index 25657d93b..e18d6fc9f 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -44,9 +44,9 @@ 不过,很多情况下,`UploadFile` 更好用。 -## 含 `UploadFile` 的 `File` 参数 +## 含 `UploadFile` 的文件参数 -定义 `File` 参数时使用 `UploadFile`: +定义文件参数时使用 `UploadFile`: ```Python hl_lines="12" {!../../../docs_src/request_files/tutorial001.py!} @@ -94,7 +94,7 @@ contents = myfile.file.read() !!! note "`async` 技术细节" - 使用 `async` 方法时,**FastAPI** 在线程池中执行文件方法,并 `awiat` 操作完成。 + 使用 `async` 方法时,**FastAPI** 在线程池中执行文件方法,并 `await` 操作完成。 !!! note "Starlette 技术细节" @@ -120,6 +120,30 @@ contents = myfile.file.read() 这不是 **FastAPI** 的问题,而是 HTTP 协议的规定。 +## 可选文件上传 + +您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选: + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +## 带有额外元数据的 `UploadFile` + +您也可以将 `File()` 与 `UploadFile` 一起使用,例如,设置额外的元数据: + +```Python hl_lines="13" +{!../../../docs_src/request_files/tutorial001_03.py!} +``` + ## 多文件上传 FastAPI 支持同时上传多个文件。 @@ -128,19 +152,20 @@ FastAPI 支持同时上传多个文件。 上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`): -```Python hl_lines="10 15" -{!../../../docs_src/request_files/tutorial002.py!} -``` +=== "Python 3.6 及以上版本" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` 接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。 -!!! note "笔记" - - 注意,截至 2019 年 4 月 14 日,Swagger UI 不支持在同一个表单字段中上传多个文件。详见 #4276#3641. - - 不过,**FastAPI** 已通过 OpenAPI 标准与之兼容。 - - 因此,只要 Swagger UI 或任何其他支持 OpenAPI 的工具支持多文件上传,都将与 **FastAPI** 兼容。 !!! note "技术细节" @@ -148,6 +173,22 @@ FastAPI 支持同时上传多个文件。 `fastapi.responses` 其实与 `starlette.responses` 相同,只是为了方便开发者调用。实际上,大多数 **FastAPI** 的响应都直接从 Starlette 调用。 +### 带有额外元数据的多文件上传 + +和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`: + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + ## 小结 本节介绍了如何用 `File` 把上传文件声明为(表单数据的)输入参数。 From 5a79564dabff2850b04e747ad85b38ecd15e0718 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 13:40:56 +0000 Subject: [PATCH 0483/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 755f315dc..7a74b7f92 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). * ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). * 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). From ee035dfa9a245669115dffe0c39e42234c4b1472 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 15:59:46 +0200 Subject: [PATCH 0484/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6e51622e7..237feb083 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - --py3-plus - --keep-runtime-typing - repo: https://github.com/myint/autoflake - rev: v1.4 + rev: v1.5.1 hooks: - id: autoflake args: From a3a8d68715bbc344a2b8b8ac6c6f49272c09ce9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 15:59:59 +0200 Subject: [PATCH 0485/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.22.0=20to=202.23.0=20(#5321)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index a4bf322f4..d42b08543 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.22.0 + uses: dawidd6/action-download-artifact@v2.23.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml From 94b7527dfd3e65903a6b34dea9a71c6e54e9c978 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 14:00:22 +0000 Subject: [PATCH 0486/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7a74b7f92..ddbc9605d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). * ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). From 7dd4a4f435b5381af379284af186862bd9f99712 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 14:00:36 +0000 Subject: [PATCH 0487/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ddbc9605d..0da899c00 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). From 81967f8f9340d9288b11e48c0fa0ad415ec6b5ac Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Sun, 4 Sep 2022 17:27:48 +0300 Subject: [PATCH 0488/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/features.md`=20(#5315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/features.md | 203 +++++++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 204 insertions(+) create mode 100644 docs/ru/docs/features.md diff --git a/docs/ru/docs/features.md b/docs/ru/docs/features.md new file mode 100644 index 000000000..0cec4eee2 --- /dev/null +++ b/docs/ru/docs/features.md @@ -0,0 +1,203 @@ +# Основные свойства + +## Основные свойства FastAPI + +**FastAPI** предлагает вам следующее: + +### Использование открытых стандартов + +* OpenAPI для создания API, включая объявления операций пути, параметров, тела запроса, безопасности и т.д. + + +* Автоматическое документирование моделей данных в соответствии с JSON Schema (так как спецификация OpenAPI сама основана на JSON Schema). +* Разработан, придерживаясь этих стандартов, после тщательного их изучения. Эти стандарты изначально включены во фреймфорк, а не являются дополнительной надстройкой. +* Это также позволяет использовать автоматическую **генерацию клиентского кода** на многих языках. + +### Автоматически генерируемая документация + +Интерактивная документация для API и исследования пользовательских веб-интерфейсов. Поскольку этот фреймворк основан на OpenAPI, существует несколько вариантов документирования, 2 из которых включены по умолчанию. + +* Swagger UI, с интерактивным взаимодействием, вызывает и тестирует ваш API прямо из браузера. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Альтернативная документация API в ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Только современный Python + +Все эти возможности основаны на стандартных **аннотациях типов Python 3.6** (благодаря Pydantic). Не нужно изучать новый синтаксис. Только лишь стандартный современный Python. + +Если вам нужно освежить знания, как использовать аннотации типов в Python (даже если вы не используете FastAPI), выделите 2 минуты и просмотрите краткое руководство: [Введение в аннотации типов Python¶ +](python-types.md){.internal-link target=_blank}. + +Вы пишете на стандартном Python с аннотациями типов: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Объявляем параметр user_id с типом `str` +# и получаем поддержку редактора внутри функции +def main(user_id: str): + return user_id + + +# Модель Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +Это можно использовать так: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! Информация + `**second_user_data` означает: + + Передать ключи и значения словаря `second_user_data`, в качестве аргументов типа "ключ-значение", это эквивалентно: `User(id=4, name="Mary", joined="2018-11-30")` . + +### Поддержка редакторов (IDE) + +Весь фреймворк был продуман так, чтобы быть простым и интуитивно понятным в использовании, все решения были проверены на множестве редакторов еще до начала разработки, чтобы обеспечить наилучшие условия при написании кода. + +В опросе Python-разработчиков было выяснено, что наиболее часто используемой функцией редакторов, является "автодополнение". + +Вся структура **FastAPI** основана на удовлетворении этой возможности. Автодополнение работает везде. + +Вам редко нужно будет возвращаться к документации. + +Вот как ваш редактор может вам помочь: + +* в Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* в PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Вы будете получать автодополнение кода даже там, где вы считали это невозможным раньше. +Как пример, ключ `price` внутри тела JSON (который может быть вложенным), приходящего в запросе. + +Больше никаких неправильных имён ключей, метания по документации или прокручивания кода вверх и вниз, в попытках узнать - использовали вы ранее `username` или `user_name`. + +### Краткость +FastAPI имеет продуманные значения **по умолчанию** для всего, с произвольными настройками везде. Все параметры могут быть тонко подстроены так, чтобы делать то, что вам нужно и определять необходимый вам API. + +Но, по умолчанию, всё это **"и так работает"**. + +### Проверка значений + +* Проверка значений для большинства (или всех?) **типов данных** Python, включая: + * Объекты JSON (`dict`). + * Массивы JSON (`list`) с установленными типами элементов. + * Строковые (`str`) поля с ограничением минимальной и максимальной длины. + * Числа (`int`, `float`) с минимальными и максимальными значениями и т.п. + +* Проверка для более экзотических типов, таких как: + * URL. + * Email. + * UUID. + * ...и другие. + +Все проверки обрабатываются хорошо зарекомендовавшим себя и надежным **Pydantic**. + +### Безопасность и аутентификация + +Встроеные функции безопасности и аутентификации. Без каких-либо компромиссов с базами данных или моделями данных. + +Все схемы безопасности, определённые в OpenAPI, включая: + +* HTTP Basic. +* **OAuth2** (также с **токенами JWT**). Ознакомьтесь с руководством [OAuth2 с JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* Ключи API в: + * Заголовках. + * Параметрах запросов. + * Cookies и т.п. + +Вдобавок все функции безопасности от Starlette (включая **сессионные cookies**). + +Все инструменты и компоненты спроектированы для многократного использования и легко интегрируются с вашими системами, хранилищами данных, реляционными и NoSQL базами данных и т. д. + +### Внедрение зависимостей + +FastAPI включает в себя чрезвычайно простую в использовании, но чрезвычайно мощную систему Внедрения зависимостей. + +* Даже зависимости могут иметь зависимости, создавая иерархию или **"графы" зависимостей**. +* Всё **автоматически обрабатывается** фреймворком. +* Все зависимости могут запрашивать данные из запросов и **дополнять операции пути** ограничениями и автоматической документацией. +* **Автоматическая проверка** даже для параметров *операций пути*, определенных в зависимостях. +* Поддержка сложных систем аутентификации пользователей, **соединений с базами данных** и т.д. +* **Никаких компромиссов** с базами данных, интерфейсами и т.д. Но легкая интеграция со всеми ними. + +### Нет ограничений на "Плагины" + +Или, другими словами, нет сложностей с ними, импортируйте и используйте нужный вам код. + +Любая интеграция разработана настолько простой в использовании (с зависимостями), что вы можете создать "плагин" для своего приложения в пару строк кода, используя ту же структуру и синтаксис, что и для ваших *операций пути*. + +### Проверен + +* 100% покрытие тестами. +* 100% аннотирование типов в кодовой базе. +* Используется в реально работающих приложениях. + +## Основные свойства Starlette + +**FastAPI** основан на Starlette и полностью совместим с ним. Так что, любой дополнительный код Starlette, который у вас есть, будет также работать. + +На самом деле, `FastAPI` - это класс, унаследованный от `Starlette`. Таким образом, если вы уже знаете или используете Starlette, большая часть функционала будет работать так же. + +С **FastAPI** вы получаете все возможности **Starlette** (так как FastAPI это всего лишь Starlette на стероидах): + +* Серьёзно впечатляющая производительность. Это один из самых быстрых фреймворков на Python, наравне с приложениями использующими **NodeJS** или **Go**. +* Поддержка **WebSocket**. +* Фоновые задачи для процессов. +* События запуска и выключения. +* Тестовый клиент построен на библиотеке `requests`. +* **CORS**, GZip, статические файлы, потоковые ответы. +* Поддержка **сессий и cookie**. +* 100% покрытие тестами. +* 100% аннотирование типов в кодовой базе. + +## Особенности и возможности Pydantic + +**FastAPI** основан на Pydantic и полностью совместим с ним. Так что, любой дополнительный код Pydantic, который у вас есть, будет также работать. + +Включая внешние библиотеки, также основанные на Pydantic, такие как: ORM'ы, ODM'ы для баз данных. + +Это также означает, что во многих случаях вы можете передавать тот же объект, который получили из запроса, **непосредственно в базу данных**, так как всё проверяется автоматически. + +И наоборот, во многих случаях вы можете просто передать объект, полученный из базы данных, **непосредственно клиенту**. + +С **FastAPI** вы получаете все возможности **Pydantic** (так как, FastAPI основан на Pydantic, для обработки данных): + +* **Никакой нервотрёпки** : + * Не нужно изучать новых схем в микроязыках. + * Если вы знаете аннотации типов в Python, вы знаете, как использовать Pydantic. +* Прекрасно сочетается с вашими **IDE/linter/мозгом**: + * Потому что структуры данных pydantic - это всего лишь экземпляры классов, определённых вами. Автодополнение, проверка кода, mypy и ваша интуиция - всё будет работать с вашими проверенными данными. +* **Быстродействие**: + * В тестовых замерах Pydantic быстрее, чем все другие проверенные библиотеки. +* Проверка **сложных структур**: + * Использование иерархических моделей Pydantic; `List`, `Dict` и т.п. из модуля `typing` (входит в стандартную библиотеку Python). + * Валидаторы позволяют четко и легко определять, проверять и документировать сложные схемы данных в виде JSON Schema. + * У вас могут быть глубоко **вложенные объекты JSON** и все они будут проверены и аннотированы. +* **Расширяемость**: + * Pydantic позволяет определять пользовательские типы данных или расширять проверку методами модели, с помощью проверочных декораторов. +* 100% покрытие тестами. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 2cb5eb8e0..381775ac6 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -58,6 +58,7 @@ nav: - tr: /tr/ - uk: /uk/ - zh: /zh/ +- features.md - python-types.md - Учебник - руководство пользователя: - tutorial/background-tasks.md From 9df22ab86475ddd507985fe74d199bd80b1da42a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 14:28:31 +0000 Subject: [PATCH 0489/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0da899c00..dcf629f81 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). * ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). From 0ae8d091892242954b12d129b627c60517c9cd1e Mon Sep 17 00:00:00 2001 From: baconfield <65581433+baconfield@users.noreply.github.com> Date: Sun, 4 Sep 2022 09:56:29 -0500 Subject: [PATCH 0490/1881] =?UTF-8?q?=E2=9C=8F=20Update=20Hypercorn=20link?= =?UTF-8?q?,=20now=20pointing=20to=20GitHub=20(#5346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- README.md | 2 +- docs/en/docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 590abf17e..bc00d9ed9 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +You will also need an ASGI server, for production such as Uvicorn or Hypercorn.
diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 24ce14e23..97ec0ffcf 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -128,7 +128,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +You will also need an ASGI server, for production such as Uvicorn or Hypercorn.
From 0195bb57062106e44b7d17bc88e91538d9249b2b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 14:57:04 +0000 Subject: [PATCH 0491/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 dcf629f81..af4b36dde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). * 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). * ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4cff206ebb925acfa4904bb5ddf62c6748f8b15f Mon Sep 17 00:00:00 2001 From: Irfanuddin Shafi Ahmed Date: Sun, 4 Sep 2022 20:37:12 +0530 Subject: [PATCH 0492/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20FastAPI=20People?= =?UTF-8?q?=20GitHub=20Action:=20set=20HTTPX=20timeout=20for=20GraphQL=20q?= =?UTF-8?q?uery=20request=20(#5222)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Irfanuddin Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/actions/people/app/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 1455d01ca..cdf423b97 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -260,6 +260,7 @@ class Settings(BaseSettings): input_token: SecretStr input_standard_token: SecretStr github_repository: str + httpx_timeout: int = 30 def get_graphql_response( @@ -270,9 +271,10 @@ def get_graphql_response( response = httpx.post( github_graphql_url, headers=headers, + timeout=settings.httpx_timeout, json={"query": query, "variables": variables, "operationName": "Q"}, ) - if not response.status_code == 200: + if response.status_code != 200: logging.error(f"Response was not 200, after: {after}") logging.error(response.text) raise RuntimeError(response.text) From dd2f759bac14b1ecf259c5d9cafe72e211c0e69e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 15:07:46 +0000 Subject: [PATCH 0493/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 af4b36dde..34183c140 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). * ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). * 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). * ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). From dacb68929082060b46848c237a3a23085eab947b Mon Sep 17 00:00:00 2001 From: Mateusz Nowak Date: Sun, 4 Sep 2022 17:12:10 +0200 Subject: [PATCH 0494/1881] =?UTF-8?q?=E2=9C=A8=20Export=20`WebSocketState`?= =?UTF-8?q?=20in=20`fastapi.websockets`=20(#4376)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/websockets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastapi/websockets.py b/fastapi/websockets.py index bed672acf..55a4ac4a1 100644 --- a/fastapi/websockets.py +++ b/fastapi/websockets.py @@ -1,2 +1,3 @@ from starlette.websockets import WebSocket as WebSocket # noqa from starlette.websockets import WebSocketDisconnect as WebSocketDisconnect # noqa +from starlette.websockets import WebSocketState as WebSocketState # noqa From 80d68a6eda93dd1e89725aca6d38fa9bb8db4f73 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 15:12:59 +0000 Subject: [PATCH 0495/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 34183c140..327175a76 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Export `WebSocketState` in `fastapi.websockets`. PR [#4376](https://github.com/tiangolo/fastapi/pull/4376) by [@matiuszka](https://github.com/matiuszka). * 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). * ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). * 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). From c70fab38e74039d9564da175f3d42c34f9581de8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Sep 2022 15:16:00 +0000 Subject: [PATCH 0496/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions Co-authored-by: Sebastián Ramírez --- docs/en/data/github_sponsors.yml | 182 +++++++++++++++++-------------- docs/en/data/people.yml | 182 +++++++++++++++---------------- 2 files changed, 189 insertions(+), 175 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 6c1efcbbd..891a990a4 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,25 +1,19 @@ sponsors: -- - login: github - avatarUrl: https://avatars.githubusercontent.com/u/9919?v=4 - url: https://github.com/github +- - login: jina-ai + avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 + url: https://github.com/jina-ai - - login: Doist avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 url: https://github.com/Doist - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi - - login: BLUE-DEVIL1134 - avatarUrl: https://avatars.githubusercontent.com/u/55914808?u=f283d674fce31be7fb3ed2665b0f20d89958e541&v=4 - url: https://github.com/BLUE-DEVIL1134 - - login: jina-ai - avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 - url: https://github.com/jina-ai - - login: DropbaseHQ - avatarUrl: https://avatars.githubusercontent.com/u/85367855?v=4 - url: https://github.com/DropbaseHQ - - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI + - login: Lovage-Labs + avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 + url: https://github.com/Lovage-Labs - login: chaserowbotham avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4 url: https://github.com/chaserowbotham @@ -47,9 +41,9 @@ sponsors: - - login: SendCloud avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 url: https://github.com/SendCloud - - login: qaas - avatarUrl: https://avatars.githubusercontent.com/u/8503759?u=10a6b4391ad6ab4cf9487ce54e3fcb61322d1efc&v=4 - url: https://github.com/qaas + - login: mercedes-benz + avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 + url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare @@ -59,10 +53,10 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP -- - login: nnfuzzy - avatarUrl: https://avatars.githubusercontent.com/u/687670?v=4 - url: https://github.com/nnfuzzy - - login: johnadjei + - login: Jonathanjwp + avatarUrl: https://avatars.githubusercontent.com/u/110426978?u=5e6ed4984022cb8886c4fdfdc0c59f55c48a2f1d&v=4 + url: https://github.com/Jonathanjwp +- - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei - login: HiredScore @@ -71,6 +65,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow + - login: khalidelboray + avatarUrl: https://avatars.githubusercontent.com/u/37024839?u=9a4b19123b5a1ba39dc581f8e4e1bc53fafdda2e&v=4 + url: https://github.com/khalidelboray - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck @@ -80,27 +77,39 @@ sponsors: - login: tizz98 avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 url: https://github.com/tizz98 + - login: Vikka + avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 + url: https://github.com/Vikka - login: jmaralc avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 url: https://github.com/jmaralc - - login: marutoraman + - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 - url: https://github.com/marutoraman + url: https://github.com/takashi-yoneya - login: leynier avatarUrl: https://avatars.githubusercontent.com/u/36774373?u=2284831c821307de562ebde5b59014d5416c7e0d&v=4 url: https://github.com/leynier - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb + - login: primer-io + avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 + url: https://github.com/primer-io +- - login: ekpyrosis + avatarUrl: https://avatars.githubusercontent.com/u/60075?v=4 + url: https://github.com/ekpyrosis + - login: ryangrose + avatarUrl: https://avatars.githubusercontent.com/u/24993976?u=052f346aa859f3e52c21bd2c1792f61a98cfbacf&v=4 + url: https://github.com/ryangrose + - login: A-Edge + avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 + url: https://github.com/A-Edge - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -116,6 +125,9 @@ sponsors: - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill + - login: dekoza + avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 + url: https://github.com/dekoza - login: deserat avatarUrl: https://avatars.githubusercontent.com/u/299332?v=4 url: https://github.com/deserat @@ -164,12 +176,18 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 + - login: aacayaco + avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 + url: https://github.com/aacayaco - login: anomaly avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 url: https://github.com/anomaly - login: peterHoburg avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 url: https://github.com/peterHoburg + - login: jgreys + avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=b579fd97033269a5e703ab509c7d5478b146cc2d&v=4 + url: https://github.com/jgreys - login: gorhack avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 url: https://github.com/gorhack @@ -188,9 +206,6 @@ sponsors: - login: ennui93 avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 url: https://github.com/ennui93 - - login: MacroPower - avatarUrl: https://avatars.githubusercontent.com/u/5648814?u=e13991efd1e03c44c911f919872e750530ded633&v=4 - url: https://github.com/MacroPower - login: Yaleesa avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 url: https://github.com/Yaleesa @@ -203,9 +218,6 @@ sponsors: - login: pkucmus avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4 url: https://github.com/pkucmus - - login: ioalloc - avatarUrl: https://avatars.githubusercontent.com/u/6737824?u=6c3a31449f1c92064287171aa9ebe6363a0c9b7b&v=4 - url: https://github.com/ioalloc - login: s3ich4n avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=ba3025d698e1c986655e776ae383a3d60d9d578e&v=4 url: https://github.com/s3ich4n @@ -218,9 +230,6 @@ sponsors: - login: Shackelford-Arden avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 url: https://github.com/Shackelford-Arden - - login: Vikka - avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 - url: https://github.com/Vikka - login: Ge0f3 avatarUrl: https://avatars.githubusercontent.com/u/11887760?u=ccd80f1ac36dcb8517ef5c4e702e8cc5a80cad2f&v=4 url: https://github.com/Ge0f3 @@ -242,9 +251,6 @@ sponsors: - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck - - login: stradivari96 - avatarUrl: https://avatars.githubusercontent.com/u/19752586?u=255f5f06a768f518b20cebd6963e840ac49294fd&v=4 - url: https://github.com/stradivari96 - login: RedCarpetUp avatarUrl: https://avatars.githubusercontent.com/u/20360440?v=4 url: https://github.com/RedCarpetUp @@ -266,9 +272,9 @@ sponsors: - login: veprimk avatarUrl: https://avatars.githubusercontent.com/u/29689749?u=f8cb5a15a286e522e5b189bc572d5a1a90217fb2&v=4 url: https://github.com/veprimk - - login: meysam81 - avatarUrl: https://avatars.githubusercontent.com/u/30233243?u=64dc9fc62d039892c6fb44d804251cad5537132b&v=4 - url: https://github.com/meysam81 + - login: BrettskiPy + avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 + url: https://github.com/BrettskiPy - login: mauroalejandrojm avatarUrl: https://avatars.githubusercontent.com/u/31569442?u=cdada990a1527926a36e95f62c30a8b48bbc49a1&v=4 url: https://github.com/mauroalejandrojm @@ -281,18 +287,12 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: guligon90 - avatarUrl: https://avatars.githubusercontent.com/u/35070513?u=b48c05f669d1ea1d329f90dc70e45f10b569ef55&v=4 - url: https://github.com/guligon90 - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler - login: ddilidili avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 url: https://github.com/ddilidili - - login: dbanty - avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 - url: https://github.com/dbanty - login: VictorCalderon avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4 url: https://github.com/VictorCalderon @@ -302,27 +302,27 @@ sponsors: - login: rafsaf avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf + - login: yezz123 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 url: https://github.com/dudikbender - - login: daisuke8000 - avatarUrl: https://avatars.githubusercontent.com/u/55035595?u=23a3f2f2925ad3efc27c7420041622b7f5fd2b79&v=4 - url: https://github.com/daisuke8000 - login: dazeddd avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 url: https://github.com/dazeddd - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut - - login: primer-io - avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 - url: https://github.com/primer-io - - login: around - avatarUrl: https://avatars.githubusercontent.com/u/62425723?v=4 - url: https://github.com/around + - login: patsatsia + avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=419516384f798a35e9d9e2dd81282cc46c151b58&v=4 + url: https://github.com/patsatsia - login: predictionmachine avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4 url: https://github.com/predictionmachine + - login: minsau + avatarUrl: https://avatars.githubusercontent.com/u/64386242?u=7e45f24b2958caf946fa3546ea33bacf5cd886f8&v=4 + url: https://github.com/minsau - login: daverin avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 url: https://github.com/daverin @@ -335,11 +335,14 @@ sponsors: - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h +- - login: raestrada95 + avatarUrl: https://avatars.githubusercontent.com/u/34411704?u=f32b7e8f57a3f7e2a99e2bc115356f89f094f340&v=4 + url: https://github.com/raestrada95 - - login: linux-china avatarUrl: https://avatars.githubusercontent.com/u/46711?v=4 url: https://github.com/linux-china - login: ddanier - avatarUrl: https://avatars.githubusercontent.com/u/113563?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier - login: jhb avatarUrl: https://avatars.githubusercontent.com/u/142217?v=4 @@ -350,6 +353,9 @@ sponsors: - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson + - login: hhatto + avatarUrl: https://avatars.githubusercontent.com/u/150309?u=3e8f63c27bf996bfc68464b0ce3f7a3e40e6ea7f&v=4 + url: https://github.com/hhatto - login: yourkin avatarUrl: https://avatars.githubusercontent.com/u/178984?u=fa7c3503b47bf16405b96d21554bc59f07a65523&v=4 url: https://github.com/yourkin @@ -369,7 +375,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/388564?v=4 url: https://github.com/dmig - login: rinckd - avatarUrl: https://avatars.githubusercontent.com/u/546002?u=1fcc7e664dc86524a0af6837a0c222829c3fd4e5&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/546002?u=8ec88ab721a5636346f19dcd677a6f323058be8b&v=4 url: https://github.com/rinckd - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 @@ -393,7 +399,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=169dba3bfbc04ed214a914640ff435969f19ddb3&v=4 url: https://github.com/Pytlicek - login: allen0125 - avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=d4feb3d06a61baa4a69857ce371cc53fb4dffd2c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4 url: https://github.com/allen0125 - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 @@ -404,6 +410,9 @@ sponsors: - login: rglsk avatarUrl: https://avatars.githubusercontent.com/u/2768101?u=e349c88673f2155fe021331377c656a9d74bcc25&v=4 url: https://github.com/rglsk + - login: Debakel + avatarUrl: https://avatars.githubusercontent.com/u/2857237?u=07df6d11c8feef9306d071cb1c1005a2dd596585&v=4 + url: https://github.com/Debakel - login: paul121 avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4 url: https://github.com/paul121 @@ -413,6 +422,9 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti + - login: jonathanhle + avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 + url: https://github.com/jonathanhle - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy @@ -425,9 +437,15 @@ sponsors: - login: unredundant avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=57dd0023365bec03f4fc566df6b81bc0a264a47d&v=4 url: https://github.com/unredundant + - login: Baghdady92 + avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 + url: https://github.com/Baghdady92 - login: holec avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4 url: https://github.com/holec + - login: hcristea + avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 + url: https://github.com/hcristea - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 @@ -449,12 +467,18 @@ sponsors: - login: satwikkansal avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4 url: https://github.com/satwikkansal + - login: mntolia + avatarUrl: https://avatars.githubusercontent.com/u/10390224?v=4 + url: https://github.com/mntolia - login: pheanex avatarUrl: https://avatars.githubusercontent.com/u/10408624?u=5b6bab6ee174aa6e991333e06eb29f628741013d&v=4 url: https://github.com/pheanex - login: JimFawkes avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 url: https://github.com/JimFawkes + - login: giuliano-oliveira + avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 + url: https://github.com/giuliano-oliveira - login: logan-connolly avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly @@ -467,9 +491,6 @@ sponsors: - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre - - login: aprilcoskun - avatarUrl: https://avatars.githubusercontent.com/u/17393603?u=29145243b4c7fadc80c7099471309cc2c04b6bcc&v=4 - url: https://github.com/aprilcoskun - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia @@ -485,15 +506,12 @@ sponsors: - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli - - login: elisoncrum - avatarUrl: https://avatars.githubusercontent.com/u/30413278?u=531190845bb0935dbc1e4f017cda3cb7b4dd0e54&v=4 - url: https://github.com/elisoncrum + - login: ruizdiazever + avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 + url: https://github.com/ruizdiazever - login: HosamAlmoghraby avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 url: https://github.com/HosamAlmoghraby - - login: kitaramu0401 - avatarUrl: https://avatars.githubusercontent.com/u/33246506?u=929e6efa2c518033b8097ba524eb5347a069bb3b&v=4 - url: https://github.com/kitaramu0401 - login: engineerjoe440 avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 @@ -516,11 +534,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4 url: https://github.com/ilias-ant - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=fee5739394fea074cb0b66929d070114a5067aae&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=2a812c1a2ec58227ed01778837f255143de9df97&v=4 url: https://github.com/arrrrrmin - - login: BomGard - avatarUrl: https://avatars.githubusercontent.com/u/47395385?u=8e9052f54e0b8dc7285099c438fa29c55a7d6407&v=4 - url: https://github.com/BomGard + - login: MauriceKuenicke + avatarUrl: https://avatars.githubusercontent.com/u/47433175?u=37455bc95c7851db296ac42626f0cacb77ca2443&v=4 + url: https://github.com/MauriceKuenicke - login: akanz1 avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4 url: https://github.com/akanz1 @@ -548,24 +566,24 @@ sponsors: - login: alessio-proietti avatarUrl: https://avatars.githubusercontent.com/u/67370599?u=8ac73db1e18e946a7681f173abdb640516f88515&v=4 url: https://github.com/alessio-proietti - - login: Mr-Sunglasses - avatarUrl: https://avatars.githubusercontent.com/u/81439109?u=a5d0762fdcec26e18a028aef05323de3c6fb195c&v=4 - url: https://github.com/Mr-Sunglasses + - login: pondDevThai + avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 + url: https://github.com/pondDevThai + - login: CY0xZ + avatarUrl: https://avatars.githubusercontent.com/u/103884082?u=0b3260c6a1af6268492f69264dbb75989afb155f&v=4 + url: https://github.com/CY0xZ - - login: backbord avatarUrl: https://avatars.githubusercontent.com/u/6814946?v=4 url: https://github.com/backbord + - login: mattwelke + avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 + url: https://github.com/mattwelke - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - - login: zachspar - avatarUrl: https://avatars.githubusercontent.com/u/41600414?u=edf29c197137f51bace3f19a2ba759662640771f&v=4 - url: https://github.com/zachspar - - login: sownt - avatarUrl: https://avatars.githubusercontent.com/u/44340502?u=c06e3c45fb00a403075172770805fe57ff17b1cf&v=4 - url: https://github.com/sownt - - login: aahouzi - avatarUrl: https://avatars.githubusercontent.com/u/75032370?u=82677ee9cd86b3ccf4e13d9cb6765d8de5713e1e&v=4 - url: https://github.com/aahouzi + - login: Moises6669 + avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=81761d977faabab60cba5a06e7d50b44416c0c99&v=4 + url: https://github.com/Moises6669 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 031c1ca4d..d804ecabb 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1248 - prs: 318 + answers: 1265 + prs: 333 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 352 + count: 360 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -29,6 +29,10 @@ experts: count: 130 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 +- login: JarroVGIT + count: 129 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: raphaelauv count: 77 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -37,10 +41,6 @@ experts: count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: JarroVGIT - count: 68 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT - login: falkben count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 @@ -57,14 +57,18 @@ experts: count: 43 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=81f0262df34e1460ca546fbd0c211169c2478532&v=4 - url: https://github.com/adriangb +- login: iudeen + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: jgould22 - count: 40 + count: 42 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: adriangb + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=75087f0cf0e9f725f3cd18a899218b6c63ae60d3&v=4 + url: https://github.com/adriangb - login: includeamin count: 39 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -73,6 +77,10 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary +- login: chbndrhnns + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -85,20 +93,16 @@ experts: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: chbndrhnns - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes - login: panla - count: 27 + count: 28 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla - login: acidjunk - count: 25 + count: 26 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk - login: ghandic @@ -125,14 +129,14 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: odiseo0 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 + url: https://github.com/odiseo0 - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: odiseo0 - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 - url: https://github.com/odiseo0 - login: Hultner count: 18 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -173,6 +177,10 @@ experts: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 url: https://github.com/haizaar +- login: yinziyan1206 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 + url: https://github.com/yinziyan1206 - login: valentin994 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 @@ -181,47 +189,31 @@ experts: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 url: https://github.com/David-Lor -- login: yinziyan1206 - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 - url: https://github.com/yinziyan1206 - login: n8sty count: 12 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: lowercase00 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/21188280?v=4 - url: https://github.com/lowercase00 -- login: zamiramir - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/40475662?u=e58ef61034e8d0d6a312cc956fb09b9c3332b449&v=4 - url: https://github.com/zamiramir -last_month_active: -- login: JarroVGIT - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT - login: zoliknemet - count: 9 + count: 12 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet +last_month_active: +- login: JarroVGIT + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: iudeen - count: 5 + count: 17 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen -- login: Kludex +- login: imacat count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/594968?v=4 + url: https://github.com/imacat +- login: Kludex + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: odiseo0 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 - url: https://github.com/odiseo0 -- login: jonatasoli - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli top_contributors: - login: waynerv count: 25 @@ -236,25 +228,29 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: jaystone776 - count: 15 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: Kludex + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl -- login: Kludex - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex - login: Smlep count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep +- login: dependabot + count: 9 + avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 + url: https://github.com/apps/dependabot - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -279,10 +275,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 url: https://github.com/Attsun1031 -- login: dependabot +- login: ComicShrimp count: 5 - avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 - url: https://github.com/apps/dependabot + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -303,25 +299,29 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 url: https://github.com/hitrust +- login: rjNemo + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: lsglucas count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: ComicShrimp - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 - url: https://github.com/ComicShrimp - login: NinaHwang count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang top_reviewers: - login: Kludex - count: 95 + count: 96 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: BilalAlpaslan + count: 51 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: tokusumi - count: 49 + count: 50 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: waynerv @@ -332,10 +332,6 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 -- login: BilalAlpaslan - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 @@ -345,7 +341,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay - login: yezz123 - count: 34 + count: 39 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: AdrianDeAnda @@ -356,6 +352,10 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: JarroVGIT + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: cassiobotaro count: 25 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 @@ -369,7 +369,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki - login: hard-coders - count: 19 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders - login: 0417taehyun @@ -380,10 +380,6 @@ top_reviewers: count: 18 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: JarroVGIT - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT - login: zy7y count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 @@ -424,6 +420,14 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 url: https://github.com/RunningIkkyu +- login: odiseo0 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 + url: https://github.com/odiseo0 +- login: LorhanSohaky + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 @@ -440,10 +444,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: odiseo0 +- login: ComicShrimp count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 - url: https://github.com/odiseo0 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + url: https://github.com/ComicShrimp - login: graingert count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 @@ -460,6 +464,10 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca +- login: izaguerreiro + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 + url: https://github.com/izaguerreiro - login: raphaelauv count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -472,10 +480,6 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 url: https://github.com/rogerbrinkmann -- login: ComicShrimp - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 - url: https://github.com/ComicShrimp - login: NinaHwang count: 8 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 @@ -488,6 +492,10 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 url: https://github.com/Serrones +- login: jovicon + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 + url: https://github.com/jovicon - login: ryuckel count: 7 avatarUrl: https://avatars.githubusercontent.com/u/36391432?u=094eec0cfddd5013f76f31e55e56147d78b19553&v=4 @@ -500,15 +508,3 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause -- login: wakabame - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/35513518?v=4 - url: https://github.com/wakabame -- login: AlexandreBiguet - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/1483079?u=ff926455cd4cab03c6c49441aa5dc2b21df3e266&v=4 - url: https://github.com/AlexandreBiguet -- login: krocdort - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/34248814?v=4 - url: https://github.com/krocdort From d0cc996dc7000542beb95286834c55bd785a78eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 15:17:43 +0000 Subject: [PATCH 0497/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 327175a76..9232db7f1 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 [#5347](https://github.com/tiangolo/fastapi/pull/5347) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✨ Export `WebSocketState` in `fastapi.websockets`. PR [#4376](https://github.com/tiangolo/fastapi/pull/4376) by [@matiuszka](https://github.com/matiuszka). * 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). * ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). From f8460a8b54fd4975ca64c7fbe8d516740781f0df Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sun, 4 Sep 2022 14:09:24 -0500 Subject: [PATCH 0498/1881] =?UTF-8?q?=F0=9F=90=9B=20Allow=20exit=20code=20?= =?UTF-8?q?for=20dependencies=20with=20`yield`=20to=20always=20execute,=20?= =?UTF-8?q?by=20removing=20capacity=20limiter=20for=20them,=20to=20e.g.=20?= =?UTF-8?q?allow=20closing=20DB=20connections=20without=20deadlocks=20(#51?= =?UTF-8?q?22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/concurrency.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index becac3f33..c728ec1d2 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -1,6 +1,8 @@ import sys from typing import AsyncGenerator, ContextManager, TypeVar +import anyio +from anyio import CapacityLimiter from starlette.concurrency import iterate_in_threadpool as iterate_in_threadpool # noqa from starlette.concurrency import run_in_threadpool as run_in_threadpool # noqa from starlette.concurrency import ( # noqa @@ -22,11 +24,24 @@ _T = TypeVar("_T") async def contextmanager_in_threadpool( cm: ContextManager[_T], ) -> AsyncGenerator[_T, None]: + # blocking __exit__ from running waiting on a free thread + # can create race conditions/deadlocks if the context manager itself + # has it's own internal pool (e.g. a database connection pool) + # to avoid this we let __exit__ run without a capacity limit + # since we're creating a new limiter for each call, any non-zero limit + # works (1 is arbitrary) + exit_limiter = CapacityLimiter(1) try: yield await run_in_threadpool(cm.__enter__) except Exception as e: - ok: bool = await run_in_threadpool(cm.__exit__, type(e), e, None) + ok = bool( + await anyio.to_thread.run_sync( + cm.__exit__, type(e), e, None, limiter=exit_limiter + ) + ) if not ok: raise e else: - await run_in_threadpool(cm.__exit__, None, None, None) + await anyio.to_thread.run_sync( + cm.__exit__, None, None, None, limiter=exit_limiter + ) From f3efaf69da4dd6e00dbc871b418f74d3338b7c3a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 4 Sep 2022 19:10:05 +0000 Subject: [PATCH 0499/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9232db7f1..f44cd4f6e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Allow exit code for dependencies with `yield` to always execute, by removing capacity limiter for them, to e.g. allow closing DB connections without deadlocks. PR [#5122](https://github.com/tiangolo/fastapi/pull/5122) by [@adriangb](https://github.com/adriangb). * 👥 Update FastAPI People. PR [#5347](https://github.com/tiangolo/fastapi/pull/5347) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✨ Export `WebSocketState` in `fastapi.websockets`. PR [#4376](https://github.com/tiangolo/fastapi/pull/4376) by [@matiuszka](https://github.com/matiuszka). * 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). From ddf6daeb0759c6e0cb2cfc6af075ba95dd271779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 4 Sep 2022 21:25:29 +0200 Subject: [PATCH 0500/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 56 ++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f44cd4f6e..4b241cdd6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,30 +2,50 @@ ## Latest Changes -* 🐛 Allow exit code for dependencies with `yield` to always execute, by removing capacity limiter for them, to e.g. allow closing DB connections without deadlocks. PR [#5122](https://github.com/tiangolo/fastapi/pull/5122) by [@adriangb](https://github.com/adriangb). -* 👥 Update FastAPI People. PR [#5347](https://github.com/tiangolo/fastapi/pull/5347) by [@github-actions[bot]](https://github.com/apps/github-actions). +🚨 This is probably the last release (or one of the last releases) to support Python 3.6. 🔥 + +Python 3.6 reached the [end-of-life and is no longer supported by Python](https://www.python.org/downloads/release/python-3615/) since around a year ago. + +You hopefully updated to a supported version of Python a while ago. If you haven't, you really should. + +### Features + * ✨ Export `WebSocketState` in `fastapi.websockets`. PR [#4376](https://github.com/tiangolo/fastapi/pull/4376) by [@matiuszka](https://github.com/matiuszka). -* 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). -* ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). -* 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). -* ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). -* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). -* ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). -* 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). -* 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). -* ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). -* 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). -* 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). -* ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). * ✨ Support Python internal description on Pydantic model's docstring. PR [#3032](https://github.com/tiangolo/fastapi/pull/3032) by [@Kludex](https://github.com/Kludex). * ✨ Update `ORJSONResponse` to support non `str` keys and serializing Numpy arrays. PR [#3892](https://github.com/tiangolo/fastapi/pull/3892) by [@baby5](https://github.com/baby5). -* 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Allow exit code for dependencies with `yield` to always execute, by removing capacity limiter for them, to e.g. allow closing DB connections without deadlocks. PR [#5122](https://github.com/tiangolo/fastapi/pull/5122) by [@adriangb](https://github.com/adriangb). +* 🐛 Fix FastAPI People GitHub Action: set HTTPX timeout for GraphQL query request. PR [#5222](https://github.com/tiangolo/fastapi/pull/5222) by [@iudeen](https://github.com/iudeen). +* 🐛 Make sure a parameter defined as required is kept required in OpenAPI even if defined as optional in another dependency. PR [#4319](https://github.com/tiangolo/fastapi/pull/4319) by [@cd17822](https://github.com/cd17822). +* 🐛 Fix support for path parameters in WebSockets. PR [#3879](https://github.com/tiangolo/fastapi/pull/3879) by [@davidbrochart](https://github.com/davidbrochart). + +### Docs + +* ✏ Update Hypercorn link, now pointing to GitHub. PR [#5346](https://github.com/tiangolo/fastapi/pull/5346) by [@baconfield](https://github.com/baconfield). +* ✏ Tweak wording in `docs/en/docs/advanced/dataclasses.md`. PR [#3698](https://github.com/tiangolo/fastapi/pull/3698) by [@pfackeldey](https://github.com/pfackeldey). +* 📝 Add note about Python 3.10 `X | Y` operator in explanation about Response Models. PR [#5307](https://github.com/tiangolo/fastapi/pull/5307) by [@MendyLanda](https://github.com/MendyLanda). +* 📝 Add link to New Relic article: "How to monitor FastAPI application performance using Python agent". PR [#5260](https://github.com/tiangolo/fastapi/pull/5260) by [@sjyothi54](https://github.com/sjyothi54). * 📝 Update docs for `ORJSONResponse` with details about improving performance. PR [#2615](https://github.com/tiangolo/fastapi/pull/2615) by [@falkben](https://github.com/falkben). * 📝 Add docs for creating a custom Response class. PR [#5331](https://github.com/tiangolo/fastapi/pull/5331) by [@tiangolo](https://github.com/tiangolo). * 📝 Add tip about using alias for form data fields. PR [#5329](https://github.com/tiangolo/fastapi/pull/5329) by [@tiangolo](https://github.com/tiangolo). -* 🐛 Fix support for path parameters in WebSockets. PR [#3879](https://github.com/tiangolo/fastapi/pull/3879) by [@davidbrochart](https://github.com/davidbrochart). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/features.md`. PR [#5315](https://github.com/tiangolo/fastapi/pull/5315) by [@Xewus](https://github.com/Xewus). +* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/request-files.md`. PR [#4529](https://github.com/tiangolo/fastapi/pull/4529) by [@ASpathfinder](https://github.com/ASpathfinder). +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/encoder.md`. PR [#4969](https://github.com/tiangolo/fastapi/pull/4969) by [@Zssaer](https://github.com/Zssaer). +* 🌐 Fix MkDocs file line for Portuguese translation of `background-task.md`. PR [#5242](https://github.com/tiangolo/fastapi/pull/5242) by [@ComicShrimp](https://github.com/ComicShrimp). + +### Internal + +* 👥 Update FastAPI People. PR [#5347](https://github.com/tiangolo/fastapi/pull/5347) by [@github-actions[bot]](https://github.com/apps/github-actions). +* ⬆ Bump dawidd6/action-download-artifact from 2.22.0 to 2.23.0. PR [#5321](https://github.com/tiangolo/fastapi/pull/5321) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5318](https://github.com/tiangolo/fastapi/pull/5318) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). +* ✏ Fix a small code highlight line error. PR [#5256](https://github.com/tiangolo/fastapi/pull/5256) by [@hjlarry](https://github.com/hjlarry). +* ♻ Internal small refactor, move `operation_id` parameter position in delete method for consistency with the code. PR [#4474](https://github.com/tiangolo/fastapi/pull/4474) by [@hiel](https://github.com/hiel). +* 🔧 Update sponsors, disable ImgWhale. PR [#5338](https://github.com/tiangolo/fastapi/pull/5338) by [@tiangolo](https://github.com/tiangolo). ## 0.81.0 From 3079ba925ec04ce2a630d3d919a1308b48a45df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 4 Sep 2022 21:26:31 +0200 Subject: [PATCH 0501/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?82.0?= 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 4b241cdd6..303e6b8e1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.82.0 + 🚨 This is probably the last release (or one of the last releases) to support Python 3.6. 🔥 Python 3.6 reached the [end-of-life and is no longer supported by Python](https://www.python.org/downloads/release/python-3615/) since around a year ago. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 3688ec89f..c50543bf9 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.81.0" +__version__ = "0.82.0" from starlette import status as status From ef5d6181b62214eee73ccdaf8617e8e3ea39a6ea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Sep 2022 16:08:33 +0200 Subject: [PATCH 0502/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5352)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 237feb083..c4c1d4d9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - --py3-plus - --keep-runtime-typing - repo: https://github.com/myint/autoflake - rev: v1.5.1 + rev: v1.5.3 hooks: - id: autoflake args: @@ -43,7 +43,7 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black ci: From 895789bed0280865b8aa85973dc35d89f33730b5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Sep 2022 14:09:15 +0000 Subject: [PATCH 0503/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 303e6b8e1..ba5d7b26f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5352](https://github.com/tiangolo/fastapi/pull/5352) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.82.0 From 3ec498af63ddc994a7c70f1a05407ea5f7095be4 Mon Sep 17 00:00:00 2001 From: DCsunset Date: Thu, 8 Sep 2022 10:29:23 -0400 Subject: [PATCH 0504/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20in=20`jso?= =?UTF-8?q?nable=5Fencoder`=20for=20include=20and=20exclude=20with=20datac?= =?UTF-8?q?lasses=20(#4923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/encoders.py | 6 +++++- tests/test_jsonable_encoder.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index f64e4b86e..93045ca27 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -74,8 +74,12 @@ def jsonable_encoder( obj_dict = dataclasses.asdict(obj) return jsonable_encoder( obj_dict, - exclude_none=exclude_none, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, + exclude_none=exclude_none, custom_encoder=custom_encoder, sqlalchemy_safe=sqlalchemy_safe, ) diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 5e55f2f91..f4fdcf601 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum from pathlib import PurePath, PurePosixPath, PureWindowsPath @@ -19,6 +20,12 @@ class Pet: self.name = name +@dataclass +class Item: + name: str + count: int + + class DictablePerson(Person): def __iter__(self): return ((k, v) for k, v in self.__dict__.items()) @@ -131,6 +138,15 @@ def test_encode_dictable(): } +def test_encode_dataclass(): + item = Item(name="foo", count=100) + assert jsonable_encoder(item) == {"name": "foo", "count": 100} + assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} + assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} + assert jsonable_encoder(item, include={}) == {} + assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} + + def test_encode_unsupported(): unserializable = Unserializable() with pytest.raises(ValueError): From c4007cb9ecacd54df95e29780976937af01ff5ae Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Sep 2022 14:30:00 +0000 Subject: [PATCH 0505/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ba5d7b26f..bc58b578d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5352](https://github.com/tiangolo/fastapi/pull/5352) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.82.0 From 0b4fe10c8fed59de02100612a6168a9aefaf3659 Mon Sep 17 00:00:00 2001 From: Thomas Meckel <14177833+tmeckel@users.noreply.github.com> Date: Thu, 8 Sep 2022 17:02:59 +0200 Subject: [PATCH 0506/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20empty=20reponse?= =?UTF-8?q?=20body=20when=20default=20`status=5Fcode`=20is=20empty=20but?= =?UTF-8?q?=20the=20a=20`Response`=20parameter=20with=20`response.status?= =?UTF-8?q?=5Fcode`=20is=20set=20(#5360)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Thomas Meckel Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/routing.py | 2 +- tests/test_reponse_set_reponse_code_empty.py | 97 ++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/test_reponse_set_reponse_code_empty.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 233f79fcb..710cb9734 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -258,7 +258,7 @@ def get_request_handler( is_coroutine=is_coroutine, ) response = actual_response_class(content, **response_args) - if not is_body_allowed_for_status_code(status_code): + if not is_body_allowed_for_status_code(response.status_code): response.body = b"" response.headers.raw.extend(sub_response.headers.raw) return response diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py new file mode 100644 index 000000000..094d54a84 --- /dev/null +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -0,0 +1,97 @@ +from typing import Any + +from fastapi import FastAPI, Response +from fastapi.testclient import TestClient + +app = FastAPI() + + +@app.delete( + "/{id}", + status_code=204, +) +async def delete_deployment( + id: int, + response: Response, +) -> Any: + response.status_code = 400 + return {"msg": "Status overwritten", "id": id} + + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/{id}": { + "delete": { + "summary": "Delete Deployment", + "operationId": "delete_deployment__id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Id", "type": "integer"}, + "name": "id", + "in": "path", + } + ], + "responses": { + "204": {"description": "Successful Response"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_dependency_set_status_code(): + response = client.delete("/1") + assert response.status_code == 400 and response.content + assert response.json() == {"msg": "Status overwritten", "id": 1} From 6620273083275f7fec446bb3456d3df6582c6813 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Sep 2022 15:03:37 +0000 Subject: [PATCH 0507/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bc58b578d..7d0a16c95 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5352](https://github.com/tiangolo/fastapi/pull/5352) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4d270463af2279d9723d99da45f0b31631424030 Mon Sep 17 00:00:00 2001 From: Irfanuddin Shafi Ahmed Date: Sun, 11 Sep 2022 21:43:36 +0530 Subject: [PATCH 0508/1881] =?UTF-8?q?=F0=9F=90=9BFix=20`RuntimeError`=20ra?= =?UTF-8?q?ised=20when=20`HTTPException`=20has=20a=20status=20code=20with?= =?UTF-8?q?=20no=20content=20(#5365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcelo Trylesinski Co-authored-by: Sebastián Ramírez --- fastapi/exception_handlers.py | 16 +++++------ tests/test_starlette_exception.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 2b286d71c..4d7ea5ec2 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,19 +1,19 @@ from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError +from fastapi.utils import is_body_allowed_for_status_code from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY -async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: +async def http_exception_handler(request: Request, exc: HTTPException) -> Response: headers = getattr(exc, "headers", None) - if headers: - return JSONResponse( - {"detail": exc.detail}, status_code=exc.status_code, headers=headers - ) - else: - return JSONResponse({"detail": exc.detail}, status_code=exc.status_code) + if not is_body_allowed_for_status_code(exc.status_code): + return Response(status_code=exc.status_code, headers=headers) + return JSONResponse( + {"detail": exc.detail}, status_code=exc.status_code, headers=headers + ) async def request_validation_exception_handler( diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 859169d3c..2b6712f7b 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -18,6 +18,16 @@ async def read_item(item_id: str): return {"item": items[item_id]} +@app.get("/http-no-body-statuscode-exception") +async def no_body_status_code_exception(): + raise HTTPException(status_code=204) + + +@app.get("/http-no-body-statuscode-with-detail-exception") +async def no_body_status_code_with_detail_exception(): + raise HTTPException(status_code=204, detail="I should just disappear!") + + @app.get("/starlette-items/{item_id}") async def read_starlette_item(item_id: str): if item_id not in items: @@ -31,6 +41,30 @@ openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { + "/http-no-body-statuscode-exception": { + "get": { + "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful " "Response", + } + }, + "summary": "No Body " "Status " "Code " "Exception", + } + }, + "/http-no-body-statuscode-with-detail-exception": { + "get": { + "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful " "Response", + } + }, + "summary": "No Body Status Code With Detail Exception", + } + }, "/items/{item_id}": { "get": { "responses": { @@ -154,3 +188,15 @@ def test_get_starlette_item_not_found(): assert response.status_code == 404, response.text assert response.headers.get("x-error") is None assert response.json() == {"detail": "Item not found"} + + +def test_no_body_status_code_exception_handlers(): + response = client.get("/http-no-body-statuscode-exception") + assert response.status_code == 204 + assert not response.content + + +def test_no_body_status_code_with_detail_exception_handlers(): + response = client.get("/http-no-body-statuscode-with-detail-exception") + assert response.status_code == 204 + assert not response.content From 275306c369d7186aebf6e6d7d56be11e6d01c43c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Sep 2022 16:14:21 +0000 Subject: [PATCH 0509/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7d0a16c95..92e1dfe77 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛Fix `RuntimeError` raised when `HTTPException` has a status code with no content. PR [#5365](https://github.com/tiangolo/fastapi/pull/5365) by [@iudeen](https://github.com/iudeen). * 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5352](https://github.com/tiangolo/fastapi/pull/5352) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4fec12b354c7d87101aac60d0143236afcd8678f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sun, 11 Sep 2022 18:15:49 +0200 Subject: [PATCH 0510/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20`SECURITY.md`?= =?UTF-8?q?=20(#5377)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 322f95f62..db412cf2c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Learn more about it below. 👇 ## Versions -The latest versions of FastAPI are supported. +The latest version of FastAPI is supported. You are encouraged to [write tests](https://fastapi.tiangolo.com/tutorial/testing/) for your application and update your FastAPI version frequently after ensuring that your tests are passing. This way you will benefit from the latest features, bug fixes, and **security fixes**. From 306326a2138eff3835901fa34190e4475aed1409 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Sep 2022 16:16:31 +0000 Subject: [PATCH 0511/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 92e1dfe77..9c86e4615 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update `SECURITY.md`. PR [#5377](https://github.com/tiangolo/fastapi/pull/5377) by [@Kludex](https://github.com/Kludex). * 🐛Fix `RuntimeError` raised when `HTTPException` has a status code with no content. PR [#5365](https://github.com/tiangolo/fastapi/pull/5365) by [@iudeen](https://github.com/iudeen). * 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). From 22a155efc1ba51b215ed0aedcc0054854e85122e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Sep 2022 18:24:46 +0200 Subject: [PATCH 0512/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9c86e4615..bea543c43 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,10 +2,21 @@ ## Latest Changes -* 📝 Update `SECURITY.md`. PR [#5377](https://github.com/tiangolo/fastapi/pull/5377) by [@Kludex](https://github.com/Kludex). -* 🐛Fix `RuntimeError` raised when `HTTPException` has a status code with no content. PR [#5365](https://github.com/tiangolo/fastapi/pull/5365) by [@iudeen](https://github.com/iudeen). -* 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). +### Features + * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). + +### Fixes + +* 🐛 Fix `RuntimeError` raised when `HTTPException` has a status code with no content. PR [#5365](https://github.com/tiangolo/fastapi/pull/5365) by [@iudeen](https://github.com/iudeen). +* 🐛 Fix empty reponse body when default `status_code` is empty but the a `Response` parameter with `response.status_code` is set. PR [#5360](https://github.com/tiangolo/fastapi/pull/5360) by [@tmeckel](https://github.com/tmeckel). + +### Docs + +* 📝 Update `SECURITY.md`. PR [#5377](https://github.com/tiangolo/fastapi/pull/5377) by [@Kludex](https://github.com/Kludex). + +### Internal + * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5352](https://github.com/tiangolo/fastapi/pull/5352) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). ## 0.82.0 From ed0fcba7cbea46b0cca74e9fce568f9c7316d8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Sep 2022 18:25:29 +0200 Subject: [PATCH 0513/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?83.0?= 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 bea543c43..63595da41 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.83.0 + ### Features * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c50543bf9..a9b44fc4a 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.82.0" +__version__ = "0.83.0" from starlette import status as status From b2aa3593be300b57973987fb2489ed01ed9c9f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Sep 2022 18:26:30 +0200 Subject: [PATCH 0514/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 63595da41..120b374e2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -5,6 +5,12 @@ ## 0.83.0 +🚨 This is probably the last release (or one of the last releases) to support Python 3.6. 🔥 + +Python 3.6 reached the [end-of-life and is no longer supported by Python](https://www.python.org/downloads/release/python-3615/) since around a year ago. + +You hopefully updated to a supported version of Python a while ago. If you haven't, you really should. + ### Features * ✨ Add support in `jsonable_encoder` for include and exclude with dataclasses. PR [#4923](https://github.com/tiangolo/fastapi/pull/4923) by [@DCsunset](https://github.com/DCsunset). From 4267bd1f4f716244a8087e5aa0e68c1de629cfa9 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Wed, 14 Sep 2022 14:31:19 -0400 Subject: [PATCH 0515/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20package=20met?= =?UTF-8?q?adata,=20drop=20support=20for=20Python=203.6,=20move=20build=20?= =?UTF-8?q?internals=20from=20Flit=20to=20Hatch=20(#5240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .github/workflows/build-docs.yml | 7 ++--- .github/workflows/publish.yml | 18 ++++++------- .github/workflows/test.yml | 8 ++---- README.md | 6 ++--- docs/de/docs/index.md | 2 +- docs/en/docs/contributing.md | 44 +++++-------------------------- docs/en/docs/index.md | 6 ++--- docs/es/docs/index.md | 2 +- docs/fr/docs/index.md | 2 +- docs/id/docs/index.md | 2 +- docs/it/docs/index.md | 2 +- docs/ja/docs/contributing.md | 45 +++++--------------------------- docs/ja/docs/index.md | 2 +- docs/ko/docs/index.md | 2 +- docs/nl/docs/index.md | 2 +- docs/pl/docs/index.md | 2 +- docs/pt/docs/contributing.md | 44 +++++-------------------------- docs/pt/docs/index.md | 2 +- docs/ru/docs/index.md | 2 +- docs/sq/docs/index.md | 2 +- docs/sv/docs/index.md | 2 +- docs/tr/docs/index.md | 2 +- docs/uk/docs/index.md | 2 +- docs/zh/docs/contributing.md | 44 +++++-------------------------- fastapi/concurrency.py | 11 ++------ fastapi/dependencies/utils.py | 3 ++- fastapi/encoders.py | 4 +-- pyproject.toml | 35 +++++++++++++------------ 28 files changed, 82 insertions(+), 223 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 0d666b82a..af8909845 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -23,17 +23,14 @@ jobs: with: path: ${{ env.pythonLocation }} key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03 - - name: Install Flit - if: steps.cache.outputs.cache-hit != 'true' - run: python3.7 -m pip install flit - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' - run: python3.7 -m flit install --deps production --extras doc + run: pip install .[doc] - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Build Docs - run: python3.7 ./scripts/docs.py build-all + run: python ./scripts/docs.py build-all - name: Zip docs run: bash ./scripts/zip-docs.sh - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 02846cefd..fe4c5ee86 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,23 +17,21 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.6" + python-version: "3.7" - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-publish - - name: Install Flit + - name: Install build dependencies if: steps.cache.outputs.cache-hit != 'true' - run: pip install flit - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: flit install --symlink + run: pip install build + - name: Build distribution + run: python -m build - name: Publish - env: - FLIT_USERNAME: ${{ secrets.FLIT_USERNAME }} - FLIT_PASSWORD: ${{ secrets.FLIT_PASSWORD }} - run: bash scripts/publish.sh + uses: pypa/gh-action-pypi-publish@v1.5.1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context env: GITHUB_CONTEXT: ${{ toJson(github) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14dc141d9..3e6225db3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] fail-fast: false steps: @@ -26,14 +26,10 @@ jobs: with: path: ${{ env.pythonLocation }} key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v02 - - name: Install Flit - if: steps.cache.outputs.cache-hit != 'true' - run: pip install flit - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - run: flit install --symlink + run: pip install -e .[all,dev,doc,test] - name: Lint - if: ${{ matrix.python-version != '3.6' }} run: bash scripts/lint.sh - name: Test run: bash scripts/test.sh diff --git a/README.md b/README.md index bc00d9ed9..9d4f1cd90 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. The key features are: @@ -112,7 +112,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: @@ -328,7 +328,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.6+**. +Just standard **Python 3.7+**. For example, for an `int`: diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 287c79cff..07f51b1be 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -111,7 +111,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index ca51c6e82..39d7dd193 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -99,61 +99,29 @@ $ python -m pip install --upgrade pip !!! tip Every time you install a new package with `pip` under that environment, activate the environment again. - This makes sure that if you use a terminal program installed by that package (like `flit`), you use the one from your local environment and not any other that could be installed globally. + This makes sure that if you use a terminal program installed by that package, you use the one from your local environment and not any other that could be installed globally. -### Flit +### pip -**FastAPI** uses Flit to build, package and publish the project. - -After activating the environment as described above, install `flit`: +After activating the environment as described above:
```console -$ pip install flit +$ pip install -e .[dev,doc,test] ---> 100% ```
-Now re-activate the environment to make sure you are using the `flit` you just installed (and not a global one). - -And now use `flit` to install the development dependencies: - -=== "Linux, macOS" - -
- - ```console - $ flit install --deps develop --symlink - - ---> 100% - ``` - -
- -=== "Windows" - - If you are on Windows, use `--pth-file` instead of `--symlink`: - -
- - ```console - $ flit install --deps develop --pth-file - - ---> 100% - ``` - -
- It will install all the dependencies and your local FastAPI in your local environment. #### Using your local FastAPI If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code. -And if you update that local FastAPI source code, as it is installed with `--symlink` (or `--pth-file` on Windows), when you run that Python file again, it will use the fresh version of FastAPI you just edited. +And if you update that local FastAPI source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastAPI you just edited. That way, you don't have to "install" your local version to be able to test every change. @@ -171,7 +139,7 @@ $ bash scripts/format.sh It will also auto-sort all your imports. -For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above using `--symlink` (or `--pth-file` on Windows). +For it to sort them correctly, you need to have FastAPI installed locally in your environment, with the command in the section above using `-e`. ## Docs diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 97ec0ffcf..afdd62cee 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. The key features are: @@ -109,7 +109,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: @@ -325,7 +325,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.6+**. +Just standard **Python 3.7+**. For example, for an `int`: diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 44c59202a..aa3fa2228 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -106,7 +106,7 @@ Si estás construyendo un app de CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md index 041e0b754..3129f9dc6 100644 --- a/docs/id/docs/index.md +++ b/docs/id/docs/index.md @@ -111,7 +111,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md index 5cbe78a71..852a5e56e 100644 --- a/docs/it/docs/index.md +++ b/docs/it/docs/index.md @@ -111,7 +111,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/ja/docs/contributing.md b/docs/ja/docs/contributing.md index 07e53eeb7..8bad864a2 100644 --- a/docs/ja/docs/contributing.md +++ b/docs/ja/docs/contributing.md @@ -88,62 +88,29 @@ $ python -m venv env !!! tip "豆知識" この環境で`pip`を使って新しいパッケージをインストールするたびに、仮想環境を再度有効化します。 - これにより、そのパッケージによってインストールされたターミナルのプログラム (`flit`など) を使用する場合、ローカル環境のものを使用し、グローバルにインストールされたものは使用されなくなります。 + これにより、そのパッケージによってインストールされたターミナルのプログラム を使用する場合、ローカル環境のものを使用し、グローバルにインストールされたものは使用されなくなります。 -### Flit +### pip -**FastAPI**はFlit を使って、ビルド、パッケージ化、公開します。 - -上記のように環境を有効化した後、`flit`をインストールします: +上記のように環境を有効化した後:
```console -$ pip install flit +$ pip install -e .[dev,doc,test] ---> 100% ```
- -次に、環境を再び有効化して、インストールしたばかりの`flit` (グローバルではない) を使用していることを確認します。 - -そして、`flit`を使用して開発のための依存関係をインストールします: - -=== "Linux, macOS" - -
- - ```console - $ flit install --deps develop --symlink - - ---> 100% - ``` - -
- -=== "Windows" - - Windowsユーザーは、`--symlink`のかわりに`--pth-file`を使用します: - -
- - ```console - $ flit install --deps develop --pth-file - - ---> 100% - ``` - -
- これで、すべての依存関係とFastAPIを、ローカル環境にインストールします。 #### ローカル環境でFastAPIを使う FastAPIをインポートして使用するPythonファイルを作成し、ローカル環境で実行すると、ローカルのFastAPIソースコードが使用されます。 -そして、`--symlink` (Windowsでは` --pth-file`) でインストールされているローカルのFastAPIソースコードを更新した場合、そのPythonファイルを再度実行すると、更新したばかりの新しいバージョンのFastAPIが使用されます。 +そして、`-e` でインストールされているローカルのFastAPIソースコードを更新した場合、そのPythonファイルを再度実行すると、更新したばかりの新しいバージョンのFastAPIが使用されます。 これにより、ローカルバージョンを「インストール」しなくても、すべての変更をテストできます。 @@ -161,7 +128,7 @@ $ bash scripts/format.sh また、すべてのインポートを自動でソートします。 -正しく並べ替えるには、上記セクションのコマンドで `--symlink` (Windowsの場合は` --pth-file`) を使い、FastAPIをローカル環境にインストールしている必要があります。 +正しく並べ替えるには、上記セクションのコマンドで `-e` を使い、FastAPIをローカル環境にインストールしている必要があります。 ### インポートの整形 diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index 51977037c..177a78786 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -107,7 +107,7 @@ FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以 ## 必要条件 -Python 3.6+ +Python 3.7+ FastAPI は巨人の肩の上に立っています。 diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index 5a258f47b..6d35afc47 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -107,7 +107,7 @@ FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트 ## 요구사항 -Python 3.6+ +Python 3.7+ FastAPI는 거인들의 어깨 위에 서 있습니다: diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md index fd52f994c..fe55f6c1b 100644 --- a/docs/nl/docs/index.md +++ b/docs/nl/docs/index.md @@ -114,7 +114,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 9cc99ba72..671c235a6 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -106,7 +106,7 @@ Jeżeli tworzysz aplikacje CLI< ## Wymagania -Python 3.6+ +Python 3.7+ FastAPI oparty jest na: diff --git a/docs/pt/docs/contributing.md b/docs/pt/docs/contributing.md index 327b8b607..dcb6a80db 100644 --- a/docs/pt/docs/contributing.md +++ b/docs/pt/docs/contributing.md @@ -89,61 +89,29 @@ Se ele exibir o binário `pip` em `env/bin/pip` então funcionou. 🎉 !!! tip Toda vez que você instalar um novo pacote com `pip` nesse ambiente, ative o ambiente novamente. - Isso garante que se você usar um programa instalado por aquele pacote (como `flit`), você utilizará aquele de seu ambiente local e não outro que possa estar instalado globalmente. + Isso garante que se você usar um programa instalado por aquele pacote, você utilizará aquele de seu ambiente local e não outro que possa estar instalado globalmente. -### Flit +### pip -**FastAPI** utiliza Flit para construir, empacotar e publicar o projeto. - -Após ativar o ambiente como descrito acima, instale o `flit`: +Após ativar o ambiente como descrito acima:
```console -$ pip install flit +$ pip install -e .[dev,doc,test] ---> 100% ```
-Ative novamente o ambiente para ter certeza que você esteja utilizando o `flit` que você acabou de instalar (e não um global). - -E agora use `flit` para instalar as dependências de desenvolvimento: - -=== "Linux, macOS" - -
- - ```console - $ flit install --deps develop --symlink - - ---> 100% - ``` - -
- -=== "Windows" - - Se você está no Windows, use `--pth-file` ao invés de `--symlink`: - -
- - ```console - $ flit install --deps develop --pth-file - - ---> 100% - ``` - -
- Isso irá instalar todas as dependências e seu FastAPI local em seu ambiente local. #### Usando seu FastAPI local Se você cria um arquivo Python que importa e usa FastAPI, e roda com Python de seu ambiente local, ele irá utilizar o código fonte de seu FastAPI local. -E se você atualizar o código fonte do FastAPI local, como ele é instalado com `--symlink` (ou `--pth-file` no Windows), quando você rodar aquele arquivo Python novamente, ele irá utilizar a nova versão do FastAPI que você acabou de editar. +E se você atualizar o código fonte do FastAPI local, como ele é instalado com `-e`, quando você rodar aquele arquivo Python novamente, ele irá utilizar a nova versão do FastAPI que você acabou de editar. Desse modo, você não tem que "instalar" sua versão local para ser capaz de testar cada mudança. @@ -161,7 +129,7 @@ $ bash scripts/format.sh Ele irá organizar também todos os seus imports. -Para que ele organize os imports corretamente, você precisa ter o FastAPI instalado localmente em seu ambiente, com o comando na seção acima usando `--symlink` (ou `--pth-file` no Windows). +Para que ele organize os imports corretamente, você precisa ter o FastAPI instalado localmente em seu ambiente, com o comando na seção acima usando `-e`. ### Formato dos imports diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 51af486b9..ccbb8dba8 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -100,7 +100,7 @@ Se você estiver construindo uma aplicação CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md index fd52f994c..fe55f6c1b 100644 --- a/docs/sv/docs/index.md +++ b/docs/sv/docs/index.md @@ -114,7 +114,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 0d5337c87..73caa6d61 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -119,7 +119,7 @@ Eğer API yerine komut satırı uygulaması ## Gereksinimler -Python 3.6+ +Python 3.7+ FastAPI iki devin omuzları üstünde duruyor: diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index 2b64003fe..e799ff8d5 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -111,7 +111,7 @@ If you are building a CLI app to be ## Requirements -Python 3.6+ +Python 3.7+ FastAPI stands on the shoulders of giants: diff --git a/docs/zh/docs/contributing.md b/docs/zh/docs/contributing.md index 95500d12b..ca3646289 100644 --- a/docs/zh/docs/contributing.md +++ b/docs/zh/docs/contributing.md @@ -88,61 +88,29 @@ $ python -m venv env !!! tip 每一次你在该环境下使用 `pip` 安装了新软件包时,请再次激活该环境。 - 这样可以确保你在使用由该软件包安装的终端程序(如 `flit`)时使用的是当前虚拟环境中的程序,而不是其他的可能是全局安装的程序。 + 这样可以确保你在使用由该软件包安装的终端程序时使用的是当前虚拟环境中的程序,而不是其他的可能是全局安装的程序。 -### Flit +### pip -**FastAPI** 使用 Flit 来构建、打包和发布项目。 - -如上所述激活环境后,安装 `flit`: +如上所述激活环境后:
```console -$ pip install flit +$ pip install -e .[dev,doc,test] ---> 100% ```
-现在重新激活环境,以确保你正在使用的是刚刚安装的 `flit`(而不是全局环境的)。 - -然后使用 `flit` 来安装开发依赖: - -=== "Linux, macOS" - -
- - ```console - $ flit install --deps develop --symlink - - ---> 100% - ``` - -
- -=== "Windows" - - If you are on Windows, use `--pth-file` instead of `--symlink`: - -
- - ```console - $ flit install --deps develop --pth-file - - ---> 100% - ``` - -
- 这将在虚拟环境中安装所有依赖和本地版本的 FastAPI。 #### 使用本地 FastAPI 如果你创建一个导入并使用 FastAPI 的 Python 文件,然后使用虚拟环境中的 Python 运行它,它将使用你本地的 FastAPI 源码。 -并且如果你更改该本地 FastAPI 的源码,由于它是通过 `--symlink` (或 Windows 上的 `--pth-file`)安装的,当你再次运行那个 Python 文件,它将使用你刚刚编辑过的最新版本的 FastAPI。 +并且如果你更改该本地 FastAPI 的源码,由于它是通过 `-e` 安装的,当你再次运行那个 Python 文件,它将使用你刚刚编辑过的最新版本的 FastAPI。 这样,你不必再去重新"安装"你的本地版本即可测试所有更改。 @@ -160,7 +128,7 @@ $ bash scripts/format.sh 它还会自动对所有导入代码进行整理。 -为了使整理正确进行,你需要在当前环境中安装本地的 FastAPI,即在运行上述段落中的命令时添加 `--symlink`(或 Windows 上的 `--pth-file`)。 +为了使整理正确进行,你需要在当前环境中安装本地的 FastAPI,即在运行上述段落中的命令时添加 `-e`。 ### 格式化导入 diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index c728ec1d2..31b878d5d 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -1,4 +1,5 @@ -import sys +from contextlib import AsyncExitStack as AsyncExitStack # noqa +from contextlib import asynccontextmanager as asynccontextmanager from typing import AsyncGenerator, ContextManager, TypeVar import anyio @@ -9,14 +10,6 @@ from starlette.concurrency import ( # noqa run_until_first_complete as run_until_first_complete, ) -if sys.version_info >= (3, 7): - from contextlib import AsyncExitStack as AsyncExitStack - from contextlib import asynccontextmanager as asynccontextmanager -else: - from contextlib2 import AsyncExitStack as AsyncExitStack # noqa - from contextlib2 import asynccontextmanager as asynccontextmanager # noqa - - _T = TypeVar("_T") diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index d098b65f1..cdc48c339 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -7,6 +7,7 @@ from typing import ( Callable, Coroutine, Dict, + ForwardRef, List, Mapping, Optional, @@ -47,7 +48,7 @@ from pydantic.fields import ( Undefined, ) from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import ForwardRef, evaluate_forwardref +from pydantic.typing import evaluate_forwardref from pydantic.utils import lenient_issubclass from starlette.background import BackgroundTasks from starlette.concurrency import run_in_threadpool diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 93045ca27..6bde9f4ab 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -54,8 +54,8 @@ def jsonable_encoder( if custom_encoder: encoder.update(custom_encoder) obj_dict = obj.dict( - include=include, # type: ignore # in Pydantic - exclude=exclude, # type: ignore # in Pydantic + include=include, + exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, exclude_none=exclude_none, diff --git a/pyproject.toml b/pyproject.toml index 3b77b113b..f5c9efd2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,16 @@ [build-system] -requires = ["flit"] -build-backend = "flit.buildapi" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.flit.metadata] -module = "fastapi" -author = "Sebastián Ramírez" -author-email = "tiangolo@gmail.com" -home-page = "https://github.com/tiangolo/fastapi" +[project] +name = "fastapi" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +readme = "README.md" +requires-python = ">=3.7" +license = "MIT" +authors = [ + { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, +] classifiers = [ "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", @@ -26,7 +30,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -34,17 +37,17 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] -requires = [ +dependencies = [ "starlette==0.19.1", "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.1" +dynamic = ["version"] -[tool.flit.metadata.urls] +[project.urls] +Homepage = "https://github.com/tiangolo/fastapi" Documentation = "https://fastapi.tiangolo.com/" -[tool.flit.metadata.requires-extra] +[project.optional-dependencies] test = [ "pytest >=6.2.4,<7.0.0", "pytest-cov >=2.12.0,<4.0.0", @@ -67,7 +70,6 @@ test = [ # types "types-ujson ==4.2.1", "types-orjson ==3.6.2", - "types-dataclasses ==0.6.5; python_version<'3.7'", ] doc = [ "mkdocs >=1.1.2,<2.0.0", @@ -99,6 +101,9 @@ all = [ "uvicorn[standard] >=0.12.0,<0.18.0", ] +[tool.hatch.version] +path = "fastapi/__init__.py" + [tool.isort] profile = "black" known_third_party = ["fastapi", "pydantic", "starlette"] @@ -128,6 +133,4 @@ filterwarnings = [ # TODO: needed by asyncio in Python 3.9.7 https://bugs.python.org/issue45097, try to remove on 3.9.8 'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning:asyncio', 'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette', - # TODO: remove after dropping support for Python 3.6 - 'ignore:Python 3.6 is no longer supported by the Python core team. Therefore, support for it is deprecated in cryptography and will be removed in a future release.:UserWarning:jose', ] From 7291cead6597e90cbe5b4abd038f9e782d0be9ce Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 14 Sep 2022 18:32:03 +0000 Subject: [PATCH 0516/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 120b374e2..29044cd5f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update package metadata, drop support for Python 3.6, move build internals from Flit to Hatch. PR [#5240](https://github.com/tiangolo/fastapi/pull/5240) by [@ofek](https://github.com/ofek). ## 0.83.0 From 1073062c7f2c48bcc28bcedbdc009c18c171f6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 14 Sep 2022 20:41:29 +0200 Subject: [PATCH 0517/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?84.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 6 ++++++ fastapi/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 29044cd5f..6ff58e52d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,12 @@ ## Latest Changes +## 0.84.0 + +### Breaking Changes + +This version of FastAPI drops support for Python 3.6. 🔥 Please upgrade to a supported version of Python (3.7 or above), Python 3.6 reached the end-of-life a long time ago. 😅☠ + * 🔧 Update package metadata, drop support for Python 3.6, move build internals from Flit to Hatch. PR [#5240](https://github.com/tiangolo/fastapi/pull/5240) by [@ofek](https://github.com/ofek). ## 0.83.0 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a9b44fc4a..d6d159794 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.83.0" +__version__ = "0.84.0" from starlette import status as status From adcf03f2bce0950ea26c29e20d37cb894dd70c04 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 15 Sep 2022 14:32:05 +0200 Subject: [PATCH 0518/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20version=20requi?= =?UTF-8?q?red=20of=20Starlette=20from=20`0.19.1`=20to=20`0.20.4`=20(#4820?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 3 ++- fastapi/security/api_key.py | 2 +- pyproject.toml | 2 +- .../test_sql_databases_peewee.py | 10 ---------- tests/test_union_inherited_body.py | 11 ----------- tests/utils.py | 1 - 6 files changed, 4 insertions(+), 25 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index a242c504c..61d4582d2 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -33,9 +33,10 @@ from fastapi.types import DecoratedCallable from fastapi.utils import generate_unique_id from starlette.applications import Starlette from starlette.datastructures import State -from starlette.exceptions import ExceptionMiddleware, HTTPException +from starlette.exceptions import HTTPException from starlette.middleware import Middleware from starlette.middleware.errors import ServerErrorMiddleware +from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 36ab60e30..bca5c721a 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -27,7 +27,7 @@ class APIKeyQuery(APIKeyBase): self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: - api_key: str = request.query_params.get(self.model.name) + api_key = request.query_params.get(self.model.name) if not api_key: if self.auto_error: raise HTTPException( diff --git a/pyproject.toml b/pyproject.toml index f5c9efd2a..6400a942b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.19.1", + "starlette==0.20.4", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py index d28ea5e76..1b4a7b302 100644 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py @@ -5,8 +5,6 @@ from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient -from ...utils import needs_py37 - openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -340,14 +338,12 @@ def client(): test_db.unlink() -@needs_py37 def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@needs_py37 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -359,7 +355,6 @@ def test_create_user(client): assert response.status_code == 400, response.text -@needs_py37 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -368,13 +363,11 @@ def test_get_user(client): assert "id" in data -@needs_py37 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text -@needs_py37 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -386,7 +379,6 @@ def test_get_users(client): time.sleep = MagicMock() -@needs_py37 def test_get_slowusers(client): response = client.get("/slowusers/") assert response.status_code == 200, response.text @@ -395,7 +387,6 @@ def test_get_slowusers(client): assert "id" in data[0] -@needs_py37 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -419,7 +410,6 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] -@needs_py37 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 60b327ebc..9ee981b24 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -4,14 +4,6 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_py37 - -# In Python 3.6: -# u = Union[ExtendedItem, Item] == __main__.Item - -# But in Python 3.7: -# u = Union[ExtendedItem, Item] == typing.Union[__main__.ExtendedItem, __main__.Item] - app = FastAPI() @@ -118,21 +110,18 @@ inherited_item_openapi_schema = { } -@needs_py37 def test_inherited_item_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == inherited_item_openapi_schema -@needs_py37 def test_post_extended_item(): response = client.post("/items/", json={"name": "Foo", "age": 5}) assert response.status_code == 200, response.text assert response.json() == {"item": {"name": "Foo", "age": 5}} -@needs_py37 def test_post_item(): response = client.post("/items/", json={"name": "Foo"}) assert response.status_code == 200, response.text diff --git a/tests/utils.py b/tests/utils.py index 777bfe81d..5305424c4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,7 +2,6 @@ import sys import pytest -needs_py37 = pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7+") needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" From 715c0341a93ad334311f0513a394b9a680b6bf99 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 12:32:54 +0000 Subject: [PATCH 0519/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6ff58e52d..72e648486 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). ## 0.84.0 ### Breaking Changes From 3658733b5e9c468907c7598ddafab2789d31fa1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:01:46 +0200 Subject: [PATCH 0520/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20test=20depend?= =?UTF-8?q?encies,=20upgrade=20Pytest,=20move=20dependencies=20from=20dev?= =?UTF-8?q?=20to=20test=20(#5396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6400a942b..744854f2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] test = [ - "pytest >=6.2.4,<7.0.0", + "pytest >=7.1.3,<8.0.0", "pytest-cov >=2.12.0,<4.0.0", "mypy ==0.910", "flake8 >=3.8.3,<6.0.0", @@ -66,6 +66,9 @@ test = [ "python-multipart >=0.0.5,<0.0.6", "flask >=1.1.2,<3.0.0", "anyio[trio] >=3.2.1,<4.0.0", + "python-jose[cryptography] >=3.3.0,<4.0.0", + "pyyaml >=5.3.1,<7.0.0", + "passlib[bcrypt] >=1.7.2,<2.0.0", # types "types-ujson ==4.2.1", @@ -82,8 +85,6 @@ doc = [ "pyyaml >=5.3.1,<7.0.0", ] dev = [ - "python-jose[cryptography] >=3.3.0,<4.0.0", - "passlib[bcrypt] >=1.7.2,<2.0.0", "autoflake >=1.4.0,<2.0.0", "flake8 >=3.8.3,<6.0.0", "uvicorn[standard] >=0.12.0,<0.18.0", From 823df88c34ff26d791b0ca47ebb1f3f213e3a544 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 13:02:23 +0000 Subject: [PATCH 0521/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 72e648486..1bd08375f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update test dependencies, upgrade Pytest, move dependencies from dev to test. PR [#5396](https://github.com/tiangolo/fastapi/pull/5396) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). ## 0.84.0 From 74ce2204ae9b6dee573e42d93fc096cca87aa37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:26:21 +0200 Subject: [PATCH 0522/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20mypy?= =?UTF-8?q?=20and=20tweak=20internal=20type=20annotations=20(#5398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 12 ++++++------ fastapi/routing.py | 2 +- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index cdc48c339..64a6c1276 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -426,22 +426,22 @@ def is_coroutine_callable(call: Callable[..., Any]) -> bool: return inspect.iscoroutinefunction(call) if inspect.isclass(call): return False - call = getattr(call, "__call__", None) - return inspect.iscoroutinefunction(call) + dunder_call = getattr(call, "__call__", None) + return inspect.iscoroutinefunction(dunder_call) def is_async_gen_callable(call: Callable[..., Any]) -> bool: if inspect.isasyncgenfunction(call): return True - call = getattr(call, "__call__", None) - return inspect.isasyncgenfunction(call) + dunder_call = getattr(call, "__call__", None) + return inspect.isasyncgenfunction(dunder_call) def is_gen_callable(call: Callable[..., Any]) -> bool: if inspect.isgeneratorfunction(call): return True - call = getattr(call, "__call__", None) - return inspect.isgeneratorfunction(call) + dunder_call = getattr(call, "__call__", None) + return inspect.isgeneratorfunction(dunder_call) async def solve_generator( diff --git a/fastapi/routing.py b/fastapi/routing.py index 710cb9734..7caf018b5 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -127,7 +127,7 @@ async def serialize_response( if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: - value, errors_ = await run_in_threadpool( # type: ignore[misc] + value, errors_ = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) if isinstance(errors_, ErrorWrapper): diff --git a/pyproject.toml b/pyproject.toml index 744854f2b..c68951160 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ Documentation = "https://fastapi.tiangolo.com/" test = [ "pytest >=7.1.3,<8.0.0", "pytest-cov >=2.12.0,<4.0.0", - "mypy ==0.910", + "mypy ==0.971", "flake8 >=3.8.3,<6.0.0", "black == 22.3.0", "isort >=5.0.6,<6.0.0", From b741ea7619288dd54aab7bbbfdf643707458c315 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 13:27:00 +0000 Subject: [PATCH 0523/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1bd08375f..ef942aaf1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade mypy and tweak internal type annotations. PR [#5398](https://github.com/tiangolo/fastapi/pull/5398) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update test dependencies, upgrade Pytest, move dependencies from dev to test. PR [#5396](https://github.com/tiangolo/fastapi/pull/5396) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). ## 0.84.0 From add7c4800ce7efec79d1794b8a09ef9fd9609259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:32:22 +0200 Subject: [PATCH 0524/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20test?= =?UTF-8?q?=20dependencies:=20Black,=20HTTPX,=20databases,=20types-ujson?= =?UTF-8?q?=20(#5399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c68951160..e7be44de4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,14 +53,14 @@ test = [ "pytest-cov >=2.12.0,<4.0.0", "mypy ==0.971", "flake8 >=3.8.3,<6.0.0", - "black == 22.3.0", + "black == 22.8.0", "isort >=5.0.6,<6.0.0", "requests >=2.24.0,<3.0.0", - "httpx >=0.14.0,<0.19.0", + "httpx >=0.23.0,<0.24.0", "email_validator >=1.1.1,<2.0.0", "sqlalchemy >=1.3.18,<1.5.0", "peewee >=3.13.3,<4.0.0", - "databases[sqlite] >=0.3.2,<0.6.0", + "databases[sqlite] >=0.3.2,<0.7.0", "orjson >=3.2.1,<4.0.0", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", "python-multipart >=0.0.5,<0.0.6", @@ -71,7 +71,7 @@ test = [ "passlib[bcrypt] >=1.7.2,<2.0.0", # types - "types-ujson ==4.2.1", + "types-ujson ==5.4.0", "types-orjson ==3.6.2", ] doc = [ From babe2f9b03a9da86c07632d04602f84468c4f25f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 13:33:12 +0000 Subject: [PATCH 0525/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ef942aaf1..9c0e49488 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade test dependencies: Black, HTTPX, databases, types-ujson. PR [#5399](https://github.com/tiangolo/fastapi/pull/5399) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade mypy and tweak internal type annotations. PR [#5398](https://github.com/tiangolo/fastapi/pull/5398) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update test dependencies, upgrade Pytest, move dependencies from dev to test. PR [#5396](https://github.com/tiangolo/fastapi/pull/5396) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). From 95cbb43b067f123526204a7f43da0815cd695145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:42:53 +0200 Subject: [PATCH 0526/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20depend?= =?UTF-8?q?encies=20for=20doc=20and=20dev=20internal=20extras:=20Typer,=20?= =?UTF-8?q?Uvicorn=20(#5400)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7be44de4..ee073a337 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,13 +81,13 @@ doc = [ "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", # TODO: upgrade and enable typer-cli once it supports Click 8.x.x # "typer-cli >=0.0.12,<0.0.13", - "typer >=0.4.1,<0.5.0", + "typer >=0.4.1,<0.7.0", "pyyaml >=5.3.1,<7.0.0", ] dev = [ "autoflake >=1.4.0,<2.0.0", "flake8 >=3.8.3,<6.0.0", - "uvicorn[standard] >=0.12.0,<0.18.0", + "uvicorn[standard] >=0.12.0,<0.19.0", "pre-commit >=2.17.0,<3.0.0", ] all = [ From e83aa432968cb2f849bc85de9a824b5426d59d60 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 13:43:34 +0000 Subject: [PATCH 0527/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9c0e49488..776c24bf6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade dependencies for doc and dev internal extras: Typer, Uvicorn. PR [#5400](https://github.com/tiangolo/fastapi/pull/5400) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade test dependencies: Black, HTTPX, databases, types-ujson. PR [#5399](https://github.com/tiangolo/fastapi/pull/5399) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade mypy and tweak internal type annotations. PR [#5398](https://github.com/tiangolo/fastapi/pull/5398) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update test dependencies, upgrade Pytest, move dependencies from dev to test. PR [#5396](https://github.com/tiangolo/fastapi/pull/5396) by [@tiangolo](https://github.com/tiangolo). From a05e8b4e6f289147cc2cb34e38a22755b9915114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:48:53 +0200 Subject: [PATCH 0528/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Uvicor?= =?UTF-8?q?n=20in=20public=20extras:=20all=20(#5401)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ee073a337..dec4cff70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ all = [ "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", "orjson >=3.2.1,<4.0.0", "email_validator >=1.1.1,<2.0.0", - "uvicorn[standard] >=0.12.0,<0.18.0", + "uvicorn[standard] >=0.12.0,<0.19.0", ] [tool.hatch.version] From 6ddd0c64b0f833a709e46fa949fe896b0651a875 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 15 Sep 2022 13:49:32 +0000 Subject: [PATCH 0529/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 776c24bf6..ceffedd25 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Uvicorn in public extras: all. PR [#5401](https://github.com/tiangolo/fastapi/pull/5401) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade dependencies for doc and dev internal extras: Typer, Uvicorn. PR [#5400](https://github.com/tiangolo/fastapi/pull/5400) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade test dependencies: Black, HTTPX, databases, types-ujson. PR [#5399](https://github.com/tiangolo/fastapi/pull/5399) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade mypy and tweak internal type annotations. PR [#5398](https://github.com/tiangolo/fastapi/pull/5398) by [@tiangolo](https://github.com/tiangolo). From e782ba43cefec67b58ecfaa4406ef050077e2d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:56:35 +0200 Subject: [PATCH 0530/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ceffedd25..bac9dbc87 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,12 +2,19 @@ ## Latest Changes -* ⬆️ Upgrade Uvicorn in public extras: all. PR [#5401](https://github.com/tiangolo/fastapi/pull/5401) by [@tiangolo](https://github.com/tiangolo). +### Features + +* ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. Initial PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). + * This includes several bug fixes in Starlette. +* ⬆️ Upgrade Uvicorn max version in public extras: all. From `>=0.12.0,<0.18.0` to `>=0.12.0,<0.19.0`. PR [#5401](https://github.com/tiangolo/fastapi/pull/5401) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * ⬆️ Upgrade dependencies for doc and dev internal extras: Typer, Uvicorn. PR [#5400](https://github.com/tiangolo/fastapi/pull/5400) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade test dependencies: Black, HTTPX, databases, types-ujson. PR [#5399](https://github.com/tiangolo/fastapi/pull/5399) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade mypy and tweak internal type annotations. PR [#5398](https://github.com/tiangolo/fastapi/pull/5398) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update test dependencies, upgrade Pytest, move dependencies from dev to test. PR [#5396](https://github.com/tiangolo/fastapi/pull/5396) by [@tiangolo](https://github.com/tiangolo). -* ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). + ## 0.84.0 ### Breaking Changes From 12132276672c51412f3fc38bd5e228cf4a87ecfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 15 Sep 2022 15:57:23 +0200 Subject: [PATCH 0531/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?85.0?= 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 bac9dbc87..b789a1a02 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.85.0 + ### Features * ⬆ Upgrade version required of Starlette from `0.19.1` to `0.20.4`. Initial PR [#4820](https://github.com/tiangolo/fastapi/pull/4820) by [@Kludex](https://github.com/Kludex). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index d6d159794..167bf4f85 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.84.0" +__version__ = "0.85.0" from starlette import status as status From 20f689ded20e75acce3862d8df42da775434c1e5 Mon Sep 17 00:00:00 2001 From: moneeka Date: Tue, 20 Sep 2022 07:29:23 -0700 Subject: [PATCH 0532/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20WayScript=20x=20?= =?UTF-8?q?FastAPI=20Tutorial=20to=20External=20Links=20section=20(#5407)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index e9c4ef2f2..934c5842b 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ articles: english: + - author: WayScript + author_link: https://www.wayscript.com + link: https://blog.wayscript.com/fast-api-quickstart/ + title: Quickstart Guide to Build and Host Responsive APIs with Fast API and WayScript - author: New Relic author_link: https://newrelic.com link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 From c6aa28bea2f751a91078bd8d845133ff83f352bf Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 20 Sep 2022 14:30:02 +0000 Subject: [PATCH 0533/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b789a1a02..babb25a6a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). ## 0.85.0 From 1d1859675fbc6aeffe3fcac4ca00ecfbabcb6391 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 11 Oct 2022 21:50:01 +0100 Subject: [PATCH 0534/1881] =?UTF-8?q?=F0=9F=94=87=20Ignore=20Trio=20warnin?= =?UTF-8?q?g=20in=20tests=20for=20CI=20(#5483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index dec4cff70..eede670fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,4 +134,6 @@ filterwarnings = [ # TODO: needed by asyncio in Python 3.9.7 https://bugs.python.org/issue45097, try to remove on 3.9.8 'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning:asyncio', 'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette', + # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 + 'ignore::trio.TrioDeprecationWarning', ] From 81115dba53d9bb4a5d6433b086f63a24cfd2f7c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 11 Oct 2022 20:50:43 +0000 Subject: [PATCH 0535/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 babb25a6a..69c435843 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔇 Ignore Trio warning in tests for CI. PR [#5483](https://github.com/tiangolo/fastapi/pull/5483) by [@samuelcolvin](https://github.com/samuelcolvin). * 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). ## 0.85.0 From ebe69913ae1f480d121db9ba1e52ef4e6cacfde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 14 Oct 2022 22:22:09 +0200 Subject: [PATCH 0536/1881] =?UTF-8?q?=F0=9F=94=A7=20Disable=20Material=20f?= =?UTF-8?q?or=20MkDocs=20search=20plugin=20(#5495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 1 - docs/de/mkdocs.yml | 1 - docs/en/mkdocs.yml | 1 - docs/es/mkdocs.yml | 1 - docs/fa/mkdocs.yml | 1 - docs/fr/mkdocs.yml | 1 - docs/he/mkdocs.yml | 1 - docs/id/mkdocs.yml | 1 - docs/it/mkdocs.yml | 1 - docs/ja/mkdocs.yml | 1 - docs/ko/mkdocs.yml | 1 - docs/nl/mkdocs.yml | 1 - docs/pl/mkdocs.yml | 1 - docs/pt/mkdocs.yml | 1 - docs/ru/mkdocs.yml | 1 - docs/sq/mkdocs.yml | 1 - docs/sv/mkdocs.yml | 1 - docs/tr/mkdocs.yml | 1 - docs/uk/mkdocs.yml | 1 - docs/zh/mkdocs.yml | 1 - 20 files changed, 20 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index d549f37a3..ef5eea239 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8c3c42b5f..9f78c39ef 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 69ad29624..5322ed90f 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index d1d6215b6..a74044739 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 7c2fe5eab..ae6b5be5c 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 6bed7be73..8d8cfccce 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 3279099b5..a96345184 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index abb31252f..1445512e2 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 532b5bc52..b2d0cb309 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index b3f18bbdd..b7c15bf16 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 50931e134..a8253ee71 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 6d46939f9..588f224c2 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 1cd129420..190a6e11f 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 59ee3cc88..27c0d10fa 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 381775ac6..96165dcaf 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index b07f3bc63..aaf3563b9 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 3332d232d..e06be635f 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index e29d25936..34890954d 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 711328771..d93419bda 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index ac8679dc0..a18a84adf 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -32,7 +32,6 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search - markdownextradata: data: data nav: From 9c6086eee6fe0eff86ecd45ee96a45e5b79233e8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 14 Oct 2022 20:22:46 +0000 Subject: [PATCH 0537/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 69c435843..f80ab4bfc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Disable Material for MkDocs search plugin. PR [#5495](https://github.com/tiangolo/fastapi/pull/5495) by [@tiangolo](https://github.com/tiangolo). * 🔇 Ignore Trio warning in tests for CI. PR [#5483](https://github.com/tiangolo/fastapi/pull/5483) by [@samuelcolvin](https://github.com/samuelcolvin). * 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). From 0ae8db447ad5f9b262b8d7264b8ee5f30426e7c2 Mon Sep 17 00:00:00 2001 From: Jarro van Ginkel Date: Fri, 14 Oct 2022 23:44:22 +0300 Subject: [PATCH 0538/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20?= =?UTF-8?q?strings=20in=20OpenAPI=20status=20codes:=20`default`,=20`1XX`,?= =?UTF-8?q?=20`2XX`,=20`3XX`,=20`4XX`,=20`5XX`=20(#5187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/utils.py | 10 ++++ tests/test_additional_responses_router.py | 63 +++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/fastapi/utils.py b/fastapi/utils.py index 89f54453b..b94dacecc 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -21,6 +21,16 @@ if TYPE_CHECKING: # pragma: nocover def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: if status_code is None: return True + # Ref: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#patterned-fields-1 + if status_code in { + "default", + "1XX", + "2XX", + "3XX", + "4XX", + "5XX", + }: + return True current_status_code = int(status_code) return not (current_status_code < 200 or current_status_code in {204, 304}) diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index d2b73058f..fe4956f8f 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -1,5 +1,11 @@ from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class ResponseModel(BaseModel): + message: str + app = FastAPI() router = APIRouter() @@ -33,6 +39,18 @@ async def c(): return "c" +@router.get( + "/d", + responses={ + "400": {"description": "Error with str"}, + "5XX": {"model": ResponseModel}, + "default": {"model": ResponseModel}, + }, +) +async def d(): + return "d" + + app.include_router(router) openapi_schema = { @@ -81,6 +99,45 @@ openapi_schema = { "operationId": "c_c_get", } }, + "/d": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": { + "description": "Server Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ResponseModel"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": { + "description": "Default Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ResponseModel"} + } + }, + }, + }, + "summary": "D", + "operationId": "d_d_get", + } + }, + }, + "components": { + "schemas": { + "ResponseModel": { + "title": "ResponseModel", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + } + } }, } @@ -109,3 +166,9 @@ def test_c(): response = client.get("/c") assert response.status_code == 200, response.text assert response.json() == "c" + + +def test_d(): + response = client.get("/d") + assert response.status_code == 200, response.text + assert response.json() == "d" From 5ba0c8c0024b8f6e93e3976f006bfe96ed110884 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 14 Oct 2022 20:44:57 +0000 Subject: [PATCH 0539/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f80ab4bfc..7e8e559a6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix support for strings in OpenAPI status codes: `default`, `1XX`, `2XX`, `3XX`, `4XX`, `5XX`. PR [#5187](https://github.com/tiangolo/fastapi/pull/5187) by [@JarroVGIT](https://github.com/JarroVGIT). * 🔧 Disable Material for MkDocs search plugin. PR [#5495](https://github.com/tiangolo/fastapi/pull/5495) by [@tiangolo](https://github.com/tiangolo). * 🔇 Ignore Trio warning in tests for CI. PR [#5483](https://github.com/tiangolo/fastapi/pull/5483) by [@samuelcolvin](https://github.com/samuelcolvin). * 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). From 3c6528460cda669c16f83cdddd914fcebb055d39 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Oct 2022 22:50:07 +0200 Subject: [PATCH 0540/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 92 ++++++++++++-------------- docs/en/data/people.yml | 110 +++++++++++++++++-------------- 2 files changed, 104 insertions(+), 98 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 891a990a4..aaf7c9b80 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -20,9 +20,6 @@ sponsors: - - login: mikeckennedy avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4 url: https://github.com/mikeckennedy - - login: Trivie - avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 - url: https://github.com/Trivie - login: deta avatarUrl: https://avatars.githubusercontent.com/u/47275976?v=4 url: https://github.com/deta @@ -38,7 +35,10 @@ sponsors: - - login: InesIvanova avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 url: https://github.com/InesIvanova -- - login: SendCloud +- - login: zopyx + avatarUrl: https://avatars.githubusercontent.com/u/594239?u=8e5ce882664f47fd61002bed51718c78c3799d24&v=4 + url: https://github.com/zopyx + - login: SendCloud avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 url: https://github.com/SendCloud - login: mercedes-benz @@ -53,21 +53,18 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP - - login: Jonathanjwp - avatarUrl: https://avatars.githubusercontent.com/u/110426978?u=5e6ed4984022cb8886c4fdfdc0c59f55c48a2f1d&v=4 - url: https://github.com/Jonathanjwp - - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore + - login: Trivie + avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 + url: https://github.com/Trivie - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: khalidelboray - avatarUrl: https://avatars.githubusercontent.com/u/37024839?u=9a4b19123b5a1ba39dc581f8e4e1bc53fafdda2e&v=4 - url: https://github.com/khalidelboray - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck @@ -86,9 +83,6 @@ sponsors: - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya - - login: leynier - avatarUrl: https://avatars.githubusercontent.com/u/36774373?u=2284831c821307de562ebde5b59014d5416c7e0d&v=4 - url: https://github.com/leynier - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries @@ -101,13 +95,7 @@ sponsors: - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: ekpyrosis - avatarUrl: https://avatars.githubusercontent.com/u/60075?v=4 - url: https://github.com/ekpyrosis - - login: ryangrose - avatarUrl: https://avatars.githubusercontent.com/u/24993976?u=052f346aa859f3e52c21bd2c1792f61a98cfbacf&v=4 - url: https://github.com/ryangrose - - login: A-Edge +- - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge - - login: Kludex @@ -128,6 +116,9 @@ sponsors: - login: dekoza avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 url: https://github.com/dekoza + - login: pamelafox + avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 + url: https://github.com/pamelafox - login: deserat avatarUrl: https://avatars.githubusercontent.com/u/299332?v=4 url: https://github.com/deserat @@ -140,6 +131,9 @@ sponsors: - login: koxudaxi avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 url: https://github.com/koxudaxi + - login: falkben + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 + url: https://github.com/falkben - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner @@ -176,6 +170,9 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 + - login: MarekBleschke + avatarUrl: https://avatars.githubusercontent.com/u/3616870?v=4 + url: https://github.com/MarekBleschke - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco @@ -210,7 +207,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 url: https://github.com/Yaleesa - login: iwpnd - avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=b2286006daafff5f991557344fee20b5da59639a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 url: https://github.com/iwpnd - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 @@ -281,6 +278,9 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 + - login: ygorpontelo + avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 + url: https://github.com/ygorpontelo - login: AlrasheedA avatarUrl: https://avatars.githubusercontent.com/u/33544979?u=7fe66bf62b47682612b222e3e8f4795ef3be769b&v=4 url: https://github.com/AlrasheedA @@ -308,9 +308,9 @@ sponsors: - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 url: https://github.com/dudikbender - - login: dazeddd - avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 - url: https://github.com/dazeddd + - login: llamington + avatarUrl: https://avatars.githubusercontent.com/u/54869395?u=42ea59b76f49449f41a4d106bb65a130797e8d7c&v=4 + url: https://github.com/llamington - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -335,9 +335,6 @@ sponsors: - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h -- - login: raestrada95 - avatarUrl: https://avatars.githubusercontent.com/u/34411704?u=f32b7e8f57a3f7e2a99e2bc115356f89f094f340&v=4 - url: https://github.com/raestrada95 - - login: linux-china avatarUrl: https://avatars.githubusercontent.com/u/46711?v=4 url: https://github.com/linux-china @@ -357,7 +354,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/150309?u=3e8f63c27bf996bfc68464b0ce3f7a3e40e6ea7f&v=4 url: https://github.com/hhatto - login: yourkin - avatarUrl: https://avatars.githubusercontent.com/u/178984?u=fa7c3503b47bf16405b96d21554bc59f07a65523&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/178984?u=b43a7e5f8818f7d9083d3b110118d9c27d48a794&v=4 url: https://github.com/yourkin - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 @@ -380,9 +377,6 @@ sponsors: - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy - - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 - url: https://github.com/falkben - login: hardbyte avatarUrl: https://avatars.githubusercontent.com/u/855189?u=aa29e92f34708814d6b67fcd47ca4cf2ce1c04ed&v=4 url: https://github.com/hardbyte @@ -390,13 +384,13 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - login: janfilips - avatarUrl: https://avatars.githubusercontent.com/u/870699?u=6034d81731ecb41ae5c717e56a901ed46fc039a8&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/870699?u=50de77b93d3a0b06887e672d4e8c7b9d643085aa&v=4 url: https://github.com/janfilips - login: woodrad avatarUrl: https://avatars.githubusercontent.com/u/1410765?u=86707076bb03d143b3b11afc1743d2aa496bd8bf&v=4 url: https://github.com/woodrad - login: Pytlicek - avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=169dba3bfbc04ed214a914640ff435969f19ddb3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=01b1f2f7671ce3131e0877d08e2e3f8bdbb0a38a&v=4 url: https://github.com/Pytlicek - login: allen0125 avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4 @@ -407,9 +401,6 @@ sponsors: - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: rglsk - avatarUrl: https://avatars.githubusercontent.com/u/2768101?u=e349c88673f2155fe021331377c656a9d74bcc25&v=4 - url: https://github.com/rglsk - login: Debakel avatarUrl: https://avatars.githubusercontent.com/u/2857237?u=07df6d11c8feef9306d071cb1c1005a2dd596585&v=4 url: https://github.com/Debakel @@ -449,9 +440,6 @@ sponsors: - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 - - login: davanstrien - avatarUrl: https://avatars.githubusercontent.com/u/8995957?u=fb2aad2b52bb4e7b56db6d7c8ecc9ae1eac1b984&v=4 - url: https://github.com/davanstrien - login: yenchenLiu avatarUrl: https://avatars.githubusercontent.com/u/9199638?u=8cdf5ae507448430d90f6f3518d1665a23afe99b&v=4 url: https://github.com/yenchenLiu @@ -483,7 +471,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly - login: sanghunka - avatarUrl: https://avatars.githubusercontent.com/u/16280020?u=960f5426ae08303229f045b9cc2ed463dcd41c15&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/16280020?u=7d8fafd8bfe6d7900bb1e52d5a5d5da0c02bc164&v=4 url: https://github.com/sanghunka - login: stevenayers avatarUrl: https://avatars.githubusercontent.com/u/16361214?u=098b797d8d48afb8cd964b717847943b61d24a6d&v=4 @@ -494,6 +482,9 @@ sponsors: - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia + - login: paulowiz + avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4 + url: https://github.com/paulowiz - login: yannicschroeer avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=4df05a7296c207b91c5d7c7a11c29df5ab313e2b&v=4 url: https://github.com/yannicschroeer @@ -519,7 +510,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon - login: alvarobartt - avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=ac9ccb8b9164eb5fe7d5276142591aa1b8080daf&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=9b38695807eb981d452989699ff72ec2d8f6508e&v=4 url: https://github.com/alvarobartt - login: d-e-h-i-o avatarUrl: https://avatars.githubusercontent.com/u/36816716?v=4 @@ -539,6 +530,9 @@ sponsors: - login: MauriceKuenicke avatarUrl: https://avatars.githubusercontent.com/u/47433175?u=37455bc95c7851db296ac42626f0cacb77ca2443&v=4 url: https://github.com/MauriceKuenicke + - login: hgalytoby + avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 + url: https://github.com/hgalytoby - login: akanz1 avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4 url: https://github.com/akanz1 @@ -569,15 +563,12 @@ sponsors: - login: pondDevThai avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 url: https://github.com/pondDevThai - - login: CY0xZ - avatarUrl: https://avatars.githubusercontent.com/u/103884082?u=0b3260c6a1af6268492f69264dbb75989afb155f&v=4 - url: https://github.com/CY0xZ -- - login: backbord - avatarUrl: https://avatars.githubusercontent.com/u/6814946?v=4 - url: https://github.com/backbord - - login: mattwelke +- - login: mattwelke avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 url: https://github.com/mattwelke + - login: cesarfreire + avatarUrl: https://avatars.githubusercontent.com/u/21126103?u=5d428f77f9b63c741f0e9ca5e15a689017b66fe8&v=4 + url: https://github.com/cesarfreire - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb @@ -585,5 +576,8 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - login: Moises6669 - avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=81761d977faabab60cba5a06e7d50b44416c0c99&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 url: https://github.com/Moises6669 + - login: zahariev-webbersof + avatarUrl: https://avatars.githubusercontent.com/u/68993494?u=b341c94a8aa0624e05e201bcf8ae5b2697e3be2f&v=4 + url: https://github.com/zahariev-webbersof diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index d804ecabb..c7354eb44 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1265 - prs: 333 + answers: 1271 + prs: 338 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 360 + count: 364 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -15,7 +15,7 @@ experts: url: https://github.com/dmontagu - login: ycd count: 221 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 url: https://github.com/ycd - login: Mause count: 207 @@ -25,14 +25,14 @@ experts: count: 166 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: JarroVGIT + count: 163 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: phy25 count: 130 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 -- login: JarroVGIT - count: 129 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT - login: raphaelauv count: 77 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -41,6 +41,10 @@ experts: count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: iudeen + count: 65 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: falkben count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 @@ -53,18 +57,14 @@ experts: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes +- login: jgould22 + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: Dustyposa count: 43 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: iudeen - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: jgould22 - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: adriangb count: 40 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=75087f0cf0e9f725f3cd18a899218b6c63ae60d3&v=4 @@ -78,7 +78,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary - login: chbndrhnns - count: 34 + count: 35 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns - login: prostomarkeloff @@ -98,11 +98,11 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes - login: panla - count: 28 + count: 29 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla - login: acidjunk - count: 26 + count: 27 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk - login: ghandic @@ -130,7 +130,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt - login: odiseo0 - count: 20 + count: 21 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 url: https://github.com/odiseo0 - login: retnikt @@ -175,7 +175,7 @@ experts: url: https://github.com/hellocoldworld - login: haizaar count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/58201?u=4f1f9843d69433ca0d380d95146cfe119e5fdac4&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 url: https://github.com/haizaar - login: yinziyan1206 count: 13 @@ -203,17 +203,25 @@ last_month_active: avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: iudeen - count: 17 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen -- login: imacat - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/594968?v=4 - url: https://github.com/imacat +- login: mbroton + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 + url: https://github.com/mbroton +- login: csrgxtu + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/5053620?u=9655a3e9661492fcdaaf99193eb16d5cbcc3849e&v=4 + url: https://github.com/csrgxtu - login: Kludex - count: 3 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: jgould22 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 top_contributors: - login: waynerv count: 25 @@ -231,14 +239,14 @@ top_contributors: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 +- login: Kludex + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: Kludex - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -289,7 +297,7 @@ top_contributors: url: https://github.com/jfunez - login: ycd count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 url: https://github.com/ycd - login: komtaki count: 4 @@ -311,13 +319,17 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang +- login: pre-commit-ci + count: 4 + avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 + url: https://github.com/apps/pre-commit-ci top_reviewers: - login: Kludex - count: 96 + count: 101 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 51 + count: 64 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: tokusumi @@ -332,18 +344,18 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 +- login: yezz123 + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: ycd count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=826f228edf0bab0d19ad1d5c4ba4df1047ccffef&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 url: https://github.com/ycd - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay -- login: yezz123 - count: 39 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: AdrianDeAnda count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 @@ -353,13 +365,17 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik - login: JarroVGIT - count: 27 + count: 31 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: cassiobotaro count: 25 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro +- login: lsglucas + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 @@ -376,10 +392,6 @@ top_reviewers: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun -- login: lsglucas - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas - login: zy7y count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 @@ -412,6 +424,10 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: iudeen + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -450,7 +466,7 @@ top_reviewers: url: https://github.com/ComicShrimp - login: graingert count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/413772?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 url: https://github.com/graingert - login: PandaHun count: 9 @@ -504,7 +520,3 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 url: https://github.com/NastasiaSaby -- login: Mause - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 - url: https://github.com/Mause From a3a37a42138e80e40ee131995140764e9d3f96c0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 14 Oct 2022 20:50:44 +0000 Subject: [PATCH 0541/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7e8e559a6..3ae403bf9 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 [#5447](https://github.com/tiangolo/fastapi/pull/5447) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🐛 Fix support for strings in OpenAPI status codes: `default`, `1XX`, `2XX`, `3XX`, `4XX`, `5XX`. PR [#5187](https://github.com/tiangolo/fastapi/pull/5187) by [@JarroVGIT](https://github.com/JarroVGIT). * 🔧 Disable Material for MkDocs search plugin. PR [#5495](https://github.com/tiangolo/fastapi/pull/5495) by [@tiangolo](https://github.com/tiangolo). * 🔇 Ignore Trio warning in tests for CI. PR [#5483](https://github.com/tiangolo/fastapi/pull/5483) by [@samuelcolvin](https://github.com/samuelcolvin). From 90fc4299d1d24a54dda8bf6f01cf3779ce6cf467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 14 Oct 2022 22:52:36 +0200 Subject: [PATCH 0542/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?85.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 15 +++++++++++++-- fastapi/__init__.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3ae403bf9..4e41960f7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,11 +2,22 @@ ## Latest Changes -* 👥 Update FastAPI People. PR [#5447](https://github.com/tiangolo/fastapi/pull/5447) by [@github-actions[bot]](https://github.com/apps/github-actions). + +## 0.85.1 + +### Fixes + * 🐛 Fix support for strings in OpenAPI status codes: `default`, `1XX`, `2XX`, `3XX`, `4XX`, `5XX`. PR [#5187](https://github.com/tiangolo/fastapi/pull/5187) by [@JarroVGIT](https://github.com/JarroVGIT). + +### Docs + +* 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). + +### Internal + +* 👥 Update FastAPI People. PR [#5447](https://github.com/tiangolo/fastapi/pull/5447) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Disable Material for MkDocs search plugin. PR [#5495](https://github.com/tiangolo/fastapi/pull/5495) by [@tiangolo](https://github.com/tiangolo). * 🔇 Ignore Trio warning in tests for CI. PR [#5483](https://github.com/tiangolo/fastapi/pull/5483) by [@samuelcolvin](https://github.com/samuelcolvin). -* 📝 Add WayScript x FastAPI Tutorial to External Links section. PR [#5407](https://github.com/tiangolo/fastapi/pull/5407) by [@moneeka](https://github.com/moneeka). ## 0.85.0 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 167bf4f85..7ccb62563 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.85.0" +__version__ = "0.85.1" from starlette import status as status From e866a2c7e1ec598684da54f0311610c63a4b5f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Oct 2022 17:01:38 +0200 Subject: [PATCH 0543/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20calling=20`mkdoc?= =?UTF-8?q?s`=20for=20languages=20as=20a=20subprocess=20to=20fix/enable=20?= =?UTF-8?q?MkDocs=20Material=20search=20plugin=20(#5501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/az/docs/index.md | 466 ++++++++++++++++++++++++++++++++++++++++++ docs/az/mkdocs.yml | 1 + docs/de/mkdocs.yml | 1 + docs/en/mkdocs.yml | 1 + docs/es/mkdocs.yml | 1 + docs/fa/mkdocs.yml | 1 + docs/fr/mkdocs.yml | 1 + docs/he/mkdocs.yml | 1 + docs/id/mkdocs.yml | 1 + docs/it/mkdocs.yml | 1 + docs/ja/mkdocs.yml | 1 + docs/ko/mkdocs.yml | 1 + docs/nl/mkdocs.yml | 1 + docs/pl/mkdocs.yml | 1 + docs/pt/mkdocs.yml | 1 + docs/ru/mkdocs.yml | 1 + docs/sq/mkdocs.yml | 1 + docs/sv/mkdocs.yml | 1 + docs/tr/mkdocs.yml | 1 + docs/uk/mkdocs.yml | 1 + docs/zh/mkdocs.yml | 1 + scripts/docs.py | 5 +- 22 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 docs/az/docs/index.md diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md new file mode 100644 index 000000000..3129f9dc6 --- /dev/null +++ b/docs/az/docs/index.md @@ -0,0 +1,466 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

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

+

+ + Test + + + Coverage + + + Package version + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). + +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Other sponsors + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.7+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +from typing import Optional + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Optional[str] = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="9 14" +from typing import Optional + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Optional[str] = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Optional[bool] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Optional[str] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.6+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * **GraphQL** + * extremely easy tests based on `requests` and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* requests - Required if you want to use the `TestClient`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* graphene - Required for `GraphQLApp` support. +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install fastapi[all]`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index ef5eea239..d549f37a3 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 9f78c39ef..8c3c42b5f 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 5322ed90f..69ad29624 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index a74044739..d1d6215b6 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index ae6b5be5c..7c2fe5eab 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 8d8cfccce..6bed7be73 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index a96345184..3279099b5 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 1445512e2..abb31252f 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index b2d0cb309..532b5bc52 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index b7c15bf16..b3f18bbdd 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index a8253ee71..50931e134 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 588f224c2..6d46939f9 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 190a6e11f..1cd129420 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 27c0d10fa..59ee3cc88 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 96165dcaf..381775ac6 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index aaf3563b9..b07f3bc63 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index e06be635f..3332d232d 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 34890954d..e29d25936 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index d93419bda..711328771 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a18a84adf..ac8679dc0 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -32,6 +32,7 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: +- search - markdownextradata: data: data nav: diff --git a/scripts/docs.py b/scripts/docs.py index 40569e193..d5fbacf59 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,6 +1,7 @@ import os import re import shutil +import subprocess from http.server import HTTPServer, SimpleHTTPRequestHandler from multiprocessing import Pool from pathlib import Path @@ -200,7 +201,7 @@ def build_lang( ) current_dir = os.getcwd() os.chdir(build_lang_path) - mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(dist_path))) + subprocess.run(["mkdocs", "build", "--site-dir", dist_path], check=True) os.chdir(current_dir) typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) @@ -275,7 +276,7 @@ def build_all(): current_dir = os.getcwd() os.chdir(en_docs_path) typer.echo("Building docs for: en") - mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(site_path))) + subprocess.run(["mkdocs", "build", "--site-dir", site_path], check=True) os.chdir(current_dir) langs = [] for lang in get_lang_paths(): From 0e3ff851c285d30dd8012a25054e597bd6d1a50b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Oct 2022 15:02:15 +0000 Subject: [PATCH 0544/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4e41960f7..a5db452fa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix calling `mkdocs` for languages as a subprocess to fix/enable MkDocs Material search plugin. PR [#5501](https://github.com/tiangolo/fastapi/pull/5501) by [@tiangolo](https://github.com/tiangolo). ## 0.85.1 From e04878edfe1930ae471a0f5485ce1b5f8dc7be61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 16 Oct 2022 17:15:47 +0200 Subject: [PATCH 0545/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Typer?= =?UTF-8?q?=20to=20include=20Rich=20in=20scripts=20for=20docs=20(#5502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eede670fd..755723224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,9 @@ test = [ "requests >=2.24.0,<3.0.0", "httpx >=0.23.0,<0.24.0", "email_validator >=1.1.1,<2.0.0", - "sqlalchemy >=1.3.18,<1.5.0", + # TODO: once removing databases from tutorial, upgrade SQLAlchemy + # probably when including SQLModel + "sqlalchemy >=1.3.18,<=1.4.41", "peewee >=3.13.3,<4.0.0", "databases[sqlite] >=0.3.2,<0.7.0", "orjson >=3.2.1,<4.0.0", @@ -81,7 +83,7 @@ doc = [ "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", # TODO: upgrade and enable typer-cli once it supports Click 8.x.x # "typer-cli >=0.0.12,<0.0.13", - "typer >=0.4.1,<0.7.0", + "typer[all] >=0.6.1,<0.7.0", "pyyaml >=5.3.1,<7.0.0", ] dev = [ From e13df8ee79d11ad8e338026d99b1dcdcb2261c9f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 16 Oct 2022 15:16:24 +0000 Subject: [PATCH 0546/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a5db452fa..ed469a5d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Typer to include Rich in scripts for docs. PR [#5502](https://github.com/tiangolo/fastapi/pull/5502) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix calling `mkdocs` for languages as a subprocess to fix/enable MkDocs Material search plugin. PR [#5501](https://github.com/tiangolo/fastapi/pull/5501) by [@tiangolo](https://github.com/tiangolo). ## 0.85.1 From 9dc1a3026d612f16dc953c8da6a6944eb5a74ea6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Oct 2022 13:15:19 +0200 Subject: [PATCH 0547/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5408)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v2.37.3 → v3.1.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v3.1.0) - https://github.com/myint/autoflake → https://github.com/PyCQA/autoflake - [github.com/PyCQA/autoflake: v1.5.3 → v1.7.6](https://github.com/PyCQA/autoflake/compare/v1.5.3...v1.7.6) - [github.com/psf/black: 22.8.0 → 22.10.0](https://github.com/psf/black/compare/22.8.0...22.10.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c4c1d4d9e..978114a65 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,14 +12,14 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v2.37.3 + rev: v3.1.0 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing -- repo: https://github.com/myint/autoflake - rev: v1.5.3 +- repo: https://github.com/PyCQA/autoflake + rev: v1.7.6 hooks: - id: autoflake args: @@ -43,7 +43,7 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 22.8.0 + rev: 22.10.0 hooks: - id: black ci: From f67b19f0f73ebdca01775b8c7e531e51b9cecfae Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 20 Oct 2022 11:15:56 +0000 Subject: [PATCH 0548/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed469a5d1..53a92f463 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5408](https://github.com/tiangolo/fastapi/pull/5408) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆️ Upgrade Typer to include Rich in scripts for docs. PR [#5502](https://github.com/tiangolo/fastapi/pull/5502) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix calling `mkdocs` for languages as a subprocess to fix/enable MkDocs Material search plugin. PR [#5501](https://github.com/tiangolo/fastapi/pull/5501) by [@tiangolo](https://github.com/tiangolo). From ee4b2bb8ae6c37208bf06d27cc6f4be024895fc1 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 31 Oct 2022 13:36:30 +0000 Subject: [PATCH 0549/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20internal=20Trio?= =?UTF-8?q?=20test=20warnings=20(#5547)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 755723224..c76538c17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,5 +137,8 @@ filterwarnings = [ 'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning:asyncio', 'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette', # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 - 'ignore::trio.TrioDeprecationWarning', + "ignore:You seem to already have a custom.*:RuntimeWarning:trio", + "ignore::trio.TrioDeprecationWarning", + # TODO remove pytest-cov + 'ignore::pytest.PytestDeprecationWarning:pytest_cov', ] From 866a24f879e161772d6a04261fbd7971b6a980fe Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 13:36:59 +0000 Subject: [PATCH 0550/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 53a92f463..c75f258cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix internal Trio test warnings. PR [#5547](https://github.com/tiangolo/fastapi/pull/5547) by [@samuelcolvin](https://github.com/samuelcolvin). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5408](https://github.com/tiangolo/fastapi/pull/5408) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆️ Upgrade Typer to include Rich in scripts for docs. PR [#5502](https://github.com/tiangolo/fastapi/pull/5502) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix calling `mkdocs` for languages as a subprocess to fix/enable MkDocs Material search plugin. PR [#5501](https://github.com/tiangolo/fastapi/pull/5501) by [@tiangolo](https://github.com/tiangolo). From fdfcb9412e1455ab43a4e2ffafecedd72b799c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Oct 2022 17:07:02 +0100 Subject: [PATCH 0551/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20config=20for=20T?= =?UTF-8?q?amil=20translations=20(#5563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/translations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml index d283ef9f7..b43aba01d 100644 --- a/.github/actions/notify-translations/app/translations.yml +++ b/.github/actions/notify-translations/app/translations.yml @@ -17,3 +17,4 @@ nl: 4701 uz: 4883 sv: 5146 he: 5157 +ta: 5434 From e0d1619362d0bf0d57df6e4da29b22ab380da378 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:07:36 +0000 Subject: [PATCH 0552/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c75f258cb..341bdc3d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix internal Trio test warnings. PR [#5547](https://github.com/tiangolo/fastapi/pull/5547) by [@samuelcolvin](https://github.com/samuelcolvin). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5408](https://github.com/tiangolo/fastapi/pull/5408) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆️ Upgrade Typer to include Rich in scripts for docs. PR [#5502](https://github.com/tiangolo/fastapi/pull/5502) by [@tiangolo](https://github.com/tiangolo). From 146b9d6e04a930ec48d171514fb0f7ef4a8bb35e Mon Sep 17 00:00:00 2001 From: Bruno Artur Torres Lopes Pereira <33462923+batlopes@users.noreply.github.com> Date: Mon, 31 Oct 2022 13:22:07 -0300 Subject: [PATCH 0553/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/response-status-code.md?= =?UTF-8?q?`=20(#4922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/pt/docs/tutorial/response-status-code.md | 91 +++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 92 insertions(+) create mode 100644 docs/pt/docs/tutorial/response-status-code.md diff --git a/docs/pt/docs/tutorial/response-status-code.md b/docs/pt/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..2df17d4ea --- /dev/null +++ b/docs/pt/docs/tutorial/response-status-code.md @@ -0,0 +1,91 @@ +# Código de status de resposta + +Da mesma forma que você pode especificar um modelo de resposta, você também pode declarar o código de status HTTP usado para a resposta com o parâmetro `status_code` em qualquer uma das *operações de caminho*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* etc. + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "Nota" + Observe que `status_code` é um parâmetro do método "decorador" (get, post, etc). Não da sua função de *operação de caminho*, como todos os parâmetros e corpo. + +O parâmetro `status_code` recebe um número com o código de status HTTP. + +!!! info "Informação" + `status_code` também pode receber um `IntEnum`, como o do Python `http.HTTPStatus`. + +Dessa forma: + +* Este código de status será retornado na resposta. +* Será documentado como tal no esquema OpenAPI (e, portanto, nas interfaces do usuário): + + + +!!! note "Nota" + Alguns códigos de resposta (consulte a próxima seção) indicam que a resposta não possui um corpo. + + O FastAPI sabe disso e produzirá documentos OpenAPI informando que não há corpo de resposta. + +## Sobre os códigos de status HTTP + +!!! note "Nota" + Se você já sabe o que são códigos de status HTTP, pule para a próxima seção. + +Em HTTP, você envia um código de status numérico de 3 dígitos como parte da resposta. + +Esses códigos de status têm um nome associado para reconhecê-los, mas o importante é o número. + +Resumidamente: + + +* `100` e acima são para "Informações". Você raramente os usa diretamente. As respostas com esses códigos de status não podem ter um corpo. +* **`200`** e acima são para respostas "Bem-sucedidas". Estes são os que você mais usaria. + * `200` é o código de status padrão, o que significa que tudo estava "OK". + * Outro exemplo seria `201`, "Criado". É comumente usado após a criação de um novo registro no banco de dados. + * Um caso especial é `204`, "Sem Conteúdo". Essa resposta é usada quando não há conteúdo para retornar ao cliente e, portanto, a resposta não deve ter um corpo. +* **`300`** e acima são para "Redirecionamento". As respostas com esses códigos de status podem ou não ter um corpo, exceto `304`, "Não modificado", que não deve ter um. +* **`400`** e acima são para respostas de "Erro do cliente". Este é o segundo tipo que você provavelmente mais usaria. + * Um exemplo é `404`, para uma resposta "Não encontrado". + * Para erros genéricos do cliente, você pode usar apenas `400`. +* `500` e acima são para erros do servidor. Você quase nunca os usa diretamente. Quando algo der errado em alguma parte do código do seu aplicativo ou servidor, ele retornará automaticamente um desses códigos de status. + +!!! tip "Dica" + Para saber mais sobre cada código de status e qual código serve para quê, verifique o MDN documentação sobre códigos de status HTTP. + +## Atalho para lembrar os nomes + +Vamos ver o exemplo anterior novamente: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` é o código de status para "Criado". + +Mas você não precisa memorizar o que cada um desses códigos significa. + +Você pode usar as variáveis de conveniência de `fastapi.status`. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +Eles são apenas uma conveniência, eles possuem o mesmo número, mas dessa forma você pode usar o autocomplete do editor para encontrá-los: + + + +!!! note "Detalhes técnicos" + Você também pode usar `from starlette import status`. + + **FastAPI** fornece o mesmo `starlette.status` como `fastapi.status` apenas como uma conveniência para você, o desenvolvedor. Mas vem diretamente da Starlette. + + +## Alterando o padrão + +Mais tarde, no [Guia do usuário avançado](../advanced/response-change-status-code.md){.internal-link target=_blank}, você verá como retornar um código de status diferente do padrão que você está declarando aqui. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 59ee3cc88..18c6bbc6d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -71,6 +71,7 @@ nav: - tutorial/query-params-str-validations.md - tutorial/cookie-params.md - tutorial/header-params.md + - tutorial/response-status-code.md - tutorial/handling-errors.md - Segurança: - tutorial/security/index.md From 42a4ed7a1804f631f971d05f3302d54361ebe10e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:22:46 +0000 Subject: [PATCH 0554/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 341bdc3d2..e70c2fcaa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). * 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix internal Trio test warnings. PR [#5547](https://github.com/tiangolo/fastapi/pull/5547) by [@samuelcolvin](https://github.com/samuelcolvin). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5408](https://github.com/tiangolo/fastapi/pull/5408) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 4a1c69e6c2e815643f3bf31765b04ca418f95418 Mon Sep 17 00:00:00 2001 From: feliciss <22887031+feliciss@users.noreply.github.com> Date: Mon, 31 Oct 2022 23:38:10 +0700 Subject: [PATCH 0555/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typo=20in=20docs?= =?UTF-8?q?=20with=20examples=20for=20Python=203.10=20instead=20of=203.9?= =?UTF-8?q?=20(#5545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Feliciss Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/request-files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 664a1102f..783783c58 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -124,7 +124,7 @@ You can make a file optional by using standard type annotations and setting a de {!> ../../../docs_src/request_files/tutorial001_02.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.10 and above" ```Python hl_lines="7 14" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} From 0cc40ed66462e6880a37955bb6050ec7095fc2a3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:39:02 +0000 Subject: [PATCH 0556/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e70c2fcaa..9c156f03e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). * 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix internal Trio test warnings. PR [#5547](https://github.com/tiangolo/fastapi/pull/5547) by [@samuelcolvin](https://github.com/samuelcolvin). From bfb14225555ceff2798a96e4f98164bcab241cad Mon Sep 17 00:00:00 2001 From: yuk115nt5now Date: Tue, 1 Nov 2022 01:39:44 +0900 Subject: [PATCH 0557/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typo=20in=20Chin?= =?UTF-8?q?ese=20translation=20for=20`docs/zh/docs/tutorial/security/first?= =?UTF-8?q?-steps.md`=20(#5530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/tutorial/security/first-steps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md index 116572411..86c3320ce 100644 --- a/docs/zh/docs/tutorial/security/first-steps.md +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -110,7 +110,7 @@ OAuth2 的设计目标是为了让后端或 API 独立于服务器验证用户 !!! info "说明" - `Beare` 令牌不是唯一的选择。 + `Bearer` 令牌不是唯一的选择。 但它是最适合这个用例的方案。 From bbb8fe1e602a4b7d8a6147b61b1a2cdce532ef2d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:40:30 +0000 Subject: [PATCH 0558/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9c156f03e..a4b7018db 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). * 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). * 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). From 80eac96c70d7273cd42f91ad6bad2e0a35f77504 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:45:41 +0100 Subject: [PATCH 0559/1881] =?UTF-8?q?=E2=AC=86=20Bump=20internal=20depende?= =?UTF-8?q?ncy=20mypy=20from=200.971=20to=200.982=20(#5541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 c76538c17..35bd816d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ Documentation = "https://fastapi.tiangolo.com/" test = [ "pytest >=7.1.3,<8.0.0", "pytest-cov >=2.12.0,<4.0.0", - "mypy ==0.971", + "mypy ==0.982", "flake8 >=3.8.3,<6.0.0", "black == 22.8.0", "isort >=5.0.6,<6.0.0", From 959f6bf209d534acf3437e5b9260d0e152903fa9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:46:16 +0000 Subject: [PATCH 0560/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a4b7018db..c0ce9216d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). * 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). From 22524a1610be93e7fc75ac9a5dc959a536ffa499 Mon Sep 17 00:00:00 2001 From: zhangbo Date: Tue, 1 Nov 2022 00:46:37 +0800 Subject: [PATCH 0561/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20docs=20?= =?UTF-8?q?about=20contributing,=20for=20compatibility=20with=20`pip`=20in?= =?UTF-8?q?=20Zsh=20(#5523)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zhangbo Co-authored-by: Sebastián Ramírez --- docs/en/docs/contributing.md | 2 +- docs/ja/docs/contributing.md | 2 +- docs/pt/docs/contributing.md | 2 +- docs/zh/docs/contributing.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 39d7dd193..58a363220 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -108,7 +108,7 @@ After activating the environment as described above:
```console -$ pip install -e .[dev,doc,test] +$ pip install -e ."[dev,doc,test]" ---> 100% ``` diff --git a/docs/ja/docs/contributing.md b/docs/ja/docs/contributing.md index 8bad864a2..9affea443 100644 --- a/docs/ja/docs/contributing.md +++ b/docs/ja/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e .[dev,doc,test] +$ pip install -e ."[dev,doc,test]" ---> 100% ``` diff --git a/docs/pt/docs/contributing.md b/docs/pt/docs/contributing.md index dcb6a80db..f95b6f4ec 100644 --- a/docs/pt/docs/contributing.md +++ b/docs/pt/docs/contributing.md @@ -98,7 +98,7 @@ Após ativar o ambiente como descrito acima:
```console -$ pip install -e .[dev,doc,test] +$ pip install -e ."[dev,doc,test]" ---> 100% ``` diff --git a/docs/zh/docs/contributing.md b/docs/zh/docs/contributing.md index ca3646289..36c3631c4 100644 --- a/docs/zh/docs/contributing.md +++ b/docs/zh/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e .[dev,doc,test] +$ pip install -e ."[dev,doc,test]" ---> 100% ``` From bbfa450991e734be65aa85b692877a622648b483 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:47:12 +0000 Subject: [PATCH 0562/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c0ce9216d..f26f0ab91 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). * ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). * 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). From 16e058697d01ce47fa95d54739eb49bbe0845309 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:48:43 +0100 Subject: [PATCH 0563/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 978114a65..bd5b8641a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - --py3-plus - --keep-runtime-typing - repo: https://github.com/PyCQA/autoflake - rev: v1.7.6 + rev: v1.7.7 hooks: - id: autoflake args: From d72599b4dc1382ca3180b62a78f1e7ec128fc9a3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 16:51:55 +0000 Subject: [PATCH 0564/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f26f0ab91..7c617db1a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5536](https://github.com/tiangolo/fastapi/pull/5536) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). * ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). From 2623a06105d152829ce9bc8197eaecfad68e91fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:09:45 +0000 Subject: [PATCH 0565/1881] =?UTF-8?q?=E2=AC=86=20Update=20internal=20depen?= =?UTF-8?q?dency=20pytest-cov=20requirement=20from=20<4.0.0,>=3D2.12.0=20t?= =?UTF-8?q?o=20>=3D2.12.0,<5.0.0=20(#5539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 35bd816d3..81d351285 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] test = [ "pytest >=7.1.3,<8.0.0", - "pytest-cov >=2.12.0,<4.0.0", + "pytest-cov >=2.12.0,<5.0.0", "mypy ==0.982", "flake8 >=3.8.3,<6.0.0", "black == 22.8.0", From 85443e64eec79b48c77851cb0544f932d27b9b89 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:10:20 +0000 Subject: [PATCH 0566/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7c617db1a..3ac0382d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update internal dependency pytest-cov requirement from <4.0.0,>=2.12.0 to >=2.12.0,<5.0.0. PR [#5539](https://github.com/tiangolo/fastapi/pull/5539) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5536](https://github.com/tiangolo/fastapi/pull/5536) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). * ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). From c75de8cba2c5213acac669a666ecc3e23f7d0d5a Mon Sep 17 00:00:00 2001 From: Shubham <75021117+su-shubham@users.noreply.github.com> Date: Mon, 31 Oct 2022 22:46:57 +0530 Subject: [PATCH 0567/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20broken=20link=20in?= =?UTF-8?q?=20`alternatives.md`=20(#5455)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/alternatives.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 68aa3150d..bcd406bf9 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -123,7 +123,7 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit. -### Marshmallow +### Marshmallow One of the main features needed by API systems is data "serialization" which is taking data from the code (Python) and converting it into something that can be sent through the network. For example, converting an object containing data from a database into a JSON object. Converting `datetime` objects into strings, etc. From 6a46532cc45e98d6e5240aace0b598b588f8dc92 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:17:35 +0000 Subject: [PATCH 0568/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3ac0382d8..646261e61 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). * ⬆ Update internal dependency pytest-cov requirement from <4.0.0,>=2.12.0 to >=2.12.0,<5.0.0. PR [#5539](https://github.com/tiangolo/fastapi/pull/5539) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5536](https://github.com/tiangolo/fastapi/pull/5536) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). From 88556dafb65b0af133620b4ca56eaaa5fbb8c619 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Mon, 31 Oct 2022 10:21:05 -0700 Subject: [PATCH 0569/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20grammar=20and=20add?= =?UTF-8?q?=20helpful=20links=20to=20dependencies=20in=20`docs/en/docs/asy?= =?UTF-8?q?nc.md`=20(#5432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/async.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index c14a2cbb7..6f34a9c9c 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -403,17 +403,17 @@ All that is what powers FastAPI (through Starlette) and what makes it have such When you declare a *path operation function* with normal `def` instead of `async def`, it is run in an external threadpool that is then awaited, instead of being called directly (as it would block the server). -If you are coming from another async framework that does not work in the way described above and you are used to define trivial compute-only *path operation functions* with plain `def` for a tiny performance gain (about 100 nanoseconds), please note that in **FastAPI** the effect would be quite opposite. In these cases, it's better to use `async def` unless your *path operation functions* use code that performs blocking I/O. +If you are coming from another async framework that does not work in the way described above and you are used to defining trivial compute-only *path operation functions* with plain `def` for a tiny performance gain (about 100 nanoseconds), please note that in **FastAPI** the effect would be quite opposite. In these cases, it's better to use `async def` unless your *path operation functions* use code that performs blocking I/O. Still, in both situations, chances are that **FastAPI** will [still be faster](/#performance){.internal-link target=_blank} than (or at least comparable to) your previous framework. ### Dependencies -The same applies for dependencies. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. +The same applies for [dependencies](/tutorial/dependencies/index.md){.internal-link target=_blank}. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. ### Sub-dependencies -You can have multiple dependencies and sub-dependencies requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". +You can have multiple dependencies and [sub-dependencies](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". ### Other utility functions From 64d512e349990efb7c659bd5d2f8a04e33d0f698 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:21:40 +0000 Subject: [PATCH 0570/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 646261e61..a4897bccd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). * ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). * ⬆ Update internal dependency pytest-cov requirement from <4.0.0,>=2.12.0 to >=2.12.0,<5.0.0. PR [#5539](https://github.com/tiangolo/fastapi/pull/5539) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5536](https://github.com/tiangolo/fastapi/pull/5536) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From f3086a7b15eb924aa8517b8e4422ee3e6ff328d7 Mon Sep 17 00:00:00 2001 From: Julian Maurin Date: Mon, 31 Oct 2022 17:34:09 +0000 Subject: [PATCH 0571/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/help-fastapi.md`=20(#2233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruidy --- docs/fr/docs/help-fastapi.md | 122 +++++++++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 123 insertions(+) create mode 100644 docs/fr/docs/help-fastapi.md diff --git a/docs/fr/docs/help-fastapi.md b/docs/fr/docs/help-fastapi.md new file mode 100644 index 000000000..0995721e1 --- /dev/null +++ b/docs/fr/docs/help-fastapi.md @@ -0,0 +1,122 @@ +# Help FastAPI - Obtenir de l'aide + +Aimez-vous **FastAPI** ? + +Vous souhaitez aider FastAPI, les autres utilisateurs et l'auteur ? + +Ou souhaitez-vous obtenir de l'aide avec le **FastAPI** ? + +Il existe des moyens très simples d'aider (plusieurs ne nécessitent qu'un ou deux clics). + +Il existe également plusieurs façons d'obtenir de l'aide. + +## Star **FastAPI** sur GitHub + +Vous pouvez "star" FastAPI dans GitHub (en cliquant sur le bouton étoile en haut à droite) : https://github.com/tiangolo/fastapi. ⭐️ + +En ajoutant une étoile, les autres utilisateurs pourront la trouver plus facilement et constater qu'elle a déjà été utile à d'autres. + +## Watch le dépôt GitHub pour les releases + +Vous pouvez "watch" FastAPI dans GitHub (en cliquant sur le bouton "watch" en haut à droite) : https://github.com/tiangolo/fastapi. 👀 + +Vous pouvez y sélectionner "Releases only". + +Ainsi, vous recevrez des notifications (dans votre courrier électronique) chaque fois qu'il y aura une nouvelle version de **FastAPI** avec des corrections de bugs et de nouvelles fonctionnalités. + +## Se rapprocher de l'auteur + +Vous pouvez vous rapprocher de moi (Sebastián Ramírez / `tiangolo`), l'auteur. + +Vous pouvez : + +* Me suivre sur **GitHub**. + * Voir d'autres projets Open Source que j'ai créés et qui pourraient vous aider. + * Suivez-moi pour voir quand je crée un nouveau projet Open Source. +* Me suivre sur **Twitter**. + * Dites-moi comment vous utilisez FastAPI (j'adore entendre ça). + * Entendre quand je fais des annonces ou que je lance de nouveaux outils. +* Vous connectez à moi sur **Linkedin**. + * Etre notifié quand je fais des annonces ou que je lance de nouveaux outils (bien que j'utilise plus souvent Twitte 🤷‍♂). +* Lire ce que j’écris (ou me suivre) sur **Dev.to** ou **Medium**. + * Lire d'autres idées, articles, et sur les outils que j'ai créés. + * Suivez-moi pour lire quand je publie quelque chose de nouveau. + +## Tweeter sur **FastAPI** + +Tweetez à propos de **FastAPI** et faites-moi savoir, ainsi qu'aux autres, pourquoi vous aimez ça. 🎉 + +J'aime entendre parler de l'utilisation du **FastAPI**, de ce que vous avez aimé dedans, dans quel projet/entreprise l'utilisez-vous, etc. + +## Voter pour FastAPI + +* Votez pour **FastAPI** sur Slant. +* Votez pour **FastAPI** sur AlternativeTo. +* Votez pour **FastAPI** sur awesome-rest. + +## Aider les autres à résoudre les problèmes dans GitHub + +Vous pouvez voir les problèmes existants et essayer d'aider les autres, la plupart du temps il s'agit de questions dont vous connaissez peut-être déjà la réponse. 🤓 + +## Watch le dépôt GitHub + +Vous pouvez "watch" FastAPI dans GitHub (en cliquant sur le bouton "watch" en haut à droite) : https://github.com/tiangolo/fastapi. 👀 + +Si vous sélectionnez "Watching" au lieu de "Releases only", vous recevrez des notifications lorsque quelqu'un crée une nouvelle Issue. + +Vous pouvez alors essayer de les aider à résoudre ces problèmes. + +## Créer une Issue + +Vous pouvez créer une Issue dans le dépôt GitHub, par exemple pour : + +* Poser une question ou s'informer sur un problème. +* Suggérer une nouvelle fonctionnalité. + +**Note** : si vous créez un problème, alors je vais vous demander d'aider aussi les autres. 😉 + +## Créer une Pull Request + +Vous pouvez créer une Pull Request, par exemple : + +* Pour corriger une faute de frappe que vous avez trouvée sur la documentation. +* Proposer de nouvelles sections de documentation. +* Pour corriger une Issue/Bug existant. +* Pour ajouter une nouvelle fonctionnalité. + +## Rejoindre le chat + + + Rejoindre le chat à https://gitter.im/tiangolo/fastapi + + +Rejoignez le chat sur Gitter: https://gitter.im/tiangolo/fastapi. + +Vous pouvez y avoir des conversations rapides avec d'autres personnes, aider les autres, partager des idées, etc. + +Mais gardez à l'esprit que, comme il permet une "conversation plus libre", il est facile de poser des questions trop générales et plus difficiles à répondre, de sorte que vous risquez de ne pas recevoir de réponses. + +Dans les Issues de GitHub, le modèle vous guidera pour écrire la bonne question afin que vous puissiez plus facilement obtenir une bonne réponse, ou même résoudre le problème vous-même avant même de le poser. Et dans GitHub, je peux m'assurer que je réponds toujours à tout, même si cela prend du temps. Je ne peux pas faire cela personnellement avec le chat Gitter. 😅 + +Les conversations dans Gitter ne sont pas non plus aussi facilement consultables que dans GitHub, de sorte que les questions et les réponses peuvent se perdre dans la conversation. + +De l'autre côté, il y a plus de 1000 personnes dans le chat, il y a donc de fortes chances que vous y trouviez quelqu'un à qui parler, presque tout le temps. 😄 + +## Parrainer l'auteur + +Vous pouvez également soutenir financièrement l'auteur (moi) via GitHub sponsors. + +Là, vous pourriez m'offrir un café ☕️ pour me remercier 😄. + +## Sponsoriser les outils qui font fonctionner FastAPI + +Comme vous l'avez vu dans la documentation, FastAPI se tient sur les épaules des géants, Starlette et Pydantic. + +Vous pouvez également parrainer : + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +Merci ! 🚀 diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 6bed7be73..e6250b2e0 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -75,6 +75,7 @@ nav: - alternatives.md - history-design-future.md - external-links.md +- help-fastapi.md markdown_extensions: - toc: permalink: true From 558d92956827f7f752729768e124137654d3a2e7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:34:47 +0000 Subject: [PATCH 0572/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a4897bccd..8a37cc04e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). * ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). * ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). * ⬆ Update internal dependency pytest-cov requirement from <4.0.0,>=2.12.0 to >=2.12.0,<5.0.0. PR [#5539](https://github.com/tiangolo/fastapi/pull/5539) by [@dependabot[bot]](https://github.com/apps/dependabot). From fcf49a04eb321b1a917f9d17776e46531e0ea62f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:35:48 +0000 Subject: [PATCH 0573/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.23.0=20to=202.24.0=20(#5508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/preview-docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index d42b08543..1678ca762 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.23.0 + uses: dawidd6/action-download-artifact@v2.24.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml From 29c36f9d3f361d0125398ea7a405bd8b7decdfea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:36:09 +0000 Subject: [PATCH 0574/1881] =?UTF-8?q?=E2=AC=86=20Bump=20internal=20depende?= =?UTF-8?q?ncy=20types-ujson=20from=205.4.0=20to=205.5.0=20(#5537)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 81d351285..543ba15c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ test = [ "passlib[bcrypt] >=1.7.2,<2.0.0", # types - "types-ujson ==5.4.0", + "types-ujson ==5.5.0", "types-orjson ==3.6.2", ] doc = [ From a96effa86ec96c8743d9eefafa202d14c4e0506e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:36:24 +0000 Subject: [PATCH 0575/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8a37cc04e..159995eed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.23.0 to 2.24.0. PR [#5508](https://github.com/tiangolo/fastapi/pull/5508) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). * ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). * ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). From f1c61dec0cc45a2c0f0be1032348ff059e2a7eed Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:36:43 +0000 Subject: [PATCH 0576/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 159995eed..3d7649000 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump internal dependency types-ujson from 5.4.0 to 5.5.0. PR [#5537](https://github.com/tiangolo/fastapi/pull/5537) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.23.0 to 2.24.0. PR [#5508](https://github.com/tiangolo/fastapi/pull/5508) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). * ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). From 5f089435e54779e90830cf72af6e16c1383b3407 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Oct 2022 17:37:23 +0000 Subject: [PATCH 0577/1881] =?UTF-8?q?=E2=AC=86=20Bump=20nwtgck/actions-net?= =?UTF-8?q?lify=20from=201.2.3=20to=201.2.4=20(#5507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/build-docs.yml | 2 +- .github/workflows/preview-docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index af8909845..3052ec1e2 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -38,7 +38,7 @@ jobs: name: docs-zip path: ./docs.zip - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.2.3 + uses: nwtgck/actions-netlify@v1.2.4 with: publish-dir: './site' production-branch: master diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 1678ca762..15c59d4bd 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -25,7 +25,7 @@ jobs: rm -f docs.zip - name: Deploy to Netlify id: netlify - uses: nwtgck/actions-netlify@v1.2.3 + uses: nwtgck/actions-netlify@v1.2.4 with: publish-dir: './site' production-deploy: false From 669b3a4cb88ad48b130728448e2258a38e993b59 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:38:37 +0000 Subject: [PATCH 0578/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3d7649000..9e22df6ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump nwtgck/actions-netlify from 1.2.3 to 1.2.4. PR [#5507](https://github.com/tiangolo/fastapi/pull/5507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump internal dependency types-ujson from 5.4.0 to 5.5.0. PR [#5537](https://github.com/tiangolo/fastapi/pull/5537) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.23.0 to 2.24.0. PR [#5508](https://github.com/tiangolo/fastapi/pull/5508) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). From 1613749cc3d0bff65e5d49873ee2a72e554850fb Mon Sep 17 00:00:00 2001 From: Ruidy Date: Mon, 31 Oct 2022 18:39:54 +0100 Subject: [PATCH 0579/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`deployment/versions.md`=20(#3690)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sam Courtemanche Co-authored-by: Ruidy Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/fr/docs/deployment/versions.md | 95 +++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 96 insertions(+) create mode 100644 docs/fr/docs/deployment/versions.md diff --git a/docs/fr/docs/deployment/versions.md b/docs/fr/docs/deployment/versions.md new file mode 100644 index 000000000..136165e9d --- /dev/null +++ b/docs/fr/docs/deployment/versions.md @@ -0,0 +1,95 @@ +# À propos des versions de FastAPI + +**FastAPI** est déjà utilisé en production dans de nombreuses applications et systèmes. Et la couverture de test est maintenue à 100 %. Mais son développement est toujours aussi rapide. + +De nouvelles fonctionnalités sont ajoutées fréquemment, des bogues sont corrigés régulièrement et le code est +amélioré continuellement. + +C'est pourquoi les versions actuelles sont toujours `0.x.x`, cela reflète que chaque version peut potentiellement +recevoir des changements non rétrocompatibles. Cela suit les conventions de versionnage sémantique. + +Vous pouvez créer des applications de production avec **FastAPI** dès maintenant (et vous le faites probablement depuis un certain temps), vous devez juste vous assurer que vous utilisez une version qui fonctionne correctement avec le reste de votre code. + +## Épinglez votre version de `fastapi` + +Tout d'abord il faut "épingler" la version de **FastAPI** que vous utilisez à la dernière version dont vous savez +qu'elle fonctionne correctement pour votre application. + +Par exemple, disons que vous utilisez la version `0.45.0` dans votre application. + +Si vous utilisez un fichier `requirements.txt`, vous pouvez spécifier la version avec : + +```txt +fastapi==0.45.0 +``` + +ce qui signifierait que vous utiliseriez exactement la version `0.45.0`. + +Ou vous pourriez aussi l'épingler avec : + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +cela signifierait que vous utiliseriez les versions `0.45.0` ou supérieures, mais inférieures à `0.46.0`, par exemple, une version `0.45.2` serait toujours acceptée. + +Si vous utilisez un autre outil pour gérer vos installations, comme Poetry, Pipenv, ou autres, ils ont tous un moyen que vous pouvez utiliser pour définir des versions spécifiques pour vos paquets. + +## Versions disponibles + +Vous pouvez consulter les versions disponibles (par exemple, pour vérifier quelle est la dernière version en date) dans les [Notes de version](../release-notes.md){.internal-link target=_blank}. + +## À propos des versions + +Suivant les conventions de versionnage sémantique, toute version inférieure à `1.0.0` peut potentiellement ajouter +des changements non rétrocompatibles. + +FastAPI suit également la convention que tout changement de version "PATCH" est pour des corrections de bogues et +des changements rétrocompatibles. + +!!! tip "Astuce" + Le "PATCH" est le dernier chiffre, par exemple, dans `0.2.3`, la version PATCH est `3`. + +Donc, vous devriez être capable d'épingler une version comme suit : + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +Les changements non rétrocompatibles et les nouvelles fonctionnalités sont ajoutés dans les versions "MINOR". + +!!! tip "Astuce" + Le "MINOR" est le numéro au milieu, par exemple, dans `0.2.3`, la version MINOR est `2`. + +## Mise à jour des versions FastAPI + +Vous devriez tester votre application. + +Avec **FastAPI** c'est très facile (merci à Starlette), consultez la documentation : [Testing](../tutorial/testing.md){.internal-link target=_blank} + +Après avoir effectué des tests, vous pouvez mettre à jour la version **FastAPI** vers une version plus récente, et vous assurer que tout votre code fonctionne correctement en exécutant vos tests. + +Si tout fonctionne, ou après avoir fait les changements nécessaires, et que tous vos tests passent, vous pouvez +épingler votre version de `fastapi` à cette nouvelle version récente. + +## À propos de Starlette + +Vous ne devriez pas épingler la version de `starlette`. + +Différentes versions de **FastAPI** utiliseront une version spécifique plus récente de Starlette. + +Ainsi, vous pouvez simplement laisser **FastAPI** utiliser la bonne version de Starlette. + +## À propos de Pydantic + +Pydantic inclut des tests pour **FastAPI** avec ses propres tests, ainsi les nouvelles versions de Pydantic (au-dessus +de `1.0.0`) sont toujours compatibles avec **FastAPI**. + +Vous pouvez épingler Pydantic à toute version supérieure à `1.0.0` qui fonctionne pour vous et inférieure à `2.0.0`. + +Par exemple : + +```txt +pydantic>=1.2.0,<2.0.0 +``` diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index e6250b2e0..820e633bf 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -70,6 +70,7 @@ nav: - async.md - Déploiement: - deployment/index.md + - deployment/versions.md - deployment/docker.md - project-generation.md - alternatives.md From 7a2e9c7aaa8f379c422a65e7f37feed07c3e90e2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:41:03 +0000 Subject: [PATCH 0580/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9e22df6ed..c601fec03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `deployment/versions.md`. PR [#3690](https://github.com/tiangolo/fastapi/pull/3690) by [@rjNemo](https://github.com/rjNemo). * ⬆ Bump nwtgck/actions-netlify from 1.2.3 to 1.2.4. PR [#5507](https://github.com/tiangolo/fastapi/pull/5507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump internal dependency types-ujson from 5.4.0 to 5.5.0. PR [#5537](https://github.com/tiangolo/fastapi/pull/5537) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.23.0 to 2.24.0. PR [#5508](https://github.com/tiangolo/fastapi/pull/5508) by [@dependabot[bot]](https://github.com/apps/dependabot). From 330e8112ac13d9f79a5facfc9cbe69fe22d1ffd9 Mon Sep 17 00:00:00 2001 From: Lucas Mendes <80999926+lbmendes@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:41:58 -0300 Subject: [PATCH 0581/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/path-params-numeric-val?= =?UTF-8?q?idations.md`=20(#4099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Pedro Victor <32584628+peidrao@users.noreply.github.com> Co-authored-by: Lucas <61513630+lsglucas@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../path-params-numeric-validations.md | 138 ++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 139 insertions(+) create mode 100644 docs/pt/docs/tutorial/path-params-numeric-validations.md diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..f478fd190 --- /dev/null +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,138 @@ +# Parâmetros da Rota e Validações Numéricas + +Do mesmo modo que você pode declarar mais validações e metadados para parâmetros de consulta com `Query`, você pode declarar os mesmos tipos de validações e metadados para parâmetros de rota com `Path`. + +## Importe `Path` + +Primeiro, importe `Path` de `fastapi`: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +## Declare metadados + +Você pode declarar todos os parâmetros da mesma maneira que na `Query`. + +Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota `item_id` você pode digitar: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +!!! note "Nota" + Um parâmetro de rota é sempre obrigatório, como se fizesse parte da rota. + + Então, você deve declará-lo com `...` para marcá-lo como obrigatório. + + Mesmo que você declare-o como `None` ou defina um valor padrão, isso não teria efeito algum, o parâmetro ainda seria obrigatório. + +## Ordene os parâmetros de acordo com sua necessidade + +Suponha que você queira declarar o parâmetro de consulta `q` como uma `str` obrigatória. + +E você não precisa declarar mais nada em relação a este parâmetro, então você não precisa necessariamente usar `Query`. + +Mas você ainda precisa usar `Path` para o parâmetro de rota `item_id`. + +O Python irá acusar se você colocar um elemento com um valor padrão definido antes de outro que não tenha um valor padrão. + +Mas você pode reordená-los, colocando primeiro o elemento sem o valor padrão (o parâmetro de consulta `q`). + +Isso não faz diferença para o **FastAPI**. Ele vai detectar os parâmetros pelos seus nomes, tipos e definições padrão (`Query`, `Path`, etc), sem se importar com a ordem. + +Então, você pode declarar sua função assim: + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## Ordene os parâmetros de a acordo com sua necessidade, truques + +Se você quiser declarar o parâmetro de consulta `q` sem um `Query` nem um valor padrão, e o parâmetro de rota `item_id` usando `Path`, e definí-los em uma ordem diferente, Python tem um pequeno truque na sintaxe para isso. + +Passe `*`, como o primeiro parâmetro da função. + +O Python não vai fazer nada com esse `*`, mas ele vai saber que a partir dali os parâmetros seguintes deverão ser chamados argumentos nomeados (pares chave-valor), também conhecidos como kwargs. Mesmo que eles não possuam um valor padrão. + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## Validações numéricas: maior que ou igual + +Com `Query` e `Path` (e outras que você verá mais tarde) você pode declarar restrições numéricas. + +Aqui, com `ge=1`, `item_id` precisará ser um número inteiro maior que ("`g`reater than") ou igual ("`e`qual") a 1. + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## Validações numéricas: maior que e menor que ou igual + +O mesmo se aplica para: + +* `gt`: maior que (`g`reater `t`han) +* `le`: menor que ou igual (`l`ess than or `e`qual) + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## Validações numéricas: valores do tipo float, maior que e menor que + +Validações numéricas também funcionam para valores do tipo `float`. + +Aqui é onde se torna importante a possibilidade de declarar gt e não apenas ge. Com isso você pode especificar, por exemplo, que um valor deve ser maior que `0`, ainda que seja menor que `1`. + +Assim, `0.5` seria um valor válido. Mas `0.0` ou `0` não seria. + +E o mesmo para lt. + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## Recapitulando + +Com `Query`, `Path` (e outras que você ainda não viu) você pode declarar metadados e validações de texto do mesmo modo que com [Parâmetros de consulta e validações de texto](query-params-str-validations.md){.internal-link target=_blank}. + +E você também pode declarar validações numéricas: + +* `gt`: maior que (`g`reater `t`han) +* `ge`: maior que ou igual (`g`reater than or `e`qual) +* `lt`: menor que (`l`ess `t`han) +* `le`: menor que ou igual (`l`ess than or `e`qual) + +!!! info "Informação" + `Query`, `Path` e outras classes que você verá a frente são subclasses de uma classe comum `Param`. + + Todas elas compartilham os mesmos parâmetros para validação adicional e metadados que você viu. + +!!! note "Detalhes Técnicos" + Quando você importa `Query`, `Path` e outras de `fastapi`, elas são na verdade funções. + + Que quando chamadas, retornam instâncias de classes de mesmo nome. + + Então, você importa `Query`, que é uma função. E quando você a chama, ela retorna uma instância de uma classe também chamada `Query`. + + Estas funções são assim (ao invés de apenas usar as classes diretamente) para que seu editor não acuse erros sobre seus tipos. + + Dessa maneira você pode user seu editor e ferramentas de desenvolvimento sem precisar adicionar configurações customizadas para ignorar estes erros. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 18c6bbc6d..fca4dbad1 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -69,6 +69,7 @@ nav: - tutorial/body-fields.md - tutorial/extra-data-types.md - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md - tutorial/cookie-params.md - tutorial/header-params.md - tutorial/response-status-code.md From 8b20eece4c21b341b018e37dfab545a69d8bcee1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:42:41 +0000 Subject: [PATCH 0582/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c601fec03..c5231e401 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-params-numeric-validations.md`. PR [#4099](https://github.com/tiangolo/fastapi/pull/4099) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add French translation for `deployment/versions.md`. PR [#3690](https://github.com/tiangolo/fastapi/pull/3690) by [@rjNemo](https://github.com/rjNemo). * ⬆ Bump nwtgck/actions-netlify from 1.2.3 to 1.2.4. PR [#5507](https://github.com/tiangolo/fastapi/pull/5507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump internal dependency types-ujson from 5.4.0 to 5.5.0. PR [#5537](https://github.com/tiangolo/fastapi/pull/5537) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2889b4a3df7fea8e6e2c0540608d2fa24b995846 Mon Sep 17 00:00:00 2001 From: Lucas Mendes <80999926+lbmendes@users.noreply.github.com> Date: Mon, 31 Oct 2022 14:42:57 -0300 Subject: [PATCH 0583/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/body-multiple-params.md?= =?UTF-8?q?`=20(#4111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lorhan Sohaky Co-authored-by: Wuerike <35462243+Wuerike@users.noreply.github.com> Co-authored-by: Lorhan Sohaky <16273730+LorhanSohaky@users.noreply.github.com> --- docs/pt/docs/tutorial/body-multiple-params.md | 213 ++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 214 insertions(+) create mode 100644 docs/pt/docs/tutorial/body-multiple-params.md diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..ac67aa47f --- /dev/null +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -0,0 +1,213 @@ +# Corpo - Múltiplos parâmetros + +Agora que nós vimos como usar `Path` e `Query`, veremos usos mais avançados de declarações no corpo da requisição. + +## Misture `Path`, `Query` e parâmetros de corpo + +Primeiro, é claro, você pode misturar `Path`, `Query` e declarações de parâmetro no corpo da requisição livremente e o **FastAPI** saberá o que fazer. + +E você também pode declarar parâmetros de corpo como opcionais, definindo o valor padrão com `None`: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +!!! nota + Repare que, neste caso, o `item` que seria capturado a partir do corpo é opcional. Visto que ele possui `None` como valor padrão. + +## Múltiplos parâmetros de corpo + +No exemplo anterior, as *operações de rota* esperariam um JSON no corpo contendo os atributos de um `Item`, exemplo: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Mas você pode também declarar múltiplos parâmetros de corpo, por exemplo, `item` e `user`: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +Neste caso, o **FastAPI** perceberá que existe mais de um parâmetro de corpo na função (dois parâmetros que são modelos Pydantic). + +Então, ele usará o nome dos parâmetros como chaves (nome dos campos) no corpo, e espera um corpo como: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! nota + Repare que mesmo que o `item` esteja declarado da mesma maneira que antes, agora é esperado que ele esteja dentro do corpo com uma chave `item`. + + +O **FastAPI** fará a conversão automática a partir da requisição, assim esse parâmetro `item` receberá seu respectivo conteúdo e o mesmo ocorrerá com `user`. + +Ele executará a validação dos dados compostos e irá documentá-los de maneira compatível com o esquema OpenAPI e documentação automática. + +## Valores singulares no corpo + +Assim como existem uma `Query` e uma `Path` para definir dados adicionais para parâmetros de consulta e de rota, o **FastAPI** provê o equivalente para `Body`. + +Por exemplo, extendendo o modelo anterior, você poder decidir por ter uma outra chave `importance` no mesmo corpo, além de `item` e `user`. + +Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumirá que se trata de um parâmetro de consulta. + +Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +Neste caso, o **FastAPI** esperará um corpo como: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +Mais uma vez, ele converterá os tipos de dados, validar, documentar, etc. + +## Múltiplos parâmetros de corpo e consulta + +Obviamente, você também pode declarar parâmetros de consulta assim que você precisar, de modo adicional a quaisquer parâmetros de corpo. + +Dado que, por padrão, valores singulares são interpretados como parâmetros de consulta, você não precisa explicitamente adicionar uma `Query`, você pode somente: + +```Python +q: Union[str, None] = None +``` + +Ou como em Python 3.10 e versões superiores: + +```Python +q: str | None = None +``` + +Por exemplo: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +!!! info "Informação" + `Body` também possui todas as validações adicionais e metadados de parâmetros como em `Query`,`Path` e outras que você verá depois. + +## Declare um único parâmetro de corpo indicando sua chave + +Suponha que você tem um único parâmetro de corpo `item`, a partir de um modelo Pydantic `Item`. + +Por padrão, o **FastAPI** esperará que seu conteúdo venha no corpo diretamente. + +Mas se você quiser que ele espere por um JSON com uma chave `item` e dentro dele os conteúdos do modelo, como ocorre ao declarar vários parâmetros de corpo, você pode usar o parâmetro especial de `Body` chamado `embed`: + +```Python +item: Item = Body(embed=True) +``` + +como em: + +=== "Python 3.6 e superiores" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "Python 3.10 e superiores" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +Neste caso o **FastAPI** esperará um corpo como: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +ao invés de: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Recapitulando + +Você pode adicionar múltiplos parâmetros de corpo para sua *função de operação de rota*, mesmo que a requisição possa ter somente um único corpo. + +E o **FastAPI** vai manipulá-los, mandar para você os dados corretos na sua função, e validar e documentar o schema correto na *operação de rota*. + +Você também pode declarar valores singulares para serem recebidos como parte do corpo. + +E você pode instruir o **FastAPI** para requisitar no corpo a indicação de chave mesmo quando existe somente um único parâmetro declarado. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index fca4dbad1..5b1e2f6a4 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -66,6 +66,7 @@ nav: - tutorial/path-params.md - tutorial/query-params.md - tutorial/body.md + - tutorial/body-multiple-params.md - tutorial/body-fields.md - tutorial/extra-data-types.md - tutorial/query-params-str-validations.md From a26a3c1cbf4bda582fd2da34dbbcbddf8e2a8b99 Mon Sep 17 00:00:00 2001 From: ASpathfinder <31813636+ASpathfinder@users.noreply.github.com> Date: Tue, 1 Nov 2022 01:43:39 +0800 Subject: [PATCH 0584/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/wsgi.md`=20(#4505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeodre Co-authored-by: KellyAlsa-massive-win Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/zh/docs/advanced/wsgi.md | 37 +++++++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 38 insertions(+) create mode 100644 docs/zh/docs/advanced/wsgi.md diff --git a/docs/zh/docs/advanced/wsgi.md b/docs/zh/docs/advanced/wsgi.md new file mode 100644 index 000000000..ad71280fc --- /dev/null +++ b/docs/zh/docs/advanced/wsgi.md @@ -0,0 +1,37 @@ +# 包含 WSGI - Flask,Django,其它 + +您可以挂载多个 WSGI 应用,正如您在 [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}, [Behind a Proxy](./behind-a-proxy.md){.internal-link target=_blank} 中所看到的那样。 + +为此, 您可以使用 `WSGIMiddleware` 来包装你的 WSGI 应用,如:Flask,Django,等等。 + +## 使用 `WSGIMiddleware` + +您需要导入 `WSGIMiddleware`。 + +然后使用该中间件包装 WSGI 应用(例如 Flask)。 + +之后将其挂载到某一个路径下。 + +```Python hl_lines="2-3 22" +{!../../../docs_src/wsgi/tutorial001.py!} +``` + +## 检查 + +现在,所有定义在 `/v1/` 路径下的请求将会被 Flask 应用处理。 + +其余的请求则会被 **FastAPI** 处理。 + +如果您使用 Uvicorn 运行应用实例并且访问 http://localhost:8000/v1/,您将会看到由 Flask 返回的响应: + +```txt +Hello, World from Flask! +``` + +并且如果您访问 http://localhost:8000/v2,您将会看到由 FastAPI 返回的响应: + +```JSON +{ + "message": "Hello World" +} +``` diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index ac8679dc0..65c999e8b 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -109,6 +109,7 @@ nav: - advanced/response-directly.md - advanced/custom-response.md - advanced/response-cookies.md + - advanced/wsgi.md - contributing.md - help-fastapi.md - benchmarks.md From e0945eb1c81f1c0755b1f86983e7a707fcf9bf12 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:44:40 +0000 Subject: [PATCH 0585/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c5231e401..cdba9043b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-multiple-params.md`. PR [#4111](https://github.com/tiangolo/fastapi/pull/4111) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-params-numeric-validations.md`. PR [#4099](https://github.com/tiangolo/fastapi/pull/4099) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add French translation for `deployment/versions.md`. PR [#3690](https://github.com/tiangolo/fastapi/pull/3690) by [@rjNemo](https://github.com/rjNemo). * ⬆ Bump nwtgck/actions-netlify from 1.2.3 to 1.2.4. PR [#5507](https://github.com/tiangolo/fastapi/pull/5507) by [@dependabot[bot]](https://github.com/apps/dependabot). From 38493f8ae1a36e036aa593b40950d71ed40b8f1e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:44:58 +0000 Subject: [PATCH 0586/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cdba9043b..f1d6fa7ee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/wsgi.md`. PR [#4505](https://github.com/tiangolo/fastapi/pull/4505) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-multiple-params.md`. PR [#4111](https://github.com/tiangolo/fastapi/pull/4111) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-params-numeric-validations.md`. PR [#4099](https://github.com/tiangolo/fastapi/pull/4099) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add French translation for `deployment/versions.md`. PR [#3690](https://github.com/tiangolo/fastapi/pull/3690) by [@rjNemo](https://github.com/rjNemo). From 7ff62468a0fc896675d87da12ff42d3bb4aafa74 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Mon, 31 Oct 2022 18:45:30 +0100 Subject: [PATCH 0587/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`deployment/https.md`=20(#3691)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sam Courtemanche Co-authored-by: Ruidy Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/fr/docs/deployment/https.md | 53 ++++++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 54 insertions(+) create mode 100644 docs/fr/docs/deployment/https.md diff --git a/docs/fr/docs/deployment/https.md b/docs/fr/docs/deployment/https.md new file mode 100644 index 000000000..ccf1f847a --- /dev/null +++ b/docs/fr/docs/deployment/https.md @@ -0,0 +1,53 @@ +# À propos de HTTPS + +Il est facile de penser que HTTPS peut simplement être "activé" ou non. + +Mais c'est beaucoup plus complexe que cela. + +!!! tip + Si vous êtes pressé ou si cela ne vous intéresse pas, passez aux sections suivantes pour obtenir des instructions étape par étape afin de tout configurer avec différentes techniques. + +Pour apprendre les bases du HTTPS, du point de vue d'un utilisateur, consultez https://howhttps.works/. + +Maintenant, du point de vue d'un développeur, voici plusieurs choses à avoir en tête en pensant au HTTPS : + +* Pour le HTTPS, le serveur a besoin de "certificats" générés par une tierce partie. + * Ces certificats sont en fait acquis auprès de la tierce partie, et non "générés". +* Les certificats ont une durée de vie. + * Ils expirent. + * Puis ils doivent être renouvelés et acquis à nouveau auprès de la tierce partie. +* Le cryptage de la connexion se fait au niveau du protocole TCP. + * C'est une couche en dessous de HTTP. + * Donc, le certificat et le traitement du cryptage sont faits avant HTTP. +* TCP ne connaît pas les "domaines", seulement les adresses IP. + * L'information sur le domaine spécifique demandé se trouve dans les données HTTP. +* Les certificats HTTPS "certifient" un certain domaine, mais le protocole et le cryptage se font au niveau TCP, avant de savoir quel domaine est traité. +* Par défaut, cela signifie que vous ne pouvez avoir qu'un seul certificat HTTPS par adresse IP. + * Quelle que soit la taille de votre serveur ou la taille de chacune des applications qu'il contient. + * Il existe cependant une solution à ce problème. +* Il existe une extension du protocole TLS (celui qui gère le cryptage au niveau TCP, avant HTTP) appelée SNI (indication du nom du serveur). + * Cette extension SNI permet à un seul serveur (avec une seule adresse IP) d'avoir plusieurs certificats HTTPS et de servir plusieurs domaines/applications HTTPS. + * Pour que cela fonctionne, un seul composant (programme) fonctionnant sur le serveur, écoutant sur l'adresse IP publique, doit avoir tous les certificats HTTPS du serveur. +* Après avoir obtenu une connexion sécurisée, le protocole de communication est toujours HTTP. + * Le contenu est crypté, même s'il est envoyé avec le protocole HTTP. + +Il est courant d'avoir un seul programme/serveur HTTP fonctionnant sur le serveur (la machine, l'hôte, etc.) et +gérant toutes les parties HTTPS : envoyer les requêtes HTTP décryptées à l'application HTTP réelle fonctionnant sur +le même serveur (dans ce cas, l'application **FastAPI**), prendre la réponse HTTP de l'application, la crypter en utilisant le certificat approprié et la renvoyer au client en utilisant HTTPS. Ce serveur est souvent appelé un Proxy de terminaison TLS. + +## Let's Encrypt + +Avant Let's Encrypt, ces certificats HTTPS étaient vendus par des tiers de confiance. + +Le processus d'acquisition d'un de ces certificats était auparavant lourd, nécessitait pas mal de paperasses et les certificats étaient assez chers. + +Mais ensuite, Let's Encrypt a été créé. + +Il s'agit d'un projet de la Fondation Linux. Il fournit des certificats HTTPS gratuitement. De manière automatisée. Ces certificats utilisent toutes les sécurités cryptographiques standard et ont une durée de vie courte (environ 3 mois), de sorte que la sécurité est en fait meilleure en raison de leur durée de vie réduite. + +Les domaines sont vérifiés de manière sécurisée et les certificats sont générés automatiquement. Cela permet également d'automatiser le renouvellement de ces certificats. + +L'idée est d'automatiser l'acquisition et le renouvellement de ces certificats, afin que vous puissiez disposer d'un HTTPS sécurisé, gratuitement et pour toujours. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 820e633bf..b980acceb 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Déploiement: - deployment/index.md - deployment/versions.md + - deployment/https.md - deployment/docker.md - project-generation.md - alternatives.md From 9974c4b6ac4949198048fd8a432996d0c1760740 Mon Sep 17 00:00:00 2001 From: Zssaer <45691504+Zssaer@users.noreply.github.com> Date: Tue, 1 Nov 2022 01:56:49 +0800 Subject: [PATCH 0588/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/sql-databases.md`=20(#4999?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: mkdir700 <56359329+mkdir700@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/zh/docs/tutorial/sql-databases.md | 770 +++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 771 insertions(+) create mode 100644 docs/zh/docs/tutorial/sql-databases.md diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md new file mode 100644 index 000000000..6b354c2b6 --- /dev/null +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -0,0 +1,770 @@ +# SQL (关系型) 数据库 + +**FastAPI**不需要你使用SQL(关系型)数据库。 + +但是您可以使用任何您想要的关系型数据库。 + +在这里,让我们看一个使用着[SQLAlchemy](https://www.sqlalchemy.org/)的示例。 + +您可以很容易地将SQLAlchemy支持任何数据库,像: + +* PostgreSQL +* MySQL +* SQLite +* Oracle +* Microsoft SQL Server,等等其它数据库 + +在此示例中,我们将使用**SQLite**,因为它使用单个文件并且 在Python中具有集成支持。因此,您可以复制此示例并按原样来运行它。 + +稍后,对于您的产品级别的应用程序,您可能会要使用像**PostgreSQL**这样的数据库服务器。 + +!!! tip + 这儿有一个**FastAPI**和**PostgreSQL**的官方项目生成器,全部基于**Docker**,包括前端和更多工具:https://github.com/tiangolo/full-stack-fastapi-postgresql + +!!! note + 请注意,大部分代码是`SQLAlchemy`的标准代码,您可以用于任何框架。FastAPI特定的代码和往常一样少。 + +## ORMs(对象关系映射) + +**FastAPI**可与任何数据库在任何样式的库中一起与 数据库进行通信。 + +一种常见的模式是使用“ORM”:对象关系映射。 + +ORM 具有在代码和数据库表(“*关系型”)中的**对象**之间转换(“*映射*”)的工具。 + +使用 ORM,您通常会在 SQL 数据库中创建一个代表映射的类,该类的每个属性代表一个列,具有名称和类型。 + +例如,一个类`Pet`可以表示一个 SQL 表`pets`。 + +该类的每个*实例对象都代表数据库中的一行数据。* + +又例如,一个对象`orion_cat`(`Pet`的一个实例)可以有一个属性`orion_cat.type`, 对标数据库中的`type`列。并且该属性的值可以是其它,例如`"cat"`。 + +这些 ORM 还具有在表或实体之间建立关系的工具(比如创建多表关系)。 + +这样,您还可以拥有一个属性`orion_cat.owner`,它包含该宠物所有者的数据,这些数据取自另外一个表。 + +因此,`orion_cat.owner.name`可能是该宠物主人的姓名(来自表`owners`中的列`name`)。 + +它可能有一个像`"Arquilian"`(一种业务逻辑)。 + +当您尝试从您的宠物对象访问它时,ORM 将完成所有工作以从相应的表*所有者那里再获取信息。* + +常见的 ORM 例如:Django-ORM(Django 框架的一部分)、SQLAlchemy ORM(SQLAlchemy 的一部分,独立于框架)和 Peewee(独立于框架)等。 + +在这里,我们将看到如何使用**SQLAlchemy ORM**。 + +以类似的方式,您也可以使用任何其他 ORM。 + +!!! tip + 在文档中也有一篇使用 Peewee 的等效的文章。 + +## 文件结构 + +对于这些示例,假设您有一个名为的目录`my_super_project`,其中包含一个名为的子目录`sql_app`,其结构如下: + +``` +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + └── schemas.py +``` + +该文件`__init__.py`只是一个空文件,但它告诉 Python 其中`sql_app`的所有模块(Python 文件)都是一个包。 + +现在让我们看看每个文件/模块的作用。 + +## 创建 SQLAlchemy 部件 + +让我们涉及到文件`sql_app/database.py`。 + +### 导入 SQLAlchemy 部件 + +```Python hl_lines="1-3" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### 为 SQLAlchemy 定义数据库 URL地址 + +```Python hl_lines="5-6" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +在这个例子中,我们正在“连接”到一个 SQLite 数据库(用 SQLite 数据库打开一个文件)。 + +该文件将位于文件中的同一目录中`sql_app.db`。 + +这就是为什么最后一部分是`./sql_app.db`. + +如果您使用的是**PostgreSQL**数据库,则只需取消注释该行: + +```Python +SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" +``` + +...并根据您的数据库数据和相关凭据(也适用于 MySQL、MariaDB 或任何其他)对其进行调整。 + +!!! tip + + 如果您想使用不同的数据库,这是就是您必须修改的地方。 + +### 创建 SQLAlchemy 引擎 + +第一步,创建一个 SQLAlchemy的“引擎”。 + +我们稍后会将这个`engine`在其他地方使用。 + +```Python hl_lines="8-10" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +#### 注意 + +参数: + +```Python +connect_args={"check_same_thread": False} +``` + +...仅用于`SQLite`,在其他数据库不需要它。 + +!!! info "技术细节" + + 默认情况下,SQLite 只允许一个线程与其通信,假设有多个线程的话,也只将处理一个独立的请求。 + + 这是为了防止意外地为不同的事物(不同的请求)共享相同的连接。 + + 但是在 FastAPI 中,普遍使用def函数,多个线程可以为同一个请求与数据库交互,所以我们需要使用`connect_args={"check_same_thread": False}`来让SQLite允许这样。 + + 此外,我们将确保每个请求都在依赖项中获得自己的数据库连接会话,因此不需要该默认机制。 + +### 创建一个`SessionLocal`类 + +每个实例`SessionLocal`都会是一个数据库会话。当然该类本身还不是数据库会话。 + +但是一旦我们创建了一个`SessionLocal`类的实例,这个实例将是实际的数据库会话。 + +我们命名它是`SessionLocal`为了将它与我们从 SQLAlchemy 导入的`Session`区别开来。 + +稍后我们将使用`Session`(从 SQLAlchemy 导入的那个)。 + +要创建`SessionLocal`类,请使用函数`sessionmaker`: + +```Python hl_lines="11" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### 创建一个`Base`类 + +现在我们将使用`declarative_base()`返回一个类。 + +稍后我们将用这个类继承,来创建每个数据库模型或类(ORM 模型): + +```Python hl_lines="13" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +## 创建数据库模型 + +现在让我们看看文件`sql_app/models.py`。 + +### 用`Base`类来创建 SQLAlchemy 模型 + +我们将使用我们之前创建的`Base`类来创建 SQLAlchemy 模型。 + +!!! tip + SQLAlchemy 使用的“**模型**”这个术语 来指代与数据库交互的这些类和实例。 + + 而 Pydantic 也使用“模型”这个术语 来指代不同的东西,即数据验证、转换以及文档类和实例。 + +从`database`(来自上面的`database.py`文件)导入`Base`。 + +创建从它继承的类。 + +这些类就是 SQLAlchemy 模型。 + +```Python hl_lines="4 7-8 18-19" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +这个`__tablename__`属性是用来告诉 SQLAlchemy 要在数据库中为每个模型使用的数据库表的名称。 + +### 创建模型属性/列 + +现在创建所有模型(类)属性。 + +这些属性中的每一个都代表其相应数据库表中的一列。 + +我们使用`Column`来表示 SQLAlchemy 中的默认值。 + +我们传递一个 SQLAlchemy “类型”,如`Integer`、`String`和`Boolean`,它定义了数据库中的类型,作为参数。 + +```Python hl_lines="1 10-13 21-24" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +### 创建关系 + +现在创建关系。 + +为此,我们使用SQLAlchemy ORM提供的`relationship`。 + +这将或多或少会成为一种“神奇”属性,其中表示该表与其他相关的表中的值。 + +```Python hl_lines="2 15 26" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +当访问 user 中的属性`items`时,如 中`my_user.items`,它将有一个`Item`SQLAlchemy 模型列表(来自`items`表),这些模型具有指向`users`表中此记录的外键。 + +当您访问`my_user.items`时,SQLAlchemy 实际上会从`items`表中的获取一批记录并在此处填充进去。 + +同样,当访问 Item中的属性`owner`时,它将包含表中的`User`SQLAlchemy 模型`users`。使用`owner_id`属性/列及其外键来了解要从`users`表中获取哪条记录。 + +## 创建 Pydantic 模型 + +现在让我们查看一下文件`sql_app/schemas.py`。 + +!!! tip + 为了避免 SQLAlchemy*模型*和 Pydantic*模型*之间的混淆,我们将有`models.py`(SQLAlchemy 模型的文件)和`schemas.py`( Pydantic 模型的文件)。 + + 这些 Pydantic 模型或多或少地定义了一个“schema”(一个有效的数据形状)。 + + 因此,这将帮助我们在使用两者时避免混淆。 + +### 创建初始 Pydantic*模型*/模式 + +创建一个`ItemBase`和`UserBase`Pydantic*模型*(或者我们说“schema”)以及在创建或读取数据时具有共同的属性。 + +`ItemCreate`为 创建一个`UserCreate`继承自它们的所有属性(因此它们将具有相同的属性),以及创建所需的任何其他数据(属性)。 + +因此在创建时也应当有一个`password`属性。 + +但是为了安全起见,`password`不会出现在其他同类 Pydantic*模型*中,例如用户请求时不应该从 API 返回响应中包含它。 + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 及以上版本" + + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +#### SQLAlchemy 风格和 Pydantic 风格 + +请注意,SQLAlchemy*模型*使用 `=`来定义属性,并将类型作为参数传递给`Column`,例如: + +```Python +name = Column(String) +``` + +虽然 Pydantic*模型*使用`:` 声明类型,但新的类型注释语法/类型提示是: + +```Python +name: str +``` + +请牢记这一点,这样您在使用`:`还是`=`时就不会感到困惑。 + +### 创建用于读取/返回的Pydantic*模型/模式* + +现在创建当从 API 返回数据时、将在读取数据时使用的Pydantic*模型(schemas)。* + +例如,在创建一个项目之前,我们不知道分配给它的 ID 是什么,但是在读取它时(从 API 返回时)我们已经知道它的 ID。 + +同样,当读取用户时,我们现在可以声明`items`,将包含属于该用户的项目。 + +不仅是这些项目的 ID,还有我们在 Pydantic*模型*中定义的用于读取项目的所有数据:`Item`. + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 及以上版本" + + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 请注意,读取用户(从 API 返回)时将使用不包括`password`的`User` Pydantic*模型*。 + +### 使用 Pydantic 的`orm_mode` + +现在,在用于查询的 Pydantic*模型*`Item`中`User`,添加一个内部`Config`类。 + +此类[`Config`](https://pydantic-docs.helpmanual.io/usage/model_config/)用于为 Pydantic 提供配置。 + +在`Config`类中,设置属性`orm_mode = True`。 + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 及以上版本" + + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 请注意,它使用`=`分配一个值,例如: + + `orm_mode = True` + + 它不使用之前的`:`来类型声明。 + + 这是设置配置值,而不是声明类型。 + +Pydantic`orm_mode`将告诉 Pydantic*模型*读取数据,即它不是一个`dict`,而是一个 ORM 模型(或任何其他具有属性的任意对象)。 + +这样,而不是仅仅试图从`dict`上 `id` 中获取值,如下所示: + +```Python +id = data["id"] +``` + +尝试从属性中获取它,如: + +```Python +id = data.id +``` + +有了这个,Pydantic*模型*与 ORM 兼容,您只需在*路径操作*`response_model`的参数中声明它即可。 + +您将能够返回一个数据库模型,它将从中读取数据。 + +#### ORM 模式的技术细节 + +SQLAlchemy 和许多其他默认情况下是“延迟加载”。 + +这意味着,例如,除非您尝试访问包含该数据的属性,否则它们不会从数据库中获取关系数据。 + +例如,访问属性`items`: + +```Python +current_user.items +``` + +将使 SQLAlchemy 转到`items`表并获取该用户的项目,在调用`.items`之前不会去查询数据库。 + +没有`orm_mode`,如果您从*路径操作*返回一个 SQLAlchemy 模型,它不会包含关系数据。 + +即使您在 Pydantic 模型中声明了这些关系,也没有用处。 + +但是在 ORM 模式下,由于 Pydantic 本身会尝试从属性访问它需要的数据(而不是假设为 `dict`),你可以声明你想要返回的特定数据,它甚至可以从 ORM 中获取它。 + +## CRUD工具 + +现在让我们看看文件`sql_app/crud.py`。 + +在这个文件中,我们将编写可重用的函数用来与数据库中的数据进行交互。 + +**CRUD**分别为:**增加**、**查询**、**更改**和**删除**,即增删改查。 + +...虽然在这个例子中我们只是新增和查询。 + +### 读取数据 + +从 `sqlalchemy.orm`中导入`Session`,这将允许您声明`db`参数的类型,并在您的函数中进行更好的类型检查和完成。 + +导入之前的`models`(SQLAlchemy 模型)和`schemas`(Pydantic*模型*/模式)。 + +创建一些实用函数来完成: + +* 通过 ID 和电子邮件查询单个用户。 +* 查询多个用户。 +* 查询多个项目。 + +```Python hl_lines="1 3 6-7 10-11 14-15 27-28" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + 通过创建仅专用于与数据库交互(获取用户或项目)的函数,独立于*路径操作函数*,您可以更轻松地在多个部分中重用它们,并为它们添加单元测试。 + +### 创建数据 + +现在创建实用程序函数来创建数据。 + +它的步骤是: + +* 使用您的数据创建一个 SQLAlchemy 模型*实例。* +* 使用`add`来将该实例对象添加到您的数据库。 +* 使用`commit`来对数据库的事务提交(以便保存它们)。 +* 使用`refresh`来刷新您的数据库实例(以便它包含来自数据库的任何新数据,例如生成的 ID)。 + +```Python hl_lines="18-24 31-36" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + SQLAlchemy 模型`User`包含一个`hashed_password`,它应该是一个包含散列的安全密码。 + + 但由于 API 客户端提供的是原始密码,因此您需要将其提取并在应用程序中生成散列密码。 + + 然后将hashed_password参数与要保存的值一起传递。 + +!!! warning + 此示例不安全,密码未经过哈希处理。 + + 在现实生活中的应用程序中,您需要对密码进行哈希处理,并且永远不要以明文形式保存它们。 + + 有关更多详细信息,请返回教程中的安全部分。 + + 在这里,我们只关注数据库的工具和机制。 + +!!! tip + 这里不是将每个关键字参数传递给Item并从Pydantic模型中读取每个参数,而是先生成一个字典,其中包含Pydantic模型的数据: + + `item.dict()` + + 然后我们将dict的键值对 作为关键字参数传递给 SQLAlchemy `Item`: + + `Item(**item.dict())` + + 然后我们传递 Pydantic模型未提供的额外关键字参数`owner_id`: + + `Item(**item.dict(), owner_id=user_id)` + +## 主**FastAPI**应用程序 + +现在在`sql_app/main.py`文件中 让我们集成和使用我们之前创建的所有其他部分。 + +### 创建数据库表 + +以非常简单的方式创建数据库表: + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +#### Alembic 注意 + +通常你可能会使用 Alembic,来进行格式化数据库(创建表等)。 + +而且您还可以将 Alembic 用于“迁移”(这是它的主要工作)。 + +“迁移”是每当您更改 SQLAlchemy 模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需的一组步骤。 + +您可以在[Project Generation - Template](https://fastapi.tiangolo.com/zh/project-generation/)的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在[`alembic`代码目录中](https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/alembic/)。 + +### 创建依赖项 + +现在使用我们在`sql_app/database.py`文件中创建的`SessionLocal`来创建依赖项。 + +我们需要每个请求有一个独立的数据库会话/连接(`SessionLocal`),在所有请求中使用相同的会话,然后在请求完成后关闭它。 + +然后将为下一个请求创建一个新会话。 + +为此,我们将创建一个新的依赖项`yield`,正如前面关于[Dependencies with`yield`](https://fastapi.tiangolo.com/zh/tutorial/dependencies/dependencies-with-yield/)的部分中所解释的那样。 + +我们的依赖项将创建一个新的 SQLAlchemy `SessionLocal`,它将在单个请求中使用,然后在请求完成后关闭它。 + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info + 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 + + 然后我们在finally块中关闭它。 + + 通过这种方式,我们确保数据库会话在请求后始终关闭。即使在处理请求时出现异常。 + + 但是您不能从退出代码中引发另一个异常(在yield之后)。可以查阅 [Dependencies with yield and HTTPException](https://fastapi.tiangolo.com/zh/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-httpexception) + +*然后,当在路径操作函数*中使用依赖项时,我们使用`Session`,直接从 SQLAlchemy 导入的类型声明它。 + +*这将为我们在路径操作函数*中提供更好的编辑器支持,因为编辑器将知道`db`参数的类型`Session`: + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info "技术细节" + 参数`db`实际上是 type `SessionLocal`,但是这个类(用 创建`sessionmaker()`)是 SQLAlchemy 的“代理” `Session`,所以,编辑器并不真正知道提供了哪些方法。 + + 但是通过将类型声明为Session,编辑器现在可以知道可用的方法(.add()、.query()、.commit()等)并且可以提供更好的支持(比如完成)。类型声明不影响实际对象。 + +### 创建您的**FastAPI** *路径操作* + +现在,到了最后,编写标准的**FastAPI** *路径操作*代码。 + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +我们在依赖项中的每个请求之前利用`yield`创建数据库会话,然后关闭它。 + +所以我们就可以在*路径操作函数*中创建需要的依赖,就能直接获取会话。 + +这样,我们就可以直接从*路径操作函数*内部调用`crud.get_user`并使用该会话,来进行对数据库操作。 + +!!! tip + 请注意,您返回的值是 SQLAlchemy 模型或 SQLAlchemy 模型列表。 + + 但是由于所有路径操作的response_model都使用 Pydantic模型/使用orm_mode模式,因此您的 Pydantic 模型中声明的数据将从它们中提取并返回给客户端,并进行所有正常的过滤和验证。 + +!!! tip + 另请注意,`response_models`应当是标准 Python 类型,例如`List[schemas.Item]`. + + 但是由于它的内容/参数List是一个 使用orm_mode模式的Pydantic模型,所以数据将被正常检索并返回给客户端,所以没有问题。 + +### 关于 `def` 对比 `async def` + +*在这里,我们在路径操作函数*和依赖项中都使用着 SQLAlchemy 模型,它将与外部数据库进行通信。 + +这会需要一些“等待时间”。 + +但是由于 SQLAlchemy 不具有`await`直接使用的兼容性,因此类似于: + +```Python +user = await db.query(User).first() +``` + +...相反,我们可以使用: + +```Python +user = db.query(User).first() +``` + +然后我们应该声明*路径操作函数*和不带 的依赖关系`async def`,只需使用普通的`def`,如下: + +```Python hl_lines="2" +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + ... +``` + +!!! info + 如果您需要异步连接到关系数据库,请参阅[Async SQL (Relational) Databases](https://fastapi.tiangolo.com/zh/advanced/async-sql-databases/) + +!!! note "Very Technical Details" + 如果您很好奇并且拥有深厚的技术知识,您可以在[Async](https://fastapi.tiangolo.com/zh/async/#very-technical-details)文档中查看有关如何处理 `async def`于`def`差别的技术细节。 + +## 迁移 + +因为我们直接使用 SQLAlchemy,并且我们不需要任何类型的插件来使用**FastAPI**,所以我们可以直接将数据库迁移至[Alembic](https://alembic.sqlalchemy.org/)进行集成。 + +由于与 SQLAlchemy 和 SQLAlchemy 模型相关的代码位于单独的独立文件中,您甚至可以使用 Alembic 执行迁移,而无需安装 FastAPI、Pydantic 或其他任何东西。 + +同样,您将能够在与**FastAPI**无关的代码的其他部分中使用相同的 SQLAlchemy 模型和实用程序。 + +例如,在具有[Celery](https://docs.celeryq.dev/)、[RQ](https://python-rq.org/)或[ARQ](https://arq-docs.helpmanual.io/)的后台任务工作者中。 + +## 审查所有文件 + +最后回顾整个案例,您应该有一个名为的目录`my_super_project`,其中包含一个名为`sql_app`。 + +`sql_app`中应该有以下文件: + +* `sql_app/__init__.py`:这是一个空文件。 + +* `sql_app/database.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +* `sql_app/models.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +* `sql_app/schemas.py`: + +=== "Python 3.6 及以上版本" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "Python 3.10 及以上版本" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +* `sql_app/crud.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +* `sql_app/main.py`: + +=== "Python 3.6 及以上版本" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +## 执行项目 + +您可以复制这些代码并按原样使用它。 + +!!! info + + 事实上,这里的代码只是大多数测试代码的一部分。 + +你可以用 Uvicorn 运行它: + + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +打开浏览器进入 http://127.0.0.1:8000/docs。 + +您将能够与您的**FastAPI**应用程序交互,从真实数据库中读取数据: + + + +## 直接与数据库交互 + +如果您想独立于 FastAPI 直接浏览 SQLite 数据库(文件)以调试其内容、添加表、列、记录、修改数据等,您可以使用[SQLite 的 DB Browser](https://sqlitebrowser.org/) + +它看起来像这样: + + + +您还可以使用[SQLite Viewer](https://inloop.github.io/sqlite-viewer/)或[ExtendsClass](https://extendsclass.com/sqlite-browser.html)等在线 SQLite 浏览器。 + +## 中间件替代数据库会话 + +如果你不能使用依赖项`yield`——例如,如果你没有使用**Python 3.7**并且不能安装上面提到的**Python 3.6**的“backports” ——你可以在类似的“中间件”中设置会话方法。 + +“中间件”基本功能是一个为每个请求执行的函数在请求之前进行执行相应的代码,以及在请求执行之后执行相应的代码。 + +### 创建中间件 + +我们将添加中间件(只是一个函数)将为每个请求创建一个新的 SQLAlchemy`SessionLocal`,将其添加到请求中,然后在请求完成后关闭它。 + +=== "Python 3.6 及以上版本" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + +=== "Python 3.9 及以上版本" + + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ``` + +!!! info + 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 + + 然后我们在finally块中关闭它。 + + 通过这种方式,我们确保数据库会话在请求后始终关闭,即使在处理请求时出现异常也会关闭。 + +### 关于`request.state` + +`request.state`是每个`Request`对象的属性。它用于存储附加到请求本身的任意对象,例如本例中的数据库会话。您可以在[Starlette 的关于`Request`state](https://www.starlette.io/requests/#other-state)的文档中了解更多信息。 + +对于这种情况下,它帮助我们确保在所有请求中使用单个数据库会话,然后关闭(在中间件中)。 + +### 使用`yield`依赖项与使用中间件的区别 + +在此处添加**中间件**与`yield`的依赖项的作用效果类似,但也有一些区别: + +* 中间件需要更多的代码并且更复杂一些。 +* 中间件必须是一个`async`函数。 + * 如果其中有代码必须“等待”网络,它可能会在那里“阻止”您的应用程序并稍微降低性能。 + * 尽管这里的`SQLAlchemy`工作方式可能不是很成问题。 + * 但是,如果您向等待大量I/O的中间件添加更多代码,则可能会出现问题。 +* *每个*请求都会运行一个中间件。 + * 将为每个请求创建一个连接。 + * 即使处理该请求的*路径操作*不需要数据库。 + +!!! tip + `tyield`当依赖项 足以满足用例时,使用`tyield`依赖项方法会更好。 + +!!! info + `yield`的依赖项是最近刚加入**FastAPI**中的。 + + 所以本教程的先前版本只有带有中间件的示例,并且可能有多个应用程序使用中间件进行数据库会话管理。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 65c999e8b..10addd8e5 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -99,6 +99,7 @@ nav: - tutorial/security/simple-oauth2.md - tutorial/security/oauth2-jwt.md - tutorial/cors.md + - tutorial/sql-databases.md - tutorial/bigger-applications.md - tutorial/metadata.md - tutorial/debugging.md From 53127efde5ec95d5343205d2e6ae7f4819510688 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:57:25 +0000 Subject: [PATCH 0589/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f1d6fa7ee..ddc9c89c6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/sql-databases.md`. PR [#4999](https://github.com/tiangolo/fastapi/pull/4999) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/wsgi.md`. PR [#4505](https://github.com/tiangolo/fastapi/pull/4505) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-multiple-params.md`. PR [#4111](https://github.com/tiangolo/fastapi/pull/4111) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-params-numeric-validations.md`. PR [#4099](https://github.com/tiangolo/fastapi/pull/4099) by [@lbmendes](https://github.com/lbmendes). From db4452d47e760a6574b4bb3041ca53a0bfaedaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Tue, 1 Nov 2022 01:57:38 +0800 Subject: [PATCH 0590/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/zh/docs/tutorial/query-params-str-valida?= =?UTF-8?q?tions.md`=20(#5255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../tutorial/query-params-str-validations.md | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 0b2b9446a..070074839 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -104,7 +104,7 @@ q: str 代替: ```Python -q: str = None +q: Union[str, None] = None ``` 但是现在我们正在用 `Query` 声明它,例如: @@ -113,17 +113,51 @@ q: str = None q: Union[str, None] = Query(default=None, min_length=3) ``` -因此,当你在使用 `Query` 且需要声明一个值是必需的时,可以将 `...` 用作第一个参数值: +因此,当你在使用 `Query` 且需要声明一个值是必需的时,只需不声明默认参数: ```Python hl_lines="7" {!../../../docs_src/query_params_str_validations/tutorial006.py!} ``` +### 使用省略号(`...`)声明必需参数 + +有另一种方法可以显式的声明一个值是必需的,即将默认参数的默认值设为 `...` : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006b.py!} +``` + !!! info 如果你之前没见过 `...` 这种用法:它是一个特殊的单独值,它是 Python 的一部分并且被称为「省略号」。 + Pydantic 和 FastAPI 使用它来显式的声明需要一个值。 这将使 **FastAPI** 知道此查询参数是必需的。 +### 使用`None`声明必需参数 + +你可以声明一个参数可以接收`None`值,但它仍然是必需的。这将强制客户端发送一个值,即使该值是`None`。 + +为此,你可以声明`None`是一个有效的类型,并仍然使用`default=...`: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial006c.py!} +``` + +!!! tip + Pydantic 是 FastAPI 中所有数据验证和序列化的核心,当你在没有设默认值的情况下使用 `Optional` 或 `Union[Something, None]` 时,它具有特殊行为,你可以在 Pydantic 文档中阅读有关必需可选字段的更多信息。 + +### 使用Pydantic中的`Required`代替省略号(`...`) + +如果你觉得使用 `...` 不舒服,你也可以从 Pydantic 导入并使用 `Required`: + +```Python hl_lines="2 8" +{!../../../docs_src/query_params_str_validations/tutorial006d.py!} +``` + +!!! tip + 请记住,在大多数情况下,当你需要某些东西时,可以简单地省略 `default` 参数,因此你通常不必使用 `...` 或 `Required` + + ## 查询参数列表 / 多个值 当你使用 `Query` 显式地定义查询参数时,你还可以声明它去接收一组值,或换句话来说,接收多个值。 From c53e5bd4b1e35d56ab119b1c7eabc78ffc6b33de Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 17:58:24 +0000 Subject: [PATCH 0591/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ddc9c89c6..9b0041bb9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/query-params-str-validations.md`. PR [#5255](https://github.com/tiangolo/fastapi/pull/5255) by [@hjlarry](https://github.com/hjlarry). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/sql-databases.md`. PR [#4999](https://github.com/tiangolo/fastapi/pull/4999) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/wsgi.md`. PR [#4505](https://github.com/tiangolo/fastapi/pull/4505) by [@ASpathfinder](https://github.com/ASpathfinder). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-multiple-params.md`. PR [#4111](https://github.com/tiangolo/fastapi/pull/4111) by [@lbmendes](https://github.com/lbmendes). From c7fe6fea33934a699799bcf0f598ead32d9fcf75 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Mon, 31 Oct 2022 19:07:22 +0100 Subject: [PATCH 0592/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`deployment/deta.md`=20(#3692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sam Courtemanche Co-authored-by: Ruidy Co-authored-by: Sebastián Ramírez --- docs/fr/docs/deployment/deta.md | 245 ++++++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 246 insertions(+) create mode 100644 docs/fr/docs/deployment/deta.md diff --git a/docs/fr/docs/deployment/deta.md b/docs/fr/docs/deployment/deta.md new file mode 100644 index 000000000..cceb7b058 --- /dev/null +++ b/docs/fr/docs/deployment/deta.md @@ -0,0 +1,245 @@ +# Déployer FastAPI sur Deta + +Dans cette section, vous apprendrez à déployer facilement une application **FastAPI** sur Deta en utilisant le plan tarifaire gratuit. 🎁 + +Cela vous prendra environ **10 minutes**. + +!!! info + Deta sponsorise **FastAPI**. 🎉 + +## Une application **FastAPI** de base + +* Créez un répertoire pour votre application, par exemple `./fastapideta/` et déplacez-vous dedans. + +### Le code FastAPI + +* Créer un fichier `main.py` avec : + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int): + return {"item_id": item_id} +``` + +### Dépendances + +Maintenant, dans le même répertoire, créez un fichier `requirements.txt` avec : + +```text +fastapi +``` + +!!! tip "Astuce" + Il n'est pas nécessaire d'installer Uvicorn pour déployer sur Deta, bien qu'il soit probablement souhaitable de l'installer localement pour tester votre application. + +### Structure du répertoire + +Vous aurez maintenant un répertoire `./fastapideta/` avec deux fichiers : + +``` +. +└── main.py +└── requirements.txt +``` + +## Créer un compte gratuit sur Deta + +Créez maintenant un compte gratuit +sur Deta, vous avez juste besoin d'une adresse email et d'un mot de passe. + +Vous n'avez même pas besoin d'une carte de crédit. + +## Installer le CLI (Interface en Ligne de Commande) + +Une fois que vous avez votre compte, installez le CLI de Deta : + +=== "Linux, macOS" + +
+ + ```console + $ curl -fsSL https://get.deta.dev/cli.sh | sh + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ iwr https://get.deta.dev/cli.ps1 -useb | iex + ``` + +
+ +Après l'avoir installé, ouvrez un nouveau terminal afin que la nouvelle installation soit détectée. + +Dans un nouveau terminal, confirmez qu'il a été correctement installé avec : + +
+ +```console +$ deta --help + +Deta command line interface for managing deta micros. +Complete documentation available at https://docs.deta.sh + +Usage: + deta [flags] + deta [command] + +Available Commands: + auth Change auth settings for a deta micro + +... +``` + +
+ +!!! tip "Astuce" + Si vous rencontrez des problèmes pour installer le CLI, consultez la documentation officielle de Deta (en anglais). + +## Connexion avec le CLI + +Maintenant, connectez-vous à Deta depuis le CLI avec : + +
+ +```console +$ deta login + +Please, log in from the web page. Waiting.. +Logged in successfully. +``` + +
+ +Cela ouvrira un navigateur web et permettra une authentification automatique. + +## Déployer avec Deta + +Ensuite, déployez votre application avec le CLI de Deta : + +
+ +```console +$ deta new + +Successfully created a new micro + +// Notice the "endpoint" 🔍 + +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} + +Adding dependencies... + + +---> 100% + + +Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 +``` + +
+ +Vous verrez un message JSON similaire à : + +```JSON hl_lines="4" +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} +``` + +!!! tip "Astuce" + Votre déploiement aura une URL `"endpoint"` différente. + +## Vérifiez + +Maintenant, dans votre navigateur ouvrez votre URL `endpoint`. Dans l'exemple ci-dessus, c'était +`https://qltnci.deta.dev`, mais la vôtre sera différente. + +Vous verrez la réponse JSON de votre application FastAPI : + +```JSON +{ + "Hello": "World" +} +``` + +Et maintenant naviguez vers `/docs` dans votre API, dans l'exemple ci-dessus ce serait `https://qltnci.deta.dev/docs`. + +Vous verrez votre documentation comme suit : + + + +## Activer l'accès public + +Par défaut, Deta va gérer l'authentification en utilisant des cookies pour votre compte. + +Mais une fois que vous êtes prêt, vous pouvez le rendre public avec : + +
+ +```console +$ deta auth disable + +Successfully disabled http auth +``` + +
+ +Maintenant, vous pouvez partager cette URL avec n'importe qui et ils seront en mesure d'accéder à votre API. 🚀 + +## HTTPS + +Félicitations ! Vous avez déployé votre application FastAPI sur Deta ! 🎉 🍰 + +Remarquez également que Deta gère correctement HTTPS pour vous, vous n'avez donc pas à vous en occuper et pouvez être sûr que vos clients auront une connexion cryptée sécurisée. ✅ 🔒 + +## Vérifiez le Visor + +À partir de l'interface graphique de votre documentation (dans une URL telle que `https://qltnci.deta.dev/docs`) +envoyez une requête à votre *opération de chemin* `/items/{item_id}`. + +Par exemple avec l'ID `5`. + +Allez maintenant sur https://web.deta.sh. + +Vous verrez qu'il y a une section à gauche appelée "Micros" avec chacune de vos applications. + +Vous verrez un onglet avec "Details", et aussi un onglet "Visor", allez à l'onglet "Visor". + +Vous pouvez y consulter les requêtes récentes envoyées à votre application. + +Vous pouvez également les modifier et les relancer. + + + +## En savoir plus + +À un moment donné, vous voudrez probablement stocker certaines données pour votre application d'une manière qui +persiste dans le temps. Pour cela, vous pouvez utiliser Deta Base, il dispose également d'un généreux **plan gratuit**. + +Vous pouvez également en lire plus dans la documentation Deta. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index b980acceb..1c4f45682 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -72,6 +72,7 @@ nav: - deployment/index.md - deployment/versions.md - deployment/https.md + - deployment/deta.md - deployment/docker.md - project-generation.md - alternatives.md From 6aa9ef0c969ca119e2803d872c86051425e0c5c8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 18:08:01 +0000 Subject: [PATCH 0593/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9b0041bb9..9c5f3a069 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `deployment/deta.md`. PR [#3692](https://github.com/tiangolo/fastapi/pull/3692) by [@rjNemo](https://github.com/rjNemo). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/query-params-str-validations.md`. PR [#5255](https://github.com/tiangolo/fastapi/pull/5255) by [@hjlarry](https://github.com/hjlarry). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/sql-databases.md`. PR [#4999](https://github.com/tiangolo/fastapi/pull/4999) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/wsgi.md`. PR [#4505](https://github.com/tiangolo/fastapi/pull/4505) by [@ASpathfinder](https://github.com/ASpathfinder). From fcab59be88e6cde5c822f0bd1821fa1e37c3f648 Mon Sep 17 00:00:00 2001 From: Zssaer <45691504+Zssaer@users.noreply.github.com> Date: Tue, 1 Nov 2022 02:08:15 +0800 Subject: [PATCH 0594/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/dependencies/classes-as-de?= =?UTF-8?q?pendencies.md`=20(#4971)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeodre Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .../dependencies/classes-as-dependencies.md | 247 ++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 248 insertions(+) create mode 100644 docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..5813272ee --- /dev/null +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,247 @@ +# 类作为依赖项 + +在深入探究 **依赖注入** 系统之前,让我们升级之前的例子。 + +## 来自前一个例子的`dict` + +在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 以及以上" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `dict`。 + +我们知道编辑器不能为 `dict` 提供很多支持(比如补全),因为编辑器不知道 `dict` 的键和值类型。 + +对此,我们可以做的更好... + +## 什么构成了依赖项? + +到目前为止,您看到的依赖项都被声明为函数。 + +但这并不是声明依赖项的唯一方法(尽管它可能是更常见的方法)。 + +关键因素是依赖项应该是 "可调用对象"。 + +Python 中的 "**可调用对象**" 是指任何 Python 可以像函数一样 "调用" 的对象。 + +所以,如果你有一个对象 `something` (可能*不是*一个函数),你可以 "调用" 它(执行它),就像: + +```Python +something() +``` + +或者 + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +这就是 "可调用对象"。 + +## 类作为依赖项 + +您可能会注意到,要创建一个 Python 类的实例,您可以使用相同的语法。 + +举个例子: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +在这个例子中, `fluffy` 是一个 `Cat` 类的实例。 + +为了创建 `fluffy`,你调用了 `Cat` 。 + +所以,Python 类也是 **可调用对象**。 + +因此,在 **FastAPI** 中,你可以使用一个 Python 类作为一个依赖项。 + +实际上 FastAPI 检查的是它是一个 "可调用对象"(函数,类或其他任何类型)以及定义的参数。 + +如果您在 **FastAPI** 中传递一个 "可调用对象" 作为依赖项,它将分析该 "可调用对象" 的参数,并以处理路径操作函数的参数的方式来处理它们。包括子依赖项。 + +这也适用于完全没有参数的可调用对象。这与不带参数的路径操作函数一样。 + +所以,我们可以将上面的依赖项 "可依赖对象" `common_parameters` 更改为类 `CommonQueryParams`: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +注意用于创建类实例的 `__init__` 方法: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +...它与我们以前的 `common_parameters` 具有相同的参数: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +这些参数就是 **FastAPI** 用来 "处理" 依赖项的。 + +在两个例子下,都有: + +* 一个可选的 `q` 查询参数,是 `str` 类型。 +* 一个 `skip` 查询参数,是 `int` 类型,默认值为 `0`。 +* 一个 `limit` 查询参数,是 `int` 类型,默认值为 `100`。 + +在两个例子下,数据都将被转换、验证、在 OpenAPI schema 上文档化,等等。 + +## 使用它 + +现在,您可以使用这个类来声明你的依赖项了。 + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +**FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。 + +## 类型注解 vs `Depends` + +注意,我们在上面的代码中编写了两次`CommonQueryParams`: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +最后的 `CommonQueryParams`: + +```Python +... = Depends(CommonQueryParams) +``` + +...实际上是 **Fastapi** 用来知道依赖项是什么的。 + +FastAPI 将从依赖项中提取声明的参数,这才是 FastAPI 实际调用的。 + +--- + +在本例中,第一个 `CommonQueryParams` : + +```Python +commons: CommonQueryParams ... +``` + +...对于 **FastAPI** 没有任何特殊的意义。FastAPI 不会使用它进行数据转换、验证等 (因为对于这,它使用 `= Depends(CommonQueryParams)`)。 + +你实际上可以只这样编写: + +```Python +commons = Depends(CommonQueryParams) +``` + +..就像: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等: + + + +## 快捷方式 + +但是您可以看到,我们在这里有一些代码重复了,编写了`CommonQueryParams`两次: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +**FastAPI** 为这些情况提供了一个快捷方式,在这些情况下,依赖项 *明确地* 是一个类,**FastAPI** 将 "调用" 它来创建类本身的一个实例。 + +对于这些特定的情况,您可以跟随以下操作: + +不是写成这样: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...而是这样写: + +```Python +commons: CommonQueryParams = Depends() +``` + +您声明依赖项作为参数的类型,并使用 `Depends()` 作为该函数的参数的 "默认" 值(在 `=` 之后),而在 `Depends()` 中没有任何参数,而不是在 `Depends(CommonQueryParams)` 编写完整的类。 + +同样的例子看起来像这样: + +=== "Python 3.6 以及 以上" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "Python 3.10 以及 以上" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +... **FastAPI** 会知道怎么处理。 + +!!! tip + 如果这看起来更加混乱而不是更加有帮助,那么请忽略它,你不*需要*它。 + + 这只是一个快捷方式。因为 **FastAPI** 关心的是帮助您减少代码重复。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 10addd8e5..f4c3c0ec1 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -89,6 +89,7 @@ nav: - tutorial/body-updates.md - 依赖项: - tutorial/dependencies/index.md + - tutorial/dependencies/classes-as-dependencies.md - tutorial/dependencies/sub-dependencies.md - tutorial/dependencies/dependencies-in-path-operation-decorators.md - tutorial/dependencies/global-dependencies.md From f4aea5852838997c82200cfcce0105c8782ecea4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 18:08:49 +0000 Subject: [PATCH 0595/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9c5f3a069..f5f5cfaa7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#4971](https://github.com/tiangolo/fastapi/pull/4971) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add French translation for `deployment/deta.md`. PR [#3692](https://github.com/tiangolo/fastapi/pull/3692) by [@rjNemo](https://github.com/rjNemo). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/query-params-str-validations.md`. PR [#5255](https://github.com/tiangolo/fastapi/pull/5255) by [@hjlarry](https://github.com/hjlarry). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/sql-databases.md`. PR [#4999](https://github.com/tiangolo/fastapi/pull/4999) by [@Zssaer](https://github.com/Zssaer). From c28337e61dc3856bccbc755f064cbb2d02053a9c Mon Sep 17 00:00:00 2001 From: Bruno Artur Torres Lopes Pereira <33462923+batlopes@users.noreply.github.com> Date: Mon, 31 Oct 2022 15:11:41 -0300 Subject: [PATCH 0596/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/request-forms.md`=20(#4?= =?UTF-8?q?934)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/pt/docs/tutorial/request-forms.md | 58 ++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 59 insertions(+) create mode 100644 docs/pt/docs/tutorial/request-forms.md diff --git a/docs/pt/docs/tutorial/request-forms.md b/docs/pt/docs/tutorial/request-forms.md new file mode 100644 index 000000000..b6c1b0e75 --- /dev/null +++ b/docs/pt/docs/tutorial/request-forms.md @@ -0,0 +1,58 @@ +# Dados do formulário + +Quando você precisar receber campos de formulário ao invés de JSON, você pode usar `Form`. + +!!! info "Informação" + Para usar formulários, primeiro instale `python-multipart`. + + Ex: `pip install python-multipart`. + +## Importe `Form` + +Importe `Form` de `fastapi`: + +```Python hl_lines="1" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +## Declare parâmetros de `Form` + +Crie parâmetros de formulário da mesma forma que você faria para `Body` ou `Query`: + +```Python hl_lines="7" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +Por exemplo, em uma das maneiras que a especificação OAuth2 pode ser usada (chamada "fluxo de senha"), é necessário enviar um `username` e uma `password` como campos do formulário. + +A spec exige que os campos sejam exatamente nomeados como `username` e `password` e sejam enviados como campos de formulário, não JSON. + +Com `Form` você pode declarar os mesmos metadados e validação que com `Body` (e `Query`, `Path`, `Cookie`). + +!!! info "Informação" + `Form` é uma classe que herda diretamente de `Body`. + +!!! tip "Dica" + Para declarar corpos de formulário, você precisa usar `Form` explicitamente, porque sem ele os parâmetros seriam interpretados como parâmetros de consulta ou parâmetros de corpo (JSON). + +## Sobre "Campos de formulário" + +A forma como os formulários HTML (`
`) enviam os dados para o servidor normalmente usa uma codificação "especial" para esses dados, é diferente do JSON. + +O **FastAPI** fará a leitura desses dados no lugar certo em vez de JSON. + +!!! note "Detalhes técnicos" + Os dados dos formulários são normalmente codificados usando o "tipo de mídia" `application/x-www-form-urlencoded`. + + Mas quando o formulário inclui arquivos, ele é codificado como `multipart/form-data`. Você lerá sobre como lidar com arquivos no próximo capítulo. + + Se você quiser ler mais sobre essas codificações e campos de formulário, vá para o MDN web docs para POST. + +!!! warning "Aviso" + Você pode declarar vários parâmetros `Form` em uma *operação de caminho*, mas não pode declarar campos `Body` que espera receber como JSON, pois a solicitação terá o corpo codificado usando `application/x-www- form-urlencoded` em vez de `application/json`. + + Esta não é uma limitação do **FastAPI**, é parte do protocolo HTTP. + +## Recapitulando + +Use `Form` para declarar os parâmetros de entrada de dados de formulário. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 5b1e2f6a4..3c50cc5f8 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/cookie-params.md - tutorial/header-params.md - tutorial/response-status-code.md + - tutorial/request-forms.md - tutorial/handling-errors.md - Segurança: - tutorial/security/index.md From e92a8649f9c4992ed8c4c2ddd26db61f6defe66e Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Oct 2022 18:12:19 +0000 Subject: [PATCH 0597/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f5f5cfaa7..4afabb610 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms.md`. PR [#4934](https://github.com/tiangolo/fastapi/pull/4934) by [@batlopes](https://github.com/batlopes). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#4971](https://github.com/tiangolo/fastapi/pull/4971) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add French translation for `deployment/deta.md`. PR [#3692](https://github.com/tiangolo/fastapi/pull/3692) by [@rjNemo](https://github.com/rjNemo). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/query-params-str-validations.md`. PR [#5255](https://github.com/tiangolo/fastapi/pull/5255) by [@hjlarry](https://github.com/hjlarry). From 60022906597ded5ca00f5eef31721acb24c5b17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Oct 2022 19:56:21 +0100 Subject: [PATCH 0598/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4afabb610..8537ddaf5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,15 @@ ## Latest Changes +### Docs + +* ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). +* ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). +* ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). +* 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). + +### Translations + * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms.md`. PR [#4934](https://github.com/tiangolo/fastapi/pull/4934) by [@batlopes](https://github.com/batlopes). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#4971](https://github.com/tiangolo/fastapi/pull/4971) by [@Zssaer](https://github.com/Zssaer). * 🌐 Add French translation for `deployment/deta.md`. PR [#3692](https://github.com/tiangolo/fastapi/pull/3692) by [@rjNemo](https://github.com/rjNemo). @@ -11,20 +20,19 @@ * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-multiple-params.md`. PR [#4111](https://github.com/tiangolo/fastapi/pull/4111) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-params-numeric-validations.md`. PR [#4099](https://github.com/tiangolo/fastapi/pull/4099) by [@lbmendes](https://github.com/lbmendes). * 🌐 Add French translation for `deployment/versions.md`. PR [#3690](https://github.com/tiangolo/fastapi/pull/3690) by [@rjNemo](https://github.com/rjNemo). +* 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). +* 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). +* 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.3 to 1.2.4. PR [#5507](https://github.com/tiangolo/fastapi/pull/5507) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump internal dependency types-ujson from 5.4.0 to 5.5.0. PR [#5537](https://github.com/tiangolo/fastapi/pull/5537) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.23.0 to 2.24.0. PR [#5508](https://github.com/tiangolo/fastapi/pull/5508) by [@dependabot[bot]](https://github.com/apps/dependabot). -* 🌐 Add French translation for `docs/fr/docs/help-fastapi.md`. PR [#2233](https://github.com/tiangolo/fastapi/pull/2233) by [@JulianMaurin](https://github.com/JulianMaurin). -* ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). -* ✏ Fix broken link in `alternatives.md`. PR [#5455](https://github.com/tiangolo/fastapi/pull/5455) by [@su-shubham](https://github.com/su-shubham). * ⬆ Update internal dependency pytest-cov requirement from <4.0.0,>=2.12.0 to >=2.12.0,<5.0.0. PR [#5539](https://github.com/tiangolo/fastapi/pull/5539) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5536](https://github.com/tiangolo/fastapi/pull/5536) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* ✏ Fix typo in docs about contributing, for compatibility with `pip` in Zsh. PR [#5523](https://github.com/tiangolo/fastapi/pull/5523) by [@zhangbo2012](https://github.com/zhangbo2012). -* ⬆ Bump internal dependency mypy from 0.971 to 0.982. PR [#5541](https://github.com/tiangolo/fastapi/pull/5541) by [@dependabot[bot]](https://github.com/apps/dependabot). -* 🌐 Fix typo in Chinese translation for `docs/zh/docs/tutorial/security/first-steps.md`. PR [#5530](https://github.com/tiangolo/fastapi/pull/5530) by [@yuki1sntSnow](https://github.com/yuki1sntSnow). -* 📝 Fix typo in docs with examples for Python 3.10 instead of 3.9. PR [#5545](https://github.com/tiangolo/fastapi/pull/5545) by [@feliciss](https://github.com/feliciss). -* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/response-status-code.md`. PR [#4922](https://github.com/tiangolo/fastapi/pull/4922) by [@batlopes](https://github.com/batlopes). -* 🔧 Add config for Tamil translations. PR [#5563](https://github.com/tiangolo/fastapi/pull/5563) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix internal Trio test warnings. PR [#5547](https://github.com/tiangolo/fastapi/pull/5547) by [@samuelcolvin](https://github.com/samuelcolvin). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5408](https://github.com/tiangolo/fastapi/pull/5408) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ⬆️ Upgrade Typer to include Rich in scripts for docs. PR [#5502](https://github.com/tiangolo/fastapi/pull/5502) by [@tiangolo](https://github.com/tiangolo). From d0917ce01566c7b04f2c09fce81e87e122aa915c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Oct 2022 19:57:16 +0100 Subject: [PATCH 0599/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?85.2?= 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 8537ddaf5..c6911566a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.85.2 + ### Docs * ✏ Fix grammar and add helpful links to dependencies in `docs/en/docs/async.md`. PR [#5432](https://github.com/tiangolo/fastapi/pull/5432) by [@pamelafox](https://github.com/pamelafox). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 7ccb62563..cb1b063aa 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.85.1" +__version__ = "0.85.2" from starlette import status as status From da0bfd22aa91ebdac4bc04e4af8b6755303bcd25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:44:51 +0100 Subject: [PATCH 0600/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 101 ++++++++++++------- docs/en/data/people.yml | 164 +++++++++++++++---------------- 2 files changed, 146 insertions(+), 119 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index aaf7c9b80..3d0b37dac 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -32,13 +32,13 @@ sponsors: - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes +- - login: getsentry + avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 + url: https://github.com/getsentry - - login: InesIvanova avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 url: https://github.com/InesIvanova -- - login: zopyx - avatarUrl: https://avatars.githubusercontent.com/u/594239?u=8e5ce882664f47fd61002bed51718c78c3799d24&v=4 - url: https://github.com/zopyx - - login: SendCloud +- - login: SendCloud avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 url: https://github.com/SendCloud - login: mercedes-benz @@ -62,21 +62,21 @@ sponsors: - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: wdwinslow - avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 - url: https://github.com/wdwinslow - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck + - login: AccentDesign + avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 + url: https://github.com/AccentDesign - login: RodneyU215 avatarUrl: https://avatars.githubusercontent.com/u/3329665?u=ec6a9adf8e7e8e306eed7d49687c398608d1604f&v=4 url: https://github.com/RodneyU215 - login: tizz98 avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 url: https://github.com/tizz98 - - login: Vikka + - login: dorianturba avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 - url: https://github.com/Vikka + url: https://github.com/dorianturba - login: jmaralc avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 url: https://github.com/jmaralc @@ -89,13 +89,19 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare +- - login: karelhusa + avatarUrl: https://avatars.githubusercontent.com/u/11407706?u=4f787cffc30ea198b15935c751940f0b48a26a17&v=4 + url: https://github.com/karelhusa - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: A-Edge +- - login: DucNgn + avatarUrl: https://avatars.githubusercontent.com/u/43587865?u=a9f9f61569aebdc0ce74df85b8cc1a219a7ab570&v=4 + url: https://github.com/DucNgn + - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge - - login: Kludex @@ -113,6 +119,9 @@ sponsors: - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill + - login: gazpachoking + avatarUrl: https://avatars.githubusercontent.com/u/187133?v=4 + url: https://github.com/gazpachoking - login: dekoza avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 url: https://github.com/dekoza @@ -170,15 +179,15 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 + - login: bauyrzhanospan + avatarUrl: https://avatars.githubusercontent.com/u/3536037?u=25c86201d0212497aefcc1688cccf509057a1dc4&v=4 + url: https://github.com/bauyrzhanospan - login: MarekBleschke avatarUrl: https://avatars.githubusercontent.com/u/3616870?v=4 url: https://github.com/MarekBleschke - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco - - login: anomaly - avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 - url: https://github.com/anomaly - login: peterHoburg avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 url: https://github.com/peterHoburg @@ -194,15 +203,15 @@ sponsors: - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: CINOAdam - avatarUrl: https://avatars.githubusercontent.com/u/4728508?u=76ef23f06ae7c604e009873bc27cf0ea9ba738c9&v=4 - url: https://github.com/CINOAdam - login: ScrimForever avatarUrl: https://avatars.githubusercontent.com/u/5040124?u=091ec38bfe16d6e762099e91309b59f248616a65&v=4 url: https://github.com/ScrimForever - login: ennui93 avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 url: https://github.com/ennui93 + - login: ternaus + avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=fabc8d75c921b3380126adb5a931c5da6e7db04f&v=4 + url: https://github.com/ternaus - login: Yaleesa avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 url: https://github.com/Yaleesa @@ -216,7 +225,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4 url: https://github.com/pkucmus - login: s3ich4n - avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=ba3025d698e1c986655e776ae383a3d60d9d578e&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=6690c5403bc1d9a1837886defdc5256e9a43b1db&v=4 url: https://github.com/s3ich4n - login: Rehket avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 @@ -227,6 +236,9 @@ sponsors: - login: Shackelford-Arden avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 url: https://github.com/Shackelford-Arden + - login: wdwinslow + avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 + url: https://github.com/wdwinslow - login: Ge0f3 avatarUrl: https://avatars.githubusercontent.com/u/11887760?u=ccd80f1ac36dcb8517ef5c4e702e8cc5a80cad2f&v=4 url: https://github.com/Ge0f3 @@ -239,6 +251,9 @@ sponsors: - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade + - login: khadrawy + avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 + url: https://github.com/khadrawy - login: pablonnaoji avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=afc15bd5a4ba9c5c7206bbb1bcaeef606a0932e0&v=4 url: https://github.com/pablonnaoji @@ -302,20 +317,17 @@ sponsors: - login: rafsaf avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf - - login: yezz123 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 url: https://github.com/dudikbender - - login: llamington - avatarUrl: https://avatars.githubusercontent.com/u/54869395?u=42ea59b76f49449f41a4d106bb65a130797e8d7c&v=4 - url: https://github.com/llamington + - login: dazeddd + avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 + url: https://github.com/dazeddd - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut - login: patsatsia - avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=419516384f798a35e9d9e2dd81282cc46c151b58&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia - login: predictionmachine avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4 @@ -332,9 +344,15 @@ sponsors: - login: dotlas avatarUrl: https://avatars.githubusercontent.com/u/88832003?v=4 url: https://github.com/dotlas + - login: programvx + avatarUrl: https://avatars.githubusercontent.com/u/96057906?v=4 + url: https://github.com/programvx - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h + - login: Dagmaara + avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 + url: https://github.com/Dagmaara - - login: linux-china avatarUrl: https://avatars.githubusercontent.com/u/46711?v=4 url: https://github.com/linux-china @@ -419,6 +437,9 @@ sponsors: - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy + - login: nikeee + avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=63f8eee593f25138e0f1032ef442e9ad24907d4c&v=4 + url: https://github.com/nikeee - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa @@ -426,7 +447,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood - login: unredundant - avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=57dd0023365bec03f4fc566df6b81bc0a264a47d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4 url: https://github.com/unredundant - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 @@ -434,6 +455,9 @@ sponsors: - login: holec avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4 url: https://github.com/holec + - login: mattwelke + avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 + url: https://github.com/mattwelke - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 url: https://github.com/hcristea @@ -470,9 +494,6 @@ sponsors: - login: logan-connolly avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly - - login: sanghunka - avatarUrl: https://avatars.githubusercontent.com/u/16280020?u=7d8fafd8bfe6d7900bb1e52d5a5d5da0c02bc164&v=4 - url: https://github.com/sanghunka - login: stevenayers avatarUrl: https://avatars.githubusercontent.com/u/16361214?u=098b797d8d48afb8cd964b717847943b61d24a6d&v=4 url: https://github.com/stevenayers @@ -494,6 +515,12 @@ sponsors: - login: fstau avatarUrl: https://avatars.githubusercontent.com/u/24669867?u=60e7c8c09f8dafabee8fc3edcd6f9e19abbff918&v=4 url: https://github.com/fstau + - login: pers0n4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=444441027bc2c9f9db68e8047d65ff23d25699cf&v=4 + url: https://github.com/pers0n4 + - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli @@ -563,12 +590,12 @@ sponsors: - login: pondDevThai avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 url: https://github.com/pondDevThai -- - login: mattwelke - avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 - url: https://github.com/mattwelke - - login: cesarfreire - avatarUrl: https://avatars.githubusercontent.com/u/21126103?u=5d428f77f9b63c741f0e9ca5e15a689017b66fe8&v=4 - url: https://github.com/cesarfreire + - login: marlonmartins2 + avatarUrl: https://avatars.githubusercontent.com/u/87719558?u=d07ffecfdabf4fd9aca987f8b5cd9128c65e598e&v=4 + url: https://github.com/marlonmartins2 +- - login: 2niuhe + avatarUrl: https://avatars.githubusercontent.com/u/13382324?u=ac918d72e0329c87ba29b3bf85e03e98a4ee9427&v=4 + url: https://github.com/2niuhe - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb @@ -578,6 +605,6 @@ sponsors: - login: Moises6669 avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 url: https://github.com/Moises6669 - - login: zahariev-webbersof - avatarUrl: https://avatars.githubusercontent.com/u/68993494?u=b341c94a8aa0624e05e201bcf8ae5b2697e3be2f&v=4 - url: https://github.com/zahariev-webbersof + - login: su-shubham + avatarUrl: https://avatars.githubusercontent.com/u/75021117?v=4 + url: https://github.com/su-shubham diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index c7354eb44..730528795 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1271 - prs: 338 + answers: 1315 + prs: 342 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 364 + count: 367 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -21,14 +21,14 @@ experts: count: 207 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause +- login: JarroVGIT + count: 187 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: euri10 count: 166 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: JarroVGIT - count: 163 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT - login: phy25 count: 130 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 @@ -37,20 +37,20 @@ experts: count: 77 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv +- login: iudeen + count: 76 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: ArcLightSlavik count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: iudeen - count: 65 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: falkben count: 58 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen - count: 49 + count: 50 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen - login: insomnes @@ -58,11 +58,11 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes - login: jgould22 - count: 45 + count: 46 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: Dustyposa - count: 43 + count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa - login: adriangb @@ -89,6 +89,10 @@ experts: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 +- login: acidjunk + count: 31 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: krishnardt count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -101,10 +105,6 @@ experts: count: 29 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla -- login: acidjunk - count: 27 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: ghandic count: 25 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -117,6 +117,10 @@ experts: count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak +- login: odiseo0 + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -129,10 +133,6 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: odiseo0 - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 - url: https://github.com/odiseo0 - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 @@ -161,6 +161,10 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv +- login: yinziyan1206 + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 + url: https://github.com/yinziyan1206 - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 @@ -177,10 +181,6 @@ experts: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 url: https://github.com/haizaar -- login: yinziyan1206 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 - url: https://github.com/yinziyan1206 - login: valentin994 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 @@ -193,35 +193,27 @@ experts: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: zoliknemet +- login: mbroton count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 - url: https://github.com/zoliknemet + avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 + url: https://github.com/mbroton last_month_active: - login: JarroVGIT - count: 27 + count: 18 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: iudeen - count: 16 + count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: mbroton - count: 6 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 url: https://github.com/mbroton -- login: csrgxtu - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/5053620?u=9655a3e9661492fcdaaf99193eb16d5cbcc3849e&v=4 - url: https://github.com/csrgxtu -- login: Kludex - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: jgould22 +- login: yinziyan1206 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 + url: https://github.com/yinziyan1206 top_contributors: - login: waynerv count: 25 @@ -243,6 +235,10 @@ top_contributors: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: dependabot + count: 14 + avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 + url: https://github.com/apps/dependabot - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 @@ -255,10 +251,6 @@ top_contributors: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep -- login: dependabot - count: 9 - avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 - url: https://github.com/apps/dependabot - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -271,6 +263,14 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: rjNemo + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo +- login: pre-commit-ci + count: 6 + avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 + url: https://github.com/apps/pre-commit-ci - login: wshayes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -285,12 +285,16 @@ top_contributors: url: https://github.com/Attsun1031 - login: ComicShrimp count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 url: https://github.com/jekirl +- login: samuelcolvin + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 + url: https://github.com/samuelcolvin - login: jfunez count: 4 avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 @@ -307,10 +311,6 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 url: https://github.com/hitrust -- login: rjNemo - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo - login: lsglucas count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 @@ -319,19 +319,23 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang -- login: pre-commit-ci +- login: batlopes count: 4 - avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 - url: https://github.com/apps/pre-commit-ci + avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 + url: https://github.com/batlopes top_reviewers: - login: Kludex - count: 101 + count: 108 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 64 + count: 65 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan +- login: yezz123 + count: 56 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + url: https://github.com/yezz123 - login: tokusumi count: 50 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 @@ -344,10 +348,6 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 -- login: yezz123 - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 - url: https://github.com/yezz123 - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 @@ -356,6 +356,10 @@ top_reviewers: count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay +- login: JarroVGIT + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT - login: AdrianDeAnda count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 @@ -364,16 +368,16 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: JarroVGIT - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT +- login: iudeen + count: 26 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: cassiobotaro count: 25 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro - login: lsglucas - count: 24 + count: 25 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas - login: dmontagu @@ -392,6 +396,14 @@ top_reviewers: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun +- login: rjNemo + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo +- login: Smlep + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 + url: https://github.com/Smlep - login: zy7y count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 @@ -404,14 +416,6 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc -- login: rjNemo - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo -- login: Smlep - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 - url: https://github.com/Smlep - login: DevDae count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 @@ -424,10 +428,10 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: iudeen +- login: odiseo0 count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -436,10 +440,6 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 url: https://github.com/RunningIkkyu -- login: odiseo0 - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=ab724eae71c3fe1cf81e8dc76e73415da926ef7d&v=4 - url: https://github.com/odiseo0 - login: LorhanSohaky count: 11 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 @@ -462,7 +462,7 @@ top_reviewers: url: https://github.com/maoyibo - login: ComicShrimp count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=b3e4d9a14d9a65d429ce62c566aef73178b7111d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp - login: graingert count: 9 From 876ea7978fd5a8413a1cdc0c655a1894c0d0c5a7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 11:45:26 +0000 Subject: [PATCH 0601/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c6911566a..5d39e0483 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 [#5571](https://github.com/tiangolo/fastapi/pull/5571) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.85.2 From 058cb6e88e56a48708e65d0b1b210201fb96eb2a Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Thu, 3 Nov 2022 19:50:48 +0800 Subject: [PATCH 0602/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/tutorial/security/oauth2-jwt.md`=20(#384?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/security/oauth2-jwt.md | 193 ++++++++++--------- 1 file changed, 99 insertions(+), 94 deletions(-) diff --git a/docs/zh/docs/tutorial/security/oauth2-jwt.md b/docs/zh/docs/tutorial/security/oauth2-jwt.md index 82ef9b897..054198545 100644 --- a/docs/zh/docs/tutorial/security/oauth2-jwt.md +++ b/docs/zh/docs/tutorial/security/oauth2-jwt.md @@ -1,34 +1,34 @@ -# 使用(哈希)密码和 JWT Bearer 令牌的 OAuth2 +# OAuth2 实现密码哈希与 Bearer JWT 令牌验证 -既然我们已经有了所有的安全流程,就让我们来使用 JWT 令牌和安全哈希密码让应用程序真正地安全吧。 +至此,我们已经编写了所有安全流,本章学习如何使用 JWT 令牌(Token)和安全密码哈希(Hash)实现真正的安全机制。 -你可以在应用程序中真正地使用这些代码,在数据库中保存密码哈希值,等等。 +本章的示例代码真正实现了在应用的数据库中保存哈希密码等功能。 -我们将从上一章结束的位置开始,然后对示例进行扩充。 +接下来,我们紧接上一章,继续完善安全机制。 -## 关于 JWT +## JWT 简介 -JWT 表示 「JSON Web Tokens」。 +JWT 即**JSON 网络令牌**(JSON Web Tokens)。 -它是一个将 JSON 对象编码为密集且没有空格的长字符串的标准。字符串看起来像这样: +JWT 是一种将 JSON 对象编码为没有空格,且难以理解的长字符串的标准。JWT 的内容如下所示: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` -它没有被加密,因此任何人都可以从字符串内容中还原数据。 +JWT 字符串没有加密,任何人都能用它恢复原始信息。 -但它经过了签名。因此,当你收到一个由你发出的令牌时,可以校验令牌是否真的由你发出。 +但 JWT 使用了签名机制。接受令牌时,可以用签名校验令牌。 -通过这种方式,你可以创建一个有效期为 1 周的令牌。然后当用户第二天使用令牌重新访问时,你知道该用户仍然处于登入状态。 +使用 JWT 创建有效期为一周的令牌。第二天,用户持令牌再次访问时,仍为登录状态。 -一周后令牌将会过期,用户将不会通过认证,必须再次登录才能获得一个新令牌。而且如果用户(或第三方)试图修改令牌以篡改过期时间,你将因为签名不匹配而能够发觉。 +令牌于一周后过期,届时,用户身份验证就会失败。只有再次登录,才能获得新的令牌。如果用户(或第三方)篡改令牌的过期时间,因为签名不匹配会导致身份验证失败。 -如果你想上手体验 JWT 令牌并了解其工作方式,可访问 https://jwt.io。 +如需深入了解 JWT 令牌,了解它的工作方式,请参阅 https://jwt.io。 ## 安装 `python-jose` -我们需要安装 `python-jose` 以在 Python 中生成和校验 JWT 令牌: +安装 `python-jose`,在 Python 中生成和校验 JWT 令牌:
@@ -40,38 +40,39 @@ $ pip install python-jose[cryptography]
-Python-jose 需要一个额外的加密后端。 +Python-jose 需要安装配套的加密后端。 -这里我们使用的是推荐的后端:pyca/cryptography。 +本教程推荐的后端是:pyca/cryptography。 -!!! tip - 本教程曾经使用过 PyJWT。 +!!! tip "提示" - 但是后来更新为使用 Python-jose,因为它提供了 PyJWT 的所有功能,以及之后与其他工具进行集成时你可能需要的一些其他功能。 + 本教程以前使用 PyJWT。 -## 哈希密码 + 但后来换成了 Python-jose,因为 Python-jose 支持 PyJWT 的所有功能,还支持与其它工具集成时可能会用到的一些其它功能。 -「哈希」的意思是:将某些内容(在本例中为密码)转换为看起来像乱码的字节序列(只是一个字符串)。 +## 密码哈希 -每次你传入完全相同的内容(完全相同的密码)时,你都会得到完全相同的乱码。 +**哈希**是指把特定内容(本例中为密码)转换为乱码形式的字节序列(其实就是字符串)。 -但是你不能从乱码转换回密码。 +每次传入完全相同的内容时(比如,完全相同的密码),返回的都是完全相同的乱码。 -### 为什么使用哈希密码 +但这个乱码无法转换回传入的密码。 -如果你的数据库被盗,小偷将无法获得用户的明文密码,只能拿到哈希值。 +### 为什么使用密码哈希 -因此,小偷将无法尝试在另一个系统中使用这些相同的密码(由于许多用户在任何地方都使用相同的密码,因此这很危险)。 +原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。 + +这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大)。 ## 安装 `passlib` -PassLib 是一个用于处理哈希密码的很棒的 Python 包。 +Passlib 是处理密码哈希的 Python 包。 -它支持许多安全哈希算法以及配合算法使用的实用程序。 +它支持很多安全哈希算法及配套工具。 -推荐的算法是 「Bcrypt」。 +本教程推荐的算法是 **Bcrypt**。 -因此,安装附带 Bcrypt 的 PassLib: +因此,请先安装附带 Bcrypt 的 PassLib:
@@ -83,46 +84,49 @@ $ pip install passlib[bcrypt]
-!!! tip - 使用 `passlib`,你甚至可以将其配置为能够读取 Django,Flask 的安全扩展或许多其他工具创建的密码。 +!!! tip "提示" - 因此,你将能够,举个例子,将数据库中来自 Django 应用的数据共享给一个 FastAPI 应用。或者使用同一数据库但逐渐将应用从 Django 迁移到 FastAPI。 + `passlib` 甚至可以读取 Django、Flask 的安全插件等工具创建的密码。 - 而你的用户将能够同时从 Django 应用或 FastAPI 应用登录。 + 例如,把 Django 应用的数据共享给 FastAPI 应用的数据库。或利用同一个数据库,可以逐步把应用从 Django 迁移到 FastAPI。 -## 哈希并校验密码 + 并且,用户可以同时从 Django 应用或 FastAPI 应用登录。 -从 `passlib` 导入我们需要的工具。 +## 密码哈希与校验 -创建一个 PassLib 「上下文」。这将用于哈希和校验密码。 +从 `passlib` 导入所需工具。 -!!! tip - PassLib 上下文还具有使用不同哈希算法的功能,包括仅允许用于校验的已弃用的旧算法等。 +创建用于密码哈希和身份校验的 PassLib **上下文**。 - 例如,你可以使用它来读取和校验由另一个系统(例如Django)生成的密码,但是使用其他算法例如 Bcrypt 生成新的密码哈希值。 +!!! tip "提示" - 并同时兼容所有的这些功能。 + PassLib 上下文还支持使用不同哈希算法的功能,包括只能校验的已弃用旧算法等。 -创建一个工具函数以哈希来自用户的密码。 + 例如,用它读取和校验其它系统(如 Django)生成的密码,但要使用其它算法,如 Bcrypt,生成新的哈希密码。 -然后创建另一个工具函数,用于校验接收的密码是否与存储的哈希值匹配。 + 同时,这些功能都是兼容的。 -再创建另一个工具函数用于认证并返回用户。 +接下来,创建三个工具函数,其中一个函数用于哈希用户的密码。 + +第一个函数用于校验接收的密码是否匹配存储的哈希值。 + +第三个函数用于身份验证,并返回用户。 ```Python hl_lines="7 48 55-56 59-60 69-75" {!../../../docs_src/security/tutorial004.py!} ``` -!!! note - 如果你查看新的(伪)数据库 `fake_users_db`,你将看到哈希后的密码现在的样子:`"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`。 +!!! note "笔记" + + 查看新的(伪)数据库 `fake_users_db`,就能看到哈希后的密码:`"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`。 ## 处理 JWT 令牌 导入已安装的模块。 -创建一个随机密钥,该密钥将用于对 JWT 令牌进行签名。 +创建用于 JWT 令牌签名的随机密钥。 -要生成一个安全的随机密钥,可使用以下命令: +使用以下命令,生成安全的随机密钥:
@@ -134,15 +138,15 @@ $ openssl rand -hex 32
-然后将输出复制到变量 「SECRET_KEY」 中(不要使用示例中的这个)。 +然后,把生成的密钥复制到变量**SECRET_KEY**,注意,不要使用本例所示的密钥。 -创建用于设定 JWT 令牌签名算法的变量 「ALGORITHM」,并将其设置为 `"HS256"`。 +创建指定 JWT 令牌签名算法的变量 **ALGORITHM**,本例中的值为 `"HS256"`。 -创建一个设置令牌过期时间的变量。 +创建设置令牌过期时间的变量。 -定义一个将在令牌端点中用于响应的 Pydantic 模型。 +定义令牌端点响应的 Pydantic 模型。 -创建一个生成新的访问令牌的工具函数。 +创建生成新的访问令牌的工具函数。 ```Python hl_lines="6 12-14 28-30 78-86" {!../../../docs_src/security/tutorial004.py!} @@ -150,11 +154,11 @@ $ openssl rand -hex 32 ## 更新依赖项 -更新 `get_current_user` 以接收与之前相同的令牌,但这次使用的是 JWT 令牌。 +更新 `get_current_user` 以接收与之前相同的令牌,但这里用的是 JWT 令牌。 -解码接收到的令牌,对其进行校验,然后返回当前用户。 +解码并校验接收到的令牌,然后,返回当前用户。 -如果令牌无效,立即返回一个 HTTP 错误。 +如果令牌无效,则直接返回 HTTP 错误。 ```Python hl_lines="89-106" {!../../../docs_src/security/tutorial004.py!} @@ -162,57 +166,57 @@ $ openssl rand -hex 32 ## 更新 `/token` *路径操作* -使用令牌的过期时间创建一个 `timedelta` 对象。 +用令牌过期时间创建 `timedelta` 对象。 -创建一个真实的 JWT 访问令牌并返回它。 +创建并返回真正的 JWT 访问令牌。 ```Python hl_lines="115-128" {!../../../docs_src/security/tutorial004.py!} ``` -### 关于 JWT 「主题」 `sub` 的技术细节 +### JWT `sub` 的技术细节 -JWT 的规范中提到有一个 `sub` 键,值为该令牌的主题。 +JWT 规范还包括 `sub` 键,值是令牌的主题。 -使用它并不是必须的,但这是你放置用户标识的地方,所以我们在示例中使用了它。 +该键是可选的,但要把用户标识放在这个键里,所以本例使用了该键。 -除了识别用户并允许他们直接在你的 API 上执行操作之外,JWT 还可以用于其他事情。 +除了识别用户与许可用户在 API 上直接执行操作之外,JWT 还可能用于其它事情。 -例如,你可以识别一个 「汽车」 或 「博客文章」。 +例如,识别**汽车**或**博客**。 -然后你可以添加关于该实体的权限,比如「驾驶」(汽车)或「编辑」(博客)。 +接着,为实体添加权限,比如**驾驶**(汽车)或**编辑**(博客)。 -然后,你可以将 JWT 令牌交给用户(或机器人),他们可以使用它来执行这些操作(驾驶汽车,或编辑博客文章),甚至不需要有一个账户,只需使用你的 API 为其生成的 JWT 令牌。 +然后,把 JWT 令牌交给用户(或机器人),他们就可以执行驾驶汽车,或编辑博客等操作。无需注册账户,只要有 API 生成的 JWT 令牌就可以。 -使用这样的思路,JWT 可以用于更复杂的场景。 +同理,JWT 可以用于更复杂的场景。 -在这些情况下,几个实体可能有相同的 ID,比如说 `foo`(一个用户 `foo`,一辆车 `foo`,一篇博客文章 `foo`)。 +在这些情况下,多个实体的 ID 可能是相同的,以 ID `foo` 为例,用户的 ID 是 `foo`,车的 ID 是 `foo`,博客的 ID 也是 `foo`。 -因此,为了避免 ID 冲突,当为用户创建 JWT 令牌时,你可以在 `sub` 键的值前加上前缀,例如 `username:`。所以,在这个例子中,`sub` 的值可以是:`username:johndoe`。 +为了避免 ID 冲突,在给用户创建 JWT 令牌时,可以为 `sub` 键的值加上前缀,例如 `username:`。因此,在本例中,`sub` 的值可以是:`username:johndoe`。 -要记住的重点是,`sub` 键在整个应用程序中应该有一个唯一的标识符,而且应该是一个字符串。 +注意,划重点,`sub` 键在整个应用中应该只有一个唯一的标识符,而且应该是字符串。 -## 检查效果 +## 检查 运行服务器并访问文档: http://127.0.0.1:8000/docs。 -你会看到如下用户界面: +可以看到如下用户界面: -像以前一样对应用程序进行认证。 +用与上一章同样的方式实现应用授权。 使用如下凭证: -用户名: `johndoe` -密码: `secret` +用户名: `johndoe` 密码: `secret` -!!! check - 请注意,代码中没有任何地方记录了明文密码 「`secret`」,我们只保存了其哈希值。 +!!! check "检查" + + 注意,代码中没有明文密码**`secret`**,只保存了它的哈希值。 -访问 `/users/me/` 端点,你将获得如下响应: +调用 `/users/me/` 端点,收到下面的响应: ```JSON { @@ -225,41 +229,42 @@ JWT 的规范中提到有一个 `sub` 键,值为该令牌的主题。 -如果你打开开发者工具,将看到数据是如何发送的并且其中仅包含了令牌,只有在第一个请求中发送了密码以校验用户身份并获取该访问令牌,但之后都不会再发送密码: +打开浏览器的开发者工具,查看数据是怎么发送的,而且数据里只包含了令牌,只有验证用户的第一个请求才发送密码,并获取访问令牌,但之后不会再发送密码: -!!! note - 注意请求中的 `Authorization` 首部,其值以 `Bearer` 开头。 +!!! note "笔记" -## 使用 `scopes` 的进阶用法 + 注意,请求中 `Authorization` 响应头的值以 `Bearer` 开头。 -OAuth2 具有「作用域」的概念。 +## `scopes` 高级用法 -你可以使用它们向 JWT 令牌添加一组特定的权限。 +OAuth2 支持**`scopes`**(作用域)。 -然后,你可以将此令牌直接提供给用户或第三方,使其在一些限制下与你的 API 进行交互。 +**`scopes`**为 JWT 令牌添加指定权限。 -你可以在之后的**进阶用户指南**中了解如何使用它们以及如何将它们集成到 **FastAPI** 中。 +让持有令牌的用户或第三方在指定限制条件下与 API 交互。 -## 总结 +**高级用户指南**中将介绍如何使用 `scopes`,及如何把 `scopes` 集成至 **FastAPI**。 -通过目前你所看到的,你可以使用像 OAuth2 和 JWT 这样的标准来构建一个安全的 **FastAPI** 应用程序。 +## 小结 -在几乎所有的框架中,处理安全性问题都很容易成为一个相当复杂的话题。 +至此,您可以使用 OAuth2 和 JWT 等标准配置安全的 **FastAPI** 应用。 -许多高度简化了安全流程的软件包不得不在数据模型、数据库和可用功能上做出很多妥协。而这些过于简化流程的软件包中,有些其实隐含了安全漏洞。 +几乎在所有框架中,处理安全问题很快都会变得非常复杂。 + +有些包为了简化安全流,不得不在数据模型、数据库和功能上做出妥协。而有些过于简化的软件包其实存在了安全隐患。 --- -**FastAPI** 不对任何数据库、数据模型或工具做任何妥协。 +**FastAPI** 不向任何数据库、数据模型或工具做妥协。 -它给了你所有的灵活性来选择最适合你项目的前者。 +开发者可以灵活选择最适合项目的安全机制。 -你可以直接使用许多维护良好且使用广泛的包,如 `passlib` 和 `python-jose`,因为 **FastAPI** 不需要任何复杂的机制来集成外部包。 +还可以直接使用 `passlib` 和 `python-jose` 等维护良好、使用广泛的包,这是因为 **FastAPI** 不需要任何复杂机制,就能集成外部的包。 -但它为你提供了一些工具,在不影响灵活性、健壮性和安全性的前提下,尽可能地简化这个过程。 +而且,**FastAPI** 还提供了一些工具,在不影响灵活、稳定和安全的前提下,尽可能地简化安全机制。 -而且你可以用相对简单的方式使用和实现安全、标准的协议,比如 OAuth2。 +**FastAPI** 还支持以相对简单的方式,使用 OAuth2 等安全、标准的协议。 -你可以在**进阶用户指南**中了解更多关于如何使用 OAuth2 「作用域」的信息,以实现更精细的权限系统,并同样遵循这些标准。带有作用域的 OAuth2 是很多大的认证提供商使用的机制,比如 Facebook、Google、GitHub、微软、Twitter 等,授权第三方应用代表用户与他们的 API 进行交互。 +**高级用户指南**中详细介绍了 OAuth2**`scopes`**的内容,遵循同样的标准,实现更精密的权限系统。OAuth2 的作用域是脸书、谷歌、GitHub、微软、推特等第三方身份验证应用使用的机制,让用户授权第三方应用与 API 交互。 From c9308cf07071b2fa96430748292bded1ae6d9d51 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 11:51:26 +0000 Subject: [PATCH 0603/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5d39e0483..e557f8bca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). * 👥 Update FastAPI People. PR [#5571](https://github.com/tiangolo/fastapi/pull/5571) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.85.2 From 9a85535e7fa278682536ceb28dc4fb239db16bdd Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:51:44 +0300 Subject: [PATCH 0604/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/deployment/index.md`=20(#5336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/ru/docs/deployment/index.md | 21 +++++++++++++++++++++ docs/ru/mkdocs.yml | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/ru/docs/deployment/index.md diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md new file mode 100644 index 000000000..4dc4e482e --- /dev/null +++ b/docs/ru/docs/deployment/index.md @@ -0,0 +1,21 @@ +# Развёртывание - Введение + +Развернуть приложение **FastAPI** довольно просто. + +## Да что такое это ваше - "развёртывание"?! + +Термин **развёртывание** (приложения) означает выполнение необходимых шагов, чтобы сделать приложение **доступным для пользователей**. + +Обычно **веб-приложения** размещают на удалённом компьютере с серверной программой, которая обеспечивает хорошую производительность, стабильность и т. д., Чтобы ваши пользователи могли эффективно, беспрерывно и беспроблемно обращаться к приложению. + +Это отличется от **разработки**, когда вы постоянно меняете код, делаете в нём намеренные ошибки и исправляете их, останавливаете и перезапускаете сервер разработки и т. д. + +## Стратегии развёртывания + +В зависимости от вашего конкретного случая, есть несколько способов сделать это. + +Вы можете **развернуть сервер** самостоятельно, используя различные инструменты. Например, можно использовать **облачный сервис**, который выполнит часть работы за вас. Также возможны и другие варианты. + +В этом блоке я покажу вам некоторые из основных концепций, которые вы, вероятно, должны иметь в виду при развертывании приложения **FastAPI** (хотя большинство из них применимо к любому другому типу веб-приложений). + +В последующих разделах вы узнаете больше деталей и методов, необходимых для этого. ✨ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 381775ac6..f35ee968c 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -59,13 +59,15 @@ nav: - uk: /uk/ - zh: /zh/ - features.md +- fastapi-people.md - python-types.md - Учебник - руководство пользователя: - tutorial/background-tasks.md -- external-links.md - async.md - Развёртывание: + - deployment/index.md - deployment/versions.md +- external-links.md markdown_extensions: - toc: permalink: true From ed9425ef5049910251dd2302b9dd3095cefe1b1c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 11:52:17 +0000 Subject: [PATCH 0605/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e557f8bca..53eb4a8cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). * 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). * 👥 Update FastAPI People. PR [#5571](https://github.com/tiangolo/fastapi/pull/5571) by [@github-actions[bot]](https://github.com/apps/github-actions). From ac9f56ea5ecc738eabd9282feae4679852155669 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 3 Nov 2022 07:06:52 -0500 Subject: [PATCH 0606/1881] =?UTF-8?q?=F0=9F=90=9B=20Close=20FormData=20(up?= =?UTF-8?q?loaded=20files)=20after=20the=20request=20is=20done=20(#5465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/routing.py | 4 ++++ tests/test_datastructures.py | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 7caf018b5..8c0bec5e6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -3,6 +3,7 @@ import dataclasses import email.message import inspect import json +from contextlib import AsyncExitStack from enum import Enum, IntEnum from typing import ( Any, @@ -190,6 +191,9 @@ def get_request_handler( if body_field: if is_body_form: body = await request.form() + stack = request.scope.get("fastapi_astack") + assert isinstance(stack, AsyncExitStack) + stack.push_async_callback(body.close) else: body_bytes = await request.body() if body_bytes: diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 43f1a116c..2e6217d34 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,10 @@ +from pathlib import Path +from typing import List + import pytest -from fastapi import UploadFile +from fastapi import FastAPI, UploadFile from fastapi.datastructures import Default +from fastapi.testclient import TestClient def test_upload_file_invalid(): @@ -20,3 +24,25 @@ def test_default_placeholder_bool(): placeholder_b = Default("") assert placeholder_a assert not placeholder_b + + +def test_upload_file_is_closed(tmp_path: Path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + app = FastAPI() + + testing_file_store: List[UploadFile] = [] + + @app.post("/uploadfile/") + def create_upload_file(file: UploadFile): + testing_file_store.append(file) + return {"filename": file.filename} + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} + + assert testing_file_store + assert testing_file_store[0].file.closed From 54aa27ca0726ea9954e2ad0e3cc4cca332c5a8d2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 12:07:29 +0000 Subject: [PATCH 0607/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 53eb4a8cb..991dfad55 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). * 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). * 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). * 👥 Update FastAPI People. PR [#5571](https://github.com/tiangolo/fastapi/pull/5571) by [@github-actions[bot]](https://github.com/apps/github-actions). From a0c677ef0d43b8dde652ddba687cc89a14805f5d Mon Sep 17 00:00:00 2001 From: ZHCai Date: Thu, 3 Nov 2022 20:07:49 +0800 Subject: [PATCH 0608/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20wording=20in?= =?UTF-8?q?=20Chinese=20translation=20for=20`docs/zh/docs/python-types.md`?= =?UTF-8?q?=20(#5416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/python-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zh/docs/python-types.md b/docs/zh/docs/python-types.md index 67a1612f0..6cdb4b588 100644 --- a/docs/zh/docs/python-types.md +++ b/docs/zh/docs/python-types.md @@ -194,7 +194,7 @@ John Doe 这表示: -* 变量 `items_t` 是一个 `tuple`,其中的每个元素都是 `int` 类型。 +* 变量 `items_t` 是一个 `tuple`,其中的前两个元素都是 `int` 类型, 最后一个元素是 `str` 类型。 * 变量 `items_s` 是一个 `set`,其中的每个元素都是 `bytes` 类型。 #### 字典 From 4cf90758091db36b10991f9192f3c67ecd4b4c51 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 12:08:23 +0000 Subject: [PATCH 0609/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 991dfad55..c95550039 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). * 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). * 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). * 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). From d62f5c1b288dbc098a718f94f90784c11c1e7c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 13:26:48 +0100 Subject: [PATCH 0610/1881] =?UTF-8?q?=E2=9C=85=20Enable=20tests=20for=20Py?= =?UTF-8?q?thon=203.11=20(#4881)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 +- pyproject.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e6225db3..9e492c1ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: diff --git a/pyproject.toml b/pyproject.toml index 543ba15c1..ad088ce33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,11 @@ filterwarnings = [ # TODO: needed by asyncio in Python 3.9.7 https://bugs.python.org/issue45097, try to remove on 3.9.8 'ignore:The loop argument is deprecated since Python 3\.8, and scheduled for removal in Python 3\.10:DeprecationWarning:asyncio', 'ignore:starlette.middleware.wsgi is deprecated and will be removed in a future release\..*:DeprecationWarning:starlette', + # TODO: remove after upgrading HTTPX to a version newer than 0.23.0 + # Including PR: https://github.com/encode/httpx/pull/2309 + "ignore:'cgi' is deprecated:DeprecationWarning", + # For passlib + "ignore:'crypt' is deprecated and slated for removal in Python 3.13:DeprecationWarning", # see https://trio.readthedocs.io/en/stable/history.html#trio-0-22-0-2022-09-28 "ignore:You seem to already have a custom.*:RuntimeWarning:trio", "ignore::trio.TrioDeprecationWarning", From cf730518bc64cd8377e867942c1446b70ffca012 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 12:27:24 +0000 Subject: [PATCH 0611/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c95550039..48992f078 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). * 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). * 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). From be3e29fb3cdc1c7858dac0d54ad67edd2ac30d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 21:00:29 +0100 Subject: [PATCH 0612/1881] =?UTF-8?q?=F0=9F=91=B7=20Switch=20from=20Codeco?= =?UTF-8?q?v=20to=20Smokeshow=20plus=20pytest-cov=20to=20pure=20coverage?= =?UTF-8?q?=20for=20internal=20tests=20(#5583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 35 +++++++++++++++++++++++++++++ .github/workflows/test.yml | 40 +++++++++++++++++++++++++++++++-- pyproject.toml | 11 ++++++++- scripts/test-cov-html.sh | 5 ++++- scripts/test.sh | 2 +- 5 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/smokeshow.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml new file mode 100644 index 000000000..606633a99 --- /dev/null +++ b/.github/workflows/smokeshow.yml @@ -0,0 +1,35 @@ +name: Smokeshow + +on: + workflow_run: + workflows: [Test] + types: [completed] + +permissions: + statuses: write + +jobs: + smokeshow: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - run: pip install smokeshow + + - uses: dawidd6/action-download-artifact@v2 + with: + workflow: test.yml + commit: ${{ github.event.workflow_run.head_sha }} + + - run: smokeshow upload coverage-html + env: + SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} + SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 + SMOKESHOW_GITHUB_CONTEXT: coverage + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e492c1ad..7f87be700 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,43 @@ jobs: run: pip install -e .[all,dev,doc,test] - name: Lint run: bash scripts/lint.sh + - run: mkdir coverage - name: Test run: bash scripts/test.sh - - name: Upload coverage - uses: codecov/codecov-action@v3 + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }} + - name: Store coverage files + uses: actions/upload-artifact@v3 + with: + name: coverage + path: coverage + coverage-combine: + needs: [test] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Get coverage files + uses: actions/download-artifact@v3 + with: + name: coverage + path: coverage + + - run: pip install coverage[toml] + + - run: ls -la coverage + - run: coverage combine coverage + - run: coverage report + - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" + + - name: Store coverage HTML + uses: actions/upload-artifact@v3 + with: + name: coverage-html + path: htmlcov diff --git a/pyproject.toml b/pyproject.toml index ad088ce33..d7b848502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] test = [ "pytest >=7.1.3,<8.0.0", - "pytest-cov >=2.12.0,<5.0.0", + "coverage[toml] >= 6.5.0,<7.0", "mypy ==0.982", "flake8 >=3.8.3,<6.0.0", "black == 22.8.0", @@ -147,3 +147,12 @@ filterwarnings = [ # TODO remove pytest-cov 'ignore::pytest.PytestDeprecationWarning:pytest_cov', ] + +[tool.coverage.run] +parallel = true +source = [ + "docs_src", + "tests", + "fastapi" +] +context = '${CONTEXT}' diff --git a/scripts/test-cov-html.sh b/scripts/test-cov-html.sh index 7957277fc..d1bdfced2 100755 --- a/scripts/test-cov-html.sh +++ b/scripts/test-cov-html.sh @@ -3,4 +3,7 @@ set -e set -x -bash scripts/test.sh --cov-report=html ${@} +bash scripts/test.sh ${@} +coverage combine +coverage report --show-missing +coverage html diff --git a/scripts/test.sh b/scripts/test.sh index d445ca174..62449ea41 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -6,4 +6,4 @@ set -x # Check README.md is up to date python ./scripts/docs.py verify-readme export PYTHONPATH=./docs_src -pytest --cov=fastapi --cov=tests --cov=docs_src --cov-report=term-missing:skip-covered --cov-report=xml tests ${@} +coverage run -m pytest tests ${@} From b6ea8414a9b27411ff084d673327b9c58f3cea99 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 20:01:17 +0000 Subject: [PATCH 0613/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 48992f078..f2b16e2e7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Switch from Codecov to Smokeshow plus pytest-cov to pure coverage for internal tests. PR [#5583](https://github.com/tiangolo/fastapi/pull/5583) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). * 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). From 5cd99a9517a555242e48dabee8fc7628efca2588 Mon Sep 17 00:00:00 2001 From: Irfanuddin Shafi Ahmed Date: Fri, 4 Nov 2022 01:36:00 +0530 Subject: [PATCH 0614/1881] =?UTF-8?q?=F0=9F=8E=A8=20Format=20OpenAPI=20JSO?= =?UTF-8?q?N=20in=20`test=5Fstarlette=5Fexception.py`=20(#5379)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_starlette_exception.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 2b6712f7b..418ddff7d 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -47,10 +47,10 @@ openapi_schema = { "responses": { "200": { "content": {"application/json": {"schema": {}}}, - "description": "Successful " "Response", + "description": "Successful Response", } }, - "summary": "No Body " "Status " "Code " "Exception", + "summary": "No Body Status Code Exception", } }, "/http-no-body-statuscode-with-detail-exception": { @@ -59,7 +59,7 @@ openapi_schema = { "responses": { "200": { "content": {"application/json": {"schema": {}}}, - "description": "Successful " "Response", + "description": "Successful Response", } }, "summary": "No Body Status Code With Detail Exception", From 5f4680201c6ede7066b54821bc91995d6878695d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 20:06:36 +0000 Subject: [PATCH 0615/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f2b16e2e7..9375f0d18 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Format OpenAPI JSON in `test_starlette_exception.py`. PR [#5379](https://github.com/tiangolo/fastapi/pull/5379) by [@iudeen](https://github.com/iudeen). * 👷 Switch from Codecov to Smokeshow plus pytest-cov to pure coverage for internal tests. PR [#5583](https://github.com/tiangolo/fastapi/pull/5583) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). * 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). From 4fa4965beb7eda6b429a6fad033a0c61883b9df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 21:55:00 +0100 Subject: [PATCH 0616/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20coverage=20ba?= =?UTF-8?q?dge=20to=20use=20Samuel=20Colvin's=20Smokeshow=20(#5585)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- docs/en/docs/index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d4f1cd90..fe0ad49de 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ Test - - Coverage + + Coverage Package version diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index afdd62cee..1ad9c7606 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -8,8 +8,8 @@ Test - - Coverage + + Coverage Package version From 9a442c9730636d6528c23c92ec6ee0ecc0436533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 21:55:32 +0100 Subject: [PATCH 0617/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20to=20exclude=20bots:=20pre-commit-ci,=20dependabot=20(#55?= =?UTF-8?q?86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index cdf423b97..31756a5fc 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -433,7 +433,7 @@ if __name__ == "__main__": ) authors = {**issue_authors, **pr_authors} maintainers_logins = {"tiangolo"} - bot_names = {"codecov", "github-actions"} + bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} maintainers = [] for login in maintainers_logins: user = authors[login] From 8a5befd099da0109f3eb81ee1b16605de0716600 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 20:56:13 +0000 Subject: [PATCH 0618/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9375f0d18..a60b0e75a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update FastAPI People to exclude bots: pre-commit-ci, dependabot. PR [#5586](https://github.com/tiangolo/fastapi/pull/5586) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format OpenAPI JSON in `test_starlette_exception.py`. PR [#5379](https://github.com/tiangolo/fastapi/pull/5379) by [@iudeen](https://github.com/iudeen). * 👷 Switch from Codecov to Smokeshow plus pytest-cov to pure coverage for internal tests. PR [#5583](https://github.com/tiangolo/fastapi/pull/5583) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). From d4e2bdb33a803da12800f8049811a32a6a5a4eeb Mon Sep 17 00:00:00 2001 From: Vivek Ashokkumar Date: Fri, 4 Nov 2022 02:30:28 +0530 Subject: [PATCH 0619/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/tutorial/security/oauth2-jwt.md`=20(#5584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/security/oauth2-jwt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 09557f1ac..41f00fd41 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -257,7 +257,7 @@ Call the endpoint `/users/me/`, you will get the response as: -If you open the developer tools, you could see how the data sent and only includes the token, the password is only sent in the first request to authenticate the user and get that access token, but not afterwards: +If you open the developer tools, you could see how the data sent only includes the token, the password is only sent in the first request to authenticate the user and get that access token, but not afterwards: From fbc13d1f5b2b779cd0df366b5544ed1ea2650497 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 21:01:09 +0000 Subject: [PATCH 0620/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a60b0e75a..855de6bb6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/tutorial/security/oauth2-jwt.md`. PR [#5584](https://github.com/tiangolo/fastapi/pull/5584) by [@vivekashok1221](https://github.com/vivekashok1221). * 👷 Update FastAPI People to exclude bots: pre-commit-ci, dependabot. PR [#5586](https://github.com/tiangolo/fastapi/pull/5586) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format OpenAPI JSON in `test_starlette_exception.py`. PR [#5379](https://github.com/tiangolo/fastapi/pull/5379) by [@iudeen](https://github.com/iudeen). * 👷 Switch from Codecov to Smokeshow plus pytest-cov to pure coverage for internal tests. PR [#5583](https://github.com/tiangolo/fastapi/pull/5583) by [@tiangolo](https://github.com/tiangolo). From 85e602d7ccebc4009422e96b2afaf5eec9578d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 22:03:00 +0100 Subject: [PATCH 0621/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 855de6bb6..392ce8451 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,15 +2,29 @@ ## Latest Changes +### Features + +* ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). + +### Fixes + +* 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). + +### Docs + * ✏ Fix typo in `docs/en/docs/tutorial/security/oauth2-jwt.md`. PR [#5584](https://github.com/tiangolo/fastapi/pull/5584) by [@vivekashok1221](https://github.com/vivekashok1221). + +### Translations + +* 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). +* 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). +* 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). + +### Internal + * 👷 Update FastAPI People to exclude bots: pre-commit-ci, dependabot. PR [#5586](https://github.com/tiangolo/fastapi/pull/5586) by [@tiangolo](https://github.com/tiangolo). * 🎨 Format OpenAPI JSON in `test_starlette_exception.py`. PR [#5379](https://github.com/tiangolo/fastapi/pull/5379) by [@iudeen](https://github.com/iudeen). * 👷 Switch from Codecov to Smokeshow plus pytest-cov to pure coverage for internal tests. PR [#5583](https://github.com/tiangolo/fastapi/pull/5583) by [@tiangolo](https://github.com/tiangolo). -* ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). -* 🌐 Update wording in Chinese translation for `docs/zh/docs/python-types.md`. PR [#5416](https://github.com/tiangolo/fastapi/pull/5416) by [@supercaizehua](https://github.com/supercaizehua). -* 🐛 Close FormData (uploaded files) after the request is done. PR [#5465](https://github.com/tiangolo/fastapi/pull/5465) by [@adriangb](https://github.com/adriangb). -* 🌐 Add Russian translation for `docs/ru/docs/deployment/index.md`. PR [#5336](https://github.com/tiangolo/fastapi/pull/5336) by [@Xewus](https://github.com/Xewus). -* 🌐 Update Chinese translation for `docs/tutorial/security/oauth2-jwt.md`. PR [#3846](https://github.com/tiangolo/fastapi/pull/3846) by [@jaystone776](https://github.com/jaystone776). * 👥 Update FastAPI People. PR [#5571](https://github.com/tiangolo/fastapi/pull/5571) by [@github-actions[bot]](https://github.com/apps/github-actions). ## 0.85.2 From 10fbfd6dc7a085f681d8e4cceee1533d6968da9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 22:12:43 +0100 Subject: [PATCH 0622/1881] =?UTF-8?q?=E2=AC=86=20Add=20Python=203.11=20to?= =?UTF-8?q?=20the=20officially=20supported=20versions=20(#5587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d7b848502..a23289fb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] From 51e768e85ada8641b96ea970d388cfeae489ee8d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Nov 2022 21:13:25 +0000 Subject: [PATCH 0623/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 392ce8451..6a92061ec 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Add Python 3.11 to the officially supported versions. PR [#5587](https://github.com/tiangolo/fastapi/pull/5587) by [@tiangolo](https://github.com/tiangolo). ### Features * ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). From 066cfae56edc6338e649e76421d0dc418c3bb739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 22:16:37 +0100 Subject: [PATCH 0624/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6a92061ec..fc7b54fdf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,9 +2,9 @@ ## Latest Changes -* ⬆ Add Python 3.11 to the officially supported versions. PR [#5587](https://github.com/tiangolo/fastapi/pull/5587) by [@tiangolo](https://github.com/tiangolo). ### Features +* ⬆ Add Python 3.11 to the officially supported versions. PR [#5587](https://github.com/tiangolo/fastapi/pull/5587) by [@tiangolo](https://github.com/tiangolo). * ✅ Enable tests for Python 3.11. PR [#4881](https://github.com/tiangolo/fastapi/pull/4881) by [@tiangolo](https://github.com/tiangolo). ### Fixes From ccd242348fb9e7e214d5cb2ff7f6c5996f96e4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Nov 2022 22:17:44 +0100 Subject: [PATCH 0625/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?86.0?= 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 fc7b54fdf..dbbd5e41e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.86.0 + ### Features * ⬆ Add Python 3.11 to the officially supported versions. PR [#5587](https://github.com/tiangolo/fastapi/pull/5587) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index cb1b063aa..a5c7aeb17 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.85.2" +__version__ = "0.86.0" from starlette import status as status From 5f0e0956897333a03145c02d059e976831ad2303 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Nov 2022 19:20:29 +0000 Subject: [PATCH 0626/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 dbbd5e41e..b005a31e7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update coverage badge to use Samuel Colvin's Smokeshow. PR [#5585](https://github.com/tiangolo/fastapi/pull/5585) by [@tiangolo](https://github.com/tiangolo). ## 0.86.0 From f36d2e2b2b1b4144ee683cfb832e2d5fd8ff5b61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Nov 2022 12:46:14 +0100 Subject: [PATCH 0627/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.24.0=20to=202.24.1=20(#5603)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.24.0 to 2.24.1. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.24.0...v2.24.1) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 15c59d4bd..4bab6df00 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.0 + uses: dawidd6/action-download-artifact@v2.24.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 606633a99..6901f2ab0 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2 + - uses: dawidd6/action-download-artifact@v2.24.1 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From a7edd0b80dab658c350c46fcaa68d8a6224347c0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 9 Nov 2022 11:46:50 +0000 Subject: [PATCH 0628/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b005a31e7..261dbc6c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.1. PR [#5603](https://github.com/tiangolo/fastapi/pull/5603) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update coverage badge to use Samuel Colvin's Smokeshow. PR [#5585](https://github.com/tiangolo/fastapi/pull/5585) by [@tiangolo](https://github.com/tiangolo). ## 0.86.0 From 678d35994a1fad6c94bcfa6399d99c0712caf0d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 12:15:55 +0100 Subject: [PATCH 0629/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.24.1=20to=202.24.2=20(#5609)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.24.1 to 2.24.2. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.24.1...v2.24.2) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 4bab6df00..a2e5976fb 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.1 + uses: dawidd6/action-download-artifact@v2.24.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 6901f2ab0..7559c24c0 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.1 + - uses: dawidd6/action-download-artifact@v2.24.2 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From 0ed9ca78bccc34c3edbbf71a704f9e3090ad5c18 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Nov 2022 11:16:30 +0000 Subject: [PATCH 0630/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 261dbc6c0..62ccd7da3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#5609](https://github.com/tiangolo/fastapi/pull/5609) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.1. PR [#5603](https://github.com/tiangolo/fastapi/pull/5603) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update coverage badge to use Samuel Colvin's Smokeshow. PR [#5585](https://github.com/tiangolo/fastapi/pull/5585) by [@tiangolo](https://github.com/tiangolo). From a0c717d784686c06f6dd98da46ec8596a3373346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 10 Nov 2022 13:22:25 +0100 Subject: [PATCH 0631/1881] =?UTF-8?q?=F0=9F=9B=A0=20Add=20Arabic=20issue?= =?UTF-8?q?=20number=20to=20Notify=20Translations=20GitHub=20Action=20(#56?= =?UTF-8?q?10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/translations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml index b43aba01d..4338e1326 100644 --- a/.github/actions/notify-translations/app/translations.yml +++ b/.github/actions/notify-translations/app/translations.yml @@ -18,3 +18,4 @@ uz: 4883 sv: 5146 he: 5157 ta: 5434 +ar: 3349 From 41735d2de9afbb2c01541d0f3052c718cb9f4f30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Nov 2022 12:22:58 +0000 Subject: [PATCH 0632/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 62ccd7da3..95e466ea9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🛠 Add Arabic issue number to Notify Translations GitHub Action. PR [#5610](https://github.com/tiangolo/fastapi/pull/5610) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#5609](https://github.com/tiangolo/fastapi/pull/5609) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.1. PR [#5603](https://github.com/tiangolo/fastapi/pull/5603) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update coverage badge to use Samuel Colvin's Smokeshow. PR [#5585](https://github.com/tiangolo/fastapi/pull/5585) by [@tiangolo](https://github.com/tiangolo). From 0781a91d194e994eccddae14c2afe892ac0d572b Mon Sep 17 00:00:00 2001 From: nikkie Date: Sun, 13 Nov 2022 22:57:52 +0900 Subject: [PATCH 0633/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20highlight=20line?= =?UTF-8?q?s=20for=20Japanese=20translation=20for=20`docs/tutorial/query-p?= =?UTF-8?q?arams.md`=20(#2969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Fix https://github.com/tiangolo/fastapi/issues/2966 --- docs/ja/docs/tutorial/query-params.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ja/docs/tutorial/query-params.md b/docs/ja/docs/tutorial/query-params.md index 9f8c6ab9f..5202009ef 100644 --- a/docs/ja/docs/tutorial/query-params.md +++ b/docs/ja/docs/tutorial/query-params.md @@ -64,7 +64,7 @@ http://127.0.0.1:8000/items/?skip=20 同様に、デフォルト値を `None` とすることで、オプショナルなクエリパラメータを宣言できます: -```Python hl_lines="7" +```Python hl_lines="9" {!../../../docs_src/query_params/tutorial002.py!} ``` @@ -82,7 +82,7 @@ http://127.0.0.1:8000/items/?skip=20 `bool` 型も宣言できます。これは以下の様に変換されます: -```Python hl_lines="7" +```Python hl_lines="9" {!../../../docs_src/query_params/tutorial003.py!} ``` @@ -126,7 +126,7 @@ http://127.0.0.1:8000/items/foo?short=yes 名前で判別されます: -```Python hl_lines="6 8" +```Python hl_lines="8 10" {!../../../docs_src/query_params/tutorial004.py!} ``` @@ -184,7 +184,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy そして当然、あるパラメータを必須に、別のパラメータにデフォルト値を設定し、また別のパラメータをオプショナルにできます: -```Python hl_lines="7" +```Python hl_lines="10" {!../../../docs_src/query_params/tutorial006.py!} ``` From 59208e4ddcbb14a64eabc99c37acf8424a8d30a2 Mon Sep 17 00:00:00 2001 From: Ryusei Ishikawa <51394682+xryuseix@users.noreply.github.com> Date: Sun, 13 Nov 2022 22:58:31 +0900 Subject: [PATCH 0634/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/advanced/websockets.md`=20(#4983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: komtaki <39375566+komtaki@users.noreply.github.com> --- docs/ja/docs/advanced/websockets.md | 186 ++++++++++++++++++++++++++++ docs/ja/mkdocs.yml | 1 + 2 files changed, 187 insertions(+) create mode 100644 docs/ja/docs/advanced/websockets.md diff --git a/docs/ja/docs/advanced/websockets.md b/docs/ja/docs/advanced/websockets.md new file mode 100644 index 000000000..65e4112a6 --- /dev/null +++ b/docs/ja/docs/advanced/websockets.md @@ -0,0 +1,186 @@ +# WebSocket + +**FastAPI**でWebSocketが使用できます。 + +## `WebSockets`のインストール + +まず `WebSockets`のインストールが必要です。 + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## WebSocket クライアント + +### 本番環境 + +本番環境では、React、Vue.js、Angularなどの最新のフレームワークで作成されたフロントエンドを使用しているでしょう。 + +そして、バックエンドとWebSocketを使用して通信するために、おそらくフロントエンドのユーティリティを使用することになるでしょう。 + +または、ネイティブコードでWebSocketバックエンドと直接通信するネイティブモバイルアプリケーションがあるかもしれません。 + +他にも、WebSocketのエンドポイントと通信する方法があるかもしれません。 + +--- + +ただし、この例では非常にシンプルなHTML文書といくつかのJavaScriptを、すべてソースコードの中に入れて使用することにします。 + +もちろん、これは最適な方法ではありませんし、本番環境で使うことはないでしょう。 + +本番環境では、上記の方法のいずれかの選択肢を採用することになるでしょう。 + +しかし、これはWebSocketのサーバーサイドに焦点を当て、実用的な例を示す最も簡単な方法です。 + +```Python hl_lines="2 6-38 41-43" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +## `websocket` を作成する + +**FastAPI** アプリケーションで、`websocket` を作成します。 + +```Python hl_lines="1 46-47" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +!!! note "技術詳細" + `from starlette.websockets import WebSocket` を使用しても構いません. + + **FastAPI** は開発者の利便性のために、同じ `WebSocket` を提供します。しかし、こちらはStarletteから直接提供されるものです。 + +## メッセージの送受信 + +WebSocketルートでは、 `await` を使ってメッセージの送受信ができます。 + +```Python hl_lines="48-52" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +バイナリやテキストデータ、JSONデータを送受信できます。 + +## 試してみる + +ファイル名が `main.py` である場合、以下の方法でアプリケーションを実行します。 + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +ブラウザで http://127.0.0.1:8000 を開きます。 + +次のようなシンプルなページが表示されます。 + + + +入力ボックスにメッセージを入力して送信できます。 + + + +そして、 WebSocketを使用した**FastAPI**アプリケーションが応答します。 + + + +複数のメッセージを送信(および受信)できます。 + + + +そして、これらの通信はすべて同じWebSocket接続を使用します。 + +## 依存関係 + +WebSocketエンドポイントでは、`fastapi` から以下をインポートして使用できます。 + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +これらは、他のFastAPI エンドポイント/*path operation* の場合と同じように機能します。 + +```Python hl_lines="58-65 68-83" +{!../../../docs_src/websockets/tutorial002.py!} +``` + +!!! info "情報" + WebSocket で `HTTPException` を発生させることはあまり意味がありません。したがって、WebSocketの接続を直接閉じる方がよいでしょう。 + + クロージングコードは、仕様で定義された有効なコードの中から使用することができます。 + + 将来的には、どこからでも `raise` できる `WebSocketException` が用意され、専用の例外ハンドラを追加できるようになる予定です。これは、Starlette の PR #527 に依存するものです。 + +### 依存関係を用いてWebSocketsを試してみる + +ファイル名が `main.py` である場合、以下の方法でアプリケーションを実行します。 + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +ブラウザで http://127.0.0.1:8000 を開きます。 + +クライアントが設定できる項目は以下の通りです。 + +* パスで使用される「Item ID」 +* クエリパラメータとして使用される「Token」 + +!!! tip "豆知識" + クエリ `token` は依存パッケージによって処理されることに注意してください。 + +これにより、WebSocketに接続してメッセージを送受信できます。 + + + +## 切断や複数クライアントへの対応 + +WebSocket接続が閉じられると、 `await websocket.receive_text()` は例外 `WebSocketDisconnect` を発生させ、この例のようにキャッチして処理することができます。 + +```Python hl_lines="81-83" +{!../../../docs_src/websockets/tutorial003.py!} +``` + +試してみるには、 + +* いくつかのブラウザタブでアプリを開きます。 +* それらのタブでメッセージを記入してください。 +* そして、タブのうち1つを閉じてください。 + +これにより例外 `WebSocketDisconnect` が発生し、他のすべてのクライアントは次のようなメッセージを受信します。 + +``` +Client #1596980209979 left the chat +``` + +!!! tip "豆知識" + 上記のアプリは、複数の WebSocket 接続に対してメッセージを処理し、ブロードキャストする方法を示すための最小限のシンプルな例です。 + + しかし、すべての接続がメモリ内の単一のリストで処理されるため、プロセスの実行中にのみ機能し、単一のプロセスでのみ機能することに注意してください。 + + もしFastAPIと簡単に統合できて、RedisやPostgreSQLなどでサポートされている、より堅牢なものが必要なら、encode/broadcaster を確認してください。 + +## その他のドキュメント + +オプションの詳細については、Starletteのドキュメントを確認してください。 + +* `WebSocket` クラス +* クラスベースのWebSocket処理 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index b3f18bbdd..5bbcce605 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -86,6 +86,7 @@ nav: - advanced/response-directly.md - advanced/custom-response.md - advanced/nosql-databases.md + - advanced/websockets.md - advanced/conditional-openapi.md - async.md - デプロイ: From 86d4073632e642fd7253b0b2ec30d384183994d5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 13:59:09 +0000 Subject: [PATCH 0635/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 95e466ea9..9e210532e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/advanced/websockets.md`. PR [#4983](https://github.com/tiangolo/fastapi/pull/4983) by [@xryuseix](https://github.com/xryuseix). * 🛠 Add Arabic issue number to Notify Translations GitHub Action. PR [#5610](https://github.com/tiangolo/fastapi/pull/5610) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#5609](https://github.com/tiangolo/fastapi/pull/5609) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.0 to 2.24.1. PR [#5603](https://github.com/tiangolo/fastapi/pull/5603) by [@dependabot[bot]](https://github.com/apps/dependabot). From c040e3602a9c061e98d3c779e436998dffcfacdc Mon Sep 17 00:00:00 2001 From: Bruno Artur Torres Lopes Pereira <33462923+batlopes@users.noreply.github.com> Date: Sun, 13 Nov 2022 10:59:16 -0300 Subject: [PATCH 0636/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/request-forms-and-files?= =?UTF-8?q?.md`=20(#5579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../docs/tutorial/request-forms-and-files.md | 36 +++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 37 insertions(+) create mode 100644 docs/pt/docs/tutorial/request-forms-and-files.md diff --git a/docs/pt/docs/tutorial/request-forms-and-files.md b/docs/pt/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..259f262f4 --- /dev/null +++ b/docs/pt/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,36 @@ +# Formulários e Arquivos da Requisição + +Você pode definir arquivos e campos de formulário ao mesmo tempo usando `File` e `Form`. + +!!! info "Informação" + Para receber arquivos carregados e/ou dados de formulário, primeiro instale `python-multipart`. + + Por exemplo: `pip install python-multipart`. + + +## Importe `File` e `Form` + +```Python hl_lines="1" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +## Defina parâmetros de `File` e `Form` + +Crie parâmetros de arquivo e formulário da mesma forma que você faria para `Body` ou `Query`: + +```Python hl_lines="8" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +Os arquivos e campos de formulário serão carregados como dados de formulário e você receberá os arquivos e campos de formulário. + +E você pode declarar alguns dos arquivos como `bytes` e alguns como `UploadFile`. + +!!! warning "Aviso" + Você pode declarar vários parâmetros `File` e `Form` em uma *operação de caminho*, mas não é possível declarar campos `Body` para receber como JSON, pois a requisição terá o corpo codificado usando `multipart/form-data` ao invés de `application/json`. + + Isso não é uma limitação do **FastAPI** , é parte do protocolo HTTP. + +## Recapitulando + +Usar `File` e `Form` juntos quando precisar receber dados e arquivos na mesma requisição. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 3c50cc5f8..fdef810fa 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -75,6 +75,7 @@ nav: - tutorial/header-params.md - tutorial/response-status-code.md - tutorial/request-forms.md + - tutorial/request-forms-and-files.md - tutorial/handling-errors.md - Segurança: - tutorial/security/index.md From f57ccd8ef36b80749342af150430f95530abb45c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 13:59:55 +0000 Subject: [PATCH 0637/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9e210532e..f62b6de71 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms-and-files.md`. PR [#5579](https://github.com/tiangolo/fastapi/pull/5579) by [@batlopes](https://github.com/batlopes). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/websockets.md`. PR [#4983](https://github.com/tiangolo/fastapi/pull/4983) by [@xryuseix](https://github.com/xryuseix). * 🛠 Add Arabic issue number to Notify Translations GitHub Action. PR [#5610](https://github.com/tiangolo/fastapi/pull/5610) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump dawidd6/action-download-artifact from 2.24.1 to 2.24.2. PR [#5609](https://github.com/tiangolo/fastapi/pull/5609) by [@dependabot[bot]](https://github.com/apps/dependabot). From ba5310f73138eb23b8bcca43d6bbe95e1d3a9b3d Mon Sep 17 00:00:00 2001 From: axel584 Date: Sun, 13 Nov 2022 15:03:48 +0100 Subject: [PATCH 0638/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/advanced/additional-status-code.md`?= =?UTF-8?q?=20(#5477)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Maurin Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/fr/docs/advanced/additional-responses.md | 240 ++++++++++++++++++ .../docs/advanced/additional-status-codes.md | 37 +++ docs/fr/mkdocs.yml | 3 + 3 files changed, 280 insertions(+) create mode 100644 docs/fr/docs/advanced/additional-responses.md create mode 100644 docs/fr/docs/advanced/additional-status-codes.md diff --git a/docs/fr/docs/advanced/additional-responses.md b/docs/fr/docs/advanced/additional-responses.md new file mode 100644 index 000000000..35b57594d --- /dev/null +++ b/docs/fr/docs/advanced/additional-responses.md @@ -0,0 +1,240 @@ +# Réponses supplémentaires dans OpenAPI + +!!! Attention + Ceci concerne un sujet plutôt avancé. + + Si vous débutez avec **FastAPI**, vous n'en aurez peut-être pas besoin. + +Vous pouvez déclarer des réponses supplémentaires, avec des codes HTTP, des types de médias, des descriptions, etc. + +Ces réponses supplémentaires seront incluses dans le schéma OpenAPI, elles apparaîtront donc également dans la documentation de l'API. + +Mais pour ces réponses supplémentaires, vous devez vous assurer de renvoyer directement une `Response` comme `JSONResponse`, avec votre code HTTP et votre contenu. + +## Réponse supplémentaire avec `model` + +Vous pouvez ajouter à votre décorateur de *paramètre de chemin* un paramètre `responses`. + +Il prend comme valeur un `dict` dont les clés sont des codes HTTP pour chaque réponse, comme `200`, et la valeur de ces clés sont d'autres `dict` avec des informations pour chacun d'eux. + +Chacun de ces `dict` de réponse peut avoir une clé `model`, contenant un modèle Pydantic, tout comme `response_model`. + +**FastAPI** prendra ce modèle, générera son schéma JSON et l'inclura au bon endroit dans OpenAPI. + +Par exemple, pour déclarer une autre réponse avec un code HTTP `404` et un modèle Pydantic `Message`, vous pouvez écrire : + +```Python hl_lines="18 22" +{!../../../docs_src/additional_responses/tutorial001.py!} +``` + +!!! Remarque + Gardez à l'esprit que vous devez renvoyer directement `JSONResponse`. + +!!! Info + La clé `model` ne fait pas partie d'OpenAPI. + + **FastAPI** prendra le modèle Pydantic à partir de là, générera le `JSON Schema` et le placera au bon endroit. + + Le bon endroit est : + + * Dans la clé `content`, qui a pour valeur un autre objet JSON (`dict`) qui contient : + * Une clé avec le type de support, par ex. `application/json`, qui contient comme valeur un autre objet JSON, qui contient : + * Une clé `schema`, qui a pour valeur le schéma JSON du modèle, voici le bon endroit. + * **FastAPI** ajoute ici une référence aux schémas JSON globaux à un autre endroit de votre OpenAPI au lieu de l'inclure directement. De cette façon, d'autres applications et clients peuvent utiliser ces schémas JSON directement, fournir de meilleurs outils de génération de code, etc. + +Les réponses générées au format OpenAPI pour cette *opération de chemin* seront : + +```JSON hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} +``` + +Les schémas sont référencés à un autre endroit du modèle OpenAPI : + +```JSON hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + } + } + } +} +``` + +## Types de médias supplémentaires pour la réponse principale + +Vous pouvez utiliser ce même paramètre `responses` pour ajouter différents types de médias pour la même réponse principale. + +Par exemple, vous pouvez ajouter un type de média supplémentaire `image/png`, en déclarant que votre *opération de chemin* peut renvoyer un objet JSON (avec le type de média `application/json`) ou une image PNG : + +```Python hl_lines="19-24 28" +{!../../../docs_src/additional_responses/tutorial002.py!} +``` + +!!! Remarque + Notez que vous devez retourner l'image en utilisant directement un `FileResponse`. + +!!! Info + À moins que vous ne spécifiiez explicitement un type de média différent dans votre paramètre `responses`, FastAPI supposera que la réponse a le même type de média que la classe de réponse principale (par défaut `application/json`). + + Mais si vous avez spécifié une classe de réponse personnalisée avec `None` comme type de média, FastAPI utilisera `application/json` pour toute réponse supplémentaire associée à un modèle. + +## Combinaison d'informations + +Vous pouvez également combiner des informations de réponse provenant de plusieurs endroits, y compris les paramètres `response_model`, `status_code` et `responses`. + +Vous pouvez déclarer un `response_model`, en utilisant le code HTTP par défaut `200` (ou un code personnalisé si vous en avez besoin), puis déclarer des informations supplémentaires pour cette même réponse dans `responses`, directement dans le schéma OpenAPI. + +**FastAPI** conservera les informations supplémentaires des `responses` et les combinera avec le schéma JSON de votre modèle. + +Par exemple, vous pouvez déclarer une réponse avec un code HTTP `404` qui utilise un modèle Pydantic et a une `description` personnalisée. + +Et une réponse avec un code HTTP `200` qui utilise votre `response_model`, mais inclut un `example` personnalisé : + +```Python hl_lines="20-31" +{!../../../docs_src/additional_responses/tutorial003.py!} +``` + +Tout sera combiné et inclus dans votre OpenAPI, et affiché dans la documentation de l'API : + + + +## Combinez les réponses prédéfinies et les réponses personnalisées + +Vous voulez peut-être avoir des réponses prédéfinies qui s'appliquent à de nombreux *paramètre de chemin*, mais vous souhaitez les combiner avec des réponses personnalisées nécessaires à chaque *opération de chemin*. + +Dans ces cas, vous pouvez utiliser la technique Python "d'affection par décomposition" (appelé _unpacking_ en anglais) d'un `dict` avec `**dict_to_unpack` : + +``` Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +Ici, `new_dict` contiendra toutes les paires clé-valeur de `old_dict` plus la nouvelle paire clé-valeur : + +``` Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +Vous pouvez utiliser cette technique pour réutiliser certaines réponses prédéfinies dans vos *paramètres de chemin* et les combiner avec des réponses personnalisées supplémentaires. + +Par exemple: + +```Python hl_lines="13-17 26" +{!../../../docs_src/additional_responses/tutorial004.py!} +``` + +## Plus d'informations sur les réponses OpenAPI + +Pour voir exactement ce que vous pouvez inclure dans les réponses, vous pouvez consulter ces sections dans la spécification OpenAPI : + +* Objet Responses de OpenAPI , il inclut le `Response Object`. +* Objet Response de OpenAPI , vous pouvez inclure n'importe quoi directement dans chaque réponse à l'intérieur de votre paramètre `responses`. Y compris `description`, `headers`, `content` (à l'intérieur de cela, vous déclarez différents types de médias et schémas JSON) et `links`. diff --git a/docs/fr/docs/advanced/additional-status-codes.md b/docs/fr/docs/advanced/additional-status-codes.md new file mode 100644 index 000000000..e7b003707 --- /dev/null +++ b/docs/fr/docs/advanced/additional-status-codes.md @@ -0,0 +1,37 @@ +# Codes HTTP supplémentaires + +Par défaut, **FastAPI** renverra les réponses à l'aide d'une structure de données `JSONResponse`, en plaçant la réponse de votre *chemin d'accès* à l'intérieur de cette `JSONResponse`. + +Il utilisera le code HTTP par défaut ou celui que vous avez défini dans votre *chemin d'accès*. + +## Codes HTTP supplémentaires + +Si vous souhaitez renvoyer des codes HTTP supplémentaires en plus du code principal, vous pouvez le faire en renvoyant directement une `Response`, comme une `JSONResponse`, et en définissant directement le code HTTP supplémentaire. + +Par exemple, disons que vous voulez avoir un *chemin d'accès* qui permet de mettre à jour les éléments et renvoie les codes HTTP 200 "OK" en cas de succès. + +Mais vous voulez aussi qu'il accepte de nouveaux éléments. Et lorsque les éléments n'existaient pas auparavant, il les crée et renvoie un code HTTP de 201 "Créé". + +Pour y parvenir, importez `JSONResponse` et renvoyez-y directement votre contenu, en définissant le `status_code` que vous souhaitez : + +```Python hl_lines="4 25" +{!../../../docs_src/additional_status_codes/tutorial001.py!} +``` + +!!! Attention + Lorsque vous renvoyez une `Response` directement, comme dans l'exemple ci-dessus, elle sera renvoyée directement. + + Elle ne sera pas sérialisée avec un modèle. + + Assurez-vous qu'il contient les données souhaitées et que les valeurs soient dans un format JSON valides (si vous utilisez une `JSONResponse`). + +!!! note "Détails techniques" + Vous pouvez également utiliser `from starlette.responses import JSONResponse`. + + Pour plus de commodités, **FastAPI** fournit les objets `starlette.responses` sous forme d'un alias accessible par `fastapi.responses`. Mais la plupart des réponses disponibles proviennent directement de Starlette. Il en est de même avec l'objet `statut`. + +## Documents OpenAPI et API + +Si vous renvoyez directement des codes HTTP et des réponses supplémentaires, ils ne seront pas inclus dans le schéma OpenAPI (la documentation de l'API), car FastAPI n'a aucun moyen de savoir à l'avance ce que vous allez renvoyer. + +Mais vous pouvez documenter cela dans votre code, en utilisant : [Réponses supplémentaires dans OpenAPI](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 1c4f45682..7dce4b127 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -67,6 +67,9 @@ nav: - tutorial/query-params.md - tutorial/body.md - tutorial/background-tasks.md +- Guide utilisateur avancé: + - advanced/additional-status-codes.md + - advanced/additional-responses.md - async.md - Déploiement: - deployment/index.md From 5f67ac6fd61a1269a8f3a9c896b98906019e2b32 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 14:04:27 +0000 Subject: [PATCH 0639/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f62b6de71..1378e45d5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/advanced/additional-status-code.md`. PR [#5477](https://github.com/tiangolo/fastapi/pull/5477) by [@axel584](https://github.com/axel584). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms-and-files.md`. PR [#5579](https://github.com/tiangolo/fastapi/pull/5579) by [@batlopes](https://github.com/batlopes). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/websockets.md`. PR [#4983](https://github.com/tiangolo/fastapi/pull/4983) by [@xryuseix](https://github.com/xryuseix). * 🛠 Add Arabic issue number to Notify Translations GitHub Action. PR [#5610](https://github.com/tiangolo/fastapi/pull/5610) by [@tiangolo](https://github.com/tiangolo). From fdbd48be5fc6ef4bfd9bdd4582006f70b7a52317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Rubin?= Date: Sun, 13 Nov 2022 15:26:09 +0100 Subject: [PATCH 0640/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20Starlette=20to?= =?UTF-8?q?=20`0.21.0`,=20including=20the=20new=20[`TestClient`=20based=20?= =?UTF-8?q?on=20HTTPX](https://github.com/encode/starlette/releases/tag/0.?= =?UTF-8?q?21.0)=20(#5471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Paweł Rubin Co-authored-by: Sebastián Ramírez --- fastapi/security/api_key.py | 2 +- fastapi/security/http.py | 8 ++++---- fastapi/security/oauth2.py | 6 +++--- fastapi/security/open_id_connect_url.py | 2 +- fastapi/security/utils.py | 6 ++++-- pyproject.toml | 2 +- tests/test_enforce_once_required_parameter.py | 2 +- tests/test_extra_routes.py | 2 +- tests/test_get_request_body.py | 2 +- tests/test_param_include_in_schema.py | 9 ++++++--- tests/test_security_api_key_cookie.py | 7 ++++--- .../test_security_api_key_cookie_description.py | 7 ++++--- tests/test_security_api_key_cookie_optional.py | 7 ++++--- tests/test_tuples.py | 8 +++----- .../test_advanced_middleware/test_tutorial001.py | 2 +- .../test_tutorial/test_body/test_tutorial001.py | 16 +++++++++------- .../test_body/test_tutorial001_py310.py | 16 +++++++++------- .../test_cookie_params/test_tutorial001.py | 5 ++--- .../test_cookie_params/test_tutorial001_py310.py | 15 +++++---------- .../test_tutorial001.py | 2 +- .../test_custom_response/test_tutorial006.py | 2 +- .../test_custom_response/test_tutorial006b.py | 2 +- .../test_custom_response/test_tutorial006c.py | 2 +- .../test_tutorial006.py | 2 +- .../test_tutorial007.py | 6 +++--- .../test_websockets/test_tutorial002.py | 12 +++++++----- 26 files changed, 79 insertions(+), 73 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index bca5c721a..24ddbf482 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -54,7 +54,7 @@ class APIKeyHeader(APIKeyBase): self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: - api_key: str = request.headers.get(self.model.name) + api_key = request.headers.get(self.model.name) if not api_key: if self.auto_error: raise HTTPException( diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 1b473c69e..8b677299d 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -38,7 +38,7 @@ class HTTPBase(SecurityBase): async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: @@ -67,7 +67,7 @@ class HTTPBasic(HTTPBase): async def __call__( # type: ignore self, request: Request ) -> Optional[HTTPBasicCredentials]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if self.realm: unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} @@ -113,7 +113,7 @@ class HTTPBearer(HTTPBase): async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: @@ -148,7 +148,7 @@ class HTTPDigest(HTTPBase): async def __call__( self, request: Request ) -> Optional[HTTPAuthorizationCredentials]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 653c3010e..eb6b4277c 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -126,7 +126,7 @@ class OAuth2(SecurityBase): self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: raise HTTPException( @@ -157,7 +157,7 @@ class OAuth2PasswordBearer(OAuth2): ) async def __call__(self, request: Request) -> Optional[str]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: @@ -200,7 +200,7 @@ class OAuth2AuthorizationCodeBearer(OAuth2): ) async def __call__(self, request: Request) -> Optional[str]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": if self.auto_error: diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index dfe9f7b25..393614f7c 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -23,7 +23,7 @@ class OpenIdConnect(SecurityBase): self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: - authorization: str = request.headers.get("Authorization") + authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: raise HTTPException( diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index 2da0dd20f..fa7a450b7 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -1,7 +1,9 @@ -from typing import Tuple +from typing import Optional, Tuple -def get_authorization_scheme_param(authorization_header_value: str) -> Tuple[str, str]: +def get_authorization_scheme_param( + authorization_header_value: Optional[str], +) -> Tuple[str, str]: if not authorization_header_value: return "", "" scheme, _, param = authorization_header_value.partition(" ") diff --git a/pyproject.toml b/pyproject.toml index a23289fb1..fc4602ea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.20.4", + "starlette==0.21.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index ba8c7353f..bf05aa585 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -101,7 +101,7 @@ def test_schema(): def test_get_invalid(): - response = client.get("/foo", params={"client_id": None}) + response = client.get("/foo") assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index 491ba61c6..e979628a5 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -333,7 +333,7 @@ def test_get_api_route_not_decorated(): def test_delete(): - response = client.delete("/items/foo", json={"name": "Foo"}) + response = client.request("DELETE", "/items/foo", json={"name": "Foo"}) assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo", "item": {"name": "Foo", "price": None}} diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index 88b9d839f..52a052faa 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -104,5 +104,5 @@ def test_openapi_schema(): def test_get_with_body(): body = {"name": "Foo", "description": "Some description", "price": 5.5} - response = client.get("/product", json=body) + response = client.request("GET", "/product", json=body) assert response.json() == body diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index 214f039b6..cb182a1cd 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -33,8 +33,6 @@ async def hidden_query( return {"hidden_query": hidden_query} -client = TestClient(app) - openapi_shema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -161,6 +159,7 @@ openapi_shema = { def test_openapi_schema(): + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == openapi_shema @@ -184,7 +183,8 @@ def test_openapi_schema(): ], ) def test_hidden_cookie(path, cookies, expected_status, expected_response): - response = client.get(path, cookies=cookies) + client = TestClient(app, cookies=cookies) + response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response @@ -207,12 +207,14 @@ def test_hidden_cookie(path, cookies, expected_status, expected_response): ], ) def test_hidden_header(path, headers, expected_status, expected_response): + client = TestClient(app) response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response def test_hidden_path(): + client = TestClient(app) response = client.get("/hidden_path/hidden_path") assert response.status_code == 200 assert response.json() == {"hidden_path": "hidden_path"} @@ -234,6 +236,7 @@ def test_hidden_path(): ], ) def test_hidden_query(path, expected_status, expected_response): + client = TestClient(app) response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index a5b2e44f0..0bf4e9bb3 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -22,8 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -client = TestClient(app) - openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -51,18 +49,21 @@ openapi_schema = { def test_openapi_schema(): + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): - response = client.get("/users/me", cookies={"key": "secret"}) + client = TestClient(app, cookies={"key": "secret"}) + response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): + client = TestClient(app) response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index 2cd3565b4..ed4e65239 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -22,8 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -client = TestClient(app) - openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -56,18 +54,21 @@ openapi_schema = { def test_openapi_schema(): + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): - response = client.get("/users/me", cookies={"key": "secret"}) + client = TestClient(app, cookies={"key": "secret"}) + response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): + client = TestClient(app) response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index 96a64f09a..3e7aa81c0 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -29,8 +29,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -client = TestClient(app) - openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -58,18 +56,21 @@ openapi_schema = { def test_openapi_schema(): + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema def test_security_api_key(): - response = client.get("/users/me", cookies={"key": "secret"}) + client = TestClient(app, cookies={"key": "secret"}) + response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"username": "secret"} def test_security_api_key_no_key(): + client = TestClient(app) response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 18ec2d048..6e2cc0db6 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -252,16 +252,14 @@ def test_tuple_with_model_invalid(): def test_tuple_form_valid(): - response = client.post("/tuple-form/", data=[("values", "1"), ("values", "2")]) + response = client.post("/tuple-form/", data={"values": ("1", "2")}) assert response.status_code == 200, response.text assert response.json() == [1, 2] def test_tuple_form_invalid(): - response = client.post( - "/tuple-form/", data=[("values", "1"), ("values", "2"), ("values", "3")] - ) + response = client.post("/tuple-form/", data={"values": ("1", "2", "3")}) assert response.status_code == 422, response.text - response = client.post("/tuple-form/", data=[("values", "1")]) + response = client.post("/tuple-form/", data={"values": ("1")}) assert response.status_code == 422, response.text diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py index 17165c0fc..157fa5caf 100644 --- a/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial001.py @@ -9,6 +9,6 @@ def test_middleware(): assert response.status_code == 200, response.text client = TestClient(app) - response = client.get("/", allow_redirects=False) + response = client.get("/", follow_redirects=False) assert response.status_code == 307, response.text assert response.headers["location"] == "https://testserver/" diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 8dbaf15db..65cdc758a 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -176,7 +176,7 @@ def test_post_broken_body(): response = client.post( "/items/", headers={"content-type": "application/json"}, - data="{some broken json}", + content="{some broken json}", ) assert response.status_code == 422, response.text assert response.json() == { @@ -214,7 +214,7 @@ def test_post_form_for_json(): def test_explicit_content_type(): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', headers={"Content-Type": "application/json"}, ) assert response.status_code == 200, response.text @@ -223,7 +223,7 @@ def test_explicit_content_type(): def test_geo_json(): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', headers={"Content-Type": "application/geo+json"}, ) assert response.status_code == 200, response.text @@ -232,7 +232,7 @@ def test_geo_json(): def test_no_content_type_is_json(): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', ) assert response.status_code == 200, response.text assert response.json() == { @@ -255,17 +255,19 @@ def test_wrong_headers(): ] } - response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"}) + response = client.post( + "/items/", content=data, headers={"Content-Type": "text/plain"} + ) assert response.status_code == 422, response.text assert response.json() == invalid_dict response = client.post( - "/items/", data=data, headers={"Content-Type": "application/geo+json-seq"} + "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text assert response.json() == invalid_dict response = client.post( - "/items/", data=data, headers={"Content-Type": "application/not-really-json"} + "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text assert response.json() == invalid_dict diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index dd9d9911e..83bcb68f3 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -185,7 +185,7 @@ def test_post_broken_body(client: TestClient): response = client.post( "/items/", headers={"content-type": "application/json"}, - data="{some broken json}", + content="{some broken json}", ) assert response.status_code == 422, response.text assert response.json() == { @@ -225,7 +225,7 @@ def test_post_form_for_json(client: TestClient): def test_explicit_content_type(client: TestClient): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', headers={"Content-Type": "application/json"}, ) assert response.status_code == 200, response.text @@ -235,7 +235,7 @@ def test_explicit_content_type(client: TestClient): def test_geo_json(client: TestClient): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', headers={"Content-Type": "application/geo+json"}, ) assert response.status_code == 200, response.text @@ -245,7 +245,7 @@ def test_geo_json(client: TestClient): def test_no_content_type_is_json(client: TestClient): response = client.post( "/items/", - data='{"name": "Foo", "price": 50.5}', + content='{"name": "Foo", "price": 50.5}', ) assert response.status_code == 200, response.text assert response.json() == { @@ -269,17 +269,19 @@ def test_wrong_headers(client: TestClient): ] } - response = client.post("/items/", data=data, headers={"Content-Type": "text/plain"}) + response = client.post( + "/items/", content=data, headers={"Content-Type": "text/plain"} + ) assert response.status_code == 422, response.text assert response.json() == invalid_dict response = client.post( - "/items/", data=data, headers={"Content-Type": "application/geo+json-seq"} + "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text assert response.json() == invalid_dict response = client.post( - "/items/", data=data, headers={"Content-Type": "application/not-really-json"} + "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text assert response.json() == invalid_dict diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index edccffec1..38ae211db 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -3,8 +3,6 @@ from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001 import app -client = TestClient(app) - openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -88,6 +86,7 @@ openapi_schema = { ], ) def test(path, cookies, expected_status, expected_response): - response = client.get(path, cookies=cookies) + client = TestClient(app, cookies=cookies) + response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index 5caa5c440..5ad52fb5e 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -70,14 +70,6 @@ openapi_schema = { } -@pytest.fixture(name="client") -def get_client(): - from docs_src.cookie_params.tutorial001_py310 import app - - client = TestClient(app) - return client - - @needs_py310 @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", @@ -94,7 +86,10 @@ def get_client(): ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), ], ) -def test(path, cookies, expected_status, expected_response, client: TestClient): - response = client.get(path, cookies=cookies) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_py310 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py index 3eb5822e2..e6da630e8 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial001.py @@ -26,7 +26,7 @@ def test_gzip_request(compress): data = gzip.compress(data) headers["Content-Encoding"] = "gzip" headers["Content-Type"] = "application/json" - response = client.post("/sum", data=data, headers=headers) + response = client.post("/sum", content=data, headers=headers) assert response.json() == {"sum": n} diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 72bbfd277..9b10916e5 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -32,6 +32,6 @@ def test_openapi_schema(): def test_get(): - response = client.get("/typer", allow_redirects=False) + response = client.get("/typer", follow_redirects=False) assert response.status_code == 307, response.text assert response.headers["location"] == "https://typer.tiangolo.com" diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index ac5a76d34..b3e60e86a 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -27,6 +27,6 @@ def test_openapi_schema(): def test_redirect_response_class(): - response = client.get("/fastapi", allow_redirects=False) + response = client.get("/fastapi", follow_redirects=False) assert response.status_code == 307 assert response.headers["location"] == "https://fastapi.tiangolo.com" diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index 009225e8c..0cb6ddaa3 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -27,6 +27,6 @@ def test_openapi_schema(): def test_redirect_status_code(): - response = client.get("/pydantic", allow_redirects=False) + response = client.get("/pydantic", follow_redirects=False) assert response.status_code == 302 assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/" diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index 5533b2957..330b4e2c7 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -47,7 +47,7 @@ def test_openapi_schema(): def test_post(): - response = client.post("/items/", data=b"this is actually not validated") + response = client.post("/items/", content=b"this is actually not validated") assert response.status_code == 200, response.text assert response.json() == { "size": 30, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index cb5dbc8eb..076f60b2f 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -58,7 +58,7 @@ def test_post(): - x-men - x-avengers """ - response = client.post("/items/", data=yaml_data) + response = client.post("/items/", content=yaml_data) assert response.status_code == 200, response.text assert response.json() == { "name": "Deadpoolio", @@ -74,7 +74,7 @@ def test_post_broken_yaml(): x - x-men x - x-avengers """ - response = client.post("/items/", data=yaml_data) + response = client.post("/items/", content=yaml_data) assert response.status_code == 422, response.text assert response.json() == {"detail": "Invalid YAML"} @@ -88,7 +88,7 @@ def test_post_invalid(): - x-avengers - sneaky: object """ - response = client.post("/items/", data=yaml_data) + response = client.post("/items/", content=yaml_data) assert response.status_code == 422, response.text assert response.json() == { "detail": [ diff --git a/tests/test_tutorial/test_websockets/test_tutorial002.py b/tests/test_tutorial/test_websockets/test_tutorial002.py index a8523c9c4..bb5ccbf8e 100644 --- a/tests/test_tutorial/test_websockets/test_tutorial002.py +++ b/tests/test_tutorial/test_websockets/test_tutorial002.py @@ -4,20 +4,18 @@ from fastapi.websockets import WebSocketDisconnect from docs_src.websockets.tutorial002 import app -client = TestClient(app) - def test_main(): + client = TestClient(app) response = client.get("/") assert response.status_code == 200, response.text assert b"" in response.content def test_websocket_with_cookie(): + client = TestClient(app, cookies={"session": "fakesession"}) with pytest.raises(WebSocketDisconnect): - with client.websocket_connect( - "/items/foo/ws", cookies={"session": "fakesession"} - ) as websocket: + with client.websocket_connect("/items/foo/ws") as websocket: message = "Message one" websocket.send_text(message) data = websocket.receive_text() @@ -33,6 +31,7 @@ def test_websocket_with_cookie(): def test_websocket_with_header(): + client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: message = "Message one" @@ -50,6 +49,7 @@ def test_websocket_with_header(): def test_websocket_with_header_and_query(): + client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: message = "Message one" @@ -71,6 +71,7 @@ def test_websocket_with_header_and_query(): def test_websocket_no_credentials(): + client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws"): pytest.fail( @@ -79,6 +80,7 @@ def test_websocket_no_credentials(): def test_websocket_invalid_data(): + client = TestClient(app) with pytest.raises(WebSocketDisconnect): with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): pytest.fail( From 46b903f70b586ae0019185e6793c6a400e6bb90c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 14:26:43 +0000 Subject: [PATCH 0641/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1378e45d5..f5d30d7ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade Starlette to `0.21.0`, including the new [`TestClient` based on HTTPX](https://github.com/encode/starlette/releases/tag/0.21.0). PR [#5471](https://github.com/tiangolo/fastapi/pull/5471) by [@pawelrubin](https://github.com/pawelrubin). * 🌐 Add French translation for `docs/fr/docs/advanced/additional-status-code.md`. PR [#5477](https://github.com/tiangolo/fastapi/pull/5477) by [@axel584](https://github.com/axel584). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms-and-files.md`. PR [#5579](https://github.com/tiangolo/fastapi/pull/5579) by [@batlopes](https://github.com/batlopes). * 🌐 Add Japanese translation for `docs/ja/docs/advanced/websockets.md`. PR [#4983](https://github.com/tiangolo/fastapi/pull/4983) by [@xryuseix](https://github.com/xryuseix). From 57141ccac48c6f585bf13141d0f8315ea639c2cb Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 14:35:13 +0000 Subject: [PATCH 0642/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f5d30d7ef..b6c4f4a9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix highlight lines for Japanese translation for `docs/tutorial/query-params.md`. PR [#2969](https://github.com/tiangolo/fastapi/pull/2969) by [@ftnext](https://github.com/ftnext). * ⬆ Upgrade Starlette to `0.21.0`, including the new [`TestClient` based on HTTPX](https://github.com/encode/starlette/releases/tag/0.21.0). PR [#5471](https://github.com/tiangolo/fastapi/pull/5471) by [@pawelrubin](https://github.com/pawelrubin). * 🌐 Add French translation for `docs/fr/docs/advanced/additional-status-code.md`. PR [#5477](https://github.com/tiangolo/fastapi/pull/5477) by [@axel584](https://github.com/axel584). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/request-forms-and-files.md`. PR [#5579](https://github.com/tiangolo/fastapi/pull/5579) by [@batlopes](https://github.com/batlopes). From f92f87d379cc5d3c9b3c159d74639411617cb0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 16:20:05 +0100 Subject: [PATCH 0643/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20references=20?= =?UTF-8?q?to=20Requests=20for=20tests=20to=20HTTPX,=20and=20add=20HTTPX?= =?UTF-8?q?=20to=20extras=20(#5628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- docs/az/docs/index.md | 2 +- docs/de/docs/features.md | 2 +- docs/de/docs/index.md | 2 +- docs/en/docs/advanced/async-tests.md | 14 ++++---------- docs/en/docs/advanced/openapi-callbacks.md | 4 ++-- docs/en/docs/alternatives.md | 2 +- docs/en/docs/features.md | 2 +- docs/en/docs/index.md | 4 ++-- docs/en/docs/tutorial/testing.md | 12 ++++++------ docs/es/docs/features.md | 2 +- docs/es/docs/index.md | 4 ++-- docs/fa/docs/index.md | 4 ++-- docs/fr/docs/index.md | 4 ++-- docs/he/docs/index.md | 2 +- docs/id/docs/index.md | 4 ++-- docs/it/docs/index.md | 4 ++-- docs/ja/docs/features.md | 2 +- docs/ja/docs/index.md | 4 ++-- docs/ja/docs/tutorial/testing.md | 8 ++++---- docs/ko/docs/index.md | 4 ++-- docs/nl/docs/index.md | 4 ++-- docs/pl/docs/index.md | 4 ++-- docs/pt/docs/features.md | 2 +- docs/pt/docs/index.md | 4 ++-- docs/ru/docs/features.md | 2 +- docs/ru/docs/index.md | 4 ++-- docs/sq/docs/index.md | 4 ++-- docs/sv/docs/index.md | 4 ++-- docs/tr/docs/features.md | 2 +- docs/tr/docs/index.md | 4 ++-- docs/uk/docs/index.md | 4 ++-- docs/zh/docs/features.md | 2 +- docs/zh/docs/index.md | 4 ++-- pyproject.toml | 3 +-- tests/test_security_http_basic_optional.py | 4 +--- tests/test_security_http_basic_realm.py | 4 +--- .../test_security_http_basic_realm_description.py | 4 +--- .../test_security/test_tutorial006.py | 4 +--- 39 files changed, 69 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index fe0ad49de..7c4a6c4b4 100644 --- a/README.md +++ b/README.md @@ -427,7 +427,7 @@ For a more complete example including more features, see the Strawberry and other libraries. * Many extra features (thanks to Starlette) as: * **WebSockets** - * extremely easy tests based on `requests` and `pytest` + * extremely easy tests based on HTTPX and `pytest` * **CORS** * **Cookie Sessions** * ...and more. @@ -447,7 +447,7 @@ Used by Pydantic: Used by Starlette: -* requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md index 3129f9dc6..282c15032 100644 --- a/docs/az/docs/index.md +++ b/docs/az/docs/index.md @@ -446,7 +446,7 @@ Used by Pydantic: Used by Starlette: -* requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index f825472a9..f281afd1e 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -169,7 +169,7 @@ Mit **FastAPI** bekommen Sie viele von **Starlette**'s Funktionen (da FastAPI nu * **WebSocket**-Unterstützung. * Hintergrundaufgaben im selben Prozess. * Ereignisse für das Starten und Herunterfahren. -* Testclient basierend auf `requests`. +* Testclient basierend auf HTTPX. * **CORS**, GZip, statische Dateien, Antwortfluss. * **Sitzungs und Cookie** Unterstützung. * 100% Testabdeckung. diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 07f51b1be..68fc8b753 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -445,7 +445,7 @@ Used by Pydantic: Used by Starlette: -* requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index e34f48f17..9b39d70fc 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -1,6 +1,6 @@ # Async Tests -You have already seen how to test your **FastAPI** applications using the provided `TestClient`, but with it, you can't test or run any other `async` function in your (synchronous) pytest functions. +You have already seen how to test your **FastAPI** applications using the provided `TestClient`. Up to now, you have only seen how to write synchronous tests, without using `async` functions. Being able to use asynchronous functions in your tests could be useful, for example, when you're querying your database asynchronously. Imagine you want to test sending requests to your FastAPI application and then verify that your backend successfully wrote the correct data in the database, while using an async database library. @@ -8,7 +8,7 @@ Let's look at how we can make that work. ## pytest.mark.anyio -If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. Anyio provides a neat plugin for this, that allows us to specify that some test functions are to be called asynchronously. +If we want to call asynchronous functions in our tests, our test functions have to be asynchronous. AnyIO provides a neat plugin for this, that allows us to specify that some test functions are to be called asynchronously. ## HTTPX @@ -16,13 +16,7 @@ Even if your **FastAPI** application uses normal `def` functions instead of `asy The `TestClient` does some magic inside to call the asynchronous FastAPI application in your normal `def` test functions, using standard pytest. But that magic doesn't work anymore when we're using it inside asynchronous functions. By running our tests asynchronously, we can no longer use the `TestClient` inside our test functions. -Luckily there's a nice alternative, called HTTPX. - -HTTPX is an HTTP client for Python 3 that allows us to query our FastAPI application similarly to how we did it with the `TestClient`. - -If you're familiar with the Requests library, you'll find that the API of HTTPX is almost identical. - -The important difference for us is that with HTTPX we are not limited to synchronous, but can also make asynchronous requests. +The `TestClient` is based on HTTPX, and luckily, we can use it directly to test the API. ## Example @@ -85,7 +79,7 @@ This is the equivalent to: response = client.get('/') ``` -that we used to make our requests with the `TestClient`. +...that we used to make our requests with the `TestClient`. !!! tip Note that we're using async/await with the new `AsyncClient` - the request is asynchronous. diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 656ddbb3f..71924ce8b 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -50,7 +50,7 @@ It could be just one or two lines of code, like: ```Python callback_url = "https://example.com/api/v1/invoices/events/" -requests.post(callback_url, json={"description": "Invoice paid", "paid": True}) +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) ``` But possibly the most important part of the callback is making sure that your API user (the external developer) implements the *external API* correctly, according to the data that *your API* is going to send in the request body of the callback, etc. @@ -64,7 +64,7 @@ This example doesn't implement the callback itself (that could be just a line of !!! tip The actual callback is just an HTTP request. - When implementing the callback yourself, you could use something like HTTPX or Requests. + When implementing the callback yourself, you could use something like HTTPX or Requests. ## Write the callback documentation code diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index bcd406bf9..0f074ccf3 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -367,7 +367,7 @@ It has: * WebSocket support. * In-process background tasks. * Startup and shutdown events. -* Test client built on requests. +* Test client built on HTTPX. * CORS, GZip, Static Files, Streaming responses. * Session and Cookie support. * 100% test coverage. diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index 02bb3ac1f..387ff86c9 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -166,7 +166,7 @@ With **FastAPI** you get all of **Starlette**'s features (as FastAPI is just Sta * **WebSocket** support. * In-process background tasks. * Startup and shutdown events. -* Test client built on `requests`. +* Test client built on HTTPX. * **CORS**, GZip, Static Files, Streaming responses. * **Session and Cookie** support. * 100% test coverage. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 1ad9c7606..deb8ab5d5 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -424,7 +424,7 @@ For a more complete example including more features, see the Strawberry and other libraries. * Many extra features (thanks to Starlette) as: * **WebSockets** - * extremely easy tests based on `requests` and `pytest` + * extremely easy tests based on HTTPX and `pytest` * **CORS** * **Cookie Sessions** * ...and more. @@ -444,7 +444,7 @@ Used by Pydantic: Used by Starlette: -* requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index d2ccd7dc7..be07aab37 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -2,16 +2,16 @@ Thanks to Starlette, testing **FastAPI** applications is easy and enjoyable. -It is based on Requests, so it's very familiar and intuitive. +It is based on HTTPX, which in turn is designed based on Requests, so it's very familiar and intuitive. With it, you can use pytest directly with **FastAPI**. ## Using `TestClient` !!! info - To use `TestClient`, first install `requests`. + To use `TestClient`, first install `httpx`. - E.g. `pip install requests`. + E.g. `pip install httpx`. Import `TestClient`. @@ -19,7 +19,7 @@ Create a `TestClient` by passing your **FastAPI** application to it. Create functions with a name that starts with `test_` (this is standard `pytest` conventions). -Use the `TestClient` object the same way as you do with `requests`. +Use the `TestClient` object the same way as you do with `httpx`. Write simple `assert` statements with the standard Python expressions that you need to check (again, standard `pytest`). @@ -130,7 +130,7 @@ You could then update `test_main.py` with the extended tests: {!> ../../../docs_src/app_testing/app_b/test_main.py!} ``` -Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `requests`. +Whenever you need the client to pass information in the request and you don't know how to, you can search (Google) how to do it in `httpx`, or even how to do it with `requests`, as HTTPX's design is based on Requests' design. Then you just do the same in your tests. @@ -142,7 +142,7 @@ E.g.: * To pass *headers*, use a `dict` in the `headers` parameter. * For *cookies*, a `dict` in the `cookies` parameter. -For more information about how to pass data to the backend (using `requests` or the `TestClient`) check the Requests documentation. +For more information about how to pass data to the backend (using `httpx` or the `TestClient`) check the HTTPX documentation. !!! info Note that the `TestClient` receives data that can be converted to JSON, not Pydantic models. diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index 3c59eb88c..5d6b6509a 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -167,7 +167,7 @@ Con **FastAPI** obtienes todas las características de **Starlette** (porque Fas * Soporte para **GraphQL**. * Tareas en background. * Eventos de startup y shutdown. -* Cliente de pruebas construido con `requests`. +* Cliente de pruebas construido con HTTPX. * **CORS**, GZip, Static Files, Streaming responses. * Soporte para **Session and Cookie**. * Cobertura de pruebas al 100%. diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index aa3fa2228..727a6617b 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -418,7 +418,7 @@ Para un ejemplo más completo que incluye más características ve el requests - Requerido si quieres usar el `TestClient`. +* httpx - Requerido si quieres usar el `TestClient`. * jinja2 - Requerido si quieres usar la configuración por defecto de templates. * python-multipart - Requerido si quieres dar soporte a "parsing" de formularios, con `request.form()`. * itsdangerous - Requerido para dar soporte a `SessionMiddleware`. diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index 0f7cd569a..dfc4d24e3 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -421,7 +421,7 @@ item: Item * قابلیت‌های اضافی دیگر (بر اساس Starlette) شامل: * **وب‌سوکت** * **GraphQL** - * تست‌های خودکار آسان مبتنی بر `requests` و `pytest` + * تست‌های خودکار آسان مبتنی بر HTTPX و `pytest` * **CORS** * **Cookie Sessions** * و موارد بیشمار دیگر. @@ -441,7 +441,7 @@ item: Item استفاده شده توسط Starlette: -* requests - در صورتی که می‌خواهید از `TestClient` استفاده کنید. +* HTTPX - در صورتی که می‌خواهید از `TestClient` استفاده کنید. * aiofiles - در صورتی که می‌خواهید از `FileResponse` و `StaticFiles` استفاده کنید. * jinja2 - در صورتی که بخواهید از پیکربندی پیش‌فرض برای قالب‌ها استفاده کنید. * python-multipart - در صورتی که بخواهید با استفاده از `request.form()` از قابلیت "تجزیه (parse)" فرم استفاده کنید. diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 695204458..e7fb9947d 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -426,7 +426,7 @@ For a more complete example including more features, see the requests - Required if you want to use the `TestClient`. +* HTTPX - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/he/docs/index.md b/docs/he/docs/index.md index fa63d8cb7..19f2f2041 100644 --- a/docs/he/docs/index.md +++ b/docs/he/docs/index.md @@ -445,7 +445,7 @@ item: Item בשימוש Starlette: -- requests - דרוש אם ברצונכם להשתמש ב - `TestClient`. +- httpx - דרוש אם ברצונכם להשתמש ב - `TestClient`. - jinja2 - דרוש אם ברצונכם להשתמש בברירת המחדל של תצורת הטמפלייטים. - python-multipart - דרוש אם ברצונכם לתמוך ב "פרסור" טפסים, באצמעות request.form(). - itsdangerous - דרוש אם ברצונכם להשתמש ב - `SessionMiddleware`. diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md index 3129f9dc6..66fc2859e 100644 --- a/docs/id/docs/index.md +++ b/docs/id/docs/index.md @@ -426,7 +426,7 @@ For a more complete example including more features, see the requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md index 852a5e56e..9d95dd6d7 100644 --- a/docs/it/docs/index.md +++ b/docs/it/docs/index.md @@ -423,7 +423,7 @@ For a more complete example including more features, see the requests - Required if you want to use the `TestClient`. +* httpx - Required if you want to use the `TestClient`. * jinja2 - Required if you want to use the default template configuration. * python-multipart - Required if you want to support form "parsing", with `request.form()`. * itsdangerous - Required for `SessionMiddleware` support. diff --git a/docs/ja/docs/features.md b/docs/ja/docs/features.md index 5ea68515d..a40b48cf0 100644 --- a/docs/ja/docs/features.md +++ b/docs/ja/docs/features.md @@ -169,7 +169,7 @@ FastAPIには非常に使いやすく、非常に強力なrequests - 使用 `TestClient` 时安装。 +* httpx - 使用 `TestClient` 时安装。 * jinja2 - 使用默认模板配置时安装。 * python-multipart - 需要通过 `request.form()` 对表单进行「解析」时安装。 * itsdangerous - 需要 `SessionMiddleware` 支持时安装。 diff --git a/pyproject.toml b/pyproject.toml index fc4602ea8..af20c8ef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ test = [ "flake8 >=3.8.3,<6.0.0", "black == 22.8.0", "isort >=5.0.6,<6.0.0", - "requests >=2.24.0,<3.0.0", "httpx >=0.23.0,<0.24.0", "email_validator >=1.1.1,<2.0.0", # TODO: once removing databases from tutorial, upgrade SQLAlchemy @@ -94,7 +93,7 @@ dev = [ "pre-commit >=2.17.0,<3.0.0", ] all = [ - "requests >=2.24.0,<3.0.0", + "httpx >=0.23.0,<0.24.0", "jinja2 >=2.11.2,<4.0.0", "python-multipart >=0.0.5,<0.0.6", "itsdangerous >=1.1.0,<3.0.0", diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 289bd5c74..91824d223 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -4,7 +4,6 @@ from typing import Optional from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient -from requests.auth import HTTPBasicAuth app = FastAPI() @@ -51,8 +50,7 @@ def test_openapi_schema(): def test_security_http_basic(): - auth = HTTPBasicAuth(username="john", password="secret") - response = client.get("/users/me", auth=auth) + response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 54867c2e0..6d760c0f9 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -3,7 +3,6 @@ from base64 import b64encode from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient -from requests.auth import HTTPBasicAuth app = FastAPI() @@ -48,8 +47,7 @@ def test_openapi_schema(): def test_security_http_basic(): - auth = HTTPBasicAuth(username="john", password="secret") - response = client.get("/users/me", auth=auth) + response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 6ff9d9d07..7cc547561 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -3,7 +3,6 @@ from base64 import b64encode from fastapi import FastAPI, Security from fastapi.security import HTTPBasic, HTTPBasicCredentials from fastapi.testclient import TestClient -from requests.auth import HTTPBasicAuth app = FastAPI() @@ -54,8 +53,7 @@ def test_openapi_schema(): def test_security_http_basic(): - auth = HTTPBasicAuth(username="john", password="secret") - response = client.get("/users/me", auth=auth) + response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 3b0a36ebc..bbfef9f7c 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -1,7 +1,6 @@ from base64 import b64encode from fastapi.testclient import TestClient -from requests.auth import HTTPBasicAuth from docs_src.security.tutorial006 import app @@ -38,8 +37,7 @@ def test_openapi_schema(): def test_security_http_basic(): - auth = HTTPBasicAuth(username="john", password="secret") - response = client.get("/users/me", auth=auth) + response = client.get("/users/me", auth=("john", "secret")) assert response.status_code == 200, response.text assert response.json() == {"username": "john", "password": "secret"} From 1c93d5523a74fa6fe47534ad0e01392e20b31087 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 15:20:44 +0000 Subject: [PATCH 0644/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b6c4f4a9a..18a9efbda 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update references to Requests for tests to HTTPX, and add HTTPX to extras. PR [#5628](https://github.com/tiangolo/fastapi/pull/5628) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix highlight lines for Japanese translation for `docs/tutorial/query-params.md`. PR [#2969](https://github.com/tiangolo/fastapi/pull/2969) by [@ftnext](https://github.com/ftnext). * ⬆ Upgrade Starlette to `0.21.0`, including the new [`TestClient` based on HTTPX](https://github.com/encode/starlette/releases/tag/0.21.0). PR [#5471](https://github.com/tiangolo/fastapi/pull/5471) by [@pawelrubin](https://github.com/pawelrubin). * 🌐 Add French translation for `docs/fr/docs/advanced/additional-status-code.md`. PR [#5477](https://github.com/tiangolo/fastapi/pull/5477) by [@axel584](https://github.com/axel584). From d537ee93d707d392d75ffbe7ff08e4ff70b75729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 17:10:54 +0100 Subject: [PATCH 0645/1881] =?UTF-8?q?=E2=9C=A8=20Re-export=20Starlette's?= =?UTF-8?q?=20`WebSocketException`=20and=20add=20it=20to=20docs=20(#5629)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/websockets.md | 6 ++---- docs_src/websockets/tutorial002.py | 12 ++++++++++-- fastapi/__init__.py | 1 + fastapi/exceptions.py | 1 + 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 0e9bc5b06..3cf840819 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -112,17 +112,15 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: -```Python hl_lines="58-65 68-83" +```Python hl_lines="66-77 76-91" {!../../../docs_src/websockets/tutorial002.py!} ``` !!! info - In a WebSocket it doesn't really make sense to raise an `HTTPException`. So it's better to close the WebSocket connection directly. + As this is a WebSocket it doesn't really make sense to raise an `HTTPException`, instead we raise a `WebSocketException`. You can use a closing code from the valid codes defined in the specification. - In the future, there will be a `WebSocketException` that you will be able to `raise` from anywhere, and add exception handlers for it. It depends on the PR #527 in Starlette. - ### Try the WebSockets with dependencies If your file is named `main.py`, run your application with: diff --git a/docs_src/websockets/tutorial002.py b/docs_src/websockets/tutorial002.py index cf5c7e805..cab749e4d 100644 --- a/docs_src/websockets/tutorial002.py +++ b/docs_src/websockets/tutorial002.py @@ -1,6 +1,14 @@ from typing import Union -from fastapi import Cookie, Depends, FastAPI, Query, WebSocket, status +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) from fastapi.responses import HTMLResponse app = FastAPI() @@ -61,7 +69,7 @@ async def get_cookie_or_token( token: Union[str, None] = Query(default=None), ): if session is None and token is None: - await websocket.close(code=status.WS_1008_POLICY_VIOLATION) + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) return session or token diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a5c7aeb17..70f363c89 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -8,6 +8,7 @@ from .applications import FastAPI as FastAPI from .background import BackgroundTasks as BackgroundTasks from .datastructures import UploadFile as UploadFile from .exceptions import HTTPException as HTTPException +from .exceptions import WebSocketException as WebSocketException from .param_functions import Body as Body from .param_functions import Cookie as Cookie from .param_functions import Depends as Depends diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 0f50acc6c..ca097b1ce 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional, Sequence, Type from pydantic import BaseModel, ValidationError, create_model from pydantic.error_wrappers import ErrorList from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 class HTTPException(StarletteHTTPException): From bcd9ab95e1fc4b702350b3075f27672ab6e356ba Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 16:11:29 +0000 Subject: [PATCH 0646/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 18a9efbda..ae78141d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Re-export Starlette's `WebSocketException` and add it to docs. PR [#5629](https://github.com/tiangolo/fastapi/pull/5629) by [@tiangolo](https://github.com/tiangolo). * 📝 Update references to Requests for tests to HTTPX, and add HTTPX to extras. PR [#5628](https://github.com/tiangolo/fastapi/pull/5628) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix highlight lines for Japanese translation for `docs/tutorial/query-params.md`. PR [#2969](https://github.com/tiangolo/fastapi/pull/2969) by [@ftnext](https://github.com/ftnext). * ⬆ Upgrade Starlette to `0.21.0`, including the new [`TestClient` based on HTTPX](https://github.com/encode/starlette/releases/tag/0.21.0). PR [#5471](https://github.com/tiangolo/fastapi/pull/5471) by [@pawelrubin](https://github.com/pawelrubin). From fa74093440aaff710009ed23646eb804417b26fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 19:19:04 +0100 Subject: [PATCH 0647/1881] =?UTF-8?q?=E2=9C=A8=20Use=20Ruff=20for=20lintin?= =?UTF-8?q?g=20(#5630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 5 ---- .pre-commit-config.yaml | 15 ++++-------- docs_src/security/tutorial005.py | 2 +- docs_src/security/tutorial005_py310.py | 2 +- docs_src/security/tutorial005_py39.py | 2 +- fastapi/dependencies/utils.py | 12 +++++----- fastapi/routing.py | 2 +- pyproject.toml | 32 +++++++++++++++++++++++--- scripts/docs.py | 4 ++-- scripts/format.sh | 2 +- scripts/lint.sh | 2 +- tests/test_custom_route_class.py | 6 ++--- tests/test_ws_router.py | 2 +- 13 files changed, 51 insertions(+), 37 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 47ef7c07f..000000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 88 -select = C,E,F,W,B,B9 -ignore = E203, E501, W503 -exclude = __init__.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bd5b8641a..e59e05abe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,19 +18,12 @@ repos: args: - --py3-plus - --keep-runtime-typing -- repo: https://github.com/PyCQA/autoflake - rev: v1.7.7 +- repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.114 hooks: - - id: autoflake + - id: ruff args: - - --recursive - - --in-place - - --remove-all-unused-imports - - --remove-unused-variables - - --expand-star-imports - - --exclude - - __init__.py - - --remove-duplicate-keys + - --fix - repo: https://github.com/pycqa/isort rev: 5.10.1 hooks: diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index ab3af9a6a..bd0a33581 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -107,7 +107,7 @@ async def get_current_user( if security_scopes.scopes: authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' else: - authenticate_value = f"Bearer" + authenticate_value = "Bearer" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index c6a095d2c..ba756ef4f 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -106,7 +106,7 @@ async def get_current_user( if security_scopes.scopes: authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' else: - authenticate_value = f"Bearer" + authenticate_value = "Bearer" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 38391308a..9e4dbcffb 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -107,7 +107,7 @@ async def get_current_user( if security_scopes.scopes: authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' else: - authenticate_value = f"Bearer" + authenticate_value = "Bearer" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 64a6c1276..3df5ccfc8 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -426,21 +426,21 @@ def is_coroutine_callable(call: Callable[..., Any]) -> bool: return inspect.iscoroutinefunction(call) if inspect.isclass(call): return False - dunder_call = getattr(call, "__call__", None) + dunder_call = getattr(call, "__call__", None) # noqa: B004 return inspect.iscoroutinefunction(dunder_call) def is_async_gen_callable(call: Callable[..., Any]) -> bool: if inspect.isasyncgenfunction(call): return True - dunder_call = getattr(call, "__call__", None) + dunder_call = getattr(call, "__call__", None) # noqa: B004 return inspect.isasyncgenfunction(dunder_call) def is_gen_callable(call: Callable[..., Any]) -> bool: if inspect.isgeneratorfunction(call): return True - dunder_call = getattr(call, "__call__", None) + dunder_call = getattr(call, "__call__", None) # noqa: B004 return inspect.isgeneratorfunction(dunder_call) @@ -724,14 +724,14 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: # in case a sub-dependency is evaluated with a single unique body field # That is combined (embedded) with other body fields for param in flat_dependant.body_params: - setattr(param.field_info, "embed", True) + setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name BodyModel: Type[BaseModel] = create_model(model_name) for f in flat_dependant.body_params: BodyModel.__fields__[f.name] = f required = any(True for f in flat_dependant.body_params if f.required) - BodyFieldInfo_kwargs: Dict[str, Any] = dict(default=None) + BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: Type[params.Body] = params.File elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): @@ -740,7 +740,7 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: BodyFieldInfo = params.Body body_param_media_types = [ - getattr(f.field_info, "media_type") + f.field_info.media_type for f in flat_dependant.body_params if isinstance(f.field_info, params.Body) ] diff --git a/fastapi/routing.py b/fastapi/routing.py index 8c0bec5e6..9a7d88efc 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -701,7 +701,7 @@ class APIRouter(routing.Router): ), "A path prefix must not end with '/', as the routes will start with '/'" else: for r in router.routes: - path = getattr(r, "path") + path = getattr(r, "path") # noqa: B009 name = getattr(r, "name", "unknown") if path is not None and not path: raise Exception( diff --git a/pyproject.toml b/pyproject.toml index af20c8ef7..3f7fd4e00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ test = [ "pytest >=7.1.3,<8.0.0", "coverage[toml] >= 6.5.0,<7.0", "mypy ==0.982", - "flake8 >=3.8.3,<6.0.0", + "ruff ==0.0.114", "black == 22.8.0", "isort >=5.0.6,<6.0.0", "httpx >=0.23.0,<0.24.0", @@ -87,8 +87,7 @@ doc = [ "pyyaml >=5.3.1,<7.0.0", ] dev = [ - "autoflake >=1.4.0,<2.0.0", - "flake8 >=3.8.3,<6.0.0", + "ruff ==0.0.114", "uvicorn[standard] >=0.12.0,<0.19.0", "pre-commit >=2.17.0,<3.0.0", ] @@ -156,3 +155,30 @@ source = [ "fastapi" ] context = '${CONTEXT}' + +[tool.ruff] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + # "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] +"docs_src/dependencies/tutorial007.py" = ["F821"] +"docs_src/dependencies/tutorial008.py" = ["F821"] +"docs_src/dependencies/tutorial009.py" = ["F821"] +"docs_src/dependencies/tutorial010.py" = ["F821"] +"docs_src/custom_response/tutorial007.py" = ["B007"] +"docs_src/dataclasses/tutorial003.py" = ["I001"] + + +[tool.ruff.isort] +known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/scripts/docs.py b/scripts/docs.py index d5fbacf59..622ba9202 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -332,7 +332,7 @@ def serve(): os.chdir("site") server_address = ("", 8008) server = HTTPServer(server_address, SimpleHTTPRequestHandler) - typer.echo(f"Serving at: http://127.0.0.1:8008") + typer.echo("Serving at: http://127.0.0.1:8008") server.serve_forever() @@ -420,7 +420,7 @@ def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: file_to_nav = {} for item in nav: if type(item) is str: - file_to_nav[item] = tuple() + file_to_nav[item] = () elif type(item) is dict: item_key = list(item.keys())[0] sub_nav = item[item_key] diff --git a/scripts/format.sh b/scripts/format.sh index ee4fbf1a5..3ac1fead8 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -1,6 +1,6 @@ #!/bin/sh -e set -x -autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place docs_src fastapi tests scripts --exclude=__init__.py +ruff fastapi tests docs_src scripts --fix black fastapi tests docs_src scripts isort fastapi tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 2e2072cf1..0feb973a8 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -4,6 +4,6 @@ set -e set -x mypy fastapi -flake8 fastapi tests +ruff fastapi tests docs_src scripts black fastapi tests --check isort fastapi tests docs_src scripts --check-only diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index 1a9ea7199..2e8d9c6de 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -110,6 +110,6 @@ def test_route_classes(): for r in app.router.routes: assert isinstance(r, Route) routes[r.path] = r - assert getattr(routes["/a/"], "x_type") == "A" - assert getattr(routes["/a/b/"], "x_type") == "B" - assert getattr(routes["/a/b/c/"], "x_type") == "C" + assert getattr(routes["/a/"], "x_type") == "A" # noqa: B009 + assert getattr(routes["/a/b/"], "x_type") == "B" # noqa: B009 + assert getattr(routes["/a/b/c/"], "x_type") == "C" # noqa: B009 diff --git a/tests/test_ws_router.py b/tests/test_ws_router.py index 206d743ba..c312821e9 100644 --- a/tests/test_ws_router.py +++ b/tests/test_ws_router.py @@ -111,7 +111,7 @@ def test_router_ws_depends(): def test_router_ws_depends_with_override(): client = TestClient(app) - app.dependency_overrides[ws_dependency] = lambda: "Override" + app.dependency_overrides[ws_dependency] = lambda: "Override" # noqa: E731 with client.websocket_connect("/router-ws-depends/") as websocket: assert websocket.receive_text() == "Override" From a0852e2f5350271805b7c690e3bda0ea70153c23 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 18:19:43 +0000 Subject: [PATCH 0648/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ae78141d0..2ec54c52c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Use Ruff for linting. PR [#5630](https://github.com/tiangolo/fastapi/pull/5630) by [@tiangolo](https://github.com/tiangolo). * ✨ Re-export Starlette's `WebSocketException` and add it to docs. PR [#5629](https://github.com/tiangolo/fastapi/pull/5629) by [@tiangolo](https://github.com/tiangolo). * 📝 Update references to Requests for tests to HTTPX, and add HTTPX to extras. PR [#5628](https://github.com/tiangolo/fastapi/pull/5628) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix highlight lines for Japanese translation for `docs/tutorial/query-params.md`. PR [#2969](https://github.com/tiangolo/fastapi/pull/2969) by [@ftnext](https://github.com/ftnext). From 50ea75ae98a319a059adeb6d6abcc09d4fbd2554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 20:34:09 +0100 Subject: [PATCH 0649/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20Help=20FastAP?= =?UTF-8?q?I:=20Help=20Maintain=20FastAPI=20(#5632)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/help-fastapi.md | 114 ++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 8d8d708ed..047462c41 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -47,7 +47,7 @@ You can: * Follow me on **GitHub**. * See other Open Source projects I have created that could help you. * Follow me to see when I create a new Open Source project. -* Follow me on **Twitter**. +* Follow me on **Twitter** or Mastodon. * Tell me how you use FastAPI (I love to hear that). * Hear when I make announcements or release new tools. * You can also follow @fastapi on Twitter (a separate account). @@ -67,13 +67,54 @@ I love to hear about how **FastAPI** is being used, what you have liked in it, i * Vote for **FastAPI** in Slant. * Vote for **FastAPI** in AlternativeTo. +* Say you use **FastAPI** on StackShare. ## Help others with issues in GitHub -You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 +You can see existing issues and try and help others, most of the times those issues are questions that you might already know the answer for. 🤓 If you are helping a lot of people with issues, you might become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 +Just remember, the most important point is: try to be kind. People come with their frustrations and in many cases don't ask in the best way, but try as best as you can to be kind. 🤗 + +The idea is for the **FastAPI** community to be kind and welcoming. At the same time, don't accept bullying or disrespectful behavior towards others. We have to take care of each other. + +--- + +Here's how to help others with issues: + +### Understand the question + +* Check if you can understand what is the **purpose** and use case of the person asking. + +* Then check if the question (the vast majority are questions) is **clear**. + +* In many cases the question asked is about an imaginary solution from the user, but there might be a **better** one. If you can understand the problem and use case better, you might be able to suggest a better **alternative solution**. + +* If you can't understand the question, ask for more **details**. + +### Reproduce the problem + +For most of the cases and most of the questions there's something related to the person's **original code**. + +In many cases they will only copy a fragment of the code, but that's not enough to **reproduce the problem**. + +* You can ask them to provide a minimal, reproducible, example, that you can **copy-paste** and run locally to see the same error or behavior they are seeing, or to understand their use case better. + +* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just have in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. + +### Suggest solutions + +* After being able to understand the question, you can give them a possible **answer**. + +* In many cases, it's better to understand their **underlying problem or use case**, because there might be a better way to solve it than what they are trying to do. + +### Ask to close + +If they reply, there's a high chance you will have solved their problem, congrats, **you're a hero**! 🦸 + +* Now you can ask them, if that solved their problem, to **close the issue**. + ## Watch the GitHub repository You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 @@ -91,6 +132,57 @@ You can . * `allow_credentials` - Indicate that cookies should be supported for cross-origin requests. Defaults to `False`. Also, `allow_origins` cannot be set to `['*']` for credentials to be allowed, origins must be specified. * `expose_headers` - Indicate any response headers that should be made accessible to the browser. Defaults to `[]`. * `max_age` - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to `600`. From ad0e923fed74b6cc0342a99e2f5843aedda7e6a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 20:29:15 +0000 Subject: [PATCH 0653/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 068084f62..439c0889f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Clarify docs on CORS. PR [#5627](https://github.com/tiangolo/fastapi/pull/5627) by [@paxcodes](https://github.com/paxcodes). ### Features From 9c483505e346a7a5e24dcc0c908f3d10e9d1fa08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 21:29:23 +0100 Subject: [PATCH 0654/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Tweak=20Help=20F?= =?UTF-8?q?astAPI=20from=20PR=20review=20after=20merging=20(#5633)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/help-fastapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 047462c41..a7ac9415f 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -111,7 +111,7 @@ In many cases they will only copy a fragment of the code, but that's not enough ### Ask to close -If they reply, there's a high chance you will have solved their problem, congrats, **you're a hero**! 🦸 +If they reply, there's a high chance you would have solved their problem, congrats, **you're a hero**! 🦸 * Now you can ask them, if that solved their problem, to **close the issue**. @@ -136,7 +136,7 @@ You can =2.17.0,<3.0.0", ] all = [ - "httpx >=0.23.0,<0.24.0", - "jinja2 >=2.11.2,<4.0.0", - "python-multipart >=0.0.5,<0.0.6", - "itsdangerous >=1.1.0,<3.0.0", - "pyyaml >=5.3.1,<7.0.0", - "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", - "orjson >=3.2.1,<4.0.0", - "email_validator >=1.1.1,<2.0.0", - "uvicorn[standard] >=0.12.0,<0.19.0", + "httpx >=0.23.0", + "jinja2 >=2.11.2", + "python-multipart >=0.0.5", + "itsdangerous >=1.1.0", + "pyyaml >=5.3.1", + "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", + "orjson >=3.2.1", + "email_validator >=1.1.1", + "uvicorn[standard] >=0.12.0", ] [tool.hatch.version] From 1d416c4c5361661d0bdbbea360cad6f38bfa8fff Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 20:51:19 +0000 Subject: [PATCH 0657/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bd36f6d52..075bdb4ff 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade and relax dependencies for extras "all". PR [#5634](https://github.com/tiangolo/fastapi/pull/5634) by [@tiangolo](https://github.com/tiangolo). * ✏️ Tweak Help FastAPI from PR review after merging. PR [#5633](https://github.com/tiangolo/fastapi/pull/5633) by [@tiangolo](https://github.com/tiangolo). * ✏️ Clarify docs on CORS. PR [#5627](https://github.com/tiangolo/fastapi/pull/5627) by [@paxcodes](https://github.com/paxcodes). From 46a509649da88637953e0cd679ac17b05c632e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 22:26:43 +0100 Subject: [PATCH 0658/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 075bdb4ff..a34a5969d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,18 +2,26 @@ ## Latest Changes -* ⬆️ Upgrade and relax dependencies for extras "all". PR [#5634](https://github.com/tiangolo/fastapi/pull/5634) by [@tiangolo](https://github.com/tiangolo). -* ✏️ Tweak Help FastAPI from PR review after merging. PR [#5633](https://github.com/tiangolo/fastapi/pull/5633) by [@tiangolo](https://github.com/tiangolo). -* ✏️ Clarify docs on CORS. PR [#5627](https://github.com/tiangolo/fastapi/pull/5627) by [@paxcodes](https://github.com/paxcodes). +Highlights of this release: + +* [Upgraded Starlette](https://github.com/encode/starlette/releases/tag/0.21.0) + * Now the `TestClient` is based on HTTPX instead of Requests. 🚀 + * There are some possible **breaking changes** in the `TestClient` usage, but [@Kludex](https://github.com/Kludex) built [bump-testclient](https://github.com/Kludex/bump-testclient) to help you automatize migrating your tests. Make sure you are using Git and that you can undo any unnecessary changes (false positive changes, etc) before using `bump-testclient`. +* New [WebSocketException (and docs)](https://fastapi.tiangolo.com/advanced/websockets/#using-depends-and-others), re-exported from Starlette. +* Upgraded and relaxed dependencies for package extras `all` (including new Uvicorn version), when you install `"fastapi[all]"`. +* New docs about how to [**Help Maintain FastAPI**](https://fastapi.tiangolo.com/help-fastapi/#help-maintain-fastapi). ### Features +* ⬆️ Upgrade and relax dependencies for extras "all". PR [#5634](https://github.com/tiangolo/fastapi/pull/5634) by [@tiangolo](https://github.com/tiangolo). * ✨ Re-export Starlette's `WebSocketException` and add it to docs. PR [#5629](https://github.com/tiangolo/fastapi/pull/5629) by [@tiangolo](https://github.com/tiangolo). * 📝 Update references to Requests for tests to HTTPX, and add HTTPX to extras. PR [#5628](https://github.com/tiangolo/fastapi/pull/5628) by [@tiangolo](https://github.com/tiangolo). * ⬆ Upgrade Starlette to `0.21.0`, including the new [`TestClient` based on HTTPX](https://github.com/encode/starlette/releases/tag/0.21.0). PR [#5471](https://github.com/tiangolo/fastapi/pull/5471) by [@pawelrubin](https://github.com/pawelrubin). ### Docs +* ✏️ Tweak Help FastAPI from PR review after merging. PR [#5633](https://github.com/tiangolo/fastapi/pull/5633) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Clarify docs on CORS. PR [#5627](https://github.com/tiangolo/fastapi/pull/5627) by [@paxcodes](https://github.com/paxcodes). * 📝 Update Help FastAPI: Help Maintain FastAPI. PR [#5632](https://github.com/tiangolo/fastapi/pull/5632) by [@tiangolo](https://github.com/tiangolo). ### Translations From da1c67338f39491e85be3e5ff7b6e3ca2f347ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 22:27:53 +0100 Subject: [PATCH 0659/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?87.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a34a5969d..9cc866f77 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.87.0 + Highlights of this release: * [Upgraded Starlette](https://github.com/encode/starlette/releases/tag/0.21.0) From 63a5ffcf577abc6015d021f65857b3826d8db74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 13 Nov 2022 22:36:53 +0100 Subject: [PATCH 0660/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?87.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 70f363c89..afdc94874 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.86.0" +__version__ = "0.87.0" from starlette import status as status From 3c01b2469f6395c87d9a36b3bbff2222e47f95f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 13 Nov 2022 23:06:28 +0100 Subject: [PATCH 0661/1881] =?UTF-8?q?=E2=AC=86=20Bump=20black=20from=2022.?= =?UTF-8?q?8.0=20to=2022.10.0=20(#5569)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 de6bd24f3..9549cc47d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ test = [ "coverage[toml] >= 6.5.0,<7.0", "mypy ==0.982", "ruff ==0.0.114", - "black == 22.8.0", + "black == 22.10.0", "isort >=5.0.6,<6.0.0", "httpx >=0.23.0,<0.24.0", "email_validator >=1.1.1,<2.0.0", From 4638b2c64e259b90bef6a44748e00e405825a111 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Nov 2022 22:07:03 +0000 Subject: [PATCH 0662/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9cc866f77..1eb2fad32 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump black from 22.8.0 to 22.10.0. PR [#5569](https://github.com/tiangolo/fastapi/pull/5569) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.87.0 From 6883f362a54122e949b3eb622293ade1f9658513 Mon Sep 17 00:00:00 2001 From: Muhammad Abdur Rakib <103581704+rifatrakib@users.noreply.github.com> Date: Tue, 22 Nov 2022 19:29:57 +0600 Subject: [PATCH 0663/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20docs=20for=20`docs/en/docs/advanced/middleware.md`=20(#5376)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typo in documentation A full-stop was missing in `TrustedHostMiddleware` section Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index ed90f29be..3bf49e392 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -68,7 +68,7 @@ Enforces that all incoming requests have a correctly set `Host` header, in order The following arguments are supported: -* `allowed_hosts` - A list of domain names that should be allowed as hostnames. Wildcard domains such as `*.example.com` are supported for matching subdomains to allow any hostname either use `allowed_hosts=["*"]` or omit the middleware. +* `allowed_hosts` - A list of domain names that should be allowed as hostnames. Wildcard domains such as `*.example.com` are supported for matching subdomains. To allow any hostname either use `allowed_hosts=["*"]` or omit the middleware. If an incoming request does not validate correctly then a `400` response will be sent. From 0eb05cabbf9db9ce5cd121f6470b90c4003a1787 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 22 Nov 2022 13:30:37 +0000 Subject: [PATCH 0664/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1eb2fad32..d99f153c2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). * ⬆ Bump black from 22.8.0 to 22.10.0. PR [#5569](https://github.com/tiangolo/fastapi/pull/5569) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.87.0 From 22837ee20255dfdbd2d0d4eb8effd3d8e5345fce Mon Sep 17 00:00:00 2001 From: Michael Adkins Date: Fri, 25 Nov 2022 05:38:55 -0600 Subject: [PATCH 0665/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20`setup-python?= =?UTF-8?q?`=20action=20in=20tests=20to=20use=20new=20caching=20feature=20?= =?UTF-8?q?(#5680)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f87be700..85779af18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,15 +19,13 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 + id: setup-python with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v3 - id: cache - with: - path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v02 + cache: "pip" + cache-dependency-path: pyproject.toml - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' + if: steps.setup-python.outputs.cache-hit != 'true' run: pip install -e .[all,dev,doc,test] - name: Lint run: bash scripts/lint.sh From ec30c3001d0ab2f3c2a0ac5402b31d6b40b17af6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 25 Nov 2022 11:39:33 +0000 Subject: [PATCH 0666/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d99f153c2..ea3e5ab43 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update `setup-python` action in tests to use new caching feature. PR [#5680](https://github.com/tiangolo/fastapi/pull/5680) by [@madkinsz](https://github.com/madkinsz). * ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). * ⬆ Bump black from 22.8.0 to 22.10.0. PR [#5569](https://github.com/tiangolo/fastapi/pull/5569) by [@dependabot[bot]](https://github.com/apps/dependabot). From 0c07e542a7b8e560bfa3b71ce65e3c67e7860a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 14:11:22 +0100 Subject: [PATCH 0667/1881] =?UTF-8?q?=F0=9F=91=B7=20Fix=20and=20tweak=20CI?= =?UTF-8?q?=20cache=20handling=20(#5696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 2 ++ .github/workflows/smokeshow.yml | 2 ++ .github/workflows/test.yml | 10 ++++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fe4c5ee86..ab27d4b85 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,8 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.7" + cache: "pip" + cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache with: diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7559c24c0..55d64517f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -17,6 +17,8 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.9' + cache: "pip" + cache-dependency-path: pyproject.toml - run: pip install smokeshow diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 85779af18..ddc43c942 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,13 +19,17 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 - id: setup-python with: python-version: ${{ matrix.python-version }} cache: "pip" cache-dependency-path: pyproject.toml + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 - name: Install Dependencies - if: steps.setup-python.outputs.cache-hit != 'true' + if: steps.cache.outputs.cache-hit != 'true' run: pip install -e .[all,dev,doc,test] - name: Lint run: bash scripts/lint.sh @@ -50,6 +54,8 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.8' + cache: "pip" + cache-dependency-path: pyproject.toml - name: Get coverage files uses: actions/download-artifact@v3 From c77384fc8f13b39ea1571f495a30c570a2fb56dc Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 13:12:12 +0000 Subject: [PATCH 0668/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ea3e5ab43..f6fd36be0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Fix and tweak CI cache handling. PR [#5696](https://github.com/tiangolo/fastapi/pull/5696) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `setup-python` action in tests to use new caching feature. PR [#5680](https://github.com/tiangolo/fastapi/pull/5680) by [@madkinsz](https://github.com/madkinsz). * ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). * ⬆ Bump black from 22.8.0 to 22.10.0. PR [#5569](https://github.com/tiangolo/fastapi/pull/5569) by [@dependabot[bot]](https://github.com/apps/dependabot). From c942a9b8d0ec7eca32f7547c580cff578bc1b88b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 14:39:17 +0100 Subject: [PATCH 0669/1881] =?UTF-8?q?=F0=9F=92=9A=20Fix=20pip=20cache=20fo?= =?UTF-8?q?r=20Smokeshow=20(#5697)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 55d64517f..c83d16f15 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -18,7 +18,6 @@ jobs: with: python-version: '3.9' cache: "pip" - cache-dependency-path: pyproject.toml - run: pip install smokeshow From 0b53ee505b47bdc6a2087367e41269ffcb5587d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 13:40:03 +0000 Subject: [PATCH 0670/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f6fd36be0..4d77f5f82 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Fix pip cache for Smokeshow. PR [#5697](https://github.com/tiangolo/fastapi/pull/5697) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix and tweak CI cache handling. PR [#5696](https://github.com/tiangolo/fastapi/pull/5696) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `setup-python` action in tests to use new caching feature. PR [#5680](https://github.com/tiangolo/fastapi/pull/5680) by [@madkinsz](https://github.com/madkinsz). * ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). From fcc4dd61f17aa13f954d866e000c6c42fa2cedb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 14:53:13 +0100 Subject: [PATCH 0671/1881] =?UTF-8?q?=F0=9F=91=B7=20Remove=20pip=20cache?= =?UTF-8?q?=20for=20Smokeshow=20as=20it=20depends=20on=20a=20requirements.?= =?UTF-8?q?txt=20(#5700)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/smokeshow.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index c83d16f15..7559c24c0 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -17,7 +17,6 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.9' - cache: "pip" - run: pip install smokeshow From 91e5a5d1cf13696383f0f3cdd89277a0c52aba9c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 13:53:52 +0000 Subject: [PATCH 0672/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4d77f5f82..ec976f510 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Remove pip cache for Smokeshow as it depends on a requirements.txt. PR [#5700](https://github.com/tiangolo/fastapi/pull/5700) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix pip cache for Smokeshow. PR [#5697](https://github.com/tiangolo/fastapi/pull/5697) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix and tweak CI cache handling. PR [#5696](https://github.com/tiangolo/fastapi/pull/5696) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `setup-python` action in tests to use new caching feature. PR [#5680](https://github.com/tiangolo/fastapi/pull/5680) by [@madkinsz](https://github.com/madkinsz). From 7c5626bef7ab09d93083c9e1b593b23ba84dd78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 14:59:32 +0100 Subject: [PATCH 0673/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Ruff?= =?UTF-8?q?=20(#5698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- fastapi/dependencies/utils.py | 4 ++-- fastapi/encoders.py | 2 +- fastapi/utils.py | 2 +- pyproject.toml | 8 +++++--- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e59e05abe..4e34cc7ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.114 + rev: v0.0.138 hooks: - id: ruff args: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3df5ccfc8..4c817d5d0 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -105,10 +105,10 @@ def check_file_field(field: ModelField) -> None: assert parse_options_header except ImportError: logger.error(multipart_incorrect_install_error) - raise RuntimeError(multipart_incorrect_install_error) + raise RuntimeError(multipart_incorrect_install_error) from None except ImportError: logger.error(multipart_not_installed_error) - raise RuntimeError(multipart_not_installed_error) + raise RuntimeError(multipart_not_installed_error) from None def get_param_sub_dependant( diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 6bde9f4ab..2f95bcbf6 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -157,7 +157,7 @@ def jsonable_encoder( data = vars(obj) except Exception as e: errors.append(e) - raise ValueError(errors) + raise ValueError(errors) from e return jsonable_encoder( data, include=include, diff --git a/fastapi/utils.py b/fastapi/utils.py index b94dacecc..b15f6a2cf 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -89,7 +89,7 @@ def create_response_field( except RuntimeError: raise fastapi.exceptions.FastAPIError( f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type" - ) + ) from None def create_cloned_field( diff --git a/pyproject.toml b/pyproject.toml index 9549cc47d..4ae380986 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ test = [ "pytest >=7.1.3,<8.0.0", "coverage[toml] >= 6.5.0,<7.0", "mypy ==0.982", - "ruff ==0.0.114", + "ruff ==0.0.138", "black == 22.10.0", "isort >=5.0.6,<6.0.0", "httpx >=0.23.0,<0.24.0", @@ -87,7 +87,7 @@ doc = [ "pyyaml >=5.3.1,<7.0.0", ] dev = [ - "ruff ==0.0.114", + "ruff ==0.0.138", "uvicorn[standard] >=0.12.0,<0.19.0", "pre-commit >=2.17.0,<3.0.0", ] @@ -168,6 +168,7 @@ select = [ ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults + "C901", # too complex ] [tool.ruff.per-file-ignores] @@ -178,7 +179,8 @@ ignore = [ "docs_src/dependencies/tutorial010.py" = ["F821"] "docs_src/custom_response/tutorial007.py" = ["B007"] "docs_src/dataclasses/tutorial003.py" = ["I001"] - +"docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002.py" = ["B904"] [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] From 99d8470a8e1cf76da8c5274e4e372630efc95736 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 14:00:09 +0000 Subject: [PATCH 0674/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ec976f510..132d61aa9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Ruff. PR [#5698](https://github.com/tiangolo/fastapi/pull/5698) by [@tiangolo](https://github.com/tiangolo). * 👷 Remove pip cache for Smokeshow as it depends on a requirements.txt. PR [#5700](https://github.com/tiangolo/fastapi/pull/5700) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix pip cache for Smokeshow. PR [#5697](https://github.com/tiangolo/fastapi/pull/5697) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix and tweak CI cache handling. PR [#5696](https://github.com/tiangolo/fastapi/pull/5696) by [@tiangolo](https://github.com/tiangolo). From ebd917a530b0f5be7ff0395f202befbd1ec6db0f Mon Sep 17 00:00:00 2001 From: Ayrton Freeman Date: Sun, 27 Nov 2022 11:13:50 -0300 Subject: [PATCH 0675/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/deployment/docker.md`=20(#5663)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/pt/docs/deployment/docker.md | 701 ++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 702 insertions(+) create mode 100644 docs/pt/docs/deployment/docker.md diff --git a/docs/pt/docs/deployment/docker.md b/docs/pt/docs/deployment/docker.md new file mode 100644 index 000000000..42c31db29 --- /dev/null +++ b/docs/pt/docs/deployment/docker.md @@ -0,0 +1,701 @@ +# FastAPI em contêineres - Docker + +Ao fazer o deploy de aplicações FastAPI uma abordagem comum é construir uma **imagem de contêiner Linux**. Isso normalmente é feito usando o **Docker**. Você pode a partir disso fazer o deploy dessa imagem de algumas maneiras. + +Usando contêineres Linux você tem diversas vantagens incluindo **segurança**, **replicabilidade**, **simplicidade**, entre outras. + +!!! Dica + Está com pressa e já sabe dessas coisas? Pode ir direto para [`Dockerfile` abaixo 👇](#build-a-docker-image-for-fastapi). + + +
+Visualização do Dockerfile 👀 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +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"] +``` + +
+ +## O que é um Contêiner + +Contêineres (especificamente contêineres Linux) são um jeito muito **leve** de empacotar aplicações contendo todas as dependências e arquivos necessários enquanto os mantém isolados de outros contêineres (outras aplicações ou componentes) no mesmo sistema. + +Contêineres Linux rodam usando o mesmo kernel Linux do hospedeiro (máquina, máquina virtual, servidor na nuvem, etc). Isso simplesmente significa que eles são muito leves (comparados com máquinas virtuais emulando um sistema operacional completo). + +Dessa forma, contêineres consomem **poucos recursos**, uma quantidade comparável com rodar os processos diretamente (uma máquina virtual consumiria muito mais). + +Contêineres também possuem seus próprios processos (comumente um único processo), sistema de arquivos e rede **isolados** simplificando deploy, segurança, desenvolvimento, etc. + +## O que é uma Imagem de Contêiner + +Um **contêiner** roda a partir de uma **imagem de contêiner**. + +Uma imagem de contêiner é uma versão **estática** de todos os arquivos, variáveis de ambiente e do comando/programa padrão que deve estar presente num contêiner. **Estática** aqui significa que a **imagem** de contêiner não está rodando, não está sendo executada, somente contém os arquivos e metadados empacotados. + +Em contraste com a "**imagem de contêiner**" que contém os conteúdos estáticos armazenados, um "**contêiner**" normalmente se refere à instância rodando, a coisa que está sendo **executada**. + +Quando o **contêiner** é iniciado e está rodando (iniciado a partir de uma **imagem de contêiner**), ele pode criar ou modificar arquivos, variáveis de ambiente, etc. Essas mudanças vão existir somente nesse contêiner, mas não persistirão na imagem subjacente do container (não serão salvas no disco). + +Uma imagem de contêiner é comparável ao arquivo de **programa** e seus conteúdos, ex.: `python` e algum arquivo `main.py`. + +E o **contêiner** em si (em contraste à **imagem de contêiner**) é a própria instância da imagem rodando, comparável a um **processo**. Na verdade, um contêiner está rodando somente quando há um **processo rodando** (e normalmente é somente um processo). O contêiner finaliza quando não há um processo rodando nele. + +## Imagens de contêiner + +Docker tem sido uma das principais ferramentas para criar e gerenciar **imagens de contêiner** e **contêineres**. + +E existe um Docker Hub público com **imagens de contêiner oficiais** pré-prontas para diversas ferramentas, ambientes, bancos de dados e aplicações. + +Por exemplo, há uma Imagem Python oficial. + +E existe muitas outras imagens para diferentes coisas, como bancos de dados, por exemplo: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +Usando imagens de contêiner pré-prontas é muito fácil **combinar** e usar diferentes ferramentas. Por exemplo, para testar um novo banco de dados. Em muitos casos, você pode usar as **imagens oficiais** precisando somente de variáveis de ambiente para configurá-las. + +Dessa forma, em muitos casos você pode aprender sobre contêineres e Docker e re-usar essa experiência com diversos componentes e ferramentas. + +Então, você rodaria **vários contêineres** com coisas diferentes, como um banco de dados, uma aplicação Python, um servidor web com uma aplicação frontend React, e conectá-los juntos via sua rede interna. + +Todos os sistemas de gerenciamento de contêineres (como Docker ou Kubernetes) possuem essas funcionalidades de rede integradas a eles. + +## Contêineres e Processos + +Uma **imagem de contêiner** normalmente inclui em seus metadados o programa padrão ou comando que deve ser executado quando o **contêiner** é iniciado e os parâmetros a serem passados para esse programa. Muito similar ao que seria se estivesse na linha de comando. + +Quando um **contêiner** é iniciado, ele irá rodar esse comando/programa (embora você possa sobrescrevê-lo e fazer com que ele rode um comando/programa diferente). + +Um contêiner está rodando enquanto o **processo principal** (comando ou programa) estiver rodando. + +Um contêiner normalmente tem um **único processo**, mas também é possível iniciar sub-processos a partir do processo principal, e dessa forma você terá **vários processos** no mesmo contêiner. + +Mas não é possível ter um contêiner rodando sem **pelo menos um processo rodando**. Se o processo principal parar, o contêiner também para. + +## Construindo uma Imagem Docker para FastAPI + +Okay, vamos construir algo agora! 🚀 + +Eu vou mostrar como construir uma **imagem Docker** para FastAPI **do zero**, baseado na **imagem oficial do Python**. + +Isso é o que você quer fazer na **maioria dos casos**, por exemplo: + +* Usando **Kubernetes** ou ferramentas similares +* Quando rodando em uma **Raspberry Pi** +* Usando um serviço em nuvem que irá rodar uma imagem de contêiner para você, etc. + +### O Pacote Requirements + +Você normalmente teria os **requisitos do pacote** para sua aplicação em algum arquivo. + +Isso pode depender principalmente da ferramenta que você usa para **instalar** esses requisitos. + +O caminho mais comum de fazer isso é ter um arquivo `requirements.txt` com os nomes dos pacotes e suas versões, um por linha. + +Você, naturalmente, usaria as mesmas ideias que você leu em [Sobre Versões do FastAPI](./versions.md){.internal-link target=_blank} para definir os intervalos de versões. + +Por exemplo, seu `requirements.txt` poderia parecer com: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +E você normalmente instalaria essas dependências de pacote com `pip`, por exemplo: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + Há outros formatos e ferramentas para definir e instalar dependências de pacote. + + Eu vou mostrar um exemplo depois usando Poetry em uma seção abaixo. 👇 + +### Criando o Código do **FastAPI** + +* Crie um diretório `app` e entre nele. +* Crie um arquivo vazio `__init__.py`. +* Crie um arquivo `main.py` com: + +```Python +from typing import Optional + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile + +Agora, no mesmo diretório do projeto, crie um arquivo `Dockerfile` com: + +```{ .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. Inicie a partir da imagem base oficial do Python. + +2. Defina o diretório de trabalho atual para `/code`. + + Esse é o diretório onde colocaremos o arquivo `requirements.txt` e o diretório `app`. + +3. Copie o arquivo com os requisitos para o diretório `/code`. + + Copie **somente** o arquivo com os requisitos primeiro, não o resto do código. + + Como esse arquivo **não muda com frequência**, o Docker irá detectá-lo e usar o **cache** para esse passo, habilitando o cache para o próximo passo também. + +4. Instale as dependências de pacote vindas do arquivo de requisitos. + + A opção `--no-cache-dir` diz ao `pip` para não salvar os pacotes baixados localmente, pois isso só aconteceria se `pip` fosse executado novamente para instalar os mesmos pacotes, mas esse não é o caso quando trabalhamos com contêineres. + + !!! note + `--no-cache-dir` é apenas relacionado ao `pip`, não tem nada a ver com Docker ou contêineres. + + A opção `--upgrade` diz ao `pip` para atualizar os pacotes se eles já estiverem instalados. + + Por causa do passo anterior de copiar o arquivo, ele pode ser detectado pelo **cache do Docker**, esse passo também **usará o cache do Docker** quando disponível. + + Usando o cache nesse passo irá **salvar** muito **tempo** quando você for construir a imagem repetidas vezes durante o desenvolvimento, ao invés de **baixar e instalar** todas as dependências **toda vez**. + +5. Copie o diretório `./app` dentro do diretório `/code`. + + Como isso tem todo o código contendo o que **muda com mais frequência**, o **cache do Docker** não será usado para esse passo ou para **qualquer passo seguinte** facilmente. + + Então, é importante colocar isso **perto do final** do `Dockerfile`, para otimizar o tempo de construção da imagem do contêiner. + +6. Defina o **comando** para rodar o servidor `uvicorn`. + + `CMD` recebe uma lista de strings, cada uma dessas strings é o que você digitaria na linha de comando separado por espaços. + + Esse comando será executado a partir do **diretório de trabalho atual**, o mesmo diretório `/code` que você definiu acima com `WORKDIR /code`. + + Porque o programa será iniciado em `/code` e dentro dele está o diretório `./app` com seu código, o **Uvicorn** será capaz de ver e **importar** `app` de `app.main`. + +!!! tip + Revise o que cada linha faz clicando em cada bolha com o número no código. 👆 + +Agora você deve ter uma estrutura de diretório como: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### Por Trás de um Proxy de Terminação TLS + +Se você está executando seu contêiner atrás de um Proxy de Terminação TLS (load balancer) como Nginx ou Traefik, adicione a opção `--proxy-headers`, isso fará com que o Uvicorn confie nos cabeçalhos enviados por esse proxy, informando que o aplicativo está sendo executado atrás do HTTPS, etc. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Cache Docker + +Existe um truque importante nesse `Dockerfile`, primeiro copiamos o **arquivo com as dependências sozinho**, não o resto do código. Deixe-me te contar o porquê disso. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker e outras ferramentas **constróem** essas imagens de contêiner **incrementalmente**, adicionando **uma camada em cima da outra**, começando do topo do `Dockerfile` e adicionando qualquer arquivo criado por cada uma das instruções do `Dockerfile`. + +Docker e ferramentas similares também usam um **cache interno** ao construir a imagem, se um arquivo não mudou desde a última vez que a imagem do contêiner foi construída, então ele irá **reutilizar a mesma camada** criada na última vez, ao invés de copiar o arquivo novamente e criar uma nova camada do zero. + +Somente evitar a cópia de arquivos não melhora muito as coisas, mas porque ele usou o cache para esse passo, ele pode **usar o cache para o próximo passo**. Por exemplo, ele pode usar o cache para a instrução que instala as dependências com: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +O arquivo com os requisitos de pacote **não muda com frequência**. Então, ao copiar apenas esse arquivo, o Docker será capaz de **usar o cache** para esse passo. + +E então, o Docker será capaz de **usar o cache para o próximo passo** que baixa e instala essas dependências. E é aqui que **salvamos muito tempo**. ✨ ...e evitamos tédio esperando. 😪😆 + +Baixar e instalar as dependências do pacote **pode levar minutos**, mas usando o **cache** leva **segundos** no máximo. + +E como você estaria construindo a imagem do contêiner novamente e novamente durante o desenvolvimento para verificar se suas alterações de código estão funcionando, há muito tempo acumulado que isso economizaria. + +A partir daí, perto do final do `Dockerfile`, copiamos todo o código. Como isso é o que **muda com mais frequência**, colocamos perto do final, porque quase sempre, qualquer coisa depois desse passo não será capaz de usar o cache. + +```Dockerfile +COPY ./app /code/app +``` + +### Construindo a Imagem Docker + +Agora que todos os arquivos estão no lugar, vamos construir a imagem do contêiner. + +* Vá para o diretório do projeto (onde está o seu `Dockerfile`, contendo o diretório `app`). +* Construa sua imagem FastAPI: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +!!! tip + Note o `.` no final, é equivalente a `./`, ele diz ao Docker o diretório a ser usado para construir a imagem do contêiner. + + Nesse caso, é o mesmo diretório atual (`.`). + +### Inicie o contêiner Docker + +* Execute um contêiner baseado na sua imagem: + +
+ +```console +$ docker run -d --name mycontêiner -p 80:80 myimage +``` + +
+ +## Verifique + +Você deve ser capaz de verificar isso no URL do seu contêiner Docker, por exemplo: http://192.168.99.100/items/5?q=somequery ou http://127.0.0.1/items/5?q=somequery (ou equivalente, usando seu host Docker). + +Você verá algo como: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## Documentação interativa da API + +Agora você pode ir para http://192.168.99.100/docs ou http://127.0.0.1/docs (ou equivalente, usando seu host Docker). + +Você verá a documentação interativa automática da API (fornecida pelo Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## Documentação alternativa da API + +E você também pode ir para http://192.168.99.100/redoc ou http://127.0.0.1/redoc (ou equivalente, usando seu host Docker). + +Você verá a documentação alternativa automática (fornecida pela ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Construindo uma Imagem Docker com um Arquivo Único FastAPI + +Se seu FastAPI for um único arquivo, por exemplo, `main.py` sem um diretório `./app`, sua estrutura de arquivos poderia ser assim: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +Então você só teria que alterar os caminhos correspondentes para copiar o arquivo dentro do `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. Copie o arquivo `main.py` para o diretório `/code` diretamente (sem nenhum diretório `./app`). + +2. Execute o Uvicorn e diga a ele para importar o objeto `app` de `main` (em vez de importar de `app.main`). + +Então ajuste o comando Uvicorn para usar o novo módulo `main` em vez de `app.main` para importar o objeto FastAPI `app`. + +## Conceitos de Implantação + +Vamos falar novamente sobre alguns dos mesmos [Conceitos de Implantação](./concepts.md){.internal-link target=_blank} em termos de contêineres. + +Contêineres são principalmente uma ferramenta para simplificar o processo de **construção e implantação** de um aplicativo, mas eles não impõem uma abordagem particular para lidar com esses **conceitos de implantação** e existem várias estratégias possíveis. + +A **boa notícia** é que com cada estratégia diferente há uma maneira de cobrir todos os conceitos de implantação. 🎉 + +Vamos revisar esses **conceitos de implantação** em termos de contêineres: + +* HTTPS +* Executando na inicialização +* Reinicializações +* Replicação (número de processos rodando) +* Memória +* Passos anteriores antes de começar + +## HTTPS + +Se nos concentrarmos apenas na **imagem do contêiner** para um aplicativo FastAPI (e posteriormente no **contêiner** em execução), o HTTPS normalmente seria tratado **externamente** por outra ferramenta. + +Isso poderia ser outro contêiner, por exemplo, com Traefik, lidando com **HTTPS** e aquisição **automática** de **certificados**. + +!!! tip + Traefik tem integrações com Docker, Kubernetes e outros, portanto, é muito fácil configurar e configurar o HTTPS para seus contêineres com ele. + +Alternativamente, o HTTPS poderia ser tratado por um provedor de nuvem como um de seus serviços (enquanto ainda executasse o aplicativo em um contêiner). + +## Executando na inicialização e reinicializações + +Normalmente, outra ferramenta é responsável por **iniciar e executar** seu contêiner. + +Ela poderia ser o **Docker** diretamente, **Docker Compose**, **Kubernetes**, um **serviço de nuvem**, etc. + +Na maioria (ou em todos) os casos, há uma opção simples para habilitar a execução do contêiner na inicialização e habilitar reinicializações em falhas. Por exemplo, no Docker, é a opção de linha de comando `--restart`. + +Sem usar contêineres, fazer aplicativos executarem na inicialização e com reinicializações pode ser trabalhoso e difícil. Mas quando **trabalhando com contêineres** em muitos casos essa funcionalidade é incluída por padrão. ✨ + +## Replicação - Número de Processos + +Se você tiver um cluster de máquinas com **Kubernetes**, Docker Swarm Mode, Nomad ou outro sistema complexo semelhante para gerenciar contêineres distribuídos em várias máquinas, então provavelmente desejará **lidar com a replicação** no **nível do cluster** em vez de usar um **gerenciador de processos** (como o Gunicorn com workers) em cada contêiner. + +Um desses sistemas de gerenciamento de contêineres distribuídos como o Kubernetes normalmente tem alguma maneira integrada de lidar com a **replicação de contêineres** enquanto ainda oferece **balanceamento de carga** para as solicitações recebidas. Tudo no **nível do cluster**. + +Nesses casos, você provavelmente desejará criar uma **imagem do contêiner do zero** como [explicado acima](#dockerfile), instalando suas dependências e executando **um único processo Uvicorn** em vez de executar algo como Gunicorn com trabalhadores Uvicorn. + +### Balanceamento de Carga + +Quando usando contêineres, normalmente você terá algum componente **escutando na porta principal**. Poderia ser outro contêiner que também é um **Proxy de Terminação TLS** para lidar com **HTTPS** ou alguma ferramenta semelhante. + +Como esse componente assumiria a **carga** de solicitações e distribuiria isso entre os trabalhadores de uma maneira (esperançosamente) **balanceada**, ele também é comumente chamado de **Balanceador de Carga**. + +!!! tip + O mesmo componente **Proxy de Terminação TLS** usado para HTTPS provavelmente também seria um **Balanceador de Carga**. + +E quando trabalhar com contêineres, o mesmo sistema que você usa para iniciar e gerenciá-los já terá ferramentas internas para transmitir a **comunicação de rede** (por exemplo, solicitações HTTP) do **balanceador de carga** (que também pode ser um **Proxy de Terminação TLS**) para o(s) contêiner(es) com seu aplicativo. + +### Um Balanceador de Carga - Múltiplos Contêineres de Workers + +Quando trabalhando com **Kubernetes** ou sistemas similares de gerenciamento de contêiner distribuído, usando seus mecanismos de rede internos permitiria que o único **balanceador de carga** que estivesse escutando na **porta principal** transmitisse comunicação (solicitações) para possivelmente **múltiplos contêineres** executando seu aplicativo. + +Cada um desses contêineres executando seu aplicativo normalmente teria **apenas um processo** (ex.: um processo Uvicorn executando seu aplicativo FastAPI). Todos seriam **contêineres idênticos**, executando a mesma coisa, mas cada um com seu próprio processo, memória, etc. Dessa forma, você aproveitaria a **paralelização** em **núcleos diferentes** da CPU, ou até mesmo em **máquinas diferentes**. + +E o sistema de contêiner com o **balanceador de carga** iria **distribuir as solicitações** para cada um dos contêineres com seu aplicativo **em turnos**. Portanto, cada solicitação poderia ser tratada por um dos múltiplos **contêineres replicados** executando seu aplicativo. + +E normalmente esse **balanceador de carga** seria capaz de lidar com solicitações que vão para *outros* aplicativos em seu cluster (por exemplo, para um domínio diferente, ou sob um prefixo de URL diferente), e transmitiria essa comunicação para os contêineres certos para *esse outro* aplicativo em execução em seu cluster. + +### Um Processo por Contêiner + +Nesse tipo de cenário, provavelmente você desejará ter **um único processo (Uvicorn) por contêiner**, pois já estaria lidando com a replicação no nível do cluster. + +Então, nesse caso, você **não** desejará ter um gerenciador de processos como o Gunicorn com trabalhadores Uvicorn, ou o Uvicorn usando seus próprios trabalhadores Uvicorn. Você desejará ter apenas um **único processo Uvicorn** por contêiner (mas provavelmente vários contêineres). + +Tendo outro gerenciador de processos dentro do contêiner (como seria com o Gunicorn ou o Uvicorn gerenciando trabalhadores Uvicorn) só adicionaria **complexidade desnecessária** que você provavelmente já está cuidando com seu sistema de cluster. + +### Contêineres com Múltiplos Processos e Casos Especiais + +Claro, existem **casos especiais** em que você pode querer ter um **contêiner** com um **gerenciador de processos Gunicorn** iniciando vários **processos trabalhadores Uvicorn** dentro. + +Nesses casos, você pode usar a **imagem oficial do Docker** que inclui o **Gunicorn** como um gerenciador de processos executando vários **processos trabalhadores Uvicorn**, e algumas configurações padrão para ajustar o número de trabalhadores com base nos atuais núcleos da CPU automaticamente. Eu vou te contar mais sobre isso abaixo em [Imagem Oficial do Docker com Gunicorn - Uvicorn](#imagem-oficial-do-docker-com-gunicorn-uvicorn). + +Aqui estão alguns exemplos de quando isso pode fazer sentido: + +#### Um Aplicativo Simples + +Você pode querer um gerenciador de processos no contêiner se seu aplicativo for **simples o suficiente** para que você não precise (pelo menos não agora) ajustar muito o número de processos, e você pode simplesmente usar um padrão automatizado (com a imagem oficial do Docker), e você está executando em um **único servidor**, não em um cluster. + +#### Docker Compose + +Você pode estar implantando em um **único servidor** (não em um cluster) com o **Docker Compose**, então você não teria uma maneira fácil de gerenciar a replicação de contêineres (com o Docker Compose) enquanto preserva a rede compartilhada e o **balanceamento de carga**. + +Então você pode querer ter **um único contêiner** com um **gerenciador de processos** iniciando **vários processos trabalhadores** dentro. + +#### Prometheus and Outros Motivos + +Você também pode ter **outros motivos** que tornariam mais fácil ter um **único contêiner** com **múltiplos processos** em vez de ter **múltiplos contêineres** com **um único processo** em cada um deles. + +Por exemplo (dependendo de sua configuração), você poderia ter alguma ferramenta como um exportador do Prometheus no mesmo contêiner que deve ter acesso a **cada uma das solicitações** que chegam. + +Nesse caso, se você tivesse **múltiplos contêineres**, por padrão, quando o Prometheus fosse **ler as métricas**, ele receberia as métricas de **um único contêiner cada vez** (para o contêiner que tratou essa solicitação específica), em vez de receber as **métricas acumuladas** de todos os contêineres replicados. + +Então, nesse caso, poderia ser mais simples ter **um único contêiner** com **múltiplos processos**, e uma ferramenta local (por exemplo, um exportador do Prometheus) no mesmo contêiner coletando métricas do Prometheus para todos os processos internos e expor essas métricas no único contêiner. + +--- + +O ponto principal é que **nenhum** desses são **regras escritas em pedra** que você deve seguir cegamente. Você pode usar essas idéias para **avaliar seu próprio caso de uso** e decidir qual é a melhor abordagem para seu sistema, verificando como gerenciar os conceitos de: + +* Segurança - HTTPS +* Executando na inicialização +* Reinicializações +* Replicação (o número de processos em execução) +* Memória +* Passos anteriores antes de inicializar + +## Memória + +Se você executar **um único processo por contêiner**, terá uma quantidade mais ou menos bem definida, estável e limitada de memória consumida por cada um desses contêineres (mais de um se eles forem replicados). + +E então você pode definir esses mesmos limites e requisitos de memória em suas configurações para seu sistema de gerenciamento de contêineres (por exemplo, no **Kubernetes**). Dessa forma, ele poderá **replicar os contêineres** nas **máquinas disponíveis** levando em consideração a quantidade de memória necessária por eles e a quantidade disponível nas máquinas no cluster. + +Se sua aplicação for **simples**, isso provavelmente **não será um problema**, e você pode não precisar especificar limites de memória rígidos. Mas se você estiver **usando muita memória** (por exemplo, com **modelos de aprendizado de máquina**), deve verificar quanta memória está consumindo e ajustar o **número de contêineres** que executa em **cada máquina** (e talvez adicionar mais máquinas ao seu cluster). + +Se você executar **múltiplos processos por contêiner** (por exemplo, com a imagem oficial do Docker), deve garantir que o número de processos iniciados não **consuma mais memória** do que o disponível. + +## Passos anteriores antes de inicializar e contêineres + +Se você estiver usando contêineres (por exemplo, Docker, Kubernetes), existem duas abordagens principais que você pode usar. + +### Contêineres Múltiplos + +Se você tiver **múltiplos contêineres**, provavelmente cada um executando um **único processo** (por exemplo, em um cluster do **Kubernetes**), então provavelmente você gostaria de ter um **contêiner separado** fazendo o trabalho dos **passos anteriores** em um único contêiner, executando um único processo, **antes** de executar os contêineres trabalhadores replicados. + +!!! info + Se você estiver usando o Kubernetes, provavelmente será um Init Container. + +Se no seu caso de uso não houver problema em executar esses passos anteriores **em paralelo várias vezes** (por exemplo, se você não estiver executando migrações de banco de dados, mas apenas verificando se o banco de dados está pronto), então você também pode colocá-los em cada contêiner logo antes de iniciar o processo principal. + +### Contêiner Único + +Se você tiver uma configuração simples, com um **único contêiner** que então inicia vários **processos trabalhadores** (ou também apenas um processo), então poderia executar esses passos anteriores no mesmo contêiner, logo antes de iniciar o processo com o aplicativo. A imagem oficial do Docker suporta isso internamente. + +## Imagem Oficial do Docker com Gunicorn - Uvicorn + +Há uma imagem oficial do Docker que inclui o Gunicorn executando com trabalhadores Uvicorn, conforme detalhado em um capítulo anterior: [Server Workers - Gunicorn com Uvicorn](./server-workers.md){.internal-link target=_blank}. + +Essa imagem seria útil principalmente nas situações descritas acima em: [Contêineres com Múltiplos Processos e Casos Especiais](#contêineres-com-múltiplos-processos-e-casos-Especiais). + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning + Existe uma grande chance de que você **não** precise dessa imagem base ou de qualquer outra semelhante, e seria melhor construir a imagem do zero, como [descrito acima em: Construa uma Imagem Docker para o FastAPI](#construa-uma-imagem-docker-para-o-fastapi). + +Essa imagem tem um mecanismo de **auto-ajuste** incluído para definir o **número de processos trabalhadores** com base nos núcleos de CPU disponíveis. + +Isso tem **padrões sensíveis**, mas você ainda pode alterar e atualizar todas as configurações com **variáveis de ambiente** ou arquivos de configuração. + +Há também suporte para executar **passos anteriores antes de iniciar** com um script. + +!!! tip + Para ver todas as configurações e opções, vá para a página da imagem Docker: tiangolo/uvicorn-gunicorn-fastapi. + +### Número de Processos na Imagem Oficial do Docker + +O **número de processos** nesta imagem é **calculado automaticamente** a partir dos **núcleos de CPU** disponíveis. + +Isso significa que ele tentará **aproveitar** o máximo de **desempenho** da CPU possível. + +Você também pode ajustá-lo com as configurações usando **variáveis de ambiente**, etc. + +Mas isso também significa que, como o número de processos depende da CPU do contêiner em execução, a **quantidade de memória consumida** também dependerá disso. + +Então, se seu aplicativo consumir muito memória (por exemplo, com modelos de aprendizado de máquina), e seu servidor tiver muitos núcleos de CPU **mas pouca memória**, então seu contêiner pode acabar tentando usar mais memória do que está disponível e degradar o desempenho muito (ou até mesmo travar). 🚨 + +### Criando um `Dockerfile` + +Aqui está como você criaria um `Dockerfile` baseado nessa imagem: + +```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 +``` + +### Aplicações Maiores + +Se você seguiu a seção sobre a criação de [Aplicações Maiores com Múltiplos Arquivos](../tutorial/bigger-applications.md){.internal-link target=_blank}, seu `Dockerfile` pode parecer com isso: + +```Dockerfile + +```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 +``` + +### Quando Usar + +Você provavelmente **não** deve usar essa imagem base oficial (ou qualquer outra semelhante) se estiver usando **Kubernetes** (ou outros) e já estiver definindo **replicação** no nível do cluster, com vários **contêineres**. Nesses casos, é melhor **construir uma imagem do zero** conforme descrito acima: [Construindo uma Imagem Docker para FastAPI](#construindo-uma-imagem-docker-para-fastapi). + +Essa imagem seria útil principalmente nos casos especiais descritos acima em [Contêineres com Múltiplos Processos e Casos Especiais](#contêineres-com-múltiplos-processos-e-casos-Especiais). Por exemplo, se sua aplicação for **simples o suficiente** para que a configuração padrão de número de processos com base na CPU funcione bem, você não quer se preocupar com a configuração manual da replicação no nível do cluster e não está executando mais de um contêiner com seu aplicativo. Ou se você estiver implantando com **Docker Compose**, executando em um único servidor, etc. + +## Deploy da Imagem do Contêiner + +Depois de ter uma imagem de contêiner (Docker), existem várias maneiras de implantá-la. + +Por exemplo: + +* Com **Docker Compose** em um único servidor +* Com um cluster **Kubernetes** +* Com um cluster Docker Swarm Mode +* Com outra ferramenta como o Nomad +* Com um serviço de nuvem que pega sua imagem de contêiner e a implanta + +## Imagem Docker com Poetry + +Se você usa Poetry para gerenciar as dependências do seu projeto, pode usar a construção multi-estágio do Docker: + +```{ .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. Esse é o primeiro estágio, ele é chamado `requirements-stage`. + +2. Defina `/tmp` como o diretório de trabalho atual. + + Aqui é onde geraremos o arquivo `requirements.txt` + +3. Instale o Poetry nesse estágio do Docker. + +4. Copie os arquivos `pyproject.toml` e `poetry.lock` para o diretório `/tmp`. + + Porque está usando `./poetry.lock*` (terminando com um `*`), não irá falhar se esse arquivo ainda não estiver disponível. + +5. Gere o arquivo `requirements.txt`. + +6. Este é o estágio final, tudo aqui será preservado na imagem final do contêiner. + +7. Defina o diretório de trabalho atual como `/code`. + +8. Copie o arquivo `requirements.txt` para o diretório `/code`. + + Essse arquivo só existe no estágio anterior do Docker, é por isso que usamos `--from-requirements-stage` para copiá-lo. + +9. Instale as dependências de pacote do arquivo `requirements.txt` gerado. + +10. Copie o diretório `app` para o diretório `/code`. + +11. Execute o comando `uvicorn`, informando-o para usar o objeto `app` importado de `app.main`. + +!!! tip + Clique nos números das bolhas para ver o que cada linha faz. + +Um **estágio do Docker** é uma parte de um `Dockerfile` que funciona como uma **imagem temporária do contêiner** que só é usada para gerar alguns arquivos para serem usados posteriormente. + +O primeiro estágio será usado apenas para **instalar Poetry** e para **gerar o `requirements.txt`** com as dependências do seu projeto a partir do arquivo `pyproject.toml` do Poetry. + +Esse arquivo `requirements.txt` será usado com `pip` mais tarde no **próximo estágio**. + +Na imagem final do contêiner, **somente o estágio final** é preservado. Os estágios anteriores serão descartados. + +Quando usar Poetry, faz sentido usar **construções multi-estágio do Docker** porque você realmente não precisa ter o Poetry e suas dependências instaladas na imagem final do contêiner, você **apenas precisa** ter o arquivo `requirements.txt` gerado para instalar as dependências do seu projeto. + +Então, no próximo (e último) estágio, você construiria a imagem mais ou menos da mesma maneira descrita anteriormente. + +### Por trás de um proxy de terminação TLS - Poetry + +Novamente, se você estiver executando seu contêiner atrás de um proxy de terminação TLS (balanceador de carga) como Nginx ou Traefik, adicione a opção `--proxy-headers` ao comando: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## Recapitulando + +Usando sistemas de contêiner (por exemplo, com **Docker** e **Kubernetes**), torna-se bastante simples lidar com todos os **conceitos de implantação**: + +* HTTPS +* Executando na inicialização +* Reinícios +* Replicação (o número de processos rodando) +* Memória +* Passos anteriores antes de inicializar + +Na maioria dos casos, você provavelmente não desejará usar nenhuma imagem base e, em vez disso, **construir uma imagem de contêiner do zero** baseada na imagem oficial do Docker Python. + +Tendo cuidado com a **ordem** das instruções no `Dockerfile` e o **cache do Docker**, você pode **minimizar os tempos de construção**, para maximizar sua produtividade (e evitar a tédio). 😎 + +Em alguns casos especiais, você pode querer usar a imagem oficial do Docker para o FastAPI. 🤓 diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index fdef810fa..0858de062 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -87,6 +87,7 @@ nav: - deployment/versions.md - deployment/https.md - deployment/deta.md + - deployment/docker.md - alternatives.md - history-design-future.md - external-links.md From 991db7b05a1babfd722741abb170cf2f785d0268 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 14:14:28 +0000 Subject: [PATCH 0676/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 132d61aa9..32352b4c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). * ⬆️ Upgrade Ruff. PR [#5698](https://github.com/tiangolo/fastapi/pull/5698) by [@tiangolo](https://github.com/tiangolo). * 👷 Remove pip cache for Smokeshow as it depends on a requirements.txt. PR [#5700](https://github.com/tiangolo/fastapi/pull/5700) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix pip cache for Smokeshow. PR [#5697](https://github.com/tiangolo/fastapi/pull/5697) by [@tiangolo](https://github.com/tiangolo). From 9b4e85f088271b1288c58f1fa81a7f70bf3a1f38 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:22:03 +0100 Subject: [PATCH 0677/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e34cc7ed..96f097caa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.1.0 + rev: v3.2.2 hooks: - id: pyupgrade args: From 884203676dfaf1d03c08fc79945493458b53e675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 15:22:30 +0100 Subject: [PATCH 0678/1881] =?UTF-8?q?=F0=9F=91=B7=20Tweak=20build-docs=20t?= =?UTF-8?q?o=20improve=20CI=20performance=20(#5699)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/docs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/docs.py b/scripts/docs.py index 622ba9202..e0953b8ed 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -284,7 +284,9 @@ def build_all(): continue langs.append(lang.name) cpu_count = os.cpu_count() or 1 - with Pool(cpu_count * 2) as p: + process_pool_size = cpu_count * 4 + typer.echo(f"Using process pool size: {process_pool_size}") + with Pool(process_pool_size) as p: p.map(build_lang, langs) From 128c925c35b5f98f5335f5b4a297259bb48fd6bd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 14:22:41 +0000 Subject: [PATCH 0679/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 32352b4c0..1af3a9c85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5566](https://github.com/tiangolo/fastapi/pull/5566) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). * ⬆️ Upgrade Ruff. PR [#5698](https://github.com/tiangolo/fastapi/pull/5698) by [@tiangolo](https://github.com/tiangolo). * 👷 Remove pip cache for Smokeshow as it depends on a requirements.txt. PR [#5700](https://github.com/tiangolo/fastapi/pull/5700) by [@tiangolo](https://github.com/tiangolo). From 89ec1f2d36e6a9aa5564c5ef49e8233efb69172c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 14:23:13 +0000 Subject: [PATCH 0680/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1af3a9c85..68cb5c7ce 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Tweak build-docs to improve CI performance. PR [#5699](https://github.com/tiangolo/fastapi/pull/5699) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5566](https://github.com/tiangolo/fastapi/pull/5566) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). * ⬆️ Upgrade Ruff. PR [#5698](https://github.com/tiangolo/fastapi/pull/5698) by [@tiangolo](https://github.com/tiangolo). From 46974c510e89c35456f40ca35d432fe0b94e0f97 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 27 Nov 2022 15:46:06 +0100 Subject: [PATCH 0681/1881] =?UTF-8?q?=E2=AC=86=20Bump=20Starlette=20to=20v?= =?UTF-8?q?ersion=20`0.22.0`=20to=20fix=20bad=20encoding=20for=20query=20p?= =?UTF-8?q?arameters=20in=20`TestClient`=20(#5659)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://github.com/tiangolo/fastapi/issues/5646 --- pyproject.toml | 2 +- tests/test_starlette_urlconvertors.py | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ae380986..06f8a97b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.21.0", + "starlette==0.22.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] diff --git a/tests/test_starlette_urlconvertors.py b/tests/test_starlette_urlconvertors.py index 5a980cbf6..5ef1b819c 100644 --- a/tests/test_starlette_urlconvertors.py +++ b/tests/test_starlette_urlconvertors.py @@ -1,4 +1,4 @@ -from fastapi import FastAPI, Path +from fastapi import FastAPI, Path, Query from fastapi.testclient import TestClient app = FastAPI() @@ -19,6 +19,11 @@ def path_convertor(param: str = Path()): return {"path": param} +@app.get("/query/") +def query_convertor(param: str = Query()): + return {"query": param} + + client = TestClient(app) @@ -45,6 +50,13 @@ def test_route_converters_path(): assert response.json() == {"path": "some/example"} +def test_route_converters_query(): + # Test query conversion + response = client.get("/query", params={"param": "Qué tal!"}) + assert response.status_code == 200, response.text + assert response.json() == {"query": "Qué tal!"} + + def test_url_path_for_path_convertor(): assert ( app.url_path_for("path_convertor", param="some/example") == "/path/some/example" From c458ca6f0792669259afd0b9a9df9e28b0228075 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 27 Nov 2022 14:46:48 +0000 Subject: [PATCH 0682/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 68cb5c7ce..d4981bc55 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump Starlette to version `0.22.0` to fix bad encoding for query parameters in `TestClient`. PR [#5659](https://github.com/tiangolo/fastapi/pull/5659) by [@azogue](https://github.com/azogue). * 👷 Tweak build-docs to improve CI performance. PR [#5699](https://github.com/tiangolo/fastapi/pull/5699) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5566](https://github.com/tiangolo/fastapi/pull/5566) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). From 46bb5d2c4b0af42ee7a700af9d224c62c343f1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 15:49:33 +0100 Subject: [PATCH 0683/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d4981bc55..2d32f2780 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,16 +2,27 @@ ## Latest Changes -* ⬆ Bump Starlette to version `0.22.0` to fix bad encoding for query parameters in `TestClient`. PR [#5659](https://github.com/tiangolo/fastapi/pull/5659) by [@azogue](https://github.com/azogue). +### Upgrades + +* ⬆ Bump Starlette to version `0.22.0` to fix bad encoding for query parameters in new `TestClient`. PR [#5659](https://github.com/tiangolo/fastapi/pull/5659) by [@azogue](https://github.com/azogue). + +### Docs + +* ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). + +### Translations + +* 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). + +### Internal + * 👷 Tweak build-docs to improve CI performance. PR [#5699](https://github.com/tiangolo/fastapi/pull/5699) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5566](https://github.com/tiangolo/fastapi/pull/5566) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* 🌐 Add Portuguese translation for `docs/pt/docs/deployment/docker.md`. PR [#5663](https://github.com/tiangolo/fastapi/pull/5663) by [@ayr-ton](https://github.com/ayr-ton). * ⬆️ Upgrade Ruff. PR [#5698](https://github.com/tiangolo/fastapi/pull/5698) by [@tiangolo](https://github.com/tiangolo). * 👷 Remove pip cache for Smokeshow as it depends on a requirements.txt. PR [#5700](https://github.com/tiangolo/fastapi/pull/5700) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix pip cache for Smokeshow. PR [#5697](https://github.com/tiangolo/fastapi/pull/5697) by [@tiangolo](https://github.com/tiangolo). * 👷 Fix and tweak CI cache handling. PR [#5696](https://github.com/tiangolo/fastapi/pull/5696) by [@tiangolo](https://github.com/tiangolo). * 👷 Update `setup-python` action in tests to use new caching feature. PR [#5680](https://github.com/tiangolo/fastapi/pull/5680) by [@madkinsz](https://github.com/madkinsz). -* ✏️ Fix typo in docs for `docs/en/docs/advanced/middleware.md`. PR [#5376](https://github.com/tiangolo/fastapi/pull/5376) by [@rifatrakib](https://github.com/rifatrakib). * ⬆ Bump black from 22.8.0 to 22.10.0. PR [#5569](https://github.com/tiangolo/fastapi/pull/5569) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.87.0 From 612b8ff168beee33fcf0c0bdff9718b4c8b1ac63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 27 Nov 2022 15:50:32 +0100 Subject: [PATCH 0684/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?88.0?= 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 2d32f2780..e741b86ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.88.0 + ### Upgrades * ⬆ Bump Starlette to version `0.22.0` to fix bad encoding for query parameters in new `TestClient`. PR [#5659](https://github.com/tiangolo/fastapi/pull/5659) by [@azogue](https://github.com/azogue). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index afdc94874..037d9804b 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.87.0" +__version__ = "0.88.0" from starlette import status as status From 7e4e0d22aeb264a512819f1c1c7045c2002ac69b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Nov 2022 08:12:37 +0100 Subject: [PATCH 0685/1881] =?UTF-8?q?=E2=AC=86=20Update=20typer[all]=20req?= =?UTF-8?q?uirement=20from=20<0.7.0,>=3D0.6.1=20to=20>=3D0.6.1,<0.8.0=20(#?= =?UTF-8?q?5639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [typer[all]](https://github.com/tiangolo/typer) to permit the latest version. - [Release notes](https://github.com/tiangolo/typer/releases) - [Changelog](https://github.com/tiangolo/typer/blob/master/docs/release-notes.md) - [Commits](https://github.com/tiangolo/typer/compare/0.6.1...0.7.0) --- updated-dependencies: - dependency-name: typer[all] dependency-type: direct:production ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 06f8a97b2..be3080ae8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ doc = [ "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", # TODO: upgrade and enable typer-cli once it supports Click 8.x.x # "typer-cli >=0.0.12,<0.0.13", - "typer[all] >=0.6.1,<0.7.0", + "typer[all] >=0.6.1,<0.8.0", "pyyaml >=5.3.1,<7.0.0", ] dev = [ From 5c4054ab8a3f5bd3a466af47d8f83036f0e628e4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 28 Nov 2022 07:13:14 +0000 Subject: [PATCH 0686/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e741b86ed..d667e9e5e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update typer[all] requirement from <0.7.0,>=0.6.1 to >=0.6.1,<0.8.0. PR [#5639](https://github.com/tiangolo/fastapi/pull/5639) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.88.0 From 8bdab084f9cbe0e483df4f6fa4bc0c004bccce7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 29 Nov 2022 19:05:40 +0100 Subject: [PATCH 0687/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20d?= =?UTF-8?q?isable=20course=20bundle=20(#5713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/overrides/main.html | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index c5eb94870..e9b9f60eb 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -22,12 +22,6 @@
- -
From 83bcdc40d853e50752d4d4a30bbc8b23a8dbc4c1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 29 Nov 2022 18:06:22 +0000 Subject: [PATCH 0688/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d667e9e5e..4252d68c5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, disable course bundle. PR [#5713](https://github.com/tiangolo/fastapi/pull/5713) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update typer[all] requirement from <0.7.0,>=0.6.1 to >=0.6.1,<0.8.0. PR [#5639](https://github.com/tiangolo/fastapi/pull/5639) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.88.0 From 41db4cba4c31ae1bebbab95d26727f200d582aad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Dec 2022 18:22:33 +0100 Subject: [PATCH 0689/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5722)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 124 +++++++++++---------- docs/en/data/people.yml | 178 ++++++++++++++++--------------- 2 files changed, 161 insertions(+), 141 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d0b37dac..1953df801 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -8,12 +8,15 @@ sponsors: - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi -- - login: ObliviousAI + - login: jrobbins-LiveData + avatarUrl: https://avatars.githubusercontent.com/u/79278744?u=bae8175fc3f09db281aca1f97a9ddc1a914a8c4f&v=4 + url: https://github.com/jrobbins-LiveData +- - login: nihpo + avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 + url: https://github.com/nihpo + - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI - - login: Lovage-Labs - avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 - url: https://github.com/Lovage-Labs - login: chaserowbotham avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4 url: https://github.com/chaserowbotham @@ -38,9 +41,18 @@ sponsors: - - login: InesIvanova avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 url: https://github.com/InesIvanova -- - login: SendCloud +- - login: vyos + avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 + url: https://github.com/vyos + - login: SendCloud avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 url: https://github.com/SendCloud + - login: matallan + avatarUrl: https://avatars.githubusercontent.com/u/12107723?v=4 + url: https://github.com/matallan + - login: takashi-yoneya + avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 + url: https://github.com/takashi-yoneya - login: mercedes-benz avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 url: https://github.com/mercedes-benz @@ -56,12 +68,18 @@ sponsors: - - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei + - login: gvisniuc + avatarUrl: https://avatars.githubusercontent.com/u/1614747?u=502dfdb2b087ddcf5460026297c98c7907bc2795&v=4 + url: https://github.com/gvisniuc - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie + - login: Lovage-Labs + avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 + url: https://github.com/Lovage-Labs - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck @@ -80,27 +98,18 @@ sponsors: - login: jmaralc avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 url: https://github.com/jmaralc - - login: takashi-yoneya - avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 - url: https://github.com/takashi-yoneya - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare -- - login: karelhusa - avatarUrl: https://avatars.githubusercontent.com/u/11407706?u=4f787cffc30ea198b15935c751940f0b48a26a17&v=4 - url: https://github.com/karelhusa - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: DucNgn - avatarUrl: https://avatars.githubusercontent.com/u/43587865?u=a9f9f61569aebdc0ce74df85b8cc1a219a7ab570&v=4 - url: https://github.com/DucNgn +- - login: indeedeng + avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 + url: https://github.com/indeedeng - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge @@ -119,9 +128,6 @@ sponsors: - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill - - login: gazpachoking - avatarUrl: https://avatars.githubusercontent.com/u/187133?v=4 - url: https://github.com/gazpachoking - login: dekoza avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 url: https://github.com/dekoza @@ -155,6 +161,12 @@ sponsors: - login: ltieman avatarUrl: https://avatars.githubusercontent.com/u/1084689?u=e69b17de17cb3ca141a17daa7ccbe173ceb1eb17&v=4 url: https://github.com/ltieman + - login: mrkmcknz + avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 + url: https://github.com/mrkmcknz + - login: coffeewasmyidea + avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 + url: https://github.com/coffeewasmyidea - login: corleyma avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 url: https://github.com/corleyma @@ -170,8 +182,11 @@ sponsors: - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 + - login: ColliotL + avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4 + url: https://github.com/ColliotL - login: grillazz - avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=0b32b7073ae1ab8b7f6d2db0188c2e1e357ff451&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=453cd1725c8d7fe3e258016bc19cff861d4fcb53&v=4 url: https://github.com/grillazz - login: dblackrun avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 @@ -179,20 +194,20 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 - - login: bauyrzhanospan - avatarUrl: https://avatars.githubusercontent.com/u/3536037?u=25c86201d0212497aefcc1688cccf509057a1dc4&v=4 - url: https://github.com/bauyrzhanospan - login: MarekBleschke avatarUrl: https://avatars.githubusercontent.com/u/3616870?v=4 url: https://github.com/MarekBleschke - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco + - login: anomaly + avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 + url: https://github.com/anomaly - login: peterHoburg avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 url: https://github.com/peterHoburg - login: jgreys - avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=b579fd97033269a5e703ab509c7d5478b146cc2d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=c66ae617d614f6c886f1f1c1799d22100b3c848d&v=4 url: https://github.com/jgreys - login: gorhack avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 @@ -203,6 +218,9 @@ sponsors: - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen + - login: Rey8d01 + avatarUrl: https://avatars.githubusercontent.com/u/4836190?u=5942598a23a377602c1669522334ab5ebeaf9165&v=4 + url: https://github.com/Rey8d01 - login: ScrimForever avatarUrl: https://avatars.githubusercontent.com/u/5040124?u=091ec38bfe16d6e762099e91309b59f248616a65&v=4 url: https://github.com/ScrimForever @@ -239,9 +257,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: Ge0f3 - avatarUrl: https://avatars.githubusercontent.com/u/11887760?u=ccd80f1ac36dcb8517ef5c4e702e8cc5a80cad2f&v=4 - url: https://github.com/Ge0f3 + - login: bapi24 + avatarUrl: https://avatars.githubusercontent.com/u/11890901?u=45cc721d8f66ad2f62b086afc3d4761d0c16b9c6&v=4 + url: https://github.com/bapi24 - login: svats2k avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 url: https://github.com/svats2k @@ -257,15 +275,12 @@ sponsors: - login: pablonnaoji avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=afc15bd5a4ba9c5c7206bbb1bcaeef606a0932e0&v=4 url: https://github.com/pablonnaoji - - login: robintully - avatarUrl: https://avatars.githubusercontent.com/u/17059673?u=862b9bb01513f5acd30df97433cb97a24dbfb772&v=4 - url: https://github.com/robintully - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck - - login: RedCarpetUp - avatarUrl: https://avatars.githubusercontent.com/u/20360440?v=4 - url: https://github.com/RedCarpetUp + - login: m4knV + avatarUrl: https://avatars.githubusercontent.com/u/19666130?u=843383978814886be93c137d10d2e20e9f13af07&v=4 + url: https://github.com/m4knV - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=75e02d102d2ee3e3d793e555fa5c63045913ccb0&v=4 url: https://github.com/Filimoa @@ -302,6 +317,9 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure + - login: faviasono + avatarUrl: https://avatars.githubusercontent.com/u/37707874?u=f0b75ca4248987c08ed8fb8ed682e7e74d5d7091&v=4 + url: https://github.com/faviasono - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -341,9 +359,12 @@ sponsors: - login: anthonycepeda avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda - - login: dotlas - avatarUrl: https://avatars.githubusercontent.com/u/88832003?v=4 - url: https://github.com/dotlas + - login: fpiem + avatarUrl: https://avatars.githubusercontent.com/u/77389987?u=f667a25cd4832b28801189013b74450e06cc232c&v=4 + url: https://github.com/fpiem + - login: DelfinaCare + avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 + url: https://github.com/DelfinaCare - login: programvx avatarUrl: https://avatars.githubusercontent.com/u/96057906?v=4 url: https://github.com/programvx @@ -354,7 +375,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 url: https://github.com/Dagmaara - - login: linux-china - avatarUrl: https://avatars.githubusercontent.com/u/46711?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4 url: https://github.com/linux-china - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 @@ -371,15 +392,9 @@ sponsors: - login: hhatto avatarUrl: https://avatars.githubusercontent.com/u/150309?u=3e8f63c27bf996bfc68464b0ce3f7a3e40e6ea7f&v=4 url: https://github.com/hhatto - - login: yourkin - avatarUrl: https://avatars.githubusercontent.com/u/178984?u=b43a7e5f8818f7d9083d3b110118d9c27d48a794&v=4 - url: https://github.com/yourkin - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs - - login: assem-ch - avatarUrl: https://avatars.githubusercontent.com/u/315228?u=e0c5ab30726d3243a40974bb9bae327866e42d9b&v=4 - url: https://github.com/assem-ch - login: adamghill avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4 url: https://github.com/adamghill @@ -494,9 +509,6 @@ sponsors: - login: logan-connolly avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly - - login: stevenayers - avatarUrl: https://avatars.githubusercontent.com/u/16361214?u=098b797d8d48afb8cd964b717847943b61d24a6d&v=4 - url: https://github.com/stevenayers - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre @@ -521,6 +533,9 @@ sponsors: - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 url: https://github.com/SebTota + - login: joerambo + avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 + url: https://github.com/joerambo - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli @@ -533,6 +548,9 @@ sponsors: - login: engineerjoe440 avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 + - login: bnkc + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=20f362505e2a994805233f42e69f9f14b4a9dd0c&v=4 + url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon @@ -590,12 +608,9 @@ sponsors: - login: pondDevThai avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 url: https://github.com/pondDevThai - - login: marlonmartins2 - avatarUrl: https://avatars.githubusercontent.com/u/87719558?u=d07ffecfdabf4fd9aca987f8b5cd9128c65e598e&v=4 - url: https://github.com/marlonmartins2 -- - login: 2niuhe - avatarUrl: https://avatars.githubusercontent.com/u/13382324?u=ac918d72e0329c87ba29b3bf85e03e98a4ee9427&v=4 - url: https://github.com/2niuhe +- - login: wardal + avatarUrl: https://avatars.githubusercontent.com/u/15804042?v=4 + url: https://github.com/wardal - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb @@ -605,6 +620,3 @@ sponsors: - login: Moises6669 avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 url: https://github.com/Moises6669 - - login: su-shubham - avatarUrl: https://avatars.githubusercontent.com/u/75021117?v=4 - url: https://github.com/su-shubham diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 730528795..51940a6b1 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1315 - prs: 342 + answers: 1837 + prs: 360 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 367 + count: 374 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -15,14 +15,14 @@ experts: url: https://github.com/dmontagu - login: ycd count: 221 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: Mause count: 207 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause - login: JarroVGIT - count: 187 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -33,22 +33,26 @@ experts: count: 130 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 +- login: iudeen + count: 87 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: raphaelauv count: 77 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: iudeen - count: 76 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: ArcLightSlavik count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik - login: falkben - count: 58 + count: 59 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben +- login: jgould22 + count: 55 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: sm-Fifteen count: 50 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 @@ -57,10 +61,6 @@ experts: count: 46 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes -- login: jgould22 - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 @@ -78,19 +78,19 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary - login: chbndrhnns - count: 35 + count: 36 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns +- login: frankie567 + count: 33 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff -- login: frankie567 - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 - login: acidjunk - count: 31 + count: 32 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk - login: krishnardt @@ -113,12 +113,16 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty +- login: yinziyan1206 + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: SirTelemak count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak - login: odiseo0 - count: 23 + count: 24 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 url: https://github.com/odiseo0 - login: acnebs @@ -137,14 +141,14 @@ experts: count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt +- login: rafsaf + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 + url: https://github.com/rafsaf - login: Hultner count: 18 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner -- login: rafsaf - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 - url: https://github.com/rafsaf - login: jorgerpo count: 17 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -161,10 +165,6 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv -- login: yinziyan1206 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 - url: https://github.com/yinziyan1206 - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 @@ -173,6 +173,10 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli +- login: mbroton + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 + url: https://github.com/mbroton - login: hellocoldworld count: 14 avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 @@ -193,27 +197,39 @@ experts: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: mbroton - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton last_month_active: -- login: JarroVGIT - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT +- login: jgould22 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: yinziyan1206 + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: iudeen count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen -- login: mbroton +- login: Kludex + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex +- login: JarroVGIT + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 + url: https://github.com/JarroVGIT +- login: TheJumpyWizard count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/90986815?u=67e9c13c9f063dd4313db7beb64eaa2f3a37f1fe&v=4 + url: https://github.com/TheJumpyWizard +- login: mbroton + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 url: https://github.com/mbroton -- login: yinziyan1206 +- login: mateoradman count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?v=4 - url: https://github.com/yinziyan1206 + avatarUrl: https://avatars.githubusercontent.com/u/48420316?u=066f36b8e8e263b0d90798113b0f291d3266db7c&v=4 + url: https://github.com/mateoradman top_contributors: - login: waynerv count: 25 @@ -223,22 +239,18 @@ top_contributors: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: jaystone776 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: dmontagu count: 16 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu -- login: jaystone776 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 - login: Kludex count: 15 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: dependabot - count: 14 - avatarUrl: https://avatars.githubusercontent.com/in/29110?v=4 - url: https://github.com/apps/dependabot - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 @@ -267,10 +279,6 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo -- login: pre-commit-ci - count: 6 - avatarUrl: https://avatars.githubusercontent.com/in/68672?v=4 - url: https://github.com/apps/pre-commit-ci - login: wshayes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -287,6 +295,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: batlopes + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 + url: https://github.com/batlopes - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -301,7 +313,7 @@ top_contributors: url: https://github.com/jfunez - login: ycd count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: komtaki count: 4 @@ -319,21 +331,17 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang -- login: batlopes - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes top_reviewers: - login: Kludex - count: 108 + count: 109 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 65 + count: 70 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 56 + count: 59 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -350,7 +358,7 @@ top_reviewers: url: https://github.com/Laineyzhang55 - login: ycd count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=fa40e037060d62bf82e16b505d870a2866725f38&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: cikay count: 41 @@ -364,30 +372,30 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda +- login: iudeen + count: 33 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: iudeen - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen +- login: komtaki + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 + url: https://github.com/komtaki - login: cassiobotaro - count: 25 + count: 26 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro - login: lsglucas - count: 25 + count: 26 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu -- login: komtaki - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 - url: https://github.com/komtaki - login: hard-coders count: 20 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 @@ -429,7 +437,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 - login: odiseo0 - count: 14 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 url: https://github.com/odiseo0 - login: sh0nk @@ -464,6 +472,14 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: peidrao + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=88c2cb42a99e0f50cdeae3606992568184783ee5&v=4 + url: https://github.com/peidrao +- login: izaguerreiro + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 + url: https://github.com/izaguerreiro - login: graingert count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 @@ -480,10 +496,6 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca -- login: izaguerreiro - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 - url: https://github.com/izaguerreiro - login: raphaelauv count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -504,6 +516,10 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 url: https://github.com/dimaqq +- login: Xewus + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 + url: https://github.com/Xewus - login: Serrones count: 7 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -512,11 +528,3 @@ top_reviewers: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 url: https://github.com/jovicon -- login: ryuckel - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/36391432?u=094eec0cfddd5013f76f31e55e56147d78b19553&v=4 - url: https://github.com/ryuckel -- login: NastasiaSaby - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/8245071?u=b3afd005f9e4bf080c219ef61a592b3a8004b764&v=4 - url: https://github.com/NastasiaSaby From 5f1cc3a5ff71b51c194e6fffe43b57501bfa22b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 2 Dec 2022 17:23:12 +0000 Subject: [PATCH 0690/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4252d68c5..29632574e 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 [#5722](https://github.com/tiangolo/fastapi/pull/5722) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Update sponsors, disable course bundle. PR [#5713](https://github.com/tiangolo/fastapi/pull/5713) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update typer[all] requirement from <0.7.0,>=0.6.1 to >=0.6.1,<0.8.0. PR [#5639](https://github.com/tiangolo/fastapi/pull/5639) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1ba515a18d563df8efff0dfafd19d187586dd9bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 23:25:39 +0100 Subject: [PATCH 0691/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.5.1=20to=201.5.2=20(#5714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.5.1 to 1.5.2. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.5.1...v1.5.2) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ab27d4b85..8ffb493a4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.5.1 + uses: pypa/gh-action-pypi-publish@v1.5.2 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From 97f04ab58c2c047d6d2ff7d803890a90190109c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Dec 2022 22:26:17 +0000 Subject: [PATCH 0692/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 29632574e..6771edb08 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.5.2. PR [#5714](https://github.com/tiangolo/fastapi/pull/5714) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5722](https://github.com/tiangolo/fastapi/pull/5722) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Update sponsors, disable course bundle. PR [#5713](https://github.com/tiangolo/fastapi/pull/5713) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update typer[all] requirement from <0.7.0,>=0.6.1 to >=0.6.1,<0.8.0. PR [#5639](https://github.com/tiangolo/fastapi/pull/5639) by [@dependabot[bot]](https://github.com/apps/dependabot). From 9efab1bd96ef061edf1753626573a0a2be1eef09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 16 Dec 2022 22:11:03 +0400 Subject: [PATCH 0693/1881] =?UTF-8?q?=F0=9F=91=B7=20Refactor=20CI=20artifa?= =?UTF-8?q?ct=20upload/download=20for=20docs=20previews=20(#5793)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/preview-docs.yml | 7 ++++++- scripts/zip-docs.sh | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 3052ec1e2..3bf986274 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/upload-artifact@v3 with: name: docs-zip - path: ./docs.zip + path: ./site/docs.zip - name: Deploy to Netlify uses: nwtgck/actions-netlify@v1.2.4 with: diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index a2e5976fb..bb3e9ebf6 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -11,6 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - name: Clean site + run: | + rm -rf ./site + mkdir ./site - name: Download Artifact Docs uses: dawidd6/action-download-artifact@v2.24.2 with: @@ -18,9 +22,10 @@ jobs: workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} name: docs-zip + path: ./site/ - name: Unzip docs run: | - rm -rf ./site + cd ./site unzip docs.zip rm -f docs.zip - name: Deploy to Netlify diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh index f2b7ba3be..69315f5dd 100644 --- a/scripts/zip-docs.sh +++ b/scripts/zip-docs.sh @@ -3,7 +3,9 @@ set -x set -e +cd ./site + if [ -f docs.zip ]; then rm -rf docs.zip fi -zip -r docs.zip ./site +zip -r docs.zip ./ From dc9fb1305a7665a6f36053fc09e9bd1804928827 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 16 Dec 2022 18:11:45 +0000 Subject: [PATCH 0694/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6771edb08..e44462458 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.5.2. PR [#5714](https://github.com/tiangolo/fastapi/pull/5714) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5722](https://github.com/tiangolo/fastapi/pull/5722) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔧 Update sponsors, disable course bundle. PR [#5713](https://github.com/tiangolo/fastapi/pull/5713) by [@tiangolo](https://github.com/tiangolo). From 63ad0ed4a66453fedb25dc002372cdfc4e9138dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Dec 2022 23:10:29 +0400 Subject: [PATCH 0695/1881] =?UTF-8?q?=E2=AC=86=20Bump=20nwtgck/actions-net?= =?UTF-8?q?lify=20from=201.2.4=20to=202.0.0=20(#5757)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 2 +- .github/workflows/preview-docs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 3bf986274..b9bd521b3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -38,7 +38,7 @@ jobs: name: docs-zip path: ./site/docs.zip - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v1.2.4 + uses: nwtgck/actions-netlify@v2.0.0 with: publish-dir: './site' production-branch: master diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index bb3e9ebf6..7d31a9c64 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -30,7 +30,7 @@ jobs: rm -f docs.zip - name: Deploy to Netlify id: netlify - uses: nwtgck/actions-netlify@v1.2.4 + uses: nwtgck/actions-netlify@v2.0.0 with: publish-dir: './site' production-deploy: false From 10500dbc035b09dbe92ee05031aeca62364b5e31 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 16 Dec 2022 19:11:14 +0000 Subject: [PATCH 0696/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e44462458..b65460294 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.5.2. PR [#5714](https://github.com/tiangolo/fastapi/pull/5714) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5722](https://github.com/tiangolo/fastapi/pull/5722) by [@github-actions[bot]](https://github.com/apps/github-actions). From 4d5cbc71ac327eedcd64580de24c71896d2e6d2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Dec 2022 23:41:48 +0400 Subject: [PATCH 0697/1881] =?UTF-8?q?=E2=AC=86=20Update=20sqlalchemy=20req?= =?UTF-8?q?uirement=20from=20<=3D1.4.41,>=3D1.3.18=20to=20>=3D1.3.18,<1.4.?= =?UTF-8?q?43=20(#5540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 be3080ae8..55856cf36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ test = [ "email_validator >=1.1.1,<2.0.0", # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel - "sqlalchemy >=1.3.18,<=1.4.41", + "sqlalchemy >=1.3.18,<1.4.43", "peewee >=3.13.3,<4.0.0", "databases[sqlite] >=0.3.2,<0.7.0", "orjson >=3.2.1,<4.0.0", From efc12c5cdbede6922a033a5234c70cb22c4d204a Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 16 Dec 2022 20:25:51 +0000 Subject: [PATCH 0698/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b65460294..1d574906b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.1 to 1.5.2. PR [#5714](https://github.com/tiangolo/fastapi/pull/5714) by [@dependabot[bot]](https://github.com/apps/dependabot). From d0573f5713b0a8ce2dbb3d12d36a7fc34b89e2ff Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Sat, 7 Jan 2023 15:45:48 +0200 Subject: [PATCH 0699/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20fun?= =?UTF-8?q?ction=20return=20type=20annotations=20to=20declare=20the=20`res?= =?UTF-8?q?ponse=5Fmodel`=20(#1436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/response-model.md | 142 ++- docs_src/response_model/tutorial001.py | 12 +- docs_src/response_model/tutorial001_01.py | 27 + .../response_model/tutorial001_01_py310.py | 25 + .../response_model/tutorial001_01_py39.py | 27 + docs_src/response_model/tutorial001_py310.py | 12 +- docs_src/response_model/tutorial001_py39.py | 12 +- docs_src/response_model/tutorial002.py | 4 +- docs_src/response_model/tutorial002_py310.py | 4 +- docs_src/response_model/tutorial003.py | 4 +- docs_src/response_model/tutorial003_01.py | 21 + .../response_model/tutorial003_01_py310.py | 19 + docs_src/response_model/tutorial003_py310.py | 4 +- fastapi/applications.py | 20 +- fastapi/dependencies/utils.py | 16 +- fastapi/routing.py | 25 +- tests/test_reponse_set_reponse_code_empty.py | 1 + ...est_response_model_as_return_annotation.py | 1051 +++++++++++++++++ 18 files changed, 1368 insertions(+), 58 deletions(-) create mode 100644 docs_src/response_model/tutorial001_01.py create mode 100644 docs_src/response_model/tutorial001_01_py310.py create mode 100644 docs_src/response_model/tutorial001_01_py39.py create mode 100644 docs_src/response_model/tutorial003_01.py create mode 100644 docs_src/response_model/tutorial003_01_py310.py create mode 100644 tests/test_response_model_as_return_annotation.py diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index ab68314e8..69c02052d 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -1,6 +1,51 @@ -# Response Model +# Response Model - Return Type -You can declare the model used for the response with the parameter `response_model` in any of the *path operations*: +You can declare the type used for the response by annotating the *path operation function* **return type**. + +You can use **type annotations** the same way you would for input data in function **parameters**, you can use Pydantic models, lists, dictionaries, scalar values like integers, booleans, etc. + +=== "Python 3.6 and above" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +FastAPI will use this return type to: + +* **Validate** the returned data. + * If the data is invalid (e.g. you are missing a field), it means that *your* app code is broken, not returning what it should, and it will return a server error instead of returning incorrect data. This way you and your clients can be certain that they will receive the data and the data shape expected. +* Add a **JSON Schema** for the response, in the OpenAPI *path operation*. + * This will be used by the **automatic docs**. + * It will also be used by automatic client code generation tools. + +But most importantly: + +* It will **limit and filter** the output data to what is defined in the return type. + * This is particularly important for **security**, we'll see more of that below. + +## `response_model` Parameter + +There are some cases where you need or want to return some data that is not exactly what the type declares. + +For example, you could want to **return a dictionary** or a database object, but **declare it as a Pydantic model**. This way the Pydantic model would do all the data documentation, validation, etc. for the object that you returned (e.g. a dictionary or database object). + +If you added the return type annotation, tools and editors would complain with a (correct) error telling you that your function is returning a type (e.g. a dict) that is different from what you declared (e.g. a Pydantic model). + +In those cases, you can use the *path operation decorator* parameter `response_model` instead of the return type. + +You can use the `response_model` parameter in any of the *path operations*: * `@app.get()` * `@app.post()` @@ -10,40 +55,39 @@ You can declare the model used for the response with the parameter `response_mod === "Python 3.6 and above" - ```Python hl_lines="17" + ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001.py!} ``` === "Python 3.9 and above" - ```Python hl_lines="17" + ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="15" + ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001_py310.py!} ``` !!! note Notice that `response_model` is a parameter of the "decorator" method (`get`, `post`, etc). Not of your *path operation function*, like all the parameters and body. -It receives the same type you would declare for a Pydantic model attribute, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`. +`response_model` receives the same type you would declare for a Pydantic model field, so, it can be a Pydantic model, but it can also be, e.g. a `list` of Pydantic models, like `List[Item]`. -FastAPI will use this `response_model` to: +FastAPI will use this `response_model` to do all the data documentation, validation, etc. and also to **convert and filter the output data** to its type declaration. -* Convert the output data to its type declaration. -* Validate the data. -* Add a JSON Schema for the response, in the OpenAPI *path operation*. -* Will be used by the automatic documentation systems. +!!! tip + If you have strict type checks in your editor, mypy, etc, you can declare the function return type as `Any`. -But most importantly: + That way you tell the editor that you are intentionally returning anything. But FastAPI will still do the data documentation, validation, filtering, etc. with the `response_model`. -* Will limit the output data to that of the model. We'll see how that's important below. +### `response_model` Priority -!!! note "Technical Details" - The response model is declared in this parameter instead of as a function return type annotation, because the path function may not actually return that response model but rather return a `dict`, database object or some other model, and then use the `response_model` to perform the field limiting and serialization. +If you declare both a return type and a `response_model`, the `response_model` will take priority and be used by FastAPI. + +This way you can add correct type annotations to your functions even when you are returning a type different than the response model, to be used by the editor and tools like mypy. And still you can have FastAPI do the data validation, documentation, etc. using the `response_model`. ## Return the same input data @@ -71,24 +115,24 @@ And we are using this model to declare our input and the same model to declare o === "Python 3.6 and above" - ```Python hl_lines="17-18" + ```Python hl_lines="18" {!> ../../../docs_src/response_model/tutorial002.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="15-16" + ```Python hl_lines="16" {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` Now, whenever a browser is creating a user with a password, the API will return the same password in the response. -In this case, it might not be a problem, because the user themself is sending the password. +In this case, it might not be a problem, because it's the same user sending the password. But if we use the same model for another *path operation*, we could be sending our user's passwords to every client. !!! danger - Never store the plain password of a user or send it in a response. + Never store the plain password of a user or send it in a response like this, unless you know all the caveats and you know what you are doing. ## Add an output model @@ -102,7 +146,7 @@ We can instead create an input model with the plaintext password and an output m === "Python 3.10 and above" - ```Python hl_lines="7 9 14" + ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` @@ -116,7 +160,7 @@ Here, even though our *path operation function* is returning the same input user === "Python 3.10 and above" - ```Python hl_lines="22" + ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` @@ -130,12 +174,66 @@ Here, even though our *path operation function* is returning the same input user === "Python 3.10 and above" - ```Python hl_lines="20" + ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). +### `response_model` or Return Type + +In this case, because the two models are different, if we annotated the function return type as `UserOut`, the editor and tools would complain that we are returning an invalid type, as those are different classes. + +That's why in this example we have to declare it in the `response_model` parameter. + +...but continue reading below to see how to overcome that. + +## Return Type and Data Filtering + +Let's continue from the previous example. We wanted to **annotate the function with one type** but return something that includes **more data**. + +We want FastAPI to keep **filtering** the data using the response model. + +In the previous example, because the classes were different, we had to use the `response_model` parameter. But that also means that we don't get the support from the editor and tools checking the function return type. + +But in most of the cases where we need to do something like this, we want the model just to **filter/remove** some of the data as in this example. + +And in those cases, we can use classes and inheritance to take advantage of function **type annotations** to get better support in the editor and tools, and still get the FastAPI **data filtering**. + +=== "Python 3.6 and above" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +With this, we get tooling support, from editors and mypy as this code is correct in terms of types, but we also get the data filtering from FastAPI. + +How does this work? Let's check that out. 🤓 + +### Type Annotations and Tooling + +First let's see how editors, mypy and other tools would see this. + +`BaseUser` has the base fields. Then `UserIn` inherits from `BaseUser` and adds the `password` field, so, it will include all the fields from both models. + +We annotate the function return type as `BaseUser`, but we are actually returning a `UserIn` instance. + +The editor, mypy, and other tools won't complain about this because, in typing terms, `UserIn` is a subclass of `BaseUser`, which means it's a *valid* type when what is expected is anything that is a `BaseUser`. + +### FastAPI Data Filtering + +Now, for FastAPI, it will see the return type and make sure that what you return includes **only** the fields that are declared in the type. + +FastAPI does several things internally with Pydantic to make sure that those same rules of class inheritance are not used for the returned data filtering, otherwise you could end up returning much more data than what you expected. + +This way, you can get the best of both worlds: type annotations with **tooling support** and **data filtering**. + ## See it in the docs When you see the automatic docs, you can check that the input model and output model will both have their own JSON Schema: diff --git a/docs_src/response_model/tutorial001.py b/docs_src/response_model/tutorial001.py index 0f6e03e5b..fd1c902a5 100644 --- a/docs_src/response_model/tutorial001.py +++ b/docs_src/response_model/tutorial001.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Any, List, Union from fastapi import FastAPI from pydantic import BaseModel @@ -15,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=List[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial001_01.py b/docs_src/response_model/tutorial001_01.py new file mode 100644 index 000000000..98d30d540 --- /dev/null +++ b/docs_src/response_model/tutorial001_01.py @@ -0,0 +1,27 @@ +from typing import List, Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + tags: List[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> List[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_01_py310.py b/docs_src/response_model/tutorial001_01_py310.py new file mode 100644 index 000000000..7951c1076 --- /dev/null +++ b/docs_src/response_model/tutorial001_01_py310.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + tags: list[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_01_py39.py b/docs_src/response_model/tutorial001_01_py39.py new file mode 100644 index 000000000..16c78aa3f --- /dev/null +++ b/docs_src/response_model/tutorial001_01_py39.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + tags: list[str] = [] + + +@app.post("/items/") +async def create_item(item: Item) -> Item: + return item + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] diff --git a/docs_src/response_model/tutorial001_py310.py b/docs_src/response_model/tutorial001_py310.py index 59efecde4..f8a2aa9fc 100644 --- a/docs_src/response_model/tutorial001_py310.py +++ b/docs_src/response_model/tutorial001_py310.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import FastAPI from pydantic import BaseModel @@ -13,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=list[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial001_py39.py b/docs_src/response_model/tutorial001_py39.py index cdcca39d2..261e252d0 100644 --- a/docs_src/response_model/tutorial001_py39.py +++ b/docs_src/response_model/tutorial001_py39.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Union from fastapi import FastAPI from pydantic import BaseModel @@ -15,5 +15,13 @@ class Item(BaseModel): @app.post("/items/", response_model=Item) -async def create_item(item: Item): +async def create_item(item: Item) -> Any: return item + + +@app.get("/items/", response_model=list[Item]) +async def read_items() -> Any: + return [ + {"name": "Portal Gun", "price": 42.0}, + {"name": "Plumbus", "price": 32.0}, + ] diff --git a/docs_src/response_model/tutorial002.py b/docs_src/response_model/tutorial002.py index c68e8b138..a58668f9e 100644 --- a/docs_src/response_model/tutorial002.py +++ b/docs_src/response_model/tutorial002.py @@ -14,6 +14,6 @@ class UserIn(BaseModel): # Don't do this in production! -@app.post("/user/", response_model=UserIn) -async def create_user(user: UserIn): +@app.post("/user/") +async def create_user(user: UserIn) -> UserIn: return user diff --git a/docs_src/response_model/tutorial002_py310.py b/docs_src/response_model/tutorial002_py310.py index 29ab9c9d2..0a91a5967 100644 --- a/docs_src/response_model/tutorial002_py310.py +++ b/docs_src/response_model/tutorial002_py310.py @@ -12,6 +12,6 @@ class UserIn(BaseModel): # Don't do this in production! -@app.post("/user/", response_model=UserIn) -async def create_user(user: UserIn): +@app.post("/user/") +async def create_user(user: UserIn) -> UserIn: return user diff --git a/docs_src/response_model/tutorial003.py b/docs_src/response_model/tutorial003.py index 37e493dcb..c42dbc707 100644 --- a/docs_src/response_model/tutorial003.py +++ b/docs_src/response_model/tutorial003.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Any, Union from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -20,5 +20,5 @@ class UserOut(BaseModel): @app.post("/user/", response_model=UserOut) -async def create_user(user: UserIn): +async def create_user(user: UserIn) -> Any: return user diff --git a/docs_src/response_model/tutorial003_01.py b/docs_src/response_model/tutorial003_01.py new file mode 100644 index 000000000..52694b551 --- /dev/null +++ b/docs_src/response_model/tutorial003_01.py @@ -0,0 +1,21 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class BaseUser(BaseModel): + username: str + email: EmailStr + full_name: Union[str, None] = None + + +class UserIn(BaseUser): + password: str + + +@app.post("/user/") +async def create_user(user: UserIn) -> BaseUser: + return user diff --git a/docs_src/response_model/tutorial003_01_py310.py b/docs_src/response_model/tutorial003_01_py310.py new file mode 100644 index 000000000..6ffddfd0a --- /dev/null +++ b/docs_src/response_model/tutorial003_01_py310.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI +from pydantic import BaseModel, EmailStr + +app = FastAPI() + + +class BaseUser(BaseModel): + username: str + email: EmailStr + full_name: str | None = None + + +class UserIn(BaseUser): + password: str + + +@app.post("/user/") +async def create_user(user: UserIn) -> BaseUser: + return user diff --git a/docs_src/response_model/tutorial003_py310.py b/docs_src/response_model/tutorial003_py310.py index fc9693e3c..3703bf888 100644 --- a/docs_src/response_model/tutorial003_py310.py +++ b/docs_src/response_model/tutorial003_py310.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import FastAPI from pydantic import BaseModel, EmailStr @@ -18,5 +20,5 @@ class UserOut(BaseModel): @app.post("/user/", response_model=UserOut) -async def create_user(user: UserIn): +async def create_user(user: UserIn) -> Any: return user diff --git a/fastapi/applications.py b/fastapi/applications.py index 61d4582d2..36dc2605d 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -274,7 +274,7 @@ class FastAPI(Starlette): path: str, endpoint: Callable[..., Coroutine[Any, Any, Response]], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -332,7 +332,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -435,7 +435,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -490,7 +490,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -545,7 +545,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -600,7 +600,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -655,7 +655,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -710,7 +710,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -765,7 +765,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, @@ -820,7 +820,7 @@ class FastAPI(Starlette): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[Depends]] = None, diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4c817d5d0..32e171f18 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -253,7 +253,7 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: name=param.name, kind=param.kind, default=param.default, - annotation=get_typed_annotation(param, globalns), + annotation=get_typed_annotation(param.annotation, globalns), ) for param in signature.parameters.values() ] @@ -261,14 +261,24 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: return typed_signature -def get_typed_annotation(param: inspect.Parameter, globalns: Dict[str, Any]) -> Any: - annotation = param.annotation +def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any: if isinstance(annotation, str): annotation = ForwardRef(annotation) annotation = evaluate_forwardref(annotation, globalns, globalns) return annotation +def get_typed_return_annotation(call: Callable[..., Any]) -> Any: + signature = inspect.signature(call) + annotation = signature.return_annotation + + if annotation is inspect.Signature.empty: + return None + + globalns = getattr(call, "__globals__", {}) + return get_typed_annotation(annotation, globalns) + + def get_dependant( *, path: str, diff --git a/fastapi/routing.py b/fastapi/routing.py index 9a7d88efc..8c73b954f 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -26,6 +26,7 @@ from fastapi.dependencies.utils import ( get_body_field, get_dependant, get_parameterless_sub_dependant, + get_typed_return_annotation, solve_dependencies, ) from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder @@ -323,7 +324,7 @@ class APIRoute(routing.Route): path: str, endpoint: Callable[..., Any], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -354,6 +355,8 @@ class APIRoute(routing.Route): ) -> None: self.path = path self.endpoint = endpoint + if isinstance(response_model, DefaultPlaceholder): + response_model = get_typed_return_annotation(endpoint) self.response_model = response_model self.summary = summary self.response_description = response_description @@ -519,7 +522,7 @@ class APIRouter(routing.Router): path: str, endpoint: Callable[..., Any], *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -600,7 +603,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -795,7 +798,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -851,7 +854,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -907,7 +910,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -963,7 +966,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1019,7 +1022,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1075,7 +1078,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1131,7 +1134,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, @@ -1187,7 +1190,7 @@ class APIRouter(routing.Router): self, path: str, *, - response_model: Any = None, + response_model: Any = Default(None), status_code: Optional[int] = None, tags: Optional[List[Union[str, Enum]]] = None, dependencies: Optional[Sequence[params.Depends]] = None, diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 094d54a84..50ec753a0 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -9,6 +9,7 @@ app = FastAPI() @app.delete( "/{id}", status_code=204, + response_model=None, ) async def delete_deployment( id: int, diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py new file mode 100644 index 000000000..f2056fecd --- /dev/null +++ b/tests/test_response_model_as_return_annotation.py @@ -0,0 +1,1051 @@ +from typing import List, Union + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, ValidationError + + +class BaseUser(BaseModel): + name: str + + +class User(BaseUser): + surname: str + + +class DBUser(User): + password_hash: str + + +class Item(BaseModel): + name: str + price: float + + +app = FastAPI() + + +@app.get("/no_response_model-no_annotation-return_model") +def no_response_model_no_annotation_return_model(): + return User(name="John", surname="Doe") + + +@app.get("/no_response_model-no_annotation-return_dict") +def no_response_model_no_annotation_return_dict(): + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model-no_annotation-return_same_model", response_model=User) +def response_model_no_annotation_return_same_model(): + return User(name="John", surname="Doe") + + +@app.get("/response_model-no_annotation-return_exact_dict", response_model=User) +def response_model_no_annotation_return_exact_dict(): + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model-no_annotation-return_invalid_dict", response_model=User) +def response_model_no_annotation_return_invalid_dict(): + return {"name": "John"} + + +@app.get("/response_model-no_annotation-return_invalid_model", response_model=User) +def response_model_no_annotation_return_invalid_model(): + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model-no_annotation-return_dict_with_extra_data", response_model=User +) +def response_model_no_annotation_return_dict_with_extra_data(): + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model-no_annotation-return_submodel_with_extra_data", response_model=User +) +def response_model_no_annotation_return_submodel_with_extra_data(): + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/no_response_model-annotation-return_same_model") +def no_response_model_annotation_return_same_model() -> User: + return User(name="John", surname="Doe") + + +@app.get("/no_response_model-annotation-return_exact_dict") +def no_response_model_annotation_return_exact_dict() -> User: + return {"name": "John", "surname": "Doe"} + + +@app.get("/no_response_model-annotation-return_invalid_dict") +def no_response_model_annotation_return_invalid_dict() -> User: + return {"name": "John"} + + +@app.get("/no_response_model-annotation-return_invalid_model") +def no_response_model_annotation_return_invalid_model() -> User: + return Item(name="Foo", price=42.0) + + +@app.get("/no_response_model-annotation-return_dict_with_extra_data") +def no_response_model_annotation_return_dict_with_extra_data() -> User: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get("/no_response_model-annotation-return_submodel_with_extra_data") +def no_response_model_annotation_return_submodel_with_extra_data() -> User: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/response_model_none-annotation-return_same_model", response_model=None) +def response_model_none_annotation_return_same_model() -> User: + return User(name="John", surname="Doe") + + +@app.get("/response_model_none-annotation-return_exact_dict", response_model=None) +def response_model_none_annotation_return_exact_dict() -> User: + return {"name": "John", "surname": "Doe"} + + +@app.get("/response_model_none-annotation-return_invalid_dict", response_model=None) +def response_model_none_annotation_return_invalid_dict() -> User: + return {"name": "John"} + + +@app.get("/response_model_none-annotation-return_invalid_model", response_model=None) +def response_model_none_annotation_return_invalid_model() -> User: + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model_none-annotation-return_dict_with_extra_data", response_model=None +) +def response_model_none_annotation_return_dict_with_extra_data() -> User: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model_none-annotation-return_submodel_with_extra_data", + response_model=None, +) +def response_model_none_annotation_return_submodel_with_extra_data() -> User: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_model1-annotation_model2-return_same_model", response_model=User +) +def response_model_model1_annotation_model2_return_same_model() -> Item: + return User(name="John", surname="Doe") + + +@app.get( + "/response_model_model1-annotation_model2-return_exact_dict", response_model=User +) +def response_model_model1_annotation_model2_return_exact_dict() -> Item: + return {"name": "John", "surname": "Doe"} + + +@app.get( + "/response_model_model1-annotation_model2-return_invalid_dict", response_model=User +) +def response_model_model1_annotation_model2_return_invalid_dict() -> Item: + return {"name": "John"} + + +@app.get( + "/response_model_model1-annotation_model2-return_invalid_model", response_model=User +) +def response_model_model1_annotation_model2_return_invalid_model() -> Item: + return Item(name="Foo", price=42.0) + + +@app.get( + "/response_model_model1-annotation_model2-return_dict_with_extra_data", + response_model=User, +) +def response_model_model1_annotation_model2_return_dict_with_extra_data() -> Item: + return {"name": "John", "surname": "Doe", "password_hash": "secret"} + + +@app.get( + "/response_model_model1-annotation_model2-return_submodel_with_extra_data", + response_model=User, +) +def response_model_model1_annotation_model2_return_submodel_with_extra_data() -> Item: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_filtering_model-annotation_submodel-return_submodel", + response_model=User, +) +def response_model_filtering_model_annotation_submodel_return_submodel() -> DBUser: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/response_model_list_of_model-no_annotation", response_model=List[User]) +def response_model_list_of_model_no_annotation(): + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get("/no_response_model-annotation_list_of_model") +def no_response_model_annotation_list_of_model() -> List[User]: + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get("/no_response_model-annotation_forward_ref_list_of_model") +def no_response_model_annotation_forward_ref_list_of_model() -> "List[User]": + return [ + DBUser(name="John", surname="Doe", password_hash="secret"), + DBUser(name="Jane", surname="Does", password_hash="secret2"), + ] + + +@app.get( + "/response_model_union-no_annotation-return_model1", + response_model=Union[User, Item], +) +def response_model_union_no_annotation_return_model1(): + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get( + "/response_model_union-no_annotation-return_model2", + response_model=Union[User, Item], +) +def response_model_union_no_annotation_return_model2(): + return Item(name="Foo", price=42.0) + + +@app.get("/no_response_model-annotation_union-return_model1") +def no_response_model_annotation_union_return_model1() -> Union[User, Item]: + return DBUser(name="John", surname="Doe", password_hash="secret") + + +@app.get("/no_response_model-annotation_union-return_model2") +def no_response_model_annotation_union_return_model2() -> Union[User, Item]: + return Item(name="Foo", price=42.0) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/no_response_model-no_annotation-return_model": { + "get": { + "summary": "No Response Model No Annotation Return Model", + "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-no_annotation-return_dict": { + "get": { + "summary": "No Response Model No Annotation Return Dict", + "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model-no_annotation-return_same_model": { + "get": { + "summary": "Response Model No Annotation Return Same Model", + "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_exact_dict": { + "get": { + "summary": "Response Model No Annotation Return Exact Dict", + "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_dict": { + "get": { + "summary": "Response Model No Annotation Return Invalid Dict", + "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_model": { + "get": { + "summary": "Response Model No Annotation Return Invalid Model", + "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Dict With Extra Data", + "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Submodel With Extra Data", + "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_same_model": { + "get": { + "summary": "No Response Model Annotation Return Same Model", + "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_exact_dict": { + "get": { + "summary": "No Response Model Annotation Return Exact Dict", + "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_dict": { + "get": { + "summary": "No Response Model Annotation Return Invalid Dict", + "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_model": { + "get": { + "summary": "No Response Model Annotation Return Invalid Model", + "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_dict_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Dict With Extra Data", + "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Submodel With Extra Data", + "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_none-annotation-return_same_model": { + "get": { + "summary": "Response Model None Annotation Return Same Model", + "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_exact_dict": { + "get": { + "summary": "Response Model None Annotation Return Exact Dict", + "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_dict": { + "get": { + "summary": "Response Model None Annotation Return Invalid Dict", + "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_model": { + "get": { + "summary": "Response Model None Annotation Return Invalid Model", + "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Dict With Extra Data", + "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Submodel With Extra Data", + "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_same_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Same Model", + "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_exact_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", + "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", + "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", + "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_dict_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_filtering_model-annotation_submodel-return_submodel": { + "get": { + "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", + "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_list_of_model-no_annotation": { + "get": { + "summary": "Response Model List Of Model No Annotation", + "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_list_of_model": { + "get": { + "summary": "No Response Model Annotation List Of Model", + "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_forward_ref_list_of_model": { + "get": { + "summary": "No Response Model Annotation Forward Ref List Of Model", + "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model1": { + "get": { + "summary": "Response Model Union No Annotation Return Model1", + "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model2": { + "get": { + "summary": "Response Model Union No Annotation Return Model2", + "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model1": { + "get": { + "summary": "No Response Model Annotation Union Return Model1", + "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model2": { + "get": { + "summary": "No Response Model Annotation Union Return Model2", + "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["name", "surname"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "surname": {"title": "Surname", "type": "string"}, + }, + }, + } + }, +} + + +client = TestClient(app) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_no_response_model_no_annotation_return_model(): + response = client.get("/no_response_model-no_annotation-return_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_no_annotation_return_dict(): + response = client.get("/no_response_model-no_annotation-return_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_same_model(): + response = client.get("/response_model-no_annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_exact_dict(): + response = client.get("/response_model-no_annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/response_model-no_annotation-return_invalid_dict") + + +def test_response_model_no_annotation_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/response_model-no_annotation-return_invalid_model") + + +def test_response_model_no_annotation_return_dict_with_extra_data(): + response = client.get("/response_model-no_annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_no_annotation_return_submodel_with_extra_data(): + response = client.get( + "/response_model-no_annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_same_model(): + response = client.get("/no_response_model-annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_exact_dict(): + response = client.get("/no_response_model-annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/no_response_model-annotation-return_invalid_dict") + + +def test_no_response_model_annotation_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/no_response_model-annotation-return_invalid_model") + + +def test_no_response_model_annotation_return_dict_with_extra_data(): + response = client.get("/no_response_model-annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_return_submodel_with_extra_data(): + response = client.get( + "/no_response_model-annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_same_model(): + response = client.get("/response_model_none-annotation-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_exact_dict(): + response = client.get("/response_model_none-annotation-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_none_annotation_return_invalid_dict(): + response = client.get("/response_model_none-annotation-return_invalid_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John"} + + +def test_response_model_none_annotation_return_invalid_model(): + response = client.get("/response_model_none-annotation-return_invalid_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_response_model_none_annotation_return_dict_with_extra_data(): + response = client.get("/response_model_none-annotation-return_dict_with_extra_data") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "John", + "surname": "Doe", + "password_hash": "secret", + } + + +def test_response_model_none_annotation_return_submodel_with_extra_data(): + response = client.get( + "/response_model_none-annotation-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "John", + "surname": "Doe", + "password_hash": "secret", + } + + +def test_response_model_model1_annotation_model2_return_same_model(): + response = client.get("/response_model_model1-annotation_model2-return_same_model") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_exact_dict(): + response = client.get("/response_model_model1-annotation_model2-return_exact_dict") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_invalid_dict(): + with pytest.raises(ValidationError): + client.get("/response_model_model1-annotation_model2-return_invalid_dict") + + +def test_response_model_model1_annotation_model2_return_invalid_model(): + with pytest.raises(ValidationError): + client.get("/response_model_model1-annotation_model2-return_invalid_model") + + +def test_response_model_model1_annotation_model2_return_dict_with_extra_data(): + response = client.get( + "/response_model_model1-annotation_model2-return_dict_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_model1_annotation_model2_return_submodel_with_extra_data(): + response = client.get( + "/response_model_model1-annotation_model2-return_submodel_with_extra_data" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_filtering_model_annotation_submodel_return_submodel(): + response = client.get( + "/response_model_filtering_model-annotation_submodel-return_submodel" + ) + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_list_of_model_no_annotation(): + response = client.get("/response_model_list_of_model-no_annotation") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_no_response_model_annotation_list_of_model(): + response = client.get("/no_response_model-annotation_list_of_model") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_no_response_model_annotation_forward_ref_list_of_model(): + response = client.get("/no_response_model-annotation_forward_ref_list_of_model") + assert response.status_code == 200, response.text + assert response.json() == [ + {"name": "John", "surname": "Doe"}, + {"name": "Jane", "surname": "Does"}, + ] + + +def test_response_model_union_no_annotation_return_model1(): + response = client.get("/response_model_union-no_annotation-return_model1") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_response_model_union_no_annotation_return_model2(): + response = client.get("/response_model_union-no_annotation-return_model2") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_no_response_model_annotation_union_return_model1(): + response = client.get("/no_response_model-annotation_union-return_model1") + assert response.status_code == 200, response.text + assert response.json() == {"name": "John", "surname": "Doe"} + + +def test_no_response_model_annotation_union_return_model2(): + response = client.get("/no_response_model-annotation_union-return_model2") + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "price": 42.0} From 18d087f9c66b06c8baa4164bdf647af362b4a4ee Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 13:46:24 +0000 Subject: [PATCH 0700/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1d574906b..a0d1a7eb0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for function return type annotations to declare the `response_model`. PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). * ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). From cb35e275e3f17badea3f3715a35b5e7028121eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 7 Jan 2023 17:58:46 +0400 Subject: [PATCH 0701/1881] =?UTF-8?q?=F0=9F=94=A7=20Remove=20Doist=20spons?= =?UTF-8?q?or=20(#5847)?= 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/overrides/main.html | 6 ------ 3 files changed, 10 deletions(-) diff --git a/README.md b/README.md index 7c4a6c4b4..f3e60306e 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 749f528c5..76128d69b 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -8,9 +8,6 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg - - url: https://doist.com/careers/9B437B1615-wa-senior-backend-engineer-python - title: Help us migrate doist to FastAPI - img: https://fastapi.tiangolo.com/img/sponsors/doist.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index e9b9f60eb..b85f0c4cf 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -34,12 +34,6 @@
-
From 679aee85ce145fc9a9d052bffafd31d281ea6c58 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 13:59:26 +0000 Subject: [PATCH 0702/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a0d1a7eb0..ed5369689 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for function return type annotations to declare the `response_model`. PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). * ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). From d70eef825e2a31cd0a7b37765c0b21514a336fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 7 Jan 2023 18:13:34 +0400 Subject: [PATCH 0703/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Svix=20(#5848)?= 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/docs/img/sponsors/svix.svg | 178 +++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 docs/en/docs/img/sponsors/svix.svg diff --git a/README.md b/README.md index f3e60306e..2b4de2a65 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 76128d69b..c39dbb589 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -30,6 +30,9 @@ silver: - url: https://careers.budget-insight.com/ title: Budget Insight is hiring! img: https://fastapi.tiangolo.com/img/sponsors/budget-insight.svg + - url: https://www.svix.com/ + title: Svix - Webhooks as a service + img: https://fastapi.tiangolo.com/img/sponsors/svix.svg bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/docs/img/sponsors/svix.svg b/docs/en/docs/img/sponsors/svix.svg new file mode 100644 index 000000000..845a860a2 --- /dev/null +++ b/docs/en/docs/img/sponsors/svix.svg @@ -0,0 +1,178 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a3edc760513f638a7dd417414787cfa23e2327e7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 14:14:07 +0000 Subject: [PATCH 0704/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed5369689..9cfce002b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). * 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for function return type annotations to declare the `response_model`. PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). * ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2a3a786dd7dc8a9112a1f47dd1949d21be33284b Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Sat, 7 Jan 2023 23:21:23 +0900 Subject: [PATCH 0705/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/cors.md`=20(#3764)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: weekwith.me <63915557+0417taehyun@users.noreply.github.com> Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ko/docs/tutorial/cors.md | 84 +++++++++++++++++++++++++++++++++++ docs/ko/mkdocs.yml | 1 + 2 files changed, 85 insertions(+) create mode 100644 docs/ko/docs/tutorial/cors.md diff --git a/docs/ko/docs/tutorial/cors.md b/docs/ko/docs/tutorial/cors.md new file mode 100644 index 000000000..39e9ea83f --- /dev/null +++ b/docs/ko/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# 교차 출처 리소스 공유 + +CORS 또는 "교차-출처 리소스 공유"란, 브라우저에서 동작하는 프론트엔드가 자바스크립트로 코드로 백엔드와 통신하고, 백엔드는 해당 프론트엔드와 다른 "출처"에 존재하는 상황을 의미합니다. + +## 출처 + +출처란 프로토콜(`http` , `https`), 도메인(`myapp.com`, `localhost`, `localhost.tiangolo.com` ), 그리고 포트(`80`, `443`, `8080` )의 조합을 의미합니다. + +따라서, 아래는 모두 상이한 출처입니다: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +모두 `localhost` 에 있지만, 서로 다른 프로토콜과 포트를 사용하고 있으므로 다른 "출처"입니다. + +## 단계 + +브라우저 내 `http://localhost:8080`에서 동작하는 프론트엔드가 있고, 자바스크립트는 `http://localhost`를 통해 백엔드와 통신한다고 가정해봅시다(포트를 명시하지 않는 경우, 브라우저는 `80` 을 기본 포트로 간주합니다). + +그러면 브라우저는 백엔드에 HTTP `OPTIONS` 요청을 보내고, 백엔드에서 이 다른 출처(`http://localhost:8080`)와의 통신을 허가하는 적절한 헤더를 보내면, 브라우저는 프론트엔드의 자바스크립트가 백엔드에 요청을 보낼 수 있도록 합니다. + +이를 위해, 백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야만 합니다. + +이 경우, 프론트엔드가 제대로 동작하기 위해 `http://localhost:8080`을 목록에 포함해야 합니다. + +## 와일드카드 + +모든 출처를 허용하기 위해 목록을 `"*"` ("와일드카드")로 선언하는 것도 가능합니다. + +하지만 이것은 특정한 유형의 통신만을 허용하며, 쿠키 및 액세스 토큰과 사용되는 인증 헤더(Authoriztion header) 등이 포함된 경우와 같이 자격 증명(credentials)이 포함된 통신은 허용되지 않습니다. + +따라서 모든 작업을 의도한대로 실행하기 위해, 허용되는 출처를 명시적으로 지정하는 것이 좋습니다. + +## `CORSMiddleware` 사용 + +`CORSMiddleware` 을 사용하여 **FastAPI** 응용 프로그램의 교차 출처 리소스 공유 환경을 설정할 수 있습니다. + +* `CORSMiddleware` 임포트. +* 허용되는 출처(문자열 형식)의 리스트 생성. +* FastAPI 응용 프로그램에 "미들웨어(middleware)"로 추가. + +백엔드에서 다음의 사항을 허용할지에 대해 설정할 수도 있습니다: + +* 자격증명 (인증 헤더, 쿠키 등). +* 특정한 HTTP 메소드(`POST`, `PUT`) 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 메소드. +* 특정한 HTTP 헤더 또는 와일드카드 `"*"` 를 사용한 모든 HTTP 헤더. + +```Python hl_lines="2 6-11 13-19" +{!../../../docs_src/cors/tutorial001.py!} +``` + +`CORSMiddleware` 에서 사용하는 기본 매개변수는 제한적이므로, 브라우저가 교차-도메인 상황에서 특정한 출처, 메소드, 헤더 등을 사용할 수 있도록 하려면 이들을 명시적으로 허용해야 합니다. + +다음의 인자들이 지원됩니다: + +* `allow_origins` - 교차-출처 요청을 보낼 수 있는 출처의 리스트입니다. 예) `['https://example.org', 'https://www.example.org']`. 모든 출처를 허용하기 위해 `['*']` 를 사용할 수 있습니다. +* `allow_origin_regex` - 교차-출처 요청을 보낼 수 있는 출처를 정규표현식 문자열로 나타냅니다. `'https://.*\.example\.org'`. +* `allow_methods` - 교차-출처 요청을 허용하는 HTTP 메소드의 리스트입니다. 기본값은 `['GET']` 입니다. `['*']` 을 사용하여 모든 표준 메소드들을 허용할 수 있습니다. +* `allow_headers` - 교차-출처를 지원하는 HTTP 요청 헤더의 리스트입니다. 기본값은 `[]` 입니다. 모든 헤더들을 허용하기 위해 `['*']` 를 사용할 수 있습니다. `Accept`, `Accept-Language`, `Content-Language` 그리고 `Content-Type` 헤더는 CORS 요청시 언제나 허용됩니다. +* `allow_credentials` - 교차-출처 요청시 쿠키 지원 여부를 설정합니다. 기본값은 `False` 입니다. 또한 해당 항목을 허용할 경우 `allow_origins` 는 `['*']` 로 설정할 수 없으며, 출처를 반드시 특정해야 합니다. +* `expose_headers` - 브라우저에 접근할 수 있어야 하는 모든 응답 헤더를 가리킵니다. 기본값은 `[]` 입니다. +* `max_age` - 브라우저가 CORS 응답을 캐시에 저장하는 최대 시간을 초 단위로 설정합니다. 기본값은 `600` 입니다. + +미들웨어는 두가지 특정한 종류의 HTTP 요청에 응답합니다... + +### CORS 사전 요청 + +`Origin` 및 `Access-Control-Request-Method` 헤더와 함께 전송하는 모든 `OPTIONS` 요청입니다. + +이 경우 미들웨어는 들어오는 요청을 가로채 적절한 CORS 헤더와, 정보 제공을 위한 `200` 또는 `400` 응답으로 응답합니다. + +### 단순한 요청 + +`Origin` 헤더를 가진 모든 요청. 이 경우 미들웨어는 요청을 정상적으로 전달하지만, 적절한 CORS 헤더를 응답에 포함시킵니다. + +## 더 많은 정보 + +CORS에 대한 더 많은 정보를 알고싶다면, Mozilla CORS 문서를 참고하기 바랍니다. + +!!! note "기술적 세부 사항" + `from starlette.middleware.cors import CORSMiddleware` 역시 사용할 수 있습니다. + + **FastAPI**는 개발자인 당신의 편의를 위해 `fastapi.middleware` 에서 몇가지의 미들웨어를 제공합니다. 하지만 대부분의 미들웨어가 Stralette으로부터 직접 제공됩니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 50931e134..3dfc208c8 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -69,6 +69,7 @@ nav: - tutorial/request-files.md - tutorial/request-forms-and-files.md - tutorial/encoder.md + - tutorial/cors.md markdown_extensions: - toc: permalink: true From 1be95ba02dc907fb864d03c287c4851c35322515 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 14:21:59 +0000 Subject: [PATCH 0706/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9cfce002b..02d2a088a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). * 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). * 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for function return type annotations to declare the `response_model`. PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). From 3178c17776d7dfbe14a270c5128a483694a3477a Mon Sep 17 00:00:00 2001 From: Ben Ho <15027668g@gmail.com> Date: Sat, 7 Jan 2023 22:33:29 +0800 Subject: [PATCH 0707/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typo=20in=20Chin?= =?UTF-8?q?ese=20translation=20for=20`docs/zh/docs/benchmarks.md`=20(#4269?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/benchmarks.md | 2 +- docs/zh/docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/docs/benchmarks.md b/docs/zh/docs/benchmarks.md index 8991c72cd..71e8d4838 100644 --- a/docs/zh/docs/benchmarks.md +++ b/docs/zh/docs/benchmarks.md @@ -1,6 +1,6 @@ # 基准测试 -第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次与 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*) +第三方机构 TechEmpower 的基准测试表明在 Uvicorn 下运行的 **FastAPI** 应用程序是 可用的最快的 Python 框架之一,仅次于 Starlette 和 Uvicorn 本身 (由 FastAPI 内部使用)。(*) 但是在查看基准得分和对比时,请注意以下几点。 diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 7901e9c2c..4db3ef10c 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -28,7 +28,7 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框 关键特性: -* **快速**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 +* **快速**:可与 **NodeJS** 和 **Go** 并肩的极高性能(归功于 Starlette 和 Pydantic)。[最快的 Python web 框架之一](#_11)。 * **高效编码**:提高功能开发速度约 200% 至 300%。* * **更少 bug**:减少约 40% 的人为(开发者)导致错误。* From 3c20b6e42b13f266a4e9652b793c042cefea3228 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 14:34:13 +0000 Subject: [PATCH 0708/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 02d2a088a..d5957c6fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). * 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). * 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). * 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). From d0027de64f96911c4083f3f6a9fc69ee5ce4a77f Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Sat, 7 Jan 2023 17:45:32 +0300 Subject: [PATCH 0709/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/fastapi-people.md`=20(#5577)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/ru/docs/fastapi-people.md | 180 +++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/ru/docs/fastapi-people.md diff --git a/docs/ru/docs/fastapi-people.md b/docs/ru/docs/fastapi-people.md new file mode 100644 index 000000000..64ae66a03 --- /dev/null +++ b/docs/ru/docs/fastapi-people.md @@ -0,0 +1,180 @@ + +# Люди, поддерживающие FastAPI + +У FastAPI замечательное сообщество, которое доброжелательно к людям с любым уровнем знаний. + +## Создатель и хранитель + +Ку! 👋 + +Это я: + +{% if people %} +
+{% for user in people.maintainers %} + +
@{{ user.login }}
Answers: {{ user.answers }}
Pull Requests: {{ user.prs }}
+{% endfor %} + +
+{% endif %} + +Я создал и продолжаю поддерживать **FastAPI**. Узнать обо мне больше можно тут [Помочь FastAPI - Получить помощь - Связаться с автором](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. + +... но на этой странице я хочу показать вам наше сообщество. + +--- + +**FastAPI** получает огромную поддержку от своего сообщества. И я хочу отметить вклад его участников. + +Это люди, которые: + +* [Помогают другим с их проблемами (вопросами) на GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. +* [Создают пул-реквесты](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* Делают ревью пул-реквестов, [что особенно важно для переводов на другие языки](contributing.md#translations){.internal-link target=_blank}. + +Поаплодируем им! 👏 🙇 + +## Самые активные участники за прошедший месяц + +Эти участники [оказали наибольшую помощь другим с решением их проблем (вопросов) на GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} в течение последнего месяца. ☕ + +{% if people %} +
+{% for user in people.last_month_active %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Эксперты + +Здесь представлены **Эксперты FastAPI**. 🤓 + +Эти участники [оказали наибольшую помощь другим с решением их проблем (вопросов) на GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} за *всё время*. + +Оказывая помощь многим другим, они подтвердили свой уровень знаний. ✨ + +{% if people %} +
+{% for user in people.experts %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Рейтинг участников, внёсших вклад в код + +Здесь представлен **Рейтинг участников, внёсших вклад в код**. 👷 + +Эти люди [сделали наибольшее количество пул-реквестов](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}, *включённых в основной код*. + +Они сделали наибольший вклад в исходный код, документацию, переводы и т.п. 📦 + +{% if people %} +
+{% for user in people.top_contributors %} + +
@{{ user.login }}
Pull Requests: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +На самом деле таких людей довольно много (более сотни), вы можете увидеть всех на этой странице FastAPI GitHub Contributors page. 👷 + +## Рейтинг ревьюеров + +Здесь представлен **Рейтинг ревьюеров**. 🕵️ + +### Проверки переводов на другие языки + +Я знаю не очень много языков (и не очень хорошо 😅). +Итак, ревьюеры - это люди, которые могут [**подтвердить предложенный вами перевод** документации](contributing.md#translations){.internal-link target=_blank}. Без них не было бы документации на многих языках. + +--- + +В **Рейтинге ревьюеров** 🕵️ представлены те, кто проверил наибольшее количество пул-реквестов других участников, обеспечивая качество кода, документации и, особенно, **переводов на другие языки**. + +{% if people %} +
+{% for user in people.top_reviewers %} + +
@{{ user.login }}
Reviews: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Спонсоры + +Здесь представлены **Спонсоры**. 😎 + +Спонсоры поддерживают мою работу над **FastAPI** (и другими проектами) главным образом через GitHub Sponsors. + +{% if sponsors %} + +{% if sponsors.gold %} + +### Золотые спонсоры + +{% for sponsor in sponsors.gold -%} + +{% endfor %} +{% endif %} + +{% if sponsors.silver %} + +### Серебрянные спонсоры + +{% for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + +{% if sponsors.bronze %} + +### Бронзовые спонсоры + +{% for sponsor in sponsors.bronze -%} + +{% endfor %} +{% endif %} + +{% endif %} + +### Индивидуальные спонсоры + +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} + +
+ +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} + + + +{% endif %} +{% endfor %} + +
+ +{% endfor %} +{% endif %} + +## О данных - технические детали + +Основная цель этой страницы - подчеркнуть усилия сообщества по оказанию помощи другим. + +Особенно это касается усилий, которые обычно менее заметны и во многих случаях более трудоемки, таких как помощь другим в решении проблем и проверка пул-реквестов с переводами. + +Данные рейтинги подсчитываются каждый месяц, ознакомиться с тем, как это работает можно тут. + +Кроме того, я также подчеркиваю вклад спонсоров. + +И я оставляю за собой право обновлять алгоритмы подсчёта, виды рейтингов, пороговые значения и т.д. (так, на всякий случай 🤷). From 59d654672f0bdc75a5cab772c50172dc987991e6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 14:46:09 +0000 Subject: [PATCH 0710/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d5957c6fb..fc544f23d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). * 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). * 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). From 2583a83f9dab0e35b7e82e01c8524253f547b562 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 7 Jan 2023 15:54:59 +0100 Subject: [PATCH 0711/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20GitHub=20Action?= =?UTF-8?q?=20gate/check=20(#5492)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddc43c942..1235516d3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,3 +75,19 @@ jobs: with: name: coverage-html path: htmlcov + + # https://github.com/marketplace/actions/alls-green#why + check: # This job does nothing and is only used for the branch protection + + if: always() + + needs: + - coverage-combine + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} From 9812116dc77318b1bfc98778ccaf775ee0433bf3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 14:55:35 +0000 Subject: [PATCH 0712/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fc544f23d..3d0955145 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). * 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). * 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). From 6e1152d31fd4a849dcb50fbed9a0b4c14f50bebc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 15:08:12 +0000 Subject: [PATCH 0713/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.5.2=20to=201.6.4=20(#5750)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8ffb493a4..c2fdb8e17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.5.2 + uses: pypa/gh-action-pypi-publish@v1.6.4 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From adef9f4c02d007bcaacf93548328d8e4be3f490a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 15:08:57 +0000 Subject: [PATCH 0714/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3d0955145..4786f6601 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). * 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). From f4e895bc8a6dad274c9dcf006f491da064cd34f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 15:12:49 +0000 Subject: [PATCH 0715/1881] =?UTF-8?q?=E2=AC=86=20Bump=20types-ujson=20from?= =?UTF-8?q?=205.5.0=20to=205.6.0.0=20(#5735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 55856cf36..f9045ef0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ test = [ "passlib[bcrypt] >=1.7.2,<2.0.0", # types - "types-ujson ==5.5.0", + "types-ujson ==5.6.0.0", "types-orjson ==3.6.2", ] doc = [ From 929c70011707b138cf9c2b8fcfbafc647ee8a90f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 15:13:27 +0000 Subject: [PATCH 0716/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4786f6601..bb8ec20af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). * 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). From bea1fdd2eb34c9933bf66f08f9cc164ca97d99f1 Mon Sep 17 00:00:00 2001 From: Kelby Faessler Date: Sat, 7 Jan 2023 10:31:03 -0500 Subject: [PATCH 0717/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/deployment/concepts.md`=20(#5824)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/deployment/concepts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md index 22604ceeb..77419f8b0 100644 --- a/docs/en/docs/deployment/concepts.md +++ b/docs/en/docs/deployment/concepts.md @@ -235,7 +235,7 @@ Here are some possible combinations and strategies: * 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 +* **Cloud services** that handle this for you * 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 From 789d649fbadea706e90e219049e9326689e881db Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 15:31:38 +0000 Subject: [PATCH 0718/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bb8ec20af..91bb9c2cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). * ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). From 2dfdcea69addd9ac31db48190b53316b910a7865 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 15:35:14 +0000 Subject: [PATCH 0719/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5825)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions Co-authored-by: Sebastián Ramírez --- docs/en/data/github_sponsors.yml | 107 +++++++++++++--------------- docs/en/data/people.yml | 116 ++++++++++++++----------------- 2 files changed, 103 insertions(+), 120 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 1953df801..3d6831db6 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,15 +2,9 @@ sponsors: - - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai -- - login: Doist - avatarUrl: https://avatars.githubusercontent.com/u/2565372?v=4 - url: https://github.com/Doist - - login: cryptapi +- - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi - - login: jrobbins-LiveData - avatarUrl: https://avatars.githubusercontent.com/u/79278744?u=bae8175fc3f09db281aca1f97a9ddc1a914a8c4f&v=4 - url: https://github.com/jrobbins-LiveData - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -32,24 +26,21 @@ sponsors: - login: investsuite avatarUrl: https://avatars.githubusercontent.com/u/73833632?v=4 url: https://github.com/investsuite + - login: svix + avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 + url: https://github.com/svix - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry -- - login: InesIvanova - avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 - url: https://github.com/InesIvanova - - login: vyos avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 url: https://github.com/vyos - login: SendCloud avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 url: https://github.com/SendCloud - - login: matallan - avatarUrl: https://avatars.githubusercontent.com/u/12107723?v=4 - url: https://github.com/matallan - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya @@ -65,12 +56,12 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP +- - login: InesIvanova + avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 + url: https://github.com/InesIvanova - - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei - - login: gvisniuc - avatarUrl: https://avatars.githubusercontent.com/u/1614747?u=502dfdb2b087ddcf5460026297c98c7907bc2795&v=4 - url: https://github.com/gvisniuc - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore @@ -80,6 +71,9 @@ sponsors: - login: Lovage-Labs avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 url: https://github.com/Lovage-Labs +- - login: xshapira + avatarUrl: https://avatars.githubusercontent.com/u/48856190?u=3b0927ad29addab29a43767b52e45bee5cd6da9f&v=4 + url: https://github.com/xshapira - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck @@ -95,6 +89,9 @@ sponsors: - login: dorianturba avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 url: https://github.com/dorianturba + - login: Qazzquimby + avatarUrl: https://avatars.githubusercontent.com/u/12368310?u=f4ed4a7167fd359cfe4502d56d7c64f9bf59bb38&v=4 + url: https://github.com/Qazzquimby - login: jmaralc avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 url: https://github.com/jmaralc @@ -107,12 +104,15 @@ sponsors: - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: indeedeng +- - login: guivaloz + avatarUrl: https://avatars.githubusercontent.com/u/1296621?u=bc4fc28f96c654aa2be7be051d03a315951e2491&v=4 + url: https://github.com/guivaloz + - login: indeedeng avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 url: https://github.com/indeedeng - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge + - login: fratambot + avatarUrl: https://avatars.githubusercontent.com/u/20300069?u=41c85ea08960c8a8f0ce967b780e242b1454690c&v=4 + url: https://github.com/fratambot - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -152,9 +152,6 @@ sponsors: - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner - - login: alexsantos - avatarUrl: https://avatars.githubusercontent.com/u/932219?v=4 - url: https://github.com/alexsantos - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith @@ -164,6 +161,9 @@ sponsors: - login: mrkmcknz avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 url: https://github.com/mrkmcknz + - login: theonlynexus + avatarUrl: https://avatars.githubusercontent.com/u/1515004?v=4 + url: https://github.com/theonlynexus - login: coffeewasmyidea avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 url: https://github.com/coffeewasmyidea @@ -185,9 +185,6 @@ sponsors: - login: ColliotL avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4 url: https://github.com/ColliotL - - login: grillazz - avatarUrl: https://avatars.githubusercontent.com/u/3415861?u=453cd1725c8d7fe3e258016bc19cff861d4fcb53&v=4 - url: https://github.com/grillazz - login: dblackrun avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 url: https://github.com/dblackrun @@ -218,12 +215,6 @@ sponsors: - login: oliverxchen avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 url: https://github.com/oliverxchen - - login: Rey8d01 - avatarUrl: https://avatars.githubusercontent.com/u/4836190?u=5942598a23a377602c1669522334ab5ebeaf9165&v=4 - url: https://github.com/Rey8d01 - - login: ScrimForever - avatarUrl: https://avatars.githubusercontent.com/u/5040124?u=091ec38bfe16d6e762099e91309b59f248616a65&v=4 - url: https://github.com/ScrimForever - login: ennui93 avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 url: https://github.com/ennui93 @@ -257,9 +248,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: bapi24 - avatarUrl: https://avatars.githubusercontent.com/u/11890901?u=45cc721d8f66ad2f62b086afc3d4761d0c16b9c6&v=4 - url: https://github.com/bapi24 + - login: jacobkrit + avatarUrl: https://avatars.githubusercontent.com/u/11823915?u=4921a7ea429b7eadcad3077f119f90d15a3318b2&v=4 + url: https://github.com/jacobkrit - login: svats2k avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 url: https://github.com/svats2k @@ -278,11 +269,8 @@ sponsors: - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck - - login: m4knV - avatarUrl: https://avatars.githubusercontent.com/u/19666130?u=843383978814886be93c137d10d2e20e9f13af07&v=4 - url: https://github.com/m4knV - login: Filimoa - avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=75e02d102d2ee3e3d793e555fa5c63045913ccb0&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 @@ -317,9 +305,6 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: faviasono - avatarUrl: https://avatars.githubusercontent.com/u/37707874?u=f0b75ca4248987c08ed8fb8ed682e7e74d5d7091&v=4 - url: https://github.com/faviasono - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -341,6 +326,9 @@ sponsors: - login: dazeddd avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 url: https://github.com/dazeddd + - login: A-Edge + avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 + url: https://github.com/A-Edge - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -359,9 +347,6 @@ sponsors: - login: anthonycepeda avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda - - login: fpiem - avatarUrl: https://avatars.githubusercontent.com/u/77389987?u=f667a25cd4832b28801189013b74450e06cc232c&v=4 - url: https://github.com/fpiem - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare @@ -404,9 +389,6 @@ sponsors: - login: dmig avatarUrl: https://avatars.githubusercontent.com/u/388564?v=4 url: https://github.com/dmig - - login: rinckd - avatarUrl: https://avatars.githubusercontent.com/u/546002?u=8ec88ab721a5636346f19dcd677a6f323058be8b&v=4 - url: https://github.com/rinckd - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy @@ -431,6 +413,9 @@ sponsors: - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan + - login: my3 + avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 + url: https://github.com/my3 - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz @@ -443,6 +428,9 @@ sponsors: - login: igorcorrea avatarUrl: https://avatars.githubusercontent.com/u/3438238?u=c57605077c31a8f7b2341fc4912507f91b4a5621&v=4 url: https://github.com/igorcorrea + - login: larsvik + avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4 + url: https://github.com/larsvik - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti @@ -519,7 +507,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4 url: https://github.com/paulowiz - login: yannicschroeer - avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=4df05a7296c207b91c5d7c7a11c29df5ab313e2b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=91515328b5418a4e7289a83f0dcec3573f1a6500&v=4 url: https://github.com/yannicschroeer - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -528,7 +516,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/24669867?u=60e7c8c09f8dafabee8fc3edcd6f9e19abbff918&v=4 url: https://github.com/fstau - login: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=444441027bc2c9f9db68e8047d65ff23d25699cf&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=7e5d2bf26d0a0670ea347f7db5febebc4e031ed1&v=4 url: https://github.com/pers0n4 - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 @@ -564,7 +552,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 url: https://github.com/ww-daniel-mora - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=9ddf8023ca3326381ba8fb77285ae36598a15de3&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=b3cb7a606207141c357e517071cf91a67fb5577e&v=4 url: https://github.com/rwxd - login: ilias-ant avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4 @@ -602,16 +590,16 @@ sponsors: - login: realabja avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 url: https://github.com/realabja - - login: alessio-proietti - avatarUrl: https://avatars.githubusercontent.com/u/67370599?u=8ac73db1e18e946a7681f173abdb640516f88515&v=4 - url: https://github.com/alessio-proietti - login: pondDevThai avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 url: https://github.com/pondDevThai -- - login: wardal - avatarUrl: https://avatars.githubusercontent.com/u/15804042?v=4 - url: https://github.com/wardal - - login: gabrielmbmb + - login: zeb0x01 + avatarUrl: https://avatars.githubusercontent.com/u/77236545?u=c62bfcfbd463f9cf171c879cea1362a63de2c582&v=4 + url: https://github.com/zeb0x01 + - login: lironmiz + avatarUrl: https://avatars.githubusercontent.com/u/91504420?u=cb93dfec613911ac8d4e84fbb560711546711ad5&v=4 + url: https://github.com/lironmiz +- - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb - login: danburonline @@ -620,3 +608,6 @@ sponsors: - login: Moises6669 avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 url: https://github.com/Moises6669 + - login: lyuboparvanov + avatarUrl: https://avatars.githubusercontent.com/u/106192895?u=367329c777320e01550afda9d8de670436181d86&v=4 + url: https://github.com/lyuboparvanov diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 51940a6b1..d46ec44ae 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1837 - prs: 360 + answers: 1878 + prs: 361 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 374 + count: 379 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 87 + count: 103 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: raphaelauv @@ -45,14 +45,14 @@ experts: count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: jgould22 + count: 68 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: falkben count: 59 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben -- login: jgould22 - count: 55 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: sm-Fifteen count: 50 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 @@ -66,8 +66,8 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa - login: adriangb - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=75087f0cf0e9f725f3cd18a899218b6c63ae60d3&v=4 + count: 41 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 url: https://github.com/adriangb - login: includeamin count: 39 @@ -82,7 +82,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns - login: frankie567 - count: 33 + count: 34 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 - login: prostomarkeloff @@ -97,14 +97,14 @@ experts: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: panla + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 + url: https://github.com/panla - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: panla - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 - url: https://github.com/panla - login: ghandic count: 25 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 @@ -169,6 +169,10 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: hellocoldworld + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 + url: https://github.com/hellocoldworld - login: jonatasoli count: 15 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 @@ -177,14 +181,14 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 url: https://github.com/mbroton -- login: hellocoldworld - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 - url: https://github.com/hellocoldworld - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 url: https://github.com/haizaar +- login: n8sty + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: valentin994 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 @@ -193,43 +197,31 @@ experts: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 url: https://github.com/David-Lor -- login: n8sty - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty last_month_active: -- login: jgould22 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: yinziyan1206 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 - login: iudeen - count: 8 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen +- login: jgould22 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: NewSouthMjos + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/77476573?v=4 + url: https://github.com/NewSouthMjos +- login: davismartens + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/69799848?u=dbdfd256dd4e0a12d93efb3463225f3e39d8df6f&v=4 + url: https://github.com/davismartens - login: Kludex - count: 5 + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: JarroVGIT - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 - url: https://github.com/JarroVGIT -- login: TheJumpyWizard - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/90986815?u=67e9c13c9f063dd4313db7beb64eaa2f3a37f1fe&v=4 - url: https://github.com/TheJumpyWizard -- login: mbroton +- login: Lenclove count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton -- login: mateoradman - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/48420316?u=066f36b8e8e263b0d90798113b0f291d3266db7c&v=4 - url: https://github.com/mateoradman + avatarUrl: https://avatars.githubusercontent.com/u/32355298?u=d0065e01650c63c2b2413f42d983634b2ea85481&v=4 + url: https://github.com/Lenclove top_contributors: - login: waynerv count: 25 @@ -269,7 +261,7 @@ top_contributors: url: https://github.com/Serrones - login: RunningIkkyu count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu - login: hard-coders count: 7 @@ -337,15 +329,15 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 70 + count: 72 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 59 + count: 66 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: tokusumi - count: 50 + count: 51 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: waynerv @@ -360,6 +352,10 @@ top_reviewers: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd +- login: iudeen + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 @@ -372,10 +368,6 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda -- login: iudeen - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 @@ -446,7 +438,7 @@ top_reviewers: url: https://github.com/sh0nk - login: RunningIkkyu count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=efb5b45b55584450507834f279ce48d4d64dea2f&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu - login: LorhanSohaky count: 11 @@ -474,7 +466,7 @@ top_reviewers: url: https://github.com/ComicShrimp - login: peidrao count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=88c2cb42a99e0f50cdeae3606992568184783ee5&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=39edf7052371484cb488277638c23e1f6b584f4b&v=4 url: https://github.com/peidrao - login: izaguerreiro count: 9 @@ -496,6 +488,10 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca +- login: dimaqq + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 + url: https://github.com/dimaqq - login: raphaelauv count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -512,10 +508,6 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang -- login: dimaqq - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 - url: https://github.com/dimaqq - login: Xewus count: 8 avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 From d202598c71c33301f65c58456fb8f3102cea597d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 15:35:51 +0000 Subject: [PATCH 0720/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 91bb9c2cb..ce8f63ee4 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 [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). * ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). From a17da3d0f4a0ac77e8c007455e60bbee7c6ab911 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 20:55:29 +0400 Subject: [PATCH 0721/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.24.2=20to=202.24.3=20(#5842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 7d31a9c64..2af56e2bc 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -16,7 +16,7 @@ jobs: rm -rf ./site mkdir ./site - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.2 + uses: dawidd6/action-download-artifact@v2.24.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 7559c24c0..d6206d697 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.2 + - uses: dawidd6/action-download-artifact@v2.24.3 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From 903e3be3b86bab3e4dff81243191ee139f8d33ee Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 16:56:07 +0000 Subject: [PATCH 0722/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ce8f63ee4..4bef2e215 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). * ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). From 78813a543dfd5e2f0031c70a3cb12dcadc187cd6 Mon Sep 17 00:00:00 2001 From: Nonso Mgbechi Date: Sat, 7 Jan 2023 17:56:58 +0100 Subject: [PATCH 0723/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/async.md`=20(#5785)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/async.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 6f34a9c9c..3d4b1956a 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -283,7 +283,7 @@ For example: ### Concurrency + Parallelism: Web + Machine Learning -With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attractive of NodeJS). +With **FastAPI** you can take the advantage of concurrency that is very common for web development (the same main attraction of NodeJS). But you can also exploit the benefits of parallelism and multiprocessing (having multiple processes running in parallel) for **CPU bound** workloads like those in Machine Learning systems. From 5c6d7b2ff3635a2880695c59804e26a5f4744c8b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 16:57:45 +0000 Subject: [PATCH 0724/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4bef2e215..040f61994 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). From f56b0d571dc2e49c9f1361947725e49d2d54a385 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 17:01:21 +0000 Subject: [PATCH 0725/1881] =?UTF-8?q?=E2=AC=86=20Update=20uvicorn[standard?= =?UTF-8?q?]=20requirement=20from=20<0.19.0,>=3D0.12.0=20to=20>=3D0.12.0, 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 f9045ef0a..1983f2e51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ doc = [ ] dev = [ "ruff ==0.0.138", - "uvicorn[standard] >=0.12.0,<0.19.0", + "uvicorn[standard] >=0.12.0,<0.21.0", "pre-commit >=2.17.0,<3.0.0", ] all = [ From 27ce2e2108001c1709cc0c321a4e29fcc5d5bd7a Mon Sep 17 00:00:00 2001 From: Xhy-5000 <45428960+Xhy-5000@users.noreply.github.com> Date: Sat, 7 Jan 2023 12:01:38 -0500 Subject: [PATCH 0726/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Authorization=20on=20FastAPI=20with=20Casbin=20(#5712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 934c5842b..b7db15038 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ articles: english: + - author: Teresa N. Fontanella De Santis + author_link: https://dev.to/ + link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og + title: Authorization on FastAPI with Casbin - author: WayScript author_link: https://www.wayscript.com link: https://blog.wayscript.com/fast-api-quickstart/ From eb39b0f8f8093c1046ca2a58b38dd308976d8bcd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 17:01:58 +0000 Subject: [PATCH 0727/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 040f61994..5c6f90401 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). From 681e5c0199863c2588fd962d49c9e6b637dc4e51 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 17:02:15 +0000 Subject: [PATCH 0728/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5c6f90401..95fae8fe0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add External Link: Authorization on FastAPI with Casbin. PR [#5712](https://github.com/tiangolo/fastapi/pull/5712) by [@Xhy-5000](https://github.com/Xhy-5000). * ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). * ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). From c482dd3d425e774af743b801e2705da8828f2b5c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Jan 2023 17:04:43 +0000 Subject: [PATCH 0729/1881] =?UTF-8?q?=E2=AC=86=20Update=20coverage[toml]?= =?UTF-8?q?=20requirement=20from=20<7.0,>=3D6.5.0=20to=20>=3D6.5.0,<8.0=20?= =?UTF-8?q?(#5801)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 1983f2e51..b29549f5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] test = [ "pytest >=7.1.3,<8.0.0", - "coverage[toml] >= 6.5.0,<7.0", + "coverage[toml] >= 6.5.0,< 8.0", "mypy ==0.982", "ruff ==0.0.138", "black == 22.10.0", From aa6a8e5d4908994318069710e8d0ce62daf67ce2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 7 Jan 2023 17:05:20 +0000 Subject: [PATCH 0730/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 95fae8fe0..0ed6c1b1d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update coverage[toml] requirement from <7.0,>=6.5.0 to >=6.5.0,<8.0. PR [#5801](https://github.com/tiangolo/fastapi/pull/5801) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Add External Link: Authorization on FastAPI with Casbin. PR [#5712](https://github.com/tiangolo/fastapi/pull/5712) by [@Xhy-5000](https://github.com/Xhy-5000). * ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). From a6af7c27f8edb138ea7887125a8471764c58c531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 7 Jan 2023 21:15:11 +0400 Subject: [PATCH 0731/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 60 ++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0ed6c1b1d..dcbc0254a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,22 +2,66 @@ ## Latest Changes -* ⬆ Update coverage[toml] requirement from <7.0,>=6.5.0 to >=6.5.0,<8.0. PR [#5801](https://github.com/tiangolo/fastapi/pull/5801) by [@dependabot[bot]](https://github.com/apps/dependabot). +### Features + +* ✨ Add support for function return type annotations to declare the `response_model`. Initial PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). + +Now you can declare the return type / `response_model` in the function return type annotation: + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + + +@app.get("/items/") +async def read_items() -> list[Item]: + return [ + Item(name="Portal Gun", price=42.0), + Item(name="Plumbus", price=32.0), + ] +``` + +FastAPI will use the return type annotation to perform: + +* Data validation +* Automatic documentation + * It could power automatic client generators +* **Data filtering** + +Before this version it was only supported via the `response_model` parameter. + +Read more about it in the new docs: [Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/). + +### Docs + * 📝 Add External Link: Authorization on FastAPI with Casbin. PR [#5712](https://github.com/tiangolo/fastapi/pull/5712) by [@Xhy-5000](https://github.com/Xhy-5000). -* ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). * ✏ Fix typo in `docs/en/docs/async.md`. PR [#5785](https://github.com/tiangolo/fastapi/pull/5785) by [@Kingdageek](https://github.com/Kingdageek). -* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). -* 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). * ✏ Fix typo in `docs/en/docs/deployment/concepts.md`. PR [#5824](https://github.com/tiangolo/fastapi/pull/5824) by [@kelbyfaessler](https://github.com/kelbyfaessler). -* ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). -* 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). + +### Translations + * 🌐 Add Russian translation for `docs/ru/docs/fastapi-people.md`. PR [#5577](https://github.com/tiangolo/fastapi/pull/5577) by [@Xewus](https://github.com/Xewus). * 🌐 Fix typo in Chinese translation for `docs/zh/docs/benchmarks.md`. PR [#4269](https://github.com/tiangolo/fastapi/pull/4269) by [@15027668g](https://github.com/15027668g). * 🌐 Add Korean translation for `docs/tutorial/cors.md`. PR [#3764](https://github.com/tiangolo/fastapi/pull/3764) by [@NinaHwang](https://github.com/NinaHwang). + +### Internal + +* ⬆ Update coverage[toml] requirement from <7.0,>=6.5.0 to >=6.5.0,<8.0. PR [#5801](https://github.com/tiangolo/fastapi/pull/5801) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update uvicorn[standard] requirement from <0.19.0,>=0.12.0 to >=0.12.0,<0.21.0 for development. PR [#5795](https://github.com/tiangolo/fastapi/pull/5795) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump dawidd6/action-download-artifact from 2.24.2 to 2.24.3. PR [#5842](https://github.com/tiangolo/fastapi/pull/5842) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👥 Update FastAPI People. PR [#5825](https://github.com/tiangolo/fastapi/pull/5825) by [@github-actions[bot]](https://github.com/apps/github-actions). +* ⬆ Bump types-ujson from 5.5.0 to 5.6.0.0. PR [#5735](https://github.com/tiangolo/fastapi/pull/5735) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.5.2 to 1.6.4. PR [#5750](https://github.com/tiangolo/fastapi/pull/5750) by [@dependabot[bot]](https://github.com/apps/dependabot). +* 👷 Add GitHub Action gate/check. PR [#5492](https://github.com/tiangolo/fastapi/pull/5492) by [@webknjaz](https://github.com/webknjaz). * 🔧 Update sponsors, add Svix. PR [#5848](https://github.com/tiangolo/fastapi/pull/5848) by [@tiangolo](https://github.com/tiangolo). * 🔧 Remove Doist sponsor. PR [#5847](https://github.com/tiangolo/fastapi/pull/5847) by [@tiangolo](https://github.com/tiangolo). -* ✨ Add support for function return type annotations to declare the `response_model`. PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). * ⬆ Update sqlalchemy requirement from <=1.4.41,>=1.3.18 to >=1.3.18,<1.4.43. PR [#5540](https://github.com/tiangolo/fastapi/pull/5540) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#5757](https://github.com/tiangolo/fastapi/pull/5757) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Refactor CI artifact upload/download for docs previews. PR [#5793](https://github.com/tiangolo/fastapi/pull/5793) by [@tiangolo](https://github.com/tiangolo). From 69bd7d8501fe77601ebd67f55b39596fdd1e3792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 7 Jan 2023 21:17:10 +0400 Subject: [PATCH 0732/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?89.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dcbc0254a..cdafa6899 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.89.0 + ### Features * ✨ Add support for function return type annotations to declare the `response_model`. Initial PR [#1436](https://github.com/tiangolo/fastapi/pull/1436) by [@uriyyo](https://github.com/uriyyo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 037d9804b..bda10f043 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.88.0" +__version__ = "0.89.0" from starlette import status as status From 929289b6302939784516790f90f7e9382032e5b6 Mon Sep 17 00:00:00 2001 From: Raf Rasenberg <46351981+rafrasenberg@users.noreply.github.com> Date: Tue, 10 Jan 2023 07:33:36 -0500 Subject: [PATCH 0733/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20FastAPI=20lambda=20container:=20serverless=20simplified=20(#?= =?UTF-8?q?5784)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index b7db15038..c1b1f1fa4 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ articles: english: + - author: Raf Rasenberg + author_link: https://rafrasenberg.com/about/ + link: https://rafrasenberg.com/fastapi-lambda/ + title: 'FastAPI lambda container: serverless simplified' - author: Teresa N. Fontanella De Santis author_link: https://dev.to/ link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og From 52a84175c11f8e17e793c1cad685d705ce2a3fdc Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Jan 2023 12:34:24 +0000 Subject: [PATCH 0734/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cdafa6899..de203b9e8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). ## 0.89.0 ### Features From 1562592bde3d3927764ce22ae146bacdd1d170c2 Mon Sep 17 00:00:00 2001 From: Kader M <48386782+Kadermiyanyedi@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:38:01 +0300 Subject: [PATCH 0735/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/tutorial/first=5Fsteps.md`=20(#5691?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/tr/docs/tutorial/first_steps.md | 336 +++++++++++++++++++++++++++ docs/tr/mkdocs.yml | 2 + 2 files changed, 338 insertions(+) create mode 100644 docs/tr/docs/tutorial/first_steps.md diff --git a/docs/tr/docs/tutorial/first_steps.md b/docs/tr/docs/tutorial/first_steps.md new file mode 100644 index 000000000..b39802f5d --- /dev/null +++ b/docs/tr/docs/tutorial/first_steps.md @@ -0,0 +1,336 @@ +# İlk Adımlar + +En basit FastAPI dosyası şu şekildedir: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bunu bir `main.py` dosyasına kopyalayın. + +Projeyi çalıştırın: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + `uvicorn main:app` komutu şunu ifade eder: + + * `main`: `main.py` dosyası (the Python "module"). + * `app`: `main.py` dosyası içerisinde `app = FastAPI()` satırıyla oluşturulan nesne. + * `--reload`: Kod değişikliği sonrasında sunucunun yeniden başlatılmasını sağlar. Yalnızca geliştirme için kullanın. + +Çıktıda şu şekilde bir satır vardır: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Bu satır, yerel makinenizde uygulamanızın sunulduğu URL'yi gösterir. + +### Kontrol Et + +Tarayıcınızda http://127.0.0.1:8000 adresini açın. + +Bir JSON yanıtı göreceksiniz: + +```JSON +{"message": "Hello World"} +``` + +### İnteraktif API dokümantasyonu + +http://127.0.0.1:8000/docs adresine gidin. + +Otomatik oluşturulmuş( Swagger UI tarafından sağlanan) interaktif bir API dokümanı göreceksiniz: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatif API dokümantasyonu + +Şimdi, http://127.0.0.1:8000/redoc adresine gidin. + +Otomatik oluşturulmuş(ReDoc tarafından sağlanan) bir API dokümanı göreceksiniz: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI**, **OpenAPI** standardını kullanarak tüm API'lerinizi açıklayan bir "şema" oluşturur. + +#### "Şema" + +Bir "şema", bir şeyin tanımı veya açıklamasıdır. Soyut bir açıklamadır, uygulayan kod değildir. + +#### API "şemaları" + +Bu durumda, OpenAPI, API şemasını nasıl tanımlayacağınızı belirten şartnamelerdir. + +Bu şema tanımı, API yollarınızı, aldıkları olası parametreleri vb. içerir. + +#### Data "şema" + +"Şema" terimi, JSON içeriği gibi bazı verilerin şeklini de ifade edebilir. + +Bu durumda, JSON öznitelikleri ve sahip oldukları veri türleri vb. anlamına gelir. + +#### OpenAPI and JSON Şema + +OpenAPI, API'niz için bir API şeması tanımlar. Ve bu şema, JSON veri şemaları standardı olan **JSON Şema** kullanılarak API'niz tarafından gönderilen ve alınan verilerin tanımlarını (veya "şemalarını") içerir. + +#### `openapi.json` kontrol et + +OpenAPI şemasının nasıl göründüğünü merak ediyorsanız, FastAPI otomatik olarak tüm API'nizin açıklamalarını içeren bir JSON (şema) oluşturur. + +Doğrudan şu adreste görebilirsiniz: http://127.0.0.1:8000/openapi.json. + +Aşağıdaki gibi bir şeyle başlayan bir JSON gösterecektir: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### OpenAPI ne içindir? + +OpenAPI şeması, dahili olarak bulunan iki etkileşimli dokümantasyon sistemine güç veren şeydir. + +Ve tamamen OpenAPI'ye dayalı düzinelerce alternatif vardır. **FastAPI** ile oluşturulmuş uygulamanıza bu alternatiflerden herhangi birini kolayca ekleyebilirsiniz. + +API'nizle iletişim kuran istemciler için otomatik olarak kod oluşturmak için de kullanabilirsiniz. Örneğin, frontend, mobil veya IoT uygulamaları. + +## Adım adım özet + +### Adım 1: `FastAPI`yi içe aktarın + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI`, API'niz için tüm fonksiyonları sağlayan bir Python sınıfıdır. + +!!! note "Teknik Detaylar" + `FastAPI` doğrudan `Starlette` kalıtım alan bir sınıftır. + + Tüm Starlette fonksiyonlarını `FastAPI` ile de kullanabilirsiniz. + +### Adım 2: Bir `FastAPI` örneği oluşturun + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Burada `app` değişkeni `FastAPI` sınıfının bir örneği olacaktır. + +Bu tüm API'yi oluşturmak için ana etkileşim noktası olacaktır. + +`uvicorn` komutunda atıfta bulunulan `app` ile aynıdır. + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Uygulamanızı aşağıdaki gibi oluşturursanız: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Ve bunu `main.py` dosyasına koyduktan sonra `uvicorn` komutunu şu şekilde çağırabilirsiniz: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Adım 3: *Path işlemleri* oluşturmak + +#### Path + +Burada "Path" URL'de ilk "\" ile başlayan son bölümü ifade eder. + +Yani, şu şekilde bir URL'de: + +``` +https://example.com/items/foo +``` + +... path şöyle olabilir: + +``` +/items/foo +``` + +!!! info + Genellikle bir "path", "endpoint" veya "route" olarak adlandırılabilir. + +Bir API oluştururken, "path", "resource" ile "concern" ayırmanın ana yoludur. + +#### İşlemler + +Burada "işlem" HTTP methodlarından birini ifade eder. + +Onlardan biri: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +... ve daha egzotik olanları: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +HTTP protokolünde, bu "methodlardan" birini (veya daha fazlasını) kullanarak her path ile iletişim kurabilirsiniz. + +--- + +API'lerinizi oluştururkan, belirli bir işlemi gerçekleştirirken belirli HTTP methodlarını kullanırsınız. + +Normalde kullanılan: + +* `POST`: veri oluşturmak. +* `GET`: veri okumak. +* `PUT`: veriyi güncellemek. +* `DELETE`: veriyi silmek. + +Bu nedenle, OpenAPI'de HTTP methodlarından her birine "işlem" denir. + +Bizde onlara "**işlemler**" diyeceğiz. + +#### Bir *Path işlem decoratorleri* tanımlanmak + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` **FastAPI'ye** aşağıdaki fonksiyonun adresine giden istekleri işlemekten sorumlu olduğunu söyler: + +* path `/` +* get işlemi kullanılarak + + +!!! info "`@decorator` Bilgisi" + Python `@something` şeklinde ifadeleri "decorator" olarak adlandırır. + + Decoratoru bir fonksiyonun üzerine koyarsınız. Dekoratif bir şapka gibi (Sanırım terim buradan gelmektedir). + + Bir "decorator" fonksiyonu alır ve bazı işlemler gerçekleştir. + + Bizim durumumzda decarator **FastAPI'ye** fonksiyonun bir `get` işlemi ile `/` pathine geldiğini söyler. + + Bu **path işlem decoratordür** + +Ayrıca diğer işlemleri de kullanabilirsiniz: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Ve daha egzotik olanları: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + Her işlemi (HTTP method) istediğiniz gibi kullanmakta özgürsünüz. + + **FastAPI** herhangi bir özel anlamı zorlamaz. + + Buradaki bilgiler bir gereklilik değil, bir kılavuz olarak sunulmaktadır. + + Örneğin, GraphQL kullanırkan normalde tüm işlemleri yalnızca `POST` işlemini kullanarak gerçekleştirirsiniz. + +### Adım 4: **path işlem fonksiyonunu** tanımlayın + +Aşağıdakiler bizim **path işlem fonksiyonlarımızdır**: + +* **path**: `/` +* **işlem**: `get` +* **function**: "decorator"ün altındaki fonksiyondur (`@app.get("/")` altında). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bu bir Python fonksiyonudur. + +Bir `GET` işlemi kullanarak "`/`" URL'sine bir istek geldiğinde **FastAPI** tarafından çağrılır. + +Bu durumda bir `async` fonksiyonudur. + +--- + +Bunu `async def` yerine normal bir fonksiyon olarakta tanımlayabilirsiniz. + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + + Eğer farkı bilmiyorsanız, [Async: *"Acelesi var?"*](../async.md#in-a-hurry){.internal-link target=_blank} kontrol edebilirsiniz. + +### Adım 5: İçeriği geri döndürün + + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bir `dict`, `list` döndürebilir veya `str`, `int` gibi tekil değerler döndürebilirsiniz. + +Ayrıca, Pydantic modellerini de döndürebilirsiniz. (Bununla ilgili daha sonra ayrıntılı bilgi göreceksiniz.) + +Otomatik olarak JSON'a dönüştürülecek(ORM'ler vb. dahil) başka birçok nesne ve model vardır. En beğendiklerinizi kullanmayı deneyin, yüksek ihtimalle destekleniyordur. + +## Özet + +* `FastAPI`'yi içe aktarın. +* Bir `app` örneği oluşturun. +* **path işlem decorator** yazın. (`@app.get("/")` gibi) +* **path işlem fonksiyonu** yazın. (`def root(): ...` gibi) +* Development sunucunuzu çalıştırın. (`uvicorn main:app --reload` gibi) diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index e29d25936..5904f71f9 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -61,6 +61,8 @@ nav: - features.md - fastapi-people.md - python-types.md +- Tutorial - User Guide: + - tutorial/first-steps.md markdown_extensions: - toc: permalink: true From 53973f7f94048121b121f8026a43463b6d4d6d3e Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Jan 2023 12:38:39 +0000 Subject: [PATCH 0736/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 de203b9e8..8a4357491 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). ## 0.89.0 From fba7493042cde54c059989ffb2ccccde65c51293 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Tue, 10 Jan 2023 13:45:18 +0100 Subject: [PATCH 0737/1881] =?UTF-8?q?=F0=9F=90=9B=20Ignore=20Response=20cl?= =?UTF-8?q?asses=20on=20return=20annotation=20(#5855)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- fastapi/routing.py | 7 ++- ...est_response_model_as_return_annotation.py | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 8c73b954f..f131fa903 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -42,6 +42,7 @@ from fastapi.utils import ( from pydantic import BaseModel from pydantic.error_wrappers import ErrorWrapper, ValidationError from pydantic.fields import ModelField, Undefined +from pydantic.utils import lenient_issubclass from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException @@ -356,7 +357,11 @@ class APIRoute(routing.Route): self.path = path self.endpoint = endpoint if isinstance(response_model, DefaultPlaceholder): - response_model = get_typed_return_annotation(endpoint) + return_annotation = get_typed_return_annotation(endpoint) + if lenient_issubclass(return_annotation, Response): + response_model = None + else: + response_model = return_annotation self.response_model = response_model self.summary = summary self.response_description = response_description diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index f2056fecd..5d347ec34 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,6 +2,7 @@ from typing import List, Union import pytest from fastapi import FastAPI +from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel, ValidationError @@ -237,6 +238,16 @@ def no_response_model_annotation_union_return_model2() -> Union[User, Item]: return Item(name="Foo", price=42.0) +@app.get("/no_response_model-annotation_response_class") +def no_response_model_annotation_response_class() -> Response: + return Response(content="Foo") + + +@app.get("/no_response_model-annotation_json_response_class") +def no_response_model_annotation_json_response_class() -> JSONResponse: + return JSONResponse(content={"foo": "bar"}) + + openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -789,6 +800,30 @@ openapi_schema = { }, } }, + "/no_response_model-annotation_response_class": { + "get": { + "summary": "No Response Model Annotation Response Class", + "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-annotation_json_response_class": { + "get": { + "summary": "No Response Model Annotation Json Response Class", + "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, }, "components": { "schemas": { @@ -1049,3 +1084,15 @@ def test_no_response_model_annotation_union_return_model2(): response = client.get("/no_response_model-annotation_union-return_model2") assert response.status_code == 200, response.text assert response.json() == {"name": "Foo", "price": 42.0} + + +def test_no_response_model_annotation_return_class(): + response = client.get("/no_response_model-annotation_response_class") + assert response.status_code == 200, response.text + assert response.text == "Foo" + + +def test_no_response_model_annotation_json_response_class(): + response = client.get("/no_response_model-annotation_json_response_class") + assert response.status_code == 200, response.text + assert response.json() == {"foo": "bar"} From 6b83525ff4282a6c84558d1bc390cc08783b982a Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Jan 2023 12:46:04 +0000 Subject: [PATCH 0738/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8a4357491..bf588ff67 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). * 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). ## 0.89.0 From fb8e9083f426aab14edf6973495f3eacbf809abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 10 Jan 2023 20:22:47 +0400 Subject: [PATCH 0739/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20and=20?= =?UTF-8?q?examples=20for=20Response=20Model=20with=20Return=20Type=20Anno?= =?UTF-8?q?tations,=20and=20update=20runtime=20error=20(#5873)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/response-model.md | 70 ++++++++++ docs_src/response_model/tutorial003_02.py | 11 ++ docs_src/response_model/tutorial003_03.py | 9 ++ docs_src/response_model/tutorial003_04.py | 13 ++ .../response_model/tutorial003_04_py310.py | 11 ++ docs_src/response_model/tutorial003_05.py | 13 ++ .../response_model/tutorial003_05_py310.py | 11 ++ fastapi/utils.py | 8 +- pyproject.toml | 4 + ...est_response_model_as_return_annotation.py | 13 ++ .../test_tutorial003_01.py | 120 ++++++++++++++++ .../test_tutorial003_01_py310.py | 129 ++++++++++++++++++ .../test_tutorial003_02.py | 93 +++++++++++++ .../test_tutorial003_03.py | 36 +++++ .../test_tutorial003_04.py | 9 ++ .../test_tutorial003_04_py310.py | 12 ++ .../test_tutorial003_05.py | 93 +++++++++++++ .../test_tutorial003_05_py310.py | 103 ++++++++++++++ 18 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 docs_src/response_model/tutorial003_02.py create mode 100644 docs_src/response_model/tutorial003_03.py create mode 100644 docs_src/response_model/tutorial003_04.py create mode 100644 docs_src/response_model/tutorial003_04_py310.py create mode 100644 docs_src/response_model/tutorial003_05.py create mode 100644 docs_src/response_model/tutorial003_05_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_01.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_02.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_03.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_04.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_05.py create mode 100644 tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 69c02052d..cd7a749d4 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -89,6 +89,8 @@ If you declare both a return type and a `response_model`, the `response_model` w This way you can add correct type annotations to your functions even when you are returning a type different than the response model, to be used by the editor and tools like mypy. And still you can have FastAPI do the data validation, documentation, etc. using the `response_model`. +You can also use `response_model=None` to disable creating a response model for that *path operation*, you might need to do it if you are adding type annotations for things that are not valid Pydantic fields, you will see an example of that in one of the sections below. + ## Return the same input data Here we are declaring a `UserIn` model, it will contain a plaintext password: @@ -244,6 +246,74 @@ And both models will be used for the interactive API documentation: +## Other Return Type Annotations + +There might be cases where you return something that is not a valid Pydantic field and you annotate it in the function, only to get the support provided by tooling (the editor, mypy, etc). + +### Return a Response Directly + +The most common case would be [returning a Response directly as explained later in the advanced docs](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +This simple case is handled automatically by FastAPI because the return type annotation is the class (or a subclass) of `Response`. + +And tools will also be happy because both `RedirectResponse` and `JSONResponse` are subclasses of `Response`, so the type annotation is correct. + +### Annotate a Response Subclass + +You can also use a subclass of `Response` in the type annotation: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +This will also work because `RedirectResponse` is a subclass of `Response`, and FastAPI will automatically handle this simple case. + +### Invalid Return Type Annotations + +But when you return some other arbitrary object that is not a valid Pydantic type (e.g. a database object) and you annotate it like that in the function, FastAPI will try to create a Pydantic response model from that type annotation, and will fail. + +The same would happen if you had something like a union between different types where one or more of them are not valid Pydantic types, for example this would fail 💥: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +...this fails because the type annotation is not a Pydantic type and is not just a single `Response` class or subclass, it's a union (any of the two) between a `Response` and a `dict`. + +### Disable Response Model + +Continuing from the example above, you might not want to have the default data validation, documentation, filtering, etc. that is performed by FastAPI. + +But you might want to still keep the return type annotation in the function to get the support from tools like editors and type checkers (e.g. mypy). + +In this case, you can disable the response model generation by setting `response_model=None`: + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +This will make FastAPI skip the response model generation and that way you can have any return type annotations you need without it affecting your FastAPI application. 🤓 + ## Response Model encoding parameters Your response model could have default values, like: diff --git a/docs_src/response_model/tutorial003_02.py b/docs_src/response_model/tutorial003_02.py new file mode 100644 index 000000000..df6a09646 --- /dev/null +++ b/docs_src/response_model/tutorial003_02.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import JSONResponse, RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Response: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return JSONResponse(content={"message": "Here's your interdimensional portal."}) diff --git a/docs_src/response_model/tutorial003_03.py b/docs_src/response_model/tutorial003_03.py new file mode 100644 index 000000000..0d4bd8de5 --- /dev/null +++ b/docs_src/response_model/tutorial003_03.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/teleport") +async def get_teleport() -> RedirectResponse: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") diff --git a/docs_src/response_model/tutorial003_04.py b/docs_src/response_model/tutorial003_04.py new file mode 100644 index 000000000..b13a92692 --- /dev/null +++ b/docs_src/response_model/tutorial003_04.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Union[Response, dict]: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_04_py310.py b/docs_src/response_model/tutorial003_04_py310.py new file mode 100644 index 000000000..cee49b83e --- /dev/null +++ b/docs_src/response_model/tutorial003_04_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal") +async def get_portal(teleport: bool = False) -> Response | dict: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_05.py b/docs_src/response_model/tutorial003_05.py new file mode 100644 index 000000000..0962061a6 --- /dev/null +++ b/docs_src/response_model/tutorial003_05.py @@ -0,0 +1,13 @@ +from typing import Union + +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal", response_model=None) +async def get_portal(teleport: bool = False) -> Union[Response, dict]: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/docs_src/response_model/tutorial003_05_py310.py b/docs_src/response_model/tutorial003_05_py310.py new file mode 100644 index 000000000..f1c0f8e12 --- /dev/null +++ b/docs_src/response_model/tutorial003_05_py310.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI, Response +from fastapi.responses import RedirectResponse + +app = FastAPI() + + +@app.get("/portal", response_model=None) +async def get_portal(teleport: bool = False) -> Response | dict: + if teleport: + return RedirectResponse(url="https://www.youtube.com/watch?v=dQw4w9WgXcQ") + return {"message": "Here's your interdimensional portal."} diff --git a/fastapi/utils.py b/fastapi/utils.py index b15f6a2cf..391c47d81 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -88,7 +88,13 @@ def create_response_field( return response_field(field_info=field_info) except RuntimeError: raise fastapi.exceptions.FastAPIError( - f"Invalid args for response field! Hint: check that {type_} is a valid pydantic field type" + "Invalid args for response field! Hint: " + f"check that {type_} is a valid Pydantic field type. " + "If you are using a return type annotation that is not a valid Pydantic " + "field (e.g. Union[Response, dict, None]) you can disable generating the " + "response model from the type annotation with the path operation decorator " + "parameter response_model=None. Read more: " + "https://fastapi.tiangolo.com/tutorial/response-model/" ) from None diff --git a/pyproject.toml b/pyproject.toml index b29549f5c..7fb8078f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,10 @@ source = [ "fastapi" ] context = '${CONTEXT}' +omit = [ + "docs_src/response_model/tutorial003_04.py", + "docs_src/response_model/tutorial003_04_py310.py", +] [tool.ruff] select = [ diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 5d347ec34..e45364149 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,6 +2,7 @@ from typing import List, Union import pytest from fastapi import FastAPI +from fastapi.exceptions import FastAPIError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient from pydantic import BaseModel, ValidationError @@ -1096,3 +1097,15 @@ def test_no_response_model_annotation_json_response_class(): response = client.get("/no_response_model-annotation_json_response_class") assert response.status_code == 200, response.text assert response.json() == {"foo": "bar"} + + +def test_invalid_response_model_field(): + app = FastAPI() + with pytest.raises(FastAPIError) as e: + + @app.get("/") + def read_root() -> Union[Response, None]: + return Response(content="Foo") # pragma: no cover + + assert "valid Pydantic field type" in e.value.args[0] + assert "parameter response_model=None" in e.value.args[0] diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py new file mode 100644 index 000000000..39a4734ed --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -0,0 +1,120 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_01 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BaseUser"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"title": "Full Name", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_user(): + response = client.post( + "/user/", + json={ + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "foo", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py new file mode 100644 index 000000000..3a04db6bc --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -0,0 +1,129 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/user/": { + "post": { + "summary": "Create User", + "operationId": "create_user_user__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserIn"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BaseUser"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "BaseUser": { + "title": "BaseUser", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "UserIn": { + "title": "UserIn", + "required": ["username", "email", "password"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string", "format": "email"}, + "full_name": {"title": "Full Name", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.response_model.tutorial003_01_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_user(client: TestClient): + response = client.post( + "/user/", + json={ + "username": "foo", + "password": "fighter", + "email": "foo@example.com", + "full_name": "Grave Dohl", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "foo", + "email": "foo@example.com", + "full_name": "Grave Dohl", + } diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py new file mode 100644 index 000000000..d933f871c --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -0,0 +1,93 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_02 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_portal(): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +def test_get_redirect(): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py new file mode 100644 index 000000000..398eb4765 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_03 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/teleport": { + "get": { + "summary": "Get Teleport", + "operationId": "get_teleport_teleport_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_portal(): + response = client.get("/teleport", follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04.py b/tests/test_tutorial/test_response_model/test_tutorial003_04.py new file mode 100644 index 000000000..4aa80145a --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_04.py @@ -0,0 +1,9 @@ +import pytest +from fastapi.exceptions import FastAPIError + + +def test_invalid_response_model(): + with pytest.raises(FastAPIError): + from docs_src.response_model.tutorial003_04 import app + + assert app # pragma: no cover diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py new file mode 100644 index 000000000..b876facc8 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_04_py310.py @@ -0,0 +1,12 @@ +import pytest +from fastapi.exceptions import FastAPIError + +from ...utils import needs_py310 + + +@needs_py310 +def test_invalid_response_model(): + with pytest.raises(FastAPIError): + from docs_src.response_model.tutorial003_04_py310 import app + + assert app # pragma: no cover diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py new file mode 100644 index 000000000..27896d490 --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -0,0 +1,93 @@ +from fastapi.testclient import TestClient + +from docs_src.response_model.tutorial003_05 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_portal(): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +def test_get_redirect(): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py new file mode 100644 index 000000000..bf36c906b --- /dev/null +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py @@ -0,0 +1,103 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/portal": { + "get": { + "summary": "Get Portal", + "operationId": "get_portal_portal_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Teleport", + "type": "boolean", + "default": False, + }, + "name": "teleport", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.response_model.tutorial003_05_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_get_portal(client: TestClient): + response = client.get("/portal") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Here's your interdimensional portal."} + + +@needs_py310 +def test_get_redirect(client: TestClient): + response = client.get("/portal", params={"teleport": True}, follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://www.youtube.com/watch?v=dQw4w9WgXcQ" From e84cb6663e006a80cc564ce01d722efb488084d3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 10 Jan 2023 16:23:24 +0000 Subject: [PATCH 0740/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bf588ff67..644521796 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error. PR [#5873](https://github.com/tiangolo/fastapi/pull/5873) by [@tiangolo](https://github.com/tiangolo). * 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). * 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). * 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). From 00f3c831f3d8c2f62ce44c5dfdf56c7bc7fc3231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 10 Jan 2023 20:30:10 +0400 Subject: [PATCH 0741/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 644521796..bd78ca7c8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,10 +2,19 @@ ## Latest Changes -* 📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error. PR [#5873](https://github.com/tiangolo/fastapi/pull/5873) by [@tiangolo](https://github.com/tiangolo). -* 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). -* 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). +### Fixes + +* 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). See the new docs in the PR below. + +### Docs + +* 📝 Update docs and examples for Response Model with Return Type Annotations, and update runtime error. PR [#5873](https://github.com/tiangolo/fastapi/pull/5873) by [@tiangolo](https://github.com/tiangolo). New docs at [Response Model - Return Type: Other Return Type Annotations](https://fastapi.tiangolo.com/tutorial/response-model/#other-return-type-annotations). * 📝 Add External Link: FastAPI lambda container: serverless simplified. PR [#5784](https://github.com/tiangolo/fastapi/pull/5784) by [@rafrasenberg](https://github.com/rafrasenberg). + +### Translations + +* 🌐 Add Turkish translation for `docs/tr/docs/tutorial/first_steps.md`. PR [#5691](https://github.com/tiangolo/fastapi/pull/5691) by [@Kadermiyanyedi](https://github.com/Kadermiyanyedi). + ## 0.89.0 ### Features From 5905c3f740c8590f1a370e36b99b760f1ee7b828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 10 Jan 2023 20:31:23 +0400 Subject: [PATCH 0742/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?89.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 bd78ca7c8..eab3d56f4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.89.1 + ### Fixes * 🐛 Ignore Response classes on return annotation. PR [#5855](https://github.com/tiangolo/fastapi/pull/5855) by [@Kludex](https://github.com/Kludex). See the new docs in the PR below. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index bda10f043..07ed78ffa 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.89.0" +__version__ = "0.89.1" from starlette import status as status From 805226c2b03a5810adbeefea742cff1498e1e00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Jan 2023 18:13:03 +0400 Subject: [PATCH 0743/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20GitHub=20Spon?= =?UTF-8?q?sors=20badge=20data=20(#5915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index a8bfb0b1b..148dc53e4 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -13,3 +13,4 @@ logins: - BLUE-DEVIL1134 - ObliviousAI - Doist + - nihpo From 97a2ebc2191927544f10c0d2187bc3865d92b12d Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 23 Jan 2023 14:13:39 +0000 Subject: [PATCH 0744/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 eab3d56f4..3b0ab4e1d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update GitHub Sponsors badge data. PR [#5915](https://github.com/tiangolo/fastapi/pull/5915) by [@tiangolo](https://github.com/tiangolo). ## 0.89.1 From fe74890b4b1ccf32ac9f5712452dbb70b78fd978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 23 Jan 2023 18:23:53 +0400 Subject: [PATCH 0745/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20Sponsor=20Bud?= =?UTF-8?q?get=20Insight=20to=20Powens=20(#5916)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 6 +++--- docs/en/docs/img/sponsors/powens.png | Bin 0 -> 15311 bytes 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/en/docs/img/sponsors/powens.png diff --git a/README.md b/README.md index 2b4de2a65..ac3e655bb 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index c39dbb589..b9be9810d 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -27,9 +27,9 @@ silver: - url: https://www.udemy.com/course/fastapi-rest/ title: Learn FastAPI by building a complete project. Extend your knowledge on advanced web development-AWS, Payments, Emails. img: https://fastapi.tiangolo.com/img/sponsors/ines-course.jpg - - url: https://careers.budget-insight.com/ - title: Budget Insight is hiring! - img: https://fastapi.tiangolo.com/img/sponsors/budget-insight.svg + - url: https://careers.powens.com/ + title: Powens is hiring! + img: https://fastapi.tiangolo.com/img/sponsors/powens.png - url: https://www.svix.com/ title: Svix - Webhooks as a service img: https://fastapi.tiangolo.com/img/sponsors/svix.svg diff --git a/docs/en/docs/img/sponsors/powens.png b/docs/en/docs/img/sponsors/powens.png new file mode 100644 index 0000000000000000000000000000000000000000..07fc6fa37f9c1b32d64bc99b14fcad178c7730c3 GIT binary patch literal 15311 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 z zC%U_*E?@rlkKeyejl21dT*x!bDlG5*pwHO+ar_-pPKy+&JS*FEWCzS6l9s8gSfsit zwV(s+eKJQFK%xR~w@hu{K5(G&@B#EyCC_Ywtj|325|XR4yy{iW)ujWN5}U-&khrAe2nCSS>Za_)YE@Q7q07u(PJ~7!c-at_3?MhR9T0g9 z&pi6%qmUTL7*9U==;M$7Ie?8<&D%g+xQ!y~(h{eql`2#EH18r{C30yvbtSl1W`ee6 z={MudT1Kxi?y~l|tl;gQTD^Y#`ulvq*4;)I!WasNnB?>FnDW?Mh|WH7v#Cf@3!|sJ zWQUH^JrR~!M8qvCVmXrU)s+PTUY8NZ>0y^!1ZZW>`#3 zecSk-3KZtaADpLU^eI$bI;`^)pSr**(B+m_6o;ZLzH!-j%*HrZw{gvpv$|KOF0Wn% z?^RH*ULNf>$L$GBB0_{@ZP+xm1T6PuKIVU2qU1=KN@pek)OZ=aI^<>S%JR9ms_GD% z3{2Dq4(~$S%&EeWX9}Nu^ihhdpdZB(`udrSZEWqfL1R)weW7@-4hS#(n(pL=SxSAbwGExI{xZ$ls1 zhHQno-mY!W(9GK&fW5!X5P>0Y6M?tossBj<{Y5&4(^>O=MY@k%WcHv#jvG}a5=&dv z!YBovYGd-mZR+jIq(xzxnnGnVDK<&OCS6`py4%oN2~&{=xtVV!p#Bt@vkKIK5|%aH)lpmb{&?+>Tm*X-Zb(9yxZgwWH1cL3es(sZQHc}QXx;KAq*v* z)+nne(`}|QD1$^*Wk$ti?ajAZ^LxzJo_so5>V((j@{;)S6ht2F=9|ae2)ZzvBkjkC zUtw(NX85i>o1t(CN|CBisxlL}Oo|Zx=?)x~@=ut)a h=ra$Sn$Z9Be*uUJr|sP-yDI Date: Mon, 23 Jan 2023 14:24:31 +0000 Subject: [PATCH 0746/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3b0ab4e1d..bbcb9ecf3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update Sponsor Budget Insight to Powens. PR [#5916](https://github.com/tiangolo/fastapi/pull/5916) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update GitHub Sponsors badge data. PR [#5915](https://github.com/tiangolo/fastapi/pull/5915) by [@tiangolo](https://github.com/tiangolo). ## 0.89.1 From 9858577cd62c9ba7ac1fa729c7f9ddb2a9e53573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 24 Jan 2023 18:30:03 +0400 Subject: [PATCH 0747/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20template=20for?= =?UTF-8?q?=20questions=20in=20Discussions=20(#5920)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/DISCUSSION_TEMPLATE/questions.yml | 144 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/question.yml | 14 +-- 2 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 .github/DISCUSSION_TEMPLATE/questions.yml diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml new file mode 100644 index 000000000..cbe2b9167 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -0,0 +1,144 @@ +labels: [question] +body: + - type: markdown + attributes: + value: | + Thanks for your interest in FastAPI! 🚀 + + Please follow these instructions, fill every question, and do every step. 🙏 + + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. + + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. + + All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. + + That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). + + By asking questions in a structured way (following this) it will be much easier to help you. + + And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 + + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + - type: checkboxes + id: checks + attributes: + label: First Check + description: Please confirm and check all the following options. + options: + - label: I added a very descriptive title here. + required: true + - label: I used the GitHub search to find a similar question and didn't find it. + required: true + - label: I searched the FastAPI documentation, with the integrated search. + required: true + - label: I already searched in Google "How to X in FastAPI" and didn't find any information. + required: true + - label: I already read and followed all the tutorial in the docs and didn't find an answer. + required: true + - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). + required: true + - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). + required: true + - label: I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). + required: true + - type: checkboxes + id: help + attributes: + label: Commit to Help + description: | + After submitting this, I commit to one of: + + * Read open questions until I find 2 where I can help someone and add a comment to help there. + * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. + * Review one Pull Request by downloading the code and following [all the review process](https://fastapi.tiangolo.com/help-fastapi/#review-pull-requests). + + options: + - label: I commit to help with one of those options 👆 + required: true + - type: textarea + id: example + attributes: + label: Example Code + description: | + Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. + + If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. + + placeholder: | + from fastapi import FastAPI + + app = FastAPI() + + + @app.get("/") + def read_root(): + return {"Hello": "World"} + render: python + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: | + What is the problem, question, or error? + + Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. + placeholder: | + * Open the browser and call the endpoint `/`. + * It returns a JSON with `{"Hello": "World"}`. + * But I expected it to return `{"Hello": "Sara"}`. + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you on? + multiple: true + options: + - Linux + - Windows + - macOS + - Other + validations: + required: true + - type: textarea + id: os-details + attributes: + label: Operating System Details + description: You can add more details about your operating system here, in particular if you chose "Other". + - type: input + id: fastapi-version + attributes: + label: FastAPI Version + description: | + What FastAPI version are you using? + + You can find the FastAPI version with: + + ```bash + python -c "import fastapi; print(fastapi.__version__)" + ``` + validations: + required: true + - type: input + id: python-version + attributes: + label: Python Version + description: | + What Python version are you using? + + You can find the Python version with: + + ```bash + python --version + ``` + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 3b16b4ad0..4971187d6 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -9,9 +9,9 @@ body: Please follow these instructions, fill every question, and do every step. 🙏 - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. + I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. + I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. @@ -21,16 +21,16 @@ body: And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 + As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - type: checkboxes id: checks attributes: label: First Check description: Please confirm and check all the following options. options: - - label: I added a very descriptive title to this issue. + - label: I added a very descriptive title here. required: true - - label: I used the GitHub search to find a similar issue and didn't find it. + - label: I used the GitHub search to find a similar question and didn't find it. required: true - label: I searched the FastAPI documentation, with the integrated search. required: true @@ -51,9 +51,9 @@ body: description: | After submitting this, I commit to one of: - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. + * Read open questions until I find 2 where I can help someone and add a comment to help there. * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. + * Review one Pull Request by downloading the code and following [all the review process](https://fastapi.tiangolo.com/help-fastapi/#review-pull-requests). options: - label: I commit to help with one of those options 👆 From 4e298356097a3e1bbe2bf28424e7d5392a9fd96b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 24 Jan 2023 14:30:38 +0000 Subject: [PATCH 0748/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bbcb9ecf3..7eceec232 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add template for questions in Discussions. PR [#5920](https://github.com/tiangolo/fastapi/pull/5920) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsor Budget Insight to Powens. PR [#5916](https://github.com/tiangolo/fastapi/pull/5916) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update GitHub Sponsors badge data. PR [#5915](https://github.com/tiangolo/fastapi/pull/5915) by [@tiangolo](https://github.com/tiangolo). From 11b6c0146dd5c2600be1aec91a47dc1e6e7427a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Jan 2023 16:09:51 +0100 Subject: [PATCH 0749/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20isort?= =?UTF-8?q?=20and=20update=20pre-commit=20(#5940)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 96f097caa..01cd6ea0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks +default_language_version: + python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 @@ -25,7 +27,7 @@ repos: args: - --fix - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort name: isort (python) From 7c23bbd96f656dbf3f78cf0cd45de9ac73dd89d7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 30 Jan 2023 15:10:29 +0000 Subject: [PATCH 0750/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7eceec232..7cbdad131 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for questions in Discussions. PR [#5920](https://github.com/tiangolo/fastapi/pull/5920) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsor Budget Insight to Powens. PR [#5916](https://github.com/tiangolo/fastapi/pull/5916) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update GitHub Sponsors badge data. PR [#5915](https://github.com/tiangolo/fastapi/pull/5915) by [@tiangolo](https://github.com/tiangolo). From 9530defba87decf43a7a2f418dad1ec623f5edc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Jan 2023 16:15:56 +0100 Subject: [PATCH 0751/1881] =?UTF-8?q?=E2=9C=A8=20Compute=20FastAPI=20Exper?= =?UTF-8?q?ts=20including=20GitHub=20Discussions=20(#5941)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/app/main.py | 206 +++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 14 deletions(-) diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 31756a5fc..05cbc71e5 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -4,7 +4,7 @@ import sys from collections import Counter, defaultdict from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Container, DefaultDict, Dict, List, Set, Union +from typing import Any, Container, DefaultDict, Dict, List, Set, Union import httpx import yaml @@ -12,6 +12,50 @@ from github import Github from pydantic import BaseModel, BaseSettings, SecretStr github_graphql_url = "https://api.github.com/graphql" +questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" + +discussions_query = """ +query Q($after: String, $category_id: ID) { + repository(name: "fastapi", owner: "tiangolo") { + discussions(first: 100, after: $after, categoryId: $category_id) { + edges { + cursor + node { + number + author { + login + avatarUrl + url + } + title + createdAt + comments(first: 100) { + nodes { + createdAt + author { + login + avatarUrl + url + } + isAnswer + replies(first: 10) { + nodes { + createdAt + author { + login + avatarUrl + url + } + } + } + } + } + } + } + } + } +} +""" issues_query = """ query Q($after: String) { @@ -131,15 +175,30 @@ class Author(BaseModel): url: str +# Issues and Discussions + + class CommentsNode(BaseModel): createdAt: datetime author: Union[Author, None] = None +class Replies(BaseModel): + nodes: List[CommentsNode] + + +class DiscussionsCommentsNode(CommentsNode): + replies: Replies + + class Comments(BaseModel): nodes: List[CommentsNode] +class DiscussionsComments(BaseModel): + nodes: List[DiscussionsCommentsNode] + + class IssuesNode(BaseModel): number: int author: Union[Author, None] = None @@ -149,27 +208,59 @@ class IssuesNode(BaseModel): comments: Comments +class DiscussionsNode(BaseModel): + number: int + author: Union[Author, None] = None + title: str + createdAt: datetime + comments: DiscussionsComments + + class IssuesEdge(BaseModel): cursor: str node: IssuesNode +class DiscussionsEdge(BaseModel): + cursor: str + node: DiscussionsNode + + class Issues(BaseModel): edges: List[IssuesEdge] +class Discussions(BaseModel): + edges: List[DiscussionsEdge] + + class IssuesRepository(BaseModel): issues: Issues +class DiscussionsRepository(BaseModel): + discussions: Discussions + + class IssuesResponseData(BaseModel): repository: IssuesRepository +class DiscussionsResponseData(BaseModel): + repository: DiscussionsRepository + + class IssuesResponse(BaseModel): data: IssuesResponseData +class DiscussionsResponse(BaseModel): + data: DiscussionsResponseData + + +# PRs + + class LabelNode(BaseModel): name: str @@ -219,6 +310,9 @@ class PRsResponse(BaseModel): data: PRsResponseData +# Sponsors + + class SponsorEntity(BaseModel): login: str avatarUrl: str @@ -264,10 +358,16 @@ class Settings(BaseSettings): def get_graphql_response( - *, settings: Settings, query: str, after: Union[str, None] = None -): + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, +) -> Dict[str, Any]: headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - variables = {"after": after} + # category_id is only used by one query, but GraphQL allows unused variables, so + # keep it here for simplicity + variables = {"after": after, "category_id": category_id} response = httpx.post( github_graphql_url, headers=headers, @@ -275,7 +375,9 @@ def get_graphql_response( json={"query": query, "variables": variables, "operationName": "Q"}, ) if response.status_code != 200: - logging.error(f"Response was not 200, after: {after}") + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) logging.error(response.text) raise RuntimeError(response.text) data = response.json() @@ -288,6 +390,21 @@ def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = Non return graphql_response.data.repository.issues.edges +def get_graphql_question_discussion_edges( + *, + settings: Settings, + after: Union[str, None] = None, +): + data = get_graphql_response( + settings=settings, + query=discussions_query, + after=after, + category_id=questions_category_id, + ) + graphql_response = DiscussionsResponse.parse_obj(data) + return graphql_response.data.repository.discussions.edges + + def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=prs_query, after=after) graphql_response = PRsResponse.parse_obj(data) @@ -300,7 +417,7 @@ def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = N return graphql_response.data.user.sponsorshipsAsMaintainer.edges -def get_experts(settings: Settings): +def get_issues_experts(settings: Settings): issue_nodes: List[IssuesNode] = [] issue_edges = get_graphql_issue_edges(settings=settings) @@ -326,13 +443,74 @@ def get_experts(settings: Settings): for comment in issue.comments.nodes: if comment.author: authors[comment.author.login] = comment.author - if comment.author.login == issue_author_name: - continue - issue_commentors.add(comment.author.login) + if comment.author.login != issue_author_name: + issue_commentors.add(comment.author.login) for author_name in issue_commentors: commentors[author_name] += 1 if issue.createdAt > one_month_ago: last_month_commentors[author_name] += 1 + + return commentors, last_month_commentors, authors + + +def get_discussions_experts(settings: Settings): + discussion_nodes: List[DiscussionsNode] = [] + discussion_edges = get_graphql_question_discussion_edges(settings=settings) + + while discussion_edges: + for discussion_edge in discussion_edges: + discussion_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + discussion_edges = get_graphql_question_discussion_edges( + settings=settings, after=last_edge.cursor + ) + + commentors = Counter() + last_month_commentors = Counter() + authors: Dict[str, Author] = {} + + now = datetime.now(tz=timezone.utc) + one_month_ago = now - timedelta(days=30) + + for discussion in discussion_nodes: + discussion_author_name = None + if discussion.author: + authors[discussion.author.login] = discussion.author + discussion_author_name = discussion.author.login + discussion_commentors = set() + for comment in discussion.comments.nodes: + if comment.author: + authors[comment.author.login] = comment.author + if comment.author.login != discussion_author_name: + discussion_commentors.add(comment.author.login) + for reply in comment.replies.nodes: + if reply.author: + authors[reply.author.login] = reply.author + if reply.author.login != discussion_author_name: + discussion_commentors.add(reply.author.login) + for author_name in discussion_commentors: + commentors[author_name] += 1 + if discussion.createdAt > one_month_ago: + last_month_commentors[author_name] += 1 + return commentors, last_month_commentors, authors + + +def get_experts(settings: Settings): + ( + issues_commentors, + issues_last_month_commentors, + issues_authors, + ) = get_issues_experts(settings=settings) + ( + discussions_commentors, + discussions_last_month_commentors, + discussions_authors, + ) = get_discussions_experts(settings=settings) + commentors = issues_commentors + discussions_commentors + last_month_commentors = ( + issues_last_month_commentors + discussions_last_month_commentors + ) + authors = {**issues_authors, **discussions_authors} return commentors, last_month_commentors, authors @@ -425,13 +603,13 @@ if __name__ == "__main__": logging.info(f"Using config: {settings.json()}") g = Github(settings.input_standard_token.get_secret_value()) repo = g.get_repo(settings.github_repository) - issue_commentors, issue_last_month_commentors, issue_authors = get_experts( + question_commentors, question_last_month_commentors, question_authors = get_experts( settings=settings ) contributors, pr_commentors, reviewers, pr_authors = get_contributors( settings=settings ) - authors = {**issue_authors, **pr_authors} + authors = {**question_authors, **pr_authors} maintainers_logins = {"tiangolo"} bot_names = {"codecov", "github-actions", "pre-commit-ci", "dependabot"} maintainers = [] @@ -440,7 +618,7 @@ if __name__ == "__main__": maintainers.append( { "login": login, - "answers": issue_commentors[login], + "answers": question_commentors[login], "prs": contributors[login], "avatarUrl": user.avatarUrl, "url": user.url, @@ -453,13 +631,13 @@ if __name__ == "__main__": min_count_reviewer = 4 skip_users = maintainers_logins | bot_names experts = get_top_users( - counter=issue_commentors, + counter=question_commentors, min_count=min_count_expert, authors=authors, skip_users=skip_users, ) last_month_active = get_top_users( - counter=issue_last_month_commentors, + counter=question_last_month_commentors, min_count=min_count_last_month, authors=authors, skip_users=skip_users, From 9012ab8bcd88a303463c7c7fd22fa6f77c4dcd8b Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 30 Jan 2023 15:16:30 +0000 Subject: [PATCH 0752/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7cbdad131..c1019ce0d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for questions in Discussions. PR [#5920](https://github.com/tiangolo/fastapi/pull/5920) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update Sponsor Budget Insight to Powens. PR [#5916](https://github.com/tiangolo/fastapi/pull/5916) by [@tiangolo](https://github.com/tiangolo). From 72b542d90ac11f13e04f0b5161fd685221e57cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Jan 2023 17:23:43 +0100 Subject: [PATCH 0753/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors=20ba?= =?UTF-8?q?dges=20(#5943)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 148dc53e4..3a7436658 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -14,3 +14,4 @@ logins: - ObliviousAI - Doist - nihpo + - svix From ca30b92dd74e877c8f5e67dd989355bcaab61d99 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 30 Jan 2023 16:24:22 +0000 Subject: [PATCH 0754/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c1019ce0d..afb3bf98b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). * ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add template for questions in Discussions. PR [#5920](https://github.com/tiangolo/fastapi/pull/5920) by [@tiangolo](https://github.com/tiangolo). From 682067cab246cc565b5f14d34031e37750d32b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 30 Jan 2023 17:49:32 +0100 Subject: [PATCH 0755/1881] =?UTF-8?q?=F0=9F=93=9D=20Recommend=20GitHub=20D?= =?UTF-8?q?iscussions=20for=20questions=20(#5944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/fastapi-people.md | 12 +++++------ docs/en/docs/help-fastapi.md | 38 ++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index 0cbcb69c9..20caaa1ee 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -28,7 +28,7 @@ I'm the creator and maintainer of **FastAPI**. You can read more about that in [ These are the people that: -* [Help others with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. +* [Help others with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank}. * [Create Pull Requests](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. * Review Pull Requests, [especially important for translations](contributing.md#translations){.internal-link target=_blank}. @@ -36,13 +36,13 @@ A round of applause to them. 👏 🙇 ## Most active users last month -These are the users that have been [helping others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} during the last month. ☕ +These are the users that have been [helping others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} during the last month. ☕ {% if people %}
{% for user in people.last_month_active %} -
@{{ user.login }}
Issues replied: {{ user.count }}
+
@{{ user.login }}
Questions replied: {{ user.count }}
{% endfor %}
@@ -52,7 +52,7 @@ These are the users that have been [helping others the most with issues (questio Here are the **FastAPI Experts**. 🤓 -These are the users that have [helped others the most with issues (questions) in GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} through *all time*. +These are the users that have [helped others the most with questions in GitHub](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} through *all time*. They have proven to be experts by helping many others. ✨ @@ -60,7 +60,7 @@ They have proven to be experts by helping many others. ✨
{% for user in people.experts %} -
@{{ user.login }}
Issues replied: {{ user.count }}
+
@{{ user.login }}
Questions replied: {{ user.count }}
{% endfor %}
@@ -169,7 +169,7 @@ They are supporting my work with **FastAPI** (and others), mainly through source code here. diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index a7ac9415f..767f7b43f 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -69,11 +69,16 @@ I love to hear about how **FastAPI** is being used, what you have liked in it, i * Vote for **FastAPI** in AlternativeTo. * Say you use **FastAPI** on StackShare. -## Help others with issues in GitHub +## Help others with questions in GitHub -You can see existing issues and try and help others, most of the times those issues are questions that you might already know the answer for. 🤓 +You can try and help others with their questions in: -If you are helping a lot of people with issues, you might become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 +* GitHub Discussions +* GitHub Issues + +In many cases you might already know the answer for those questions. 🤓 + +If you are helping a lot of people with their questions, you will become an official [FastAPI Expert](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 Just remember, the most important point is: try to be kind. People come with their frustrations and in many cases don't ask in the best way, but try as best as you can to be kind. 🤗 @@ -81,7 +86,7 @@ The idea is for the **FastAPI** community to be kind and welcoming. At the same --- -Here's how to help others with issues: +Here's how to help others with questions (in discussions or issues): ### Understand the question @@ -113,24 +118,27 @@ In many cases they will only copy a fragment of the code, but that's not enough If they reply, there's a high chance you would have solved their problem, congrats, **you're a hero**! 🦸 -* Now you can ask them, if that solved their problem, to **close the issue**. +* Now, if that solved their problem, you can ask them to: + + * In GitHub Discussions: mark the comment as the **answer**. + * In GitHub Issues: **close** the issue**. ## Watch the GitHub repository You can "watch" FastAPI in GitHub (clicking the "watch" button at the top right): https://github.com/tiangolo/fastapi. 👀 -If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue. +If you select "Watching" instead of "Releases only" you will receive notifications when someone creates a new issue or question. You can also specify that you only want to be notified about new issues, or discussions, or PRs, etc. -Then you can try and help them solve those issues. +Then you can try and help them solve those questions. -## Create issues +## Ask Questions -You can create a new issue in the GitHub repository, for example to: +You can create a new question in the GitHub repository, for example to: * Ask a **question** or ask about a **problem**. * Suggest a new **feature**. -**Note**: if you create an issue, then I'm going to ask you to also help others. 😉 +**Note**: if you do it, then I'm going to ask you to also help others. 😉 ## Review Pull Requests @@ -144,7 +152,7 @@ Here's what to have in mind and how to review a pull request: ### Understand the problem -* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in an issue. +* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or Issue. * There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that. @@ -207,7 +215,7 @@ There's a lot of work to do, and for most of it, **YOU** can do it. The main tasks that you can do right now are: -* [Help others with issues in GitHub](#help-others-with-issues-in-github){.internal-link target=_blank} (see the section above). +* [Help others with questions in GitHub](#help-others-with-questions-in-github){.internal-link target=_blank} (see the section above). * [Review Pull Requests](#review-pull-requests){.internal-link target=_blank} (see the section above). Those two tasks are what **consume time the most**. That's the main work of maintaining FastAPI. @@ -219,7 +227,7 @@ If you can help me with that, **you are helping me maintain FastAPI** and making Join the 👥 Discord chat server 👥 and hang out with others in the FastAPI community. !!! tip - For questions, ask them in GitHub issues, there's a much better chance you will receive help by the [FastAPI Experts](fastapi-people.md#experts){.internal-link target=_blank}. + For questions, ask them in GitHub Discussions, there's a much better chance you will receive help by the [FastAPI Experts](fastapi-people.md#experts){.internal-link target=_blank}. Use the chat only for other general conversations. @@ -229,9 +237,9 @@ There is also the previous Date: Mon, 30 Jan 2023 16:50:10 +0000 Subject: [PATCH 0756/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 afb3bf98b..d250bb1b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). * ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). From 0b0af37b0ed2c89dadb801f2c7ebd29327b7f28b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 31 Jan 2023 15:02:52 +0100 Subject: [PATCH 0757/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20new=20issue?= =?UTF-8?q?=20chooser=20to=20direct=20to=20GitHub=20Discussions=20(#5948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/config.yml | 12 ++ .github/ISSUE_TEMPLATE/feature-request.yml | 181 --------------------- .github/ISSUE_TEMPLATE/privileged.yml | 22 +++ .github/ISSUE_TEMPLATE/question.yml | 146 ----------------- 4 files changed, 34 insertions(+), 327 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/ISSUE_TEMPLATE/privileged.yml delete mode 100644 .github/ISSUE_TEMPLATE/question.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 55749398f..a8f4c4de2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,3 +2,15 @@ blank_issues_enabled: false contact_links: - name: Security Contact about: Please report security vulnerabilities to security@tiangolo.com + - name: Question or Problem + about: Ask a question or ask about a problem in GitHub Discussions. + url: https://github.com/tiangolo/fastapi/discussions/categories/questions + - name: Feature Request + about: To suggest an idea or ask about a feature, please start with a question saying what you would like to achieve. There might be a way to do it already. + url: https://github.com/tiangolo/fastapi/discussions/categories/questions + - name: Show and tell + about: Show what you built with FastAPI or to be used with FastAPI. + url: https://github.com/tiangolo/fastapi/discussions/categories/show-and-tell + - name: Translations + about: Coordinate translations in GitHub Discussions. + url: https://github.com/tiangolo/fastapi/discussions/categories/translations diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 322b6536a..000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Feature Request -description: Suggest an idea or ask for a feature that you would like to have in FastAPI -labels: [enhancement] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in FastAPI! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub issues is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling issues. - - All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. - - That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many issues with questions, I'll have to close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title to this issue. - required: true - - label: I used the GitHub search to find a similar issue and didn't find it. - required: true - - label: I searched the FastAPI documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in FastAPI" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). - required: true - - label: I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Implement a Pull Request for a confirmed bug. - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from fastapi import FastAPI - - app = FastAPI() - - - @app.get("/") - def read_root(): - return {"Hello": "World"} - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is your feature request? - - Write a short description telling me what you are trying to solve and what you are currently doing. - placeholder: | - * Open the browser and call the endpoint `/`. - * It returns a JSON with `{"Hello": "World"}`. - * I would like it to have an extra parameter to teleport me to the moon and back. - validations: - required: true - - type: textarea - id: wanted-solution - attributes: - label: Wanted Solution - description: | - Tell me what's the solution you would like. - placeholder: | - I would like it to have a `teleport_to_moon` parameter that defaults to `False`, and can be set to `True` to teleport me. - validations: - required: true - - type: textarea - id: wanted-code - attributes: - label: Wanted Code - description: Show me an example of how you would want the code to look like. - placeholder: | - from fastapi import FastAPI - - app = FastAPI() - - - @app.get("/", teleport_to_moon=True) - def read_root(): - return {"Hello": "World"} - render: python - validations: - required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives - description: | - Tell me about alternatives you've considered. - placeholder: | - To wait for Space X moon travel plans to drop down long after they release them. But I would rather teleport. - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: fastapi-version - attributes: - label: FastAPI Version - description: | - What FastAPI version are you using? - - You can find the FastAPI version with: - - ```bash - python -c "import fastapi; print(fastapi.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. diff --git a/.github/ISSUE_TEMPLATE/privileged.yml b/.github/ISSUE_TEMPLATE/privileged.yml new file mode 100644 index 000000000..c01e34b6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/privileged.yml @@ -0,0 +1,22 @@ +name: Privileged +description: You are @tiangolo or he asked you directly to create an issue here. If not, check the other options. 👇 +body: + - type: markdown + attributes: + value: | + Thanks for your interest in FastAPI! 🚀 + + If you are not @tiangolo or he didn't ask you directly to create an issue here, please start the conversation in a [Question in GitHub Discussions](https://github.com/tiangolo/fastapi/discussions/categories/questions) instead. + - type: checkboxes + id: privileged + attributes: + label: Privileged issue + description: Confirm that you are allowed to create an issue here. + options: + - label: I'm @tiangolo or he asked me directly to create an issue here. + required: true + - type: textarea + id: content + attributes: + label: Issue Content + description: Add the content of the issue here. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index 4971187d6..000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,146 +0,0 @@ -name: Question or Problem -description: Ask a question or ask about a problem -labels: [question] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in FastAPI! 🚀 - - Please follow these instructions, fill every question, and do every step. 🙏 - - I'm asking this because answering questions and solving problems in GitHub is what consumes most of the time. - - I end up not being able to add new features, fix bugs, review pull requests, etc. as fast as I wish because I have to spend too much time handling questions. - - All that, on top of all the incredible help provided by a bunch of community members, the [FastAPI Experts](https://fastapi.tiangolo.com/fastapi-people/#experts), that give a lot of their time to come here and help others. - - That's a lot of work they are doing, but if more FastAPI users came to help others like them just a little bit more, it would be much less effort for them (and you and me 😅). - - By asking questions in a structured way (following this) it will be much easier to help you. - - And there's a high chance that you will find the solution along the way and you won't even have to submit it and wait for an answer. 😎 - - As there are too many questions, I'll have to discard and close the incomplete ones. That will allow me (and others) to focus on helping people like you that follow the whole process and help us help you. 🤓 - - type: checkboxes - id: checks - attributes: - label: First Check - description: Please confirm and check all the following options. - options: - - label: I added a very descriptive title here. - required: true - - label: I used the GitHub search to find a similar question and didn't find it. - required: true - - label: I searched the FastAPI documentation, with the integrated search. - required: true - - label: I already searched in Google "How to X in FastAPI" and didn't find any information. - required: true - - label: I already read and followed all the tutorial in the docs and didn't find an answer. - required: true - - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). - required: true - - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). - required: true - - label: I already checked if it is not related to FastAPI but to [ReDoc](https://github.com/Redocly/redoc). - required: true - - type: checkboxes - id: help - attributes: - label: Commit to Help - description: | - After submitting this, I commit to one of: - - * Read open questions until I find 2 where I can help someone and add a comment to help there. - * I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future. - * Review one Pull Request by downloading the code and following [all the review process](https://fastapi.tiangolo.com/help-fastapi/#review-pull-requests). - - options: - - label: I commit to help with one of those options 👆 - required: true - - type: textarea - id: example - attributes: - label: Example Code - description: | - Please add a self-contained, [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example) with your use case. - - If I (or someone) can copy it, run it, and see it right away, there's a much higher chance I (or someone) will be able to help you. - - placeholder: | - from fastapi import FastAPI - - app = FastAPI() - - - @app.get("/") - def read_root(): - return {"Hello": "World"} - render: python - validations: - required: true - - type: textarea - id: description - attributes: - label: Description - description: | - What is the problem, question, or error? - - Write a short description telling me what you are doing, what you expect to happen, and what is currently happening. - placeholder: | - * Open the browser and call the endpoint `/`. - * It returns a JSON with `{"Hello": "World"}`. - * But I expected it to return `{"Hello": "Sara"}`. - validations: - required: true - - type: dropdown - id: os - attributes: - label: Operating System - description: What operating system are you on? - multiple: true - options: - - Linux - - Windows - - macOS - - Other - validations: - required: true - - type: textarea - id: os-details - attributes: - label: Operating System Details - description: You can add more details about your operating system here, in particular if you chose "Other". - - type: input - id: fastapi-version - attributes: - label: FastAPI Version - description: | - What FastAPI version are you using? - - You can find the FastAPI version with: - - ```bash - python -c "import fastapi; print(fastapi.__version__)" - ``` - validations: - required: true - - type: input - id: python-version - attributes: - label: Python Version - description: | - What Python version are you using? - - You can find the Python version with: - - ```bash - python --version - ``` - validations: - required: true - - type: textarea - id: context - attributes: - label: Additional Context - description: Add any additional context information or screenshots you think are useful. From 40df42f5c7d92815ceeb546df60256dacb3eed57 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 31 Jan 2023 14:03:27 +0000 Subject: [PATCH 0758/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d250bb1b2..8160352d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). * 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). * ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). From fc7da62005e60c08252c53c71a0d7f34d1a20666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 3 Feb 2023 18:54:22 +0100 Subject: [PATCH 0759/1881] =?UTF-8?q?=F0=9F=93=9D=20Micro-tweak=20help=20d?= =?UTF-8?q?ocs=20(#5960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/help-fastapi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 767f7b43f..48a88ee96 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -152,7 +152,7 @@ Here's what to have in mind and how to review a pull request: ### Understand the problem -* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or Issue. +* First, make sure you **understand the problem** that the pull request is trying to solve. It might have a longer discussion in a GitHub Discussion or issue. * There's also a good chance that the pull request is not actually needed because the problem can be solved in a **different way**. Then you can suggest or ask about that. From 62fc0b49237c31ce54aaaa009924b3da03907a13 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 3 Feb 2023 17:55:03 +0000 Subject: [PATCH 0760/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8160352d6..c8bf5a1f9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). * 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). From 7a64587d7f2f27e9b114352a5c7dc1a4f2759898 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Feb 2023 18:55:25 +0100 Subject: [PATCH 0761/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#5954)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 99 ++++++------- docs/en/data/people.yml | 238 +++++++++++++++++-------------- 2 files changed, 169 insertions(+), 168 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3d6831db6..c7ce38f59 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -38,9 +38,6 @@ sponsors: - - login: vyos avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 url: https://github.com/vyos - - login: SendCloud - avatarUrl: https://avatars.githubusercontent.com/u/7831959?v=4 - url: https://github.com/SendCloud - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya @@ -56,9 +53,6 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP -- - login: InesIvanova - avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 - url: https://github.com/InesIvanova - - login: johnadjei avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 url: https://github.com/johnadjei @@ -71,12 +65,12 @@ sponsors: - login: Lovage-Labs avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 url: https://github.com/Lovage-Labs -- - login: xshapira - avatarUrl: https://avatars.githubusercontent.com/u/48856190?u=3b0927ad29addab29a43767b52e45bee5cd6da9f&v=4 - url: https://github.com/xshapira - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck + - login: birkjernstrom + avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 + url: https://github.com/birkjernstrom - login: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign @@ -89,9 +83,6 @@ sponsors: - login: dorianturba avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 url: https://github.com/dorianturba - - login: Qazzquimby - avatarUrl: https://avatars.githubusercontent.com/u/12368310?u=f4ed4a7167fd359cfe4502d56d7c64f9bf59bb38&v=4 - url: https://github.com/Qazzquimby - login: jmaralc avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 url: https://github.com/jmaralc @@ -104,15 +95,9 @@ sponsors: - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: guivaloz - avatarUrl: https://avatars.githubusercontent.com/u/1296621?u=bc4fc28f96c654aa2be7be051d03a315951e2491&v=4 - url: https://github.com/guivaloz - - login: indeedeng +- - login: indeedeng avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 url: https://github.com/indeedeng - - login: fratambot - avatarUrl: https://avatars.githubusercontent.com/u/20300069?u=41c85ea08960c8a8f0ce967b780e242b1454690c&v=4 - url: https://github.com/fratambot - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -167,15 +152,15 @@ sponsors: - login: coffeewasmyidea avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 url: https://github.com/coffeewasmyidea + - login: jonakoudijs + avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4 + url: https://github.com/jonakoudijs - login: corleyma avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 url: https://github.com/corleyma - login: madisonmay avatarUrl: https://avatars.githubusercontent.com/u/2645393?u=f22b93c6ea345a4d26a90a3834dfc7f0789fcb63&v=4 url: https://github.com/madisonmay - - login: saivarunk - avatarUrl: https://avatars.githubusercontent.com/u/2976867?u=71f4385e781e9a9e871a52f2d4686f9a8d69ba2f&v=4 - url: https://github.com/saivarunk - login: andre1sk avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4 url: https://github.com/andre1sk @@ -191,9 +176,6 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 - - login: MarekBleschke - avatarUrl: https://avatars.githubusercontent.com/u/3616870?v=4 - url: https://github.com/MarekBleschke - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco @@ -239,6 +221,9 @@ sponsors: - login: Rehket avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 url: https://github.com/Rehket + - login: ValentinCalomme + avatarUrl: https://avatars.githubusercontent.com/u/7288672?u=e09758c7a36c49f0fb3574abe919cbd344fdc2d6&v=4 + url: https://github.com/ValentinCalomme - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm @@ -248,15 +233,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: jacobkrit - avatarUrl: https://avatars.githubusercontent.com/u/11823915?u=4921a7ea429b7eadcad3077f119f90d15a3318b2&v=4 - url: https://github.com/jacobkrit - login: svats2k avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 url: https://github.com/svats2k - - login: gokulyc - avatarUrl: https://avatars.githubusercontent.com/u/13468848?u=269f269d3e70407b5fb80138c52daba7af783997&v=4 - url: https://github.com/gokulyc - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade @@ -305,6 +284,9 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure + - login: AbdulwahabDev + avatarUrl: https://avatars.githubusercontent.com/u/34792253?u=9d27cbb6e196c95d747abf002df7fe0539764265&v=4 + url: https://github.com/AbdulwahabDev - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -314,18 +296,15 @@ sponsors: - login: VictorCalderon avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4 url: https://github.com/VictorCalderon - - login: arthuRHD - avatarUrl: https://avatars.githubusercontent.com/u/48015496?u=05a0d5b8b9320eeb7990d35c9337b823f269d2ff&v=4 - url: https://github.com/arthuRHD - login: rafsaf avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 url: https://github.com/dudikbender - - login: dazeddd - avatarUrl: https://avatars.githubusercontent.com/u/59472056?u=7a1b668449bf8b448db13e4c575576d24d7d658b&v=4 - url: https://github.com/dazeddd + - login: thisistheplace + avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 + url: https://github.com/thisistheplace - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge @@ -425,9 +404,6 @@ sponsors: - login: paul121 avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4 url: https://github.com/paul121 - - login: igorcorrea - avatarUrl: https://avatars.githubusercontent.com/u/3438238?u=c57605077c31a8f7b2341fc4912507f91b4a5621&v=4 - url: https://github.com/igorcorrea - login: larsvik avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4 url: https://github.com/larsvik @@ -497,6 +473,9 @@ sponsors: - login: logan-connolly avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 url: https://github.com/logan-connolly + - login: harripj + avatarUrl: https://avatars.githubusercontent.com/u/16853829?u=14db1ad132af9ec340f3f1da564620a73b6e4981&v=4 + url: https://github.com/harripj - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre @@ -516,11 +495,14 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/24669867?u=60e7c8c09f8dafabee8fc3edcd6f9e19abbff918&v=4 url: https://github.com/fstau - login: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=7e5d2bf26d0a0670ea347f7db5febebc4e031ed1&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 url: https://github.com/SebTota + - login: hoenie-ams + avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 + url: https://github.com/hoenie-ams - login: joerambo avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 url: https://github.com/joerambo @@ -537,7 +519,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=20f362505e2a994805233f42e69f9f14b4a9dd0c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=76cdc0a8b4e88c7d3e58dccb4b2670839e1247b4&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 @@ -552,10 +534,10 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 url: https://github.com/ww-daniel-mora - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=b3cb7a606207141c357e517071cf91a67fb5577e&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=34cba2eaca6a52072498e06bccebe141694fe1d7&v=4 url: https://github.com/rwxd - login: ilias-ant - avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a2d6121bac4d125d92ec207460fa3f1842d37e66&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a84d169eb6f6bbcb85434c2bed0b4a6d4d13c10e&v=4 url: https://github.com/ilias-ant - login: arrrrrmin avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=2a812c1a2ec58227ed01778837f255143de9df97&v=4 @@ -593,21 +575,24 @@ sponsors: - login: pondDevThai avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 url: https://github.com/pondDevThai - - login: zeb0x01 - avatarUrl: https://avatars.githubusercontent.com/u/77236545?u=c62bfcfbd463f9cf171c879cea1362a63de2c582&v=4 - url: https://github.com/zeb0x01 - - login: lironmiz - avatarUrl: https://avatars.githubusercontent.com/u/91504420?u=cb93dfec613911ac8d4e84fbb560711546711ad5&v=4 - url: https://github.com/lironmiz -- - login: gabrielmbmb + - login: lukzmu + avatarUrl: https://avatars.githubusercontent.com/u/80778518?u=f636ad03cab8e8de15183fa81e768bfad3f515d0&v=4 + url: https://github.com/lukzmu +- - login: chrislemke + avatarUrl: https://avatars.githubusercontent.com/u/11752694?u=70ceb6ee7c51d9a52302ab9220ffbf09eaa9c2a4&v=4 + url: https://github.com/chrislemke + - login: gabrielmbmb avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 url: https://github.com/gabrielmbmb - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - - login: Moises6669 - avatarUrl: https://avatars.githubusercontent.com/u/66188523?u=96af25b8d5be9f983cb96e9dd7c605c716caf1f5&v=4 - url: https://github.com/Moises6669 - - login: lyuboparvanov - avatarUrl: https://avatars.githubusercontent.com/u/106192895?u=367329c777320e01550afda9d8de670436181d86&v=4 - url: https://github.com/lyuboparvanov + - login: buabaj + avatarUrl: https://avatars.githubusercontent.com/u/49881677?u=a85952891036eb448f86eb847902f25badd5f9f7&v=4 + url: https://github.com/buabaj + - login: SoulPancake + avatarUrl: https://avatars.githubusercontent.com/u/70265851?u=9cdd82f2835da7d6a56a2e29e1369d5bf251e8f2&v=4 + url: https://github.com/SoulPancake + - login: junah201 + avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 + url: https://github.com/junah201 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index d46ec44ae..7e917abd0 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1878 - prs: 361 + answers: 1956 + prs: 372 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 379 + count: 400 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -14,15 +14,15 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: ycd - count: 221 + count: 224 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: Mause - count: 207 + count: 223 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause - login: JarroVGIT - count: 192 + count: 196 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 @@ -34,27 +34,31 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 103 + count: 118 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen +- login: jgould22 + count: 95 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: raphaelauv - count: 77 + count: 79 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: ArcLightSlavik - count: 71 + count: 74 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik -- login: jgould22 - count: 68 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 +- login: ghandic + count: 72 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: falkben count: 59 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen - count: 50 + count: 52 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen - login: insomnes @@ -65,14 +69,26 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa +- login: acidjunk + count: 44 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: adriangb - count: 41 + count: 44 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 url: https://github.com/adriangb +- login: frankie567 + count: 41 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 - login: includeamin - count: 39 + count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: odiseo0 + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 @@ -81,50 +97,34 @@ experts: count: 36 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns -- login: frankie567 - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 +- login: krishnardt + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 + url: https://github.com/krishnardt - login: prostomarkeloff count: 33 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 url: https://github.com/prostomarkeloff -- login: acidjunk - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk -- login: krishnardt - count: 31 - avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 - url: https://github.com/krishnardt +- login: yinziyan1206 + count: 33 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: panla - count: 30 + count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla - login: wshayes count: 29 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: ghandic - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic - login: dbanty - count: 25 + count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty -- login: yinziyan1206 - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 - login: SirTelemak count: 24 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: odiseo0 - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 - login: acnebs count: 22 avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 @@ -137,18 +137,22 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: rafsaf + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 + url: https://github.com/rafsaf +- login: Hultner + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 + url: https://github.com/Hultner - login: retnikt count: 19 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: rafsaf - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 - url: https://github.com/rafsaf -- login: Hultner +- login: zoliknemet count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 - url: https://github.com/Hultner + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet - login: jorgerpo count: 17 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 @@ -169,18 +173,22 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jonatasoli + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli - login: hellocoldworld count: 15 avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 url: https://github.com/hellocoldworld -- login: jonatasoli - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli - login: mbroton count: 15 avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 url: https://github.com/mbroton +- login: simondale00 + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4 + url: https://github.com/simondale00 - login: haizaar count: 13 avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 @@ -189,39 +197,43 @@ experts: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: valentin994 - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/42819267?u=fdeeaa9242a59b243f8603496b00994f6951d5a2&v=4 - url: https://github.com/valentin994 -- login: David-Lor - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/17401854?u=474680c02b94cba810cb9032fb7eb787d9cc9d22&v=4 - url: https://github.com/David-Lor last_month_active: -- login: iudeen - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: jgould22 - count: 13 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: NewSouthMjos - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/77476573?v=4 - url: https://github.com/NewSouthMjos -- login: davismartens - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/69799848?u=dbdfd256dd4e0a12d93efb3463225f3e39d8df6f&v=4 - url: https://github.com/davismartens +- login: yinziyan1206 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: Kludex - count: 3 + count: 6 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: Lenclove +- login: moadennagi + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/16942283?v=4 + url: https://github.com/moadennagi +- login: iudeen + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen +- login: anthonycorletti + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 + url: https://github.com/anthonycorletti +- login: ThirVondukr + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=56010d6430583b2096a96f0946501156cdb79c75&v=4 + url: https://github.com/ThirVondukr +- login: ebottos94 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 +- login: odiseo0 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32355298?u=d0065e01650c63c2b2413f42d983634b2ea85481&v=4 - url: https://github.com/Lenclove + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 top_contributors: - login: waynerv count: 25 @@ -240,7 +252,7 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: Kludex - count: 15 + count: 16 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: euri10 @@ -287,6 +299,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: NinaHwang + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 + url: https://github.com/NinaHwang - login: batlopes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 @@ -319,13 +335,13 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: NinaHwang +- login: Xewus count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 + url: https://github.com/Xewus top_reviewers: - login: Kludex - count: 109 + count: 110 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan @@ -333,7 +349,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 66 + count: 70 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -353,7 +369,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: iudeen - count: 42 + count: 44 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: cikay @@ -372,14 +388,14 @@ top_reviewers: count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: cassiobotaro + count: 28 + avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 + url: https://github.com/cassiobotaro - login: komtaki count: 27 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: cassiobotaro - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 - url: https://github.com/cassiobotaro - login: lsglucas count: 26 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 @@ -392,14 +408,22 @@ top_reviewers: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: LorhanSohaky + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky - login: 0417taehyun count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - login: rjNemo - count: 17 + count: 18 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo +- login: odiseo0 + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 - login: Smlep count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -428,10 +452,6 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: odiseo0 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -440,14 +460,18 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu -- login: LorhanSohaky - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky +- login: Ryandaydev + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 + url: https://github.com/Ryandaydev - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 url: https://github.com/solomein-sv +- login: Xewus + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 + url: https://github.com/Xewus - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -466,7 +490,7 @@ top_reviewers: url: https://github.com/ComicShrimp - login: peidrao count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=39edf7052371484cb488277638c23e1f6b584f4b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4 url: https://github.com/peidrao - login: izaguerreiro count: 9 @@ -496,6 +520,10 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv +- login: axel584 + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?v=4 + url: https://github.com/axel584 - login: blt232018 count: 8 avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 @@ -508,15 +536,3 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 url: https://github.com/NinaHwang -- login: Xewus - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 - url: https://github.com/Xewus -- login: Serrones - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 - url: https://github.com/Serrones -- login: jovicon - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/21287303?u=b049eac3e51a4c0473c2efe66b4d28a7d8f2b572&v=4 - url: https://github.com/jovicon From c59539913dbed20b980f512fcf7d29ce55fa0304 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 3 Feb 2023 17:56:25 +0000 Subject: [PATCH 0762/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c8bf5a1f9..4c53679f7 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 [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). * 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). From e1129af819f3e78eaba17a1728776d2470629618 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:04:38 +0300 Subject: [PATCH 0763/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/contributing.md`=20(#5870)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ru/docs/contributing.md | 469 +++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 470 insertions(+) create mode 100644 docs/ru/docs/contributing.md diff --git a/docs/ru/docs/contributing.md b/docs/ru/docs/contributing.md new file mode 100644 index 000000000..cb460beb0 --- /dev/null +++ b/docs/ru/docs/contributing.md @@ -0,0 +1,469 @@ +# Участие в разработке фреймворка + +Возможно, для начала Вам стоит ознакомиться с основными способами [помочь FastAPI или получить помощь](help-fastapi.md){.internal-link target=_blank}. + +## Разработка + +Если Вы уже склонировали репозиторий и знаете, что Вам нужно более глубокое погружение в код фреймворка, то здесь представлены некоторые инструкции по настройке виртуального окружения. + +### Виртуальное окружение с помощью `venv` + +Находясь в нужной директории, Вы можете создать виртуальное окружение при помощи Python модуля `venv`. + +
+ +```console +$ python -m venv env +``` + +
+ +Эта команда создаст директорию `./env/` с бинарными (двоичными) файлами Python, а затем Вы сможете скачивать и устанавливать необходимые библиотеки в изолированное виртуальное окружение. + +### Активация виртуального окружения + +Активируйте виртуально окружение командой: + +=== "Linux, macOS" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "Windows Bash" + + Если Вы пользуетесь Bash для Windows (например:
Git Bash): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +Проверьте, что всё сработало: + +=== "Linux, macOS, Windows Bash" + +
+ + ```console + $ which pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +Ели в терминале появится ответ, что бинарник `pip` расположен по пути `.../env/bin/pip`, значит всё в порядке. 🎉 + +Во избежание ошибок в дальнейших шагах, удостоверьтесь, что в Вашем виртуальном окружении установлена последняя версия `pip`: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +!!! tip "Подсказка" + Каждый раз, перед установкой новой библиотеки в виртуальное окружение при помощи `pip`, не забудьте активировать это виртуальное окружение. + + Это гарантирует, что если Вы используете библиотеку, установленную этим пакетом, то Вы используете библиотеку из Вашего локального окружения, а не любую другую, которая может быть установлена глобально. + +### pip + +После активации виртуального окружения, как было указано ранее, введите следующую команду: + +
+ +```console +$ pip install -e ."[dev,doc,test]" + +---> 100% +``` + +
+ +Это установит все необходимые зависимости в локальное окружение для Вашего локального FastAPI. + +#### Использование локального FastAPI + +Если Вы создаёте Python файл, который импортирует и использует FastAPI,а затем запускаете его интерпретатором Python из Вашего локального окружения, то он будет использовать код из локального FastAPI. + +И, так как при вводе вышеупомянутой команды был указан флаг `-e`, если Вы измените код локального FastAPI, то при следующем запуске этого файла, он будет использовать свежую версию локального FastAPI, который Вы только что изменили. + +Таким образом, Вам не нужно "переустанавливать" Вашу локальную версию, чтобы протестировать каждое изменение. + +### Форматировние + +Скачанный репозиторий содержит скрипт, который может отформатировать и подчистить Ваш код: + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +Заодно он упорядочит Ваши импорты. + +Чтобы он сортировал их правильно, необходимо, чтобы FastAPI был установлен локально в Вашей среде, с помощью команды из раздела выше, использующей флаг `-e`. + +## Документация + +Прежде всего, убедитесь, что Вы настроили своё окружение, как описано выше, для установки всех зависимостей. + +Документация использует MkDocs. + +Также существуют дополнительные инструменты/скрипты для работы с переводами в `./scripts/docs.py`. + +!!! tip "Подсказка" + + Нет необходимости заглядывать в `./scripts/docs.py`, просто используйте это в командной строке. + +Вся документация имеет формат Markdown и расположена в директории `./docs/en/`. + +Многие руководства содержат блоки кода. + +В большинстве случаев эти блоки кода представляют собой вполне законченные приложения, которые можно запускать как есть. + +На самом деле, эти блоки кода не написаны внутри Markdown, это Python файлы в директории `./docs_src/`. + +И эти Python файлы включаются/вводятся в документацию при создании сайта. + +### Тестирование документации + + +Фактически, большинство тестов запускаются с примерами исходных файлов в документации. + +Это помогает убедиться, что: + +* Документация находится в актуальном состоянии. +* Примеры из документации могут быть запущены как есть. +* Большинство функций описаны в документации и покрыты тестами. + +Существует скрипт, который во время локальной разработки создаёт сайт и проверяет наличие любых изменений, перезагружая его в реальном времени: + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Он запустит сайт документации по адресу: `http://127.0.0.1:8008`. + + +Таким образом, Вы сможете редактировать файлы с документацией или кодом и наблюдать изменения вживую. + +#### Typer CLI (опционально) + + +Приведенная ранее инструкция показала Вам, как запускать скрипт `./scripts/docs.py` непосредственно через интерпретатор `python` . + +Но также можно использовать Typer CLI, что позволит Вам воспользоваться автозаполнением команд в Вашем терминале. + +Если Вы установили Typer CLI, то для включения функции автозаполнения, введите эту команду: + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### Приложения и документация одновременно + +Если Вы запускаете приложение, например так: + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +По умолчанию Uvicorn будет использовать порт `8000` и не будет конфликтовать с сайтом документации, использующим порт `8008`. + +### Переводы на другие языки + +Помощь с переводами ценится КРАЙНЕ ВЫСОКО! И переводы не могут быть сделаны без помощи сообщества. 🌎 🚀 + +Ниже приведены шаги, как помочь с переводами. + +#### Подсказки и инструкции + +* Проверьте существующие пул-реквесты для Вашего языка. Добавьте отзывы с просьбой внести изменения, если они необходимы, или одобрите их. + +!!! tip "Подсказка" + Вы можете добавлять комментарии с предложениями по изменению в существующие пул-реквесты. + + Ознакомьтесь с документацией о добавлении отзыва к пул-реквесту, чтобы утвердить его или запросить изменения. + +* Проверьте проблемы и вопросы, чтобы узнать, есть ли кто-то, координирующий переводы для Вашего языка. + +* Добавляйте один пул-реквест для каждой отдельной переведённой страницы. Это значительно облегчит другим его просмотр. + +Для языков, которые я не знаю, прежде чем добавить перевод в основную ветку, я подожду пока несколько других участников сообщества проверят его. + +* Вы также можете проверить, есть ли переводы для Вашего языка и добавить к ним отзыв, который поможет мне убедиться в правильности перевода. Тогда я смогу объединить его с основной веткой. + +* Используйте те же самые примеры кода Python. Переводите только текст документации. Вам не нужно ничего менять, чтобы эти примеры работали. + +* Используйте те же самые изображения, имена файлов и ссылки. Вы не должны менять ничего для сохранения работоспособности. + +* Чтобы узнать 2-буквенный код языка, на который Вы хотите сделать перевод, Вы можете воспользоваться таблицей Список кодов языков ISO 639-1. + +#### Существующий язык + +Допустим, Вы хотите перевести страницу на язык, на котором уже есть какие-то переводы, например, на испанский. + +Кодом испанского языка является `es`. А значит директория для переводов на испанский язык: `docs/es/`. + +!!! tip "Подсказка" + Главный ("официальный") язык - английский, директория для него `docs/en/`. + +Вы можете запустить сервер документации на испанском: + +
+ +```console +// Используйте команду "live" и передайте код языка в качестве аргумента командной строки +$ python ./scripts/docs.py live es + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Теперь Вы можете перейти по адресу: http://127.0.0.1:8008 и наблюдать вносимые Вами изменения вживую. + + +Если Вы посмотрите на сайт документации FastAPI, то увидите, что все страницы есть на каждом языке. Но некоторые страницы не переведены и имеют уведомление об отсутствующем переводе. + +Но когда Вы запускаете сайт локально, Вы видите только те страницы, которые уже переведены. + + +Предположим, что Вы хотите добавить перевод страницы [Основные свойства](features.md){.internal-link target=_blank}. + +* Скопируйте файл: + +``` +docs/en/docs/features.md +``` + +* Вставьте его точно в то же место, но в директорию языка, на который Вы хотите сделать перевод, например: + +``` +docs/es/docs/features.md +``` + +!!! tip "Подсказка" + Заметьте, что в пути файла мы изменили только код языка с `en` на `es`. + +* Теперь откройте файл конфигурации MkDocs для английского языка, расположенный тут: + +``` +docs/en/mkdocs.yml +``` + +* Найдите в файле конфигурации место, где расположена строка `docs/features.md`. Похожее на это: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* Откройте файл конфигурации MkDocs для языка, на который Вы переводите, например: + +``` +docs/es/mkdocs.yml +``` + +* Добавьте строку `docs/features.md` точно в то же место, как и в случае для английского, как-то так: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +Убедитесь, что при наличии других записей, новая запись с Вашим переводом находится точно в том же порядке, что и в английской версии. + +Если Вы зайдёте в свой браузер, то увидите, что в документации стал отображаться Ваш новый раздел.🎉 + +Теперь Вы можете переводить эту страницу и смотреть, как она выглядит при сохранении файла. + +#### Новый язык + +Допустим, Вы хотите добавить перевод для языка, на который пока что не переведена ни одна страница. + +Скажем, Вы решили сделать перевод для креольского языка, но его еще нет в документации. + +Перейдите в таблицу кодов языков по ссылке указанной выше, где найдёте, что кодом креольского языка является `ht`. + +Затем запустите скрипт, генерирующий директорию для переводов на новые языки: + +
+ +```console +// Используйте команду new-lang и передайте код языка в качестве аргумента командной строки +$ python ./scripts/docs.py new-lang ht + +Successfully initialized: docs/ht +Updating ht +Updating en +``` + +
+ +После чего Вы можете проверить в своем редакторе кода, что появился новый каталог `docs/ht/`. + +!!! tip "Подсказка" + Создайте первый пул-реквест, который будет содержать только пустую директорию для нового языка, прежде чем добавлять переводы. + + Таким образом, другие участники могут переводить другие страницы, пока Вы работаете над одной. 🚀 + +Начните перевод с главной страницы `docs/ht/index.md`. + +В дальнейшем можно действовать, как указано в предыдущих инструкциях для "существующего языка". + +##### Новый язык не поддерживается + +Если при запуске скрипта `./scripts/docs.py live` Вы получаете сообщение об ошибке, что язык не поддерживается, что-то вроде: + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +Сие означает, что тема не поддерживает этот язык (в данном случае с поддельным 2-буквенным кодом `xx`). + +Но не стоит переживать. Вы можете установить языком темы английский, а затем перевести текст документации. + +Если возникла такая необходимость, отредактируйте `mkdocs.yml` для Вашего нового языка. Это будет выглядеть как-то так: + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +Измените `xx` (код Вашего языка) на `en` и перезапустите сервер. + +#### Предпросмотр результата + +Когда Вы запускаете скрипт `./scripts/docs.py` с командой `live`, то будут показаны файлы и переводы для указанного языка. + +Но когда Вы закончите, то можете посмотреть, как это будет выглядеть по-настоящему. + +Для этого сначала создайте всю документацию: + +
+ +```console +// Используйте команду "build-all", это займёт немного времени +$ python ./scripts/docs.py build-all + +Updating es +Updating en +Building docs for: en +Building docs for: es +Successfully built docs for: es +Copying en index.md to README.md +``` + +
+ +Скрипт сгенерирует `./docs_build/` для каждого языка. Он добавит все файлы с отсутствующими переводами с пометкой о том, что "у этого файла еще нет перевода". Но Вам не нужно ничего делать с этим каталогом. + +Затем он создаст независимые сайты MkDocs для каждого языка, объединит их и сгенерирует конечный результат на `./site/`. + +После чего Вы сможете запустить сервер со всеми языками командой `serve`: + +
+ +```console +// Используйте команду "serve" после того, как отработает команда "build-all" +$ python ./scripts/docs.py serve + +Warning: this is a very simple server. For development, use mkdocs serve instead. +This is here only to preview a site with translations already built. +Make sure you run the build-all command first. +Serving at: http://127.0.0.1:8008 +``` + +
+ +## Тесты + +Также в репозитории есть скрипт, который Вы можете запустить локально, чтобы протестировать весь код и сгенерировать отчеты о покрытии тестами в HTML: + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +Эта команда создаст директорию `./htmlcov/`, в которой будет файл `./htmlcov/index.html`. Открыв его в Вашем браузере, Вы можете в интерактивном режиме изучить, все ли части кода охвачены тестами. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index f35ee968c..45ead2274 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -68,6 +68,7 @@ nav: - deployment/index.md - deployment/versions.md - external-links.md +- contributing.md markdown_extensions: - toc: permalink: true From 23d0efa8940f92ee9c3d6f1dff41ffb370b27e0d Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 13:05:18 +0000 Subject: [PATCH 0764/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4c53679f7..d7fa86692 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). * 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). From 9ad2cb29f948c295cefe5949eb61f1dccfc41241 Mon Sep 17 00:00:00 2001 From: felipebpl <62957465+felipebpl@users.noreply.github.com> Date: Tue, 7 Feb 2023 10:09:00 -0300 Subject: [PATCH 0765/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/encoder.md`=20(#5525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/pt/docs/tutorial/encoder.md | 42 ++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 43 insertions(+) create mode 100644 docs/pt/docs/tutorial/encoder.md diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md new file mode 100644 index 000000000..bb04e9ca2 --- /dev/null +++ b/docs/pt/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# Codificador Compatível com JSON + +Existem alguns casos em que você pode precisar converter um tipo de dados (como um modelo Pydantic) para algo compatível com JSON (como um `dict`, `list`, etc). + +Por exemplo, se você precisar armazená-lo em um banco de dados. + +Para isso, **FastAPI** fornece uma função `jsonable_encoder()`. + +## Usando a função `jsonable_encoder` + +Vamos imaginar que você tenha um banco de dados `fake_db` que recebe apenas dados compatíveis com JSON. + +Por exemplo, ele não recebe objetos `datetime`, pois estes objetos não são compatíveis com JSON. + +Então, um objeto `datetime` teria que ser convertido em um `str` contendo os dados no formato ISO. + +Da mesma forma, este banco de dados não receberia um modelo Pydantic (um objeto com atributos), apenas um `dict`. + +Você pode usar a função `jsonable_encoder` para resolver isso. + +A função recebe um objeto, como um modelo Pydantic e retorna uma versão compatível com JSON: + +=== "Python 3.6 e acima" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "Python 3.10 e acima" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +Neste exemplo, ele converteria o modelo Pydantic em um `dict`, e o `datetime` em um `str`. + +O resultado de chamar a função é algo que pode ser codificado com o padrão do Python `json.dumps()`. + +A função não retorna um grande `str` contendo os dados no formato JSON (como uma string). Mas sim, retorna uma estrutura de dados padrão do Python (por exemplo, um `dict`) com valores e subvalores compatíveis com JSON. + +!!! nota + `jsonable_encoder` é realmente usado pelo **FastAPI** internamente para converter dados. Mas também é útil em muitos outros cenários. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 0858de062..8161cf689 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -77,6 +77,7 @@ nav: - tutorial/request-forms.md - tutorial/request-forms-and-files.md - tutorial/handling-errors.md + - tutorial/encoder.md - Segurança: - tutorial/security/index.md - tutorial/background-tasks.md From 8115282ed36695369d370fbde6a8375679f39899 Mon Sep 17 00:00:00 2001 From: Bruno Artur Torres Lopes Pereira <33462923+batlopes@users.noreply.github.com> Date: Tue, 7 Feb 2023 10:09:32 -0300 Subject: [PATCH 0766/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/static-files.md`=20(#58?= =?UTF-8?q?58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/pt/docs/tutorial/static-files.md | 39 +++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 40 insertions(+) create mode 100644 docs/pt/docs/tutorial/static-files.md diff --git a/docs/pt/docs/tutorial/static-files.md b/docs/pt/docs/tutorial/static-files.md new file mode 100644 index 000000000..009158fc6 --- /dev/null +++ b/docs/pt/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# Arquivos Estáticos + +Você pode servir arquivos estáticos automaticamente de um diretório usando `StaticFiles`. + +## Use `StaticFiles` + +* Importe `StaticFiles`. +* "Monte" uma instância de `StaticFiles()` em um caminho específico. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "Detalhes técnicos" + Você também pode usar `from starlette.staticfiles import StaticFiles`. + + O **FastAPI** fornece o mesmo que `starlette.staticfiles` como `fastapi.staticfiles` apenas como uma conveniência para você, o desenvolvedor. Mas na verdade vem diretamente da Starlette. + +### O que é "Montagem" + +"Montagem" significa adicionar um aplicativo completamente "independente" em uma rota específica, que então cuida de todas as subrotas. + +Isso é diferente de usar um `APIRouter`, pois um aplicativo montado é completamente independente. A OpenAPI e a documentação do seu aplicativo principal não incluirão nada do aplicativo montado, etc. + +Você pode ler mais sobre isso no **Guia Avançado do Usuário**. + +## Detalhes + +O primeiro `"/static"` refere-se à subrota em que este "subaplicativo" será "montado". Portanto, qualquer caminho que comece com `"/static"` será tratado por ele. + +O `directory="static"` refere-se ao nome do diretório que contém seus arquivos estáticos. + +O `name="static"` dá a ela um nome que pode ser usado internamente pelo FastAPI. + +Todos esses parâmetros podem ser diferentes de "`static`", ajuste-os de acordo com as necessidades e detalhes específicos de sua própria aplicação. + +## Mais informações + +Para mais detalhes e opções, verifique Starlette's docs about Static Files. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 8161cf689..c598c00e7 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -81,6 +81,7 @@ nav: - Segurança: - tutorial/security/index.md - tutorial/background-tasks.md + - tutorial/static-files.md - Guia de Usuário Avançado: - advanced/index.md - Implantação: From 05342cc26460c88906977f92b6658dec19841430 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 13:09:45 +0000 Subject: [PATCH 0767/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d7fa86692..370cf49fb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). * 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). From a4f3bc5a693a0e70f2ea8abcc715152e6a37f86c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 13:10:13 +0000 Subject: [PATCH 0768/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 370cf49fb..cc708b3f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). * 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). From 94fa15188125acd1b6f0c3db6aa64bee2fab591b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Feb 2023 14:28:10 +0100 Subject: [PATCH 0769/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/help-fastapi.md`=20(#5970)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Xewus Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/help-fastapi.md | 257 +++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 docs/ru/docs/help-fastapi.md diff --git a/docs/ru/docs/help-fastapi.md b/docs/ru/docs/help-fastapi.md new file mode 100644 index 000000000..a69e37bd8 --- /dev/null +++ b/docs/ru/docs/help-fastapi.md @@ -0,0 +1,257 @@ +# Помочь FastAPI - Получить помощь + +Нравится ли Вам **FastAPI**? + +Хотели бы Вы помочь FastAPI, его пользователям и автору? + +Может быть у Вас возникли трудности с **FastAPI** и Вам нужна помощь? + +Есть несколько очень простых способов оказания помощи (иногда достаточно всего лишь одного или двух кликов). + +И также есть несколько способов получить помощь. + +## Подписаться на новостную рассылку + +Вы можете подписаться на редкую [новостную рассылку **FastAPI и его друзья**](/newsletter/){.internal-link target=_blank} и быть в курсе о: + +* Новостях о FastAPI и его друзьях 🚀 +* Руководствах 📝 +* Возможностях ✨ +* Исправлениях 🚨 +* Подсказках и хитростях ✅ + +## Подписаться на FastAPI в Twitter + +Подписаться на @fastapi в **Twitter** для получения наисвежайших новостей о **FastAPI**. 🐦 + +## Добавить **FastAPI** звезду на GitHub + +Вы можете добавить FastAPI "звезду" на GitHub (кликнуть на кнопку звезды в верхнем правом углу экрана): https://github.com/tiangolo/fastapi. ⭐️ + +Чем больше звёзд, тем легче другим пользователям найти нас и увидеть, что проект уже стал полезным для многих. + +## Отслеживать свежие выпуски в репозитории на GitHub + +Вы можете "отслеживать" FastAPI на GitHub (кликните по кнопке "watch" наверху справа): https://github.com/tiangolo/fastapi. 👀 + +Там же Вы можете указать в настройках - "Releases only". + +С такой настройкой Вы будете получать уведомления на вашу электронную почту каждый раз, когда появится новый релиз (новая версия) **FastAPI** с исправлениями ошибок и новыми возможностями. + +## Связаться с автором + +Можно связаться со мной (Себястьян Рамирез / `tiangolo`), автором FastAPI. + +Вы можете: + +* Подписаться на меня на **GitHub**. + * Посмотреть другие мои проекты с открытым кодом, которые могут быть полезны Вам. + * Подписавшись на меня Вы сможете получать уведомления, что я создал новый проект с открытым кодом,. +* Подписаться на меня в **Twitter** или в Mastodon. + * Поделиться со мной, как Вы используете FastAPI (я обожаю читать про это). + * Получать уведомления, когда я делаю объявления и представляю новые инструменты. + * Вы также можете подписаться на @fastapi в Twitter (это отдельный аккаунт). +* Подписаться на меня в **Linkedin**. + * Получать уведомления, когда я делаю объявления и представляю новые инструменты (правда чаще всего я использую Twitter 🤷‍♂). +* Читать, что я пишу (или подписаться на меня) в **Dev.to** или в **Medium**. + * Читать другие идеи, статьи и читать об инструментах созданных мной. + * Подпишитесь на меня, чтобы прочитать, когда я опубликую что-нибудь новое. + +## Оставить сообщение в Twitter о **FastAPI** + +Оставьте сообщение в Twitter о **FastAPI** и позвольте мне и другим узнать - почему он Вам нравится. 🎉 + +Я люблю узнавать о том, как **FastAPI** используется, что Вам понравилось в нём, в каких проектах/компаниях Вы используете его и т.п. + +## Оставить голос за FastAPI + +* Голосуйте за **FastAPI** в Slant. +* Голосуйте за **FastAPI** в AlternativeTo. +* Расскажите, как Вы используете **FastAPI** на StackShare. + +## Помочь другим с их проблемами на GitHub + +Вы можете посмотреть, какие проблемы испытывают другие люди и попытаться помочь им. Чаще всего это вопросы, на которые, весьма вероятно, Вы уже знаете ответ. 🤓 + +Если Вы будете много помогать людям с решением их проблем, Вы можете стать официальным [Экспертом FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 + +Только помните, самое важное при этом - доброта. Столкнувшись с проблемой, люди расстраиваются и часто задают вопросы не лучшим образом, но постарайтесь быть максимально доброжелательным. 🤗 + +Идея сообщества **FastAPI** в том, чтобы быть добродушным и гостеприимными. Не допускайте издевательств или неуважительного поведения по отношению к другим. Мы должны заботиться друг о друге. + +--- + +Как помочь другим с их проблемами: + +### Понять вопрос + +* Удостоверьтесь, что поняли **цель** и обстоятельства случая вопрошающего. + +* Затем проверьте, что вопрос (в подавляющем большинстве - это вопросы) Вам **ясен**. + +* Во многих случаях вопрос касается решения, которое пользователь придумал сам, но может быть и решение **получше**. Если Вы поймёте проблему и обстоятельства случая, то сможете предложить **альтернативное решение**. + +* Ежели вопрос Вам непонятен, запросите больше **деталей**. + +### Воспроизвести проблему + +В большинстве случаев есть что-то связанное с **исходным кодом** вопрошающего. + +И во многих случаях будет предоставлен только фрагмент этого кода, которого недостаточно для **воспроизведения проблемы**. + +* Попросите предоставить минимальный воспроизводимый пример, который можно **скопировать** и запустить локально дабы увидеть такую же ошибку, или поведение, или лучше понять обстоятельства случая. + +* Если на Вас нахлынуло великодушие, то можете попытаться **создать похожий пример** самостоятельно, основываясь только на описании проблемы. Но имейте в виду, что это может занять много времени и, возможно, стоит сначала позадавать вопросы для прояснения проблемы. + +### Предложить решение + +* После того как Вы поняли вопрос, Вы можете дать **ответ**. + +* Следует понять **основную проблему и обстоятельства случая**, потому что может быть решение лучше, чем то, которое пытались реализовать. + +### Попросить закрыть проблему + +Если Вам ответили, высоки шансы, что Вам удалось решить проблему, поздравляю, **Вы - герой**! 🦸 + +* В таком случае, если вопрос решён, попросите **закрыть проблему**. + +## Отслеживать репозиторий на GitHub + +Вы можете "отслеживать" FastAPI на GitHub (кликните по кнопке "watch" наверху справа): https://github.com/tiangolo/fastapi. 👀 + +Если Вы выберете "Watching" вместо "Releases only", то будете получать уведомления когда кто-либо попросит о помощи с решением его проблемы. + +Тогда Вы можете попробовать решить эту проблему. + +## Запросить помощь с решением проблемы + +Вы можете создать новый запрос с просьбой о помощи в репозитории на GitHub, например: + +* Задать **вопрос** или попросить помощи в решении **проблемы**. +* Предложить новое **улучшение**. + +**Заметка**: Если Вы создаёте подобные запросы, то я попрошу Вас также оказывать аналогичную помощь другим. 😉 + +## Проверять пул-реквесты + +Вы можете помочь мне проверять пул-реквесты других участников. + +И повторюсь, постарайтесь быть доброжелательным. 🤗 + +--- + +О том, что нужно иметь в виду при проверке пул-реквестов: + +### Понять проблему + +* Во-первых, убедитесь, что **поняли проблему**, которую пул-реквест пытается решить. Для этого может потребоваться продолжительное обсуждение. + +* Также есть вероятность, что пул-реквест не актуален, так как проблему можно решить **другим путём**. В таком случае Вы можете указать на этот факт. + +### Не переживайте о стиле + +* Не стоит слишком беспокоиться о таких вещах, как стиль сообщений в коммитах или количество коммитов. При слиянии пул-реквеста с основной веткой, я буду сжимать и настраивать всё вручную. + +* Также не беспокойтесь о правилах стиля, для проверки сего есть автоматизированные инструменты. + +И если всё же потребуется какой-то другой стиль, я попрошу Вас об этом напрямую или добавлю сам коммиты с необходимыми изменениями. + +### Проверить код + +* Проверьте и прочитайте код, посмотрите, какой он имеет смысл, **запустите его локально** и посмотрите, действительно ли он решает поставленную задачу. + +* Затем, используя **комментарий**, сообщите, что Вы сделали проверку, тогда я буду знать, что Вы действительно проверили код. + +!!! Информация + К сожалению, я не могу так просто доверять пул-реквестам, у которых уже есть несколько одобрений. + + Бывали случаи, что пул-реквесты имели 3, 5 или больше одобрений, вероятно из-за привлекательного описания, но когда я проверял эти пул-реквесты, они оказывались сломаны, содержали ошибки или вовсе не решали проблему, которую, как они утверждали, должны были решить. 😅 + + Потому это действительно важно - проверять и запускать код, и комментарием уведомлять меня, что Вы проделали эти действия. 🤓 + +* Если Вы считаете, что пул-реквест можно упростить, то можете попросить об этом, но не нужно быть слишком придирчивым, может быть много субъективных точек зрения (и у меня тоже будет своя 🙈), поэтому будет лучше, если Вы сосредоточитесь на фундаментальных вещах. + +### Тестировать + +* Помогите мне проверить, что у пул-реквеста есть **тесты**. + +* Проверьте, что тесты **падали** до пул-реквеста. 🚨 + +* Затем проверьте, что тесты **не валятся** после пул-реквеста. ✅ + +* Многие пул-реквесты не имеют тестов, Вы можете **напомнить** о необходимости добавления тестов или даже **предложить** какие-либо свои тесты. Это одна из тех вещей, которые отнимают много времени и Вы можете помочь с этим. + +* Затем добавьте комментарий, что Вы испробовали в ходе проверки. Таким образом я буду знать, как Вы произвели проверку. 🤓 + +## Создать пул-реквест + +Вы можете [сделать вклад](contributing.md){.internal-link target=_blank} в код фреймворка используя пул-реквесты, например: + +* Исправить опечатку, которую Вы нашли в документации. +* Поделиться статьёй, видео или подкастом о FastAPI, которые Вы создали или нашли изменив этот файл. + * Убедитесь, что Вы добавили свою ссылку в начало соответствующего раздела. +* Помочь с [переводом документации](contributing.md#translations){.internal-link target=_blank} на Ваш язык. + * Вы также можете проверять переводы сделанные другими. +* Предложить новые разделы документации. +* Исправить существующуе проблемы/баги. + * Убедитесь, что добавили тесты. +* Добавить новую возможность. + * Убедитесь, что добавили тесты. + * Убедитесь, что добавили документацию, если она необходима. + +## Помочь поддерживать FastAPI + +Помогите мне поддерживать **FastAPI**! 🤓 + +Предстоит ещё много работы и, по большей части, **ВЫ** можете её сделать. + +Основные задачи, которые Вы можете выполнить прямо сейчас: + +* [Помочь другим с их проблемами на GitHub](#help-others-with-issues-in-github){.internal-link target=_blank} (смотрите вышестоящую секцию). +* [Проверить пул-реквесты](#review-pull-requests){.internal-link target=_blank} (смотрите вышестоящую секцию). + +Эти две задачи **отнимают больше всего времени**. Это основная работа по поддержке FastAPI. + +Если Вы можете помочь мне с этим, **Вы помогаете поддерживать FastAPI** и следить за тем, чтобы он продолжал **развиваться быстрее и лучше**. 🚀 + +## Подключиться к чату + +Подключайтесь к 👥 чату в Discord 👥 и общайтесь с другими участниками сообщества FastAPI. + +!!! Подсказка + Вопросы по проблемам с фреймворком лучше задавать в GitHub issues, так больше шансов, что Вы получите помощь от [Экспертов FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. + + Используйте этот чат только для бесед на отвлечённые темы. + +Существует также чат в Gitter, но поскольку в нем нет каналов и расширенных функций, общение в нём сложнее, потому рекомендуемой системой является Discord. + +### Не использовать чаты для вопросов + +Имейте в виду, что чаты позволяют больше "свободного общения", потому там легко задавать вопросы, которые слишком общие и на которые труднее ответить, так что Вы можете не получить нужные Вам ответы. + +В разделе "проблемы" на GitHub, есть шаблон, который поможет Вам написать вопрос правильно, чтобы Вам было легче получить хороший ответ или даже решить проблему самостоятельно, прежде чем Вы зададите вопрос. В GitHub я могу быть уверен, что всегда отвечаю на всё, даже если это займет какое-то время. И я не могу сделать то же самое в чатах. 😅 + +Кроме того, общение в чатах не так легкодоступно для поиска, как в GitHub, потому вопросы и ответы могут потеряться среди другого общения. И только проблемы решаемые на GitHub учитываются в получении лычки [Эксперт FastAPI](fastapi-people.md#experts){.internal-link target=_blank}, так что весьма вероятно, что Вы получите больше внимания на GitHub. + +С другой стороны, в чатах тысячи пользователей, а значит есть большие шансы в любое время найти там кого-то, с кем можно поговорить. 😄 + +## Спонсировать автора + +Вы также можете оказать мне финансовую поддержку посредством спонсорства через GitHub. + +Там можно просто купить мне кофе ☕️ в знак благодарности. 😄 + +А ещё Вы можете стать Серебряным или Золотым спонсором для FastAPI. 🏅🎉 + +## Спонсировать инструменты, на которых зиждется мощь FastAPI + +Как Вы могли заметить в документации, FastAPI опирается на плечи титанов: Starlette и Pydantic. + +Им тоже можно оказать спонсорскую поддержку: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +Благодарствую! 🚀 From 6e3c707c603aff90651611de57961b9f0ac19a7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 13:28:45 +0000 Subject: [PATCH 0770/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cc708b3f5..9281fb0ba 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). From 8b62319d6c01e944e76d52dcae06fe094cfc128c Mon Sep 17 00:00:00 2001 From: Alexander Sviridov <78508673+simatheone@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:33:16 +0300 Subject: [PATCH 0771/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/body-fields.md`=20(#5898)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/body-fields.md | 69 ++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 70 insertions(+) create mode 100644 docs/ru/docs/tutorial/body-fields.md diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md new file mode 100644 index 000000000..e8507c171 --- /dev/null +++ b/docs/ru/docs/tutorial/body-fields.md @@ -0,0 +1,69 @@ +# Body - Поля + +Таким же способом, как вы объявляете дополнительную валидацию и метаданные в параметрах *функции обработки пути* с помощью функций `Query`, `Path` и `Body`, вы можете объявлять валидацию и метаданные внутри Pydantic моделей, используя функцию `Field` из Pydantic. + +## Импорт `Field` + +Сначала вы должны импортировать его: + +=== "Python 3.6 и выше" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +!!! warning "Внимание" + Обратите внимание, что функция `Field` импортируется непосредственно из `pydantic`, а не из `fastapi`, как все остальные функции (`Query`, `Path`, `Body` и т.д.). + +## Объявление атрибутов модели + +Вы можете использовать функцию `Field` с атрибутами модели: + +=== "Python 3.6 и выше" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +Функция `Field` работает так же, как `Query`, `Path` и `Body`, у ее такие же параметры и т.д. + +!!! note "Технические детали" + На самом деле, `Query`, `Path` и другие функции, которые вы увидите в дальнейшем, создают объекты подклассов общего класса `Param`, который сам по себе является подклассом `FieldInfo` из Pydantic. + + И `Field` (из Pydantic), и `Body`, оба возвращают объекты подкласса `FieldInfo`. + + У класса `Body` есть и другие подклассы, с которыми вы ознакомитесь позже. + + Помните, что когда вы импортируете `Query`, `Path` и другое из `fastapi`, это фактически функции, которые возвращают специальные классы. + +!!! tip "Подсказка" + Обратите внимание, что каждый атрибут модели с типом, значением по умолчанию и `Field` имеет ту же структуру, что и параметр *функции обработки пути* с `Field` вместо `Path`, `Query` и `Body`. + +## Добавление дополнительной информации + +Вы можете объявлять дополнительную информацию в `Field`, `Query`, `Body` и т.п. Она будет включена в сгенерированную JSON схему. + +Вы узнаете больше о добавлении дополнительной информации позже в документации, когда будете изучать, как задавать примеры принимаемых данных. + + +!!! warning "Внимание" + Дополнительные ключи, переданные в функцию `Field`, также будут присутствовать в сгенерированной OpenAPI схеме вашего приложения. + Поскольку эти ключи не являются обязательной частью спецификации OpenAPI, некоторые инструменты OpenAPI, например, [валидатор OpenAPI](https://validator.swagger.io/), могут не работать с вашей сгенерированной схемой. + +## Резюме + +Вы можете использовать функцию `Field` из Pydantic, чтобы задавать дополнительную валидацию и метаданные для атрибутов модели. + +Вы также можете использовать дополнительные ключевые аргументы, чтобы добавить метаданные JSON схемы. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 45ead2274..837209fd4 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -62,6 +62,7 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/body-fields.md - tutorial/background-tasks.md - async.md - Развёртывание: From 9cb25864992daade7a90cc6944e60f41e02b46f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 13:33:51 +0000 Subject: [PATCH 0772/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9281fb0ba..8d78bd7b9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). * 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). From 3e4840f21b96df45f1f0d078500f658dd61bbe28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Feb 2023 17:46:03 +0100 Subject: [PATCH 0773/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Ubuntu?= =?UTF-8?q?=20version=20for=20docs=20workflow=20(#5971)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index b9bd521b3..68a180e38 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -7,7 +7,7 @@ on: types: [opened, synchronize] jobs: build-docs: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Dump GitHub context env: @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" - uses: actions/cache@v3 id: cache with: From c9d3656a6ec3e44b7bbc8117131b10185ca31f16 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 16:46:40 +0000 Subject: [PATCH 0774/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8d78bd7b9..67f53f711 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). * 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). From 88dc4ce3d7427431e25d6729ad03407066a6be07 Mon Sep 17 00:00:00 2001 From: Leon Date: Wed, 8 Feb 2023 00:50:02 +0800 Subject: [PATCH 0775/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20article=20"Torto?= =?UTF-8?q?ise=20ORM=20/=20FastAPI=20=E6=95=B4=E5=90=88=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E7=AD=86=E8=A8=98"=20to=20External=20Links=20(#5496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/data/external_links.yml | 5 +++++ docs/en/docs/external-links.md | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index c1b1f1fa4..af5810778 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -308,6 +308,11 @@ articles: author_link: https://fullstackstation.com/author/figonking/ link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' + taiwanese: + - author: Leon + author_link: http://editor.leonh.space/ + link: https://editor.leonh.space/2022/tortoise/ + title: 'Tortoise ORM / FastAPI 整合快速筆記' podcasts: english: - author: Podcast.`__init__` diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 55db55599..0c91470bc 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -56,6 +56,15 @@ Here's an incomplete list of some of them. {% endfor %} {% endif %} +### Taiwanese + +{% if external_links %} +{% for article in external_links.articles.taiwanese %} + +* {{ article.title }} by {{ article.author }}. +{% endfor %} +{% endif %} + ## Podcasts {% if external_links %} From 58757f63af438e65d5540fc844786b7b8973c8b2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Feb 2023 16:51:06 +0000 Subject: [PATCH 0776/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 67f53f711..69efb0dd3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add article "Tortoise ORM / FastAPI 整合快速筆記" to External Links. PR [#5496](https://github.com/tiangolo/fastapi/pull/5496) by [@Leon0824](https://github.com/Leon0824). * ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). * 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). From 9293795e99afbda07d2744f1eb7d23d2f0ea0154 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 8 Feb 2023 11:23:07 +0100 Subject: [PATCH 0777/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20Starlette?= =?UTF-8?q?=20from=200.22.0=20to=200.23.0=20(#5739)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs_src/app_testing/tutorial002.py | 2 +- fastapi/applications.py | 33 +++++++++++++++++++++++++ fastapi/routing.py | 37 +++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_route_scope.py | 4 ++-- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/docs_src/app_testing/tutorial002.py b/docs_src/app_testing/tutorial002.py index b4a9c0586..71c898b3c 100644 --- a/docs_src/app_testing/tutorial002.py +++ b/docs_src/app_testing/tutorial002.py @@ -10,7 +10,7 @@ async def read_main(): return {"msg": "Hello World"} -@app.websocket_route("/ws") +@app.websocket("/ws") async def websocket(websocket: WebSocket): await websocket.accept() await websocket.send_json({"msg": "Hello WebSocket"}) diff --git a/fastapi/applications.py b/fastapi/applications.py index 36dc2605d..160d66301 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -35,6 +35,7 @@ from starlette.applications import Starlette from starlette.datastructures import State from starlette.exceptions import HTTPException from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.errors import ServerErrorMiddleware from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request @@ -870,3 +871,35 @@ class FastAPI(Starlette): openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, ) + + def websocket_route( + self, path: str, name: Union[str, None] = None + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.router.add_websocket_route(path, func, name=name) + return func + + return decorator + + def on_event( + self, event_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + return self.router.on_event(event_type) + + def middleware( + self, middleware_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_middleware(BaseHTTPMiddleware, dispatch=func) + return func + + return decorator + + def exception_handler( + self, exc_class_or_status_code: Union[int, Type[Exception]] + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_exception_handler(exc_class_or_status_code, func) + return func + + return decorator diff --git a/fastapi/routing.py b/fastapi/routing.py index f131fa903..7ab6275b6 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -522,6 +522,25 @@ class APIRouter(routing.Router): self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + def route( + self, + path: str, + methods: Optional[List[str]] = None, + name: Optional[str] = None, + include_in_schema: bool = True, + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_route( + path, + func, + methods=methods, + name=name, + include_in_schema=include_in_schema, + ) + return func + + return decorator + def add_api_route( self, path: str, @@ -686,6 +705,15 @@ class APIRouter(routing.Router): return decorator + def websocket_route( + self, path: str, name: Union[str, None] = None + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_websocket_route(path, func, name=name) + return func + + return decorator + def include_router( self, router: "APIRouter", @@ -1247,3 +1275,12 @@ class APIRouter(routing.Router): openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, ) + + def on_event( + self, event_type: str + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + def decorator(func: DecoratedCallable) -> DecoratedCallable: + self.add_event_handler(event_type, func) + return func + + return decorator diff --git a/pyproject.toml b/pyproject.toml index 7fb8078f9..4498f9432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette==0.22.0", + "starlette>=0.22.0,<=0.23.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] diff --git a/tests/test_route_scope.py b/tests/test_route_scope.py index a188e9a5f..2021c828f 100644 --- a/tests/test_route_scope.py +++ b/tests/test_route_scope.py @@ -46,5 +46,5 @@ def test_websocket(): def test_websocket_invalid_path_doesnt_match(): with pytest.raises(WebSocketDisconnect): - with client.websocket_connect("/itemsx/portal-gun") as websocket: - websocket.receive_json() + with client.websocket_connect("/itemsx/portal-gun"): + pass From b313f863389f7696bbc207a5499f4cbf40987073 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 8 Feb 2023 10:23:46 +0000 Subject: [PATCH 0778/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 69efb0dd3..6c39e4487 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Bump Starlette from 0.22.0 to 0.23.0. PR [#5739](https://github.com/tiangolo/fastapi/pull/5739) by [@Kludex](https://github.com/Kludex). * 📝 Add article "Tortoise ORM / FastAPI 整合快速筆記" to External Links. PR [#5496](https://github.com/tiangolo/fastapi/pull/5496) by [@Leon0824](https://github.com/Leon0824). * ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). From e4c8df062bad2e0e3a978d180f243ffe3ad13e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Feb 2023 11:28:55 +0100 Subject: [PATCH 0779/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6c39e4487..07fcf48da 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,18 +2,29 @@ ## Latest Changes -* ⬆️ Bump Starlette from 0.22.0 to 0.23.0. PR [#5739](https://github.com/tiangolo/fastapi/pull/5739) by [@Kludex](https://github.com/Kludex). +### Upgrades + +* ⬆️ Bump Starlette from 0.22.0 to 0.23.0. Initial PR [#5739](https://github.com/tiangolo/fastapi/pull/5739) by [@Kludex](https://github.com/Kludex). + +### Docs + * 📝 Add article "Tortoise ORM / FastAPI 整合快速筆記" to External Links. PR [#5496](https://github.com/tiangolo/fastapi/pull/5496) by [@Leon0824](https://github.com/Leon0824). -* ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). +* 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). +* 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). +* 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#5898](https://github.com/tiangolo/fastapi/pull/5898) by [@simatheone](https://github.com/simatheone). * 🌐 Add Russian translation for `docs/ru/docs/help-fastapi.md`. PR [#5970](https://github.com/tiangolo/fastapi/pull/5970) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/static-files.md`. PR [#5858](https://github.com/tiangolo/fastapi/pull/5858) by [@batlopes](https://github.com/batlopes). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/encoder.md`. PR [#5525](https://github.com/tiangolo/fastapi/pull/5525) by [@felipebpl](https://github.com/felipebpl). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#5870](https://github.com/tiangolo/fastapi/pull/5870) by [@Xewus](https://github.com/Xewus). -* 👥 Update FastAPI People. PR [#5954](https://github.com/tiangolo/fastapi/pull/5954) by [@github-actions[bot]](https://github.com/apps/github-actions). -* 📝 Micro-tweak help docs. PR [#5960](https://github.com/tiangolo/fastapi/pull/5960) by [@tiangolo](https://github.com/tiangolo). -* 🔧 Update new issue chooser to direct to GitHub Discussions. PR [#5948](https://github.com/tiangolo/fastapi/pull/5948) by [@tiangolo](https://github.com/tiangolo). -* 📝 Recommend GitHub Discussions for questions. PR [#5944](https://github.com/tiangolo/fastapi/pull/5944) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* ⬆️ Upgrade Ubuntu version for docs workflow. PR [#5971](https://github.com/tiangolo/fastapi/pull/5971) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges. PR [#5943](https://github.com/tiangolo/fastapi/pull/5943) by [@tiangolo](https://github.com/tiangolo). * ✨ Compute FastAPI Experts including GitHub Discussions. PR [#5941](https://github.com/tiangolo/fastapi/pull/5941) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade isort and update pre-commit. PR [#5940](https://github.com/tiangolo/fastapi/pull/5940) by [@tiangolo](https://github.com/tiangolo). From 148bcf5ce4c0fb58e8d9431291d1d6e004a4afe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 8 Feb 2023 11:30:01 +0100 Subject: [PATCH 0780/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?90.0?= 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 07fcf48da..466022f3b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.90.0 + ### Upgrades * ⬆️ Bump Starlette from 0.22.0 to 0.23.0. Initial PR [#5739](https://github.com/tiangolo/fastapi/pull/5739) by [@Kludex](https://github.com/Kludex). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 07ed78ffa..656bb879a 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.89.1" +__version__ = "0.90.0" from starlette import status as status From 16599b73560294b1a45aac36960c9aa7ff5bc695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 9 Feb 2023 19:46:38 +0100 Subject: [PATCH 0781/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20range=20to=20allow=200.23.1=20(#5980)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4498f9432..7b6138d09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.22.0,<=0.23.0", + "starlette>=0.22.0,<0.24.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From 70688eb7b28b9c7a27f5f2ea319631be997381a6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 18:47:16 +0000 Subject: [PATCH 0782/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 466022f3b..79c67de55 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). ## 0.90.0 From e37d504e277b1c119d67ffd378f2b9bdec012cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 9 Feb 2023 19:57:49 +0100 Subject: [PATCH 0783/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20opinion=20from?= =?UTF-8?q?=20Cisco=20(#5981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ docs/en/docs/index.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index ac3e655bb..39030ef52 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ The key features are: --- +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + ## **Typer**, the FastAPI of CLIs diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index deb8ab5d5..9a81f14d1 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -99,6 +99,12 @@ The key features are: --- +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + ## **Typer**, the FastAPI of CLIs From 79eed9d7d9f4074fc3cd2353ee7f5f39233534ab Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 18:58:23 +0000 Subject: [PATCH 0784/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 79c67de55..136eb5b9f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). ## 0.90.0 From 3c5536a780ba62a89111d0a832c1dce4f7d9e1ba Mon Sep 17 00:00:00 2001 From: Igor Shevchenko <39371503+bnzone@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:27:16 -0600 Subject: [PATCH 0785/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/cookie-params.md`=20(#5890?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/cookie-params.md | 49 ++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 50 insertions(+) create mode 100644 docs/ru/docs/tutorial/cookie-params.md diff --git a/docs/ru/docs/tutorial/cookie-params.md b/docs/ru/docs/tutorial/cookie-params.md new file mode 100644 index 000000000..75e9d9064 --- /dev/null +++ b/docs/ru/docs/tutorial/cookie-params.md @@ -0,0 +1,49 @@ +# Параметры Cookie + +Вы можете задать параметры Cookie таким же способом, как `Query` и `Path` параметры. + +## Импорт `Cookie` + +Сначала импортируйте `Cookie`: + +=== "Python 3.6 и выше" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +## Объявление параметров `Cookie` + +Затем объявляйте параметры cookie, используя ту же структуру, что и с `Path` и `Query`. + +Первое значение - это значение по умолчанию, вы можете передать все дополнительные параметры проверки или аннотации: + +=== "Python 3.6 и выше" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +!!! note "Технические детали" + `Cookie` - это класс, родственный `Path` и `Query`. Он также наследуется от общего класса `Param`. + + Но помните, что когда вы импортируете `Query`, `Path`, `Cookie` и другое из `fastapi`, это фактически функции, которые возвращают специальные классы. + +!!! info "Дополнительная информация" + Для объявления cookies, вам нужно использовать `Cookie`, иначе параметры будут интерпретированы как параметры запроса. + +## Резюме + +Объявляйте cookies с помощью `Cookie`, используя тот же общий шаблон, что и `Query`, и `Path`. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 837209fd4..da03d258a 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Учебник - руководство пользователя: - tutorial/body-fields.md - tutorial/background-tasks.md + - tutorial/cookie-params.md - async.md - Развёртывание: - deployment/index.md From 18e6c3ff6a2299cc8b9d8cc669e57cd8a442ef15 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 19:27:57 +0000 Subject: [PATCH 0786/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 136eb5b9f..0846de1bb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). * 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). From 99deead7fcd536c2b7bd1521caf73cda664faf2d Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Thu, 9 Feb 2023 23:28:54 +0400 Subject: [PATCH 0787/1881] =?UTF-8?q?=E2=9C=8F=20Update=20Pydantic=20GitHu?= =?UTF-8?q?b=20URLs=20(#5952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/DISCUSSION_TEMPLATE/questions.yml | 2 +- docs/en/docs/release-notes.md | 6 +++--- tests/test_tutorial/test_dataclasses/test_tutorial002.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index cbe2b9167..3726b7d18 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -36,7 +36,7 @@ body: required: true - label: I already read and followed all the tutorial in the docs and didn't find an answer. required: true - - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/samuelcolvin/pydantic). + - label: I already checked if it is not related to FastAPI but to [Pydantic](https://github.com/pydantic/pydantic). required: true - label: I already checked if it is not related to FastAPI but to [Swagger UI](https://github.com/swagger-api/swagger-ui). required: true diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0846de1bb..f24c54799 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -1244,7 +1244,7 @@ Thanks to [Dima Boger](https://twitter.com/b0g3r) for the security report! 🙇 ### Security fixes -* 📌 Upgrade pydantic pin, to handle security vulnerability [CVE-2021-29510](https://github.com/samuelcolvin/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh). PR [#3213](https://github.com/tiangolo/fastapi/pull/3213) by [@tiangolo](https://github.com/tiangolo). +* 📌 Upgrade pydantic pin, to handle security vulnerability [CVE-2021-29510](https://github.com/pydantic/pydantic/security/advisories/GHSA-5jqp-qgf6-3pvh). PR [#3213](https://github.com/tiangolo/fastapi/pull/3213) by [@tiangolo](https://github.com/tiangolo). ## 0.65.0 @@ -1799,11 +1799,11 @@ Note: all the previous parameters are still there, so it's still possible to dec ## 0.55.1 -* Fix handling of enums with their own schema in path parameters. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). +* Fix handling of enums with their own schema in path parameters. To support [pydantic/pydantic#1432](https://github.com/pydantic/pydantic/pull/1432) in FastAPI. PR [#1463](https://github.com/tiangolo/fastapi/pull/1463). ## 0.55.0 -* Allow enums to allow them to have their own schemas in OpenAPI. To support [samuelcolvin/pydantic#1432](https://github.com/samuelcolvin/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). +* Allow enums to allow them to have their own schemas in OpenAPI. To support [pydantic/pydantic#1432](https://github.com/pydantic/pydantic/pull/1432) in FastAPI. PR [#1461](https://github.com/tiangolo/fastapi/pull/1461). * Add links for funding through [GitHub sponsors](https://github.com/sponsors/tiangolo). PR [#1425](https://github.com/tiangolo/fastapi/pull/1425). * Update issue template for for questions. PR [#1344](https://github.com/tiangolo/fastapi/pull/1344) by [@retnikt](https://github.com/retnikt). * Update warning about storing passwords in docs. PR [#1336](https://github.com/tiangolo/fastapi/pull/1336) by [@skorokithakis](https://github.com/skorokithakis). diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 34aeb0be5..f5597e30c 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 # TODO: remove this once Pydantic 1.9 is released - # Ref: https://github.com/samuelcolvin/pydantic/pull/2557 + # Ref: https://github.com/pydantic/pydantic/pull/2557 data = response.json() alternative_data1 = deepcopy(data) alternative_data2 = deepcopy(data) From 9a5147382e2ad1657e4a42a78baaac5bf418b4c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 19:29:30 +0000 Subject: [PATCH 0788/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f24c54799..ea4e4c969 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Update Pydantic GitHub URLs. PR [#5952](https://github.com/tiangolo/fastapi/pull/5952) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). * 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). From d6fb9429d21c6492b2541b7210ecc9707b8677e2 Mon Sep 17 00:00:00 2001 From: Chandra Deb <37209383+chandra-deb@users.noreply.github.com> Date: Fri, 10 Feb 2023 01:35:01 +0600 Subject: [PATCH 0789/1881] =?UTF-8?q?=E2=9C=8F=20Tweak=20wording=20to=20cl?= =?UTF-8?q?arify=20`docs/en/docs/project-generation.md`=20(#5930)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/project-generation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/project-generation.md b/docs/en/docs/project-generation.md index 2f3d6c1d3..8ba34fa11 100644 --- a/docs/en/docs/project-generation.md +++ b/docs/en/docs/project-generation.md @@ -1,6 +1,6 @@ # Project Generation - Template -You can use a project generator to get started, as it includes a lot of the initial set up, security, database and first API endpoints already done for you. +You can use a project generator to get started, as it includes a lot of the initial set up, security, database and some API endpoints already done for you. A project generator will always have a very opinionated setup that you should update and adapt for your own needs, but it might be a good starting point for your project. From 5ed70f285b3f236a8ffcab6cdcbb61222c80bc28 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 19:35:40 +0000 Subject: [PATCH 0790/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ea4e4c969..88ddb2672 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Tweak wording to clarify `docs/en/docs/project-generation.md`. PR [#5930](https://github.com/tiangolo/fastapi/pull/5930) by [@chandra-deb](https://github.com/chandra-deb). * ✏ Update Pydantic GitHub URLs. PR [#5952](https://github.com/tiangolo/fastapi/pull/5952) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). * 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). From 392766bcfa4fed8fdef3df438f398fd1ea15d547 Mon Sep 17 00:00:00 2001 From: Jakepys <81931114+JuanPerdomo00@users.noreply.github.com> Date: Thu, 9 Feb 2023 14:36:46 -0500 Subject: [PATCH 0791/1881] =?UTF-8?q?=E2=9C=8F=20Update=20`zip-docs.sh`=20?= =?UTF-8?q?internal=20script,=20remove=20extra=20space=20(#5931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/zip-docs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh index 69315f5dd..47c3b0977 100644 --- a/scripts/zip-docs.sh +++ b/scripts/zip-docs.sh @@ -1,4 +1,4 @@ -#! /usr/bin/env bash +#!/usr/bin/env bash set -x set -e From 445e611a29f4f3846707e75feecd05eb012b254f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 9 Feb 2023 19:37:21 +0000 Subject: [PATCH 0792/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 88ddb2672..e9f82e8a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Update `zip-docs.sh` internal script, remove extra space. PR [#5931](https://github.com/tiangolo/fastapi/pull/5931) by [@JuanPerdomo00](https://github.com/JuanPerdomo00). * ✏ Tweak wording to clarify `docs/en/docs/project-generation.md`. PR [#5930](https://github.com/tiangolo/fastapi/pull/5930) by [@chandra-deb](https://github.com/chandra-deb). * ✏ Update Pydantic GitHub URLs. PR [#5952](https://github.com/tiangolo/fastapi/pull/5952) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). From e85c109bcd1e568dad28b9efc78b0d1d15133f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 9 Feb 2023 20:40:53 +0100 Subject: [PATCH 0793/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e9f82e8a8..3552418b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,12 +2,24 @@ ## Latest Changes -* ✏ Update `zip-docs.sh` internal script, remove extra space. PR [#5931](https://github.com/tiangolo/fastapi/pull/5931) by [@JuanPerdomo00](https://github.com/JuanPerdomo00). + +### Upgrades + +* ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). + +### Docs + * ✏ Tweak wording to clarify `docs/en/docs/project-generation.md`. PR [#5930](https://github.com/tiangolo/fastapi/pull/5930) by [@chandra-deb](https://github.com/chandra-deb). * ✏ Update Pydantic GitHub URLs. PR [#5952](https://github.com/tiangolo/fastapi/pull/5952) by [@yezz123](https://github.com/yezz123). -* 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). * 📝 Add opinion from Cisco. PR [#5981](https://github.com/tiangolo/fastapi/pull/5981) by [@tiangolo](https://github.com/tiangolo). -* ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/cookie-params.md`. PR [#5890](https://github.com/tiangolo/fastapi/pull/5890) by [@bnzone](https://github.com/bnzone). + +### Internal + +* ✏ Update `zip-docs.sh` internal script, remove extra space. PR [#5931](https://github.com/tiangolo/fastapi/pull/5931) by [@JuanPerdomo00](https://github.com/JuanPerdomo00). ## 0.90.0 From 6e94ec2bf089740a2cc1a71883ef24e0e8029c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 9 Feb 2023 20:41:40 +0100 Subject: [PATCH 0794/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?90.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3552418b7..272f94dcd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3,6 +3,8 @@ ## Latest Changes +## 0.90.1 + ### Upgrades * ⬆️ Upgrade Starlette range to allow 0.23.1. PR [#5980](https://github.com/tiangolo/fastapi/pull/5980) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 656bb879a..5482f8d6c 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.90.0" +__version__ = "0.90.1" from starlette import status as status From d566c6cbca227afef0edd0028cdb49894b972357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Feb 2023 15:13:04 +0100 Subject: [PATCH 0795/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20version=20to=20`0.24.0`=20and=20refactor=20internals=20fo?= =?UTF-8?q?r=20compatibility=20(#5985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 160d66301..204bd46b3 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -87,7 +87,7 @@ class FastAPI(Starlette): ), **extra: Any, ) -> None: - self._debug: bool = debug + self.debug = debug self.title = title self.description = description self.version = version @@ -144,7 +144,7 @@ class FastAPI(Starlette): self.user_middleware: List[Middleware] = ( [] if middleware is None else list(middleware) ) - self.middleware_stack: ASGIApp = self.build_middleware_stack() + self.middleware_stack: Union[ASGIApp, None] = None self.setup() def build_middleware_stack(self) -> ASGIApp: diff --git a/pyproject.toml b/pyproject.toml index 7b6138d09..696bcda3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.22.0,<0.24.0", + "starlette>=0.24.0,<0.25.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From a04acf690040a3ead5156a70368b1460b0144f1b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Feb 2023 14:13:57 +0000 Subject: [PATCH 0796/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 272f94dcd..926f6be62 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette version to `0.24.0` and refactor internals for compatibility. PR [#5985](https://github.com/tiangolo/fastapi/pull/5985) by [@tiangolo](https://github.com/tiangolo). ## 0.90.1 From 3c05189b063ef3e33ac8cb3a41abd3b75a70ed71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Feb 2023 15:32:23 +0100 Subject: [PATCH 0797/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 926f6be62..fa6aafe99 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,11 @@ ## Latest Changes +### Upgrades + * ⬆️ Upgrade Starlette version to `0.24.0` and refactor internals for compatibility. PR [#5985](https://github.com/tiangolo/fastapi/pull/5985) by [@tiangolo](https://github.com/tiangolo). + * This can solve nuanced errors when using middlewares. Before Starlette `0.24.0`, a new instance of each middleware class would be created when a new middleware was added. That normally was not a problem, unless the middleware class expected to be created only once, with only one instance, that happened in some cases. This upgrade would solve those cases (thanks [@adriangb](https://github.com/adriangb)! Starlette PR [#2017](https://github.com/encode/starlette/pull/2017)). Now the middleware class instances are created once, right before the first request (the first time the app is called). + * If you depended on that previous behavior, you might need to update your code. As always, make sure your tests pass before merging the upgrade. ## 0.90.1 From 2ca77f9a4de14de08ed6615ad0bb783a078678f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Feb 2023 15:33:25 +0100 Subject: [PATCH 0798/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?91.0?= 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 fa6aafe99..6e53531ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.91.0 + ### Upgrades * ⬆️ Upgrade Starlette version to `0.24.0` and refactor internals for compatibility. PR [#5985](https://github.com/tiangolo/fastapi/pull/5985) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5482f8d6c..f26dc09a0 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.90.1" +__version__ = "0.91.0" from starlette import status as status From 75e7e9e0a221e7a724f28656f1d43d21fb057230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 14 Feb 2023 10:13:22 +0100 Subject: [PATCH 0799/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20to=200.25.0=20(#5996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 696bcda3e..5d5e34c19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.24.0,<0.25.0", + "starlette>=0.25.0,<0.26.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From 9e283ef66b3669f8c55aa95cf2d741d44eb41a06 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Feb 2023 09:13:56 +0000 Subject: [PATCH 0800/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6e53531ab..c1ba01e8b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette to 0.25.0. PR [#5996](https://github.com/tiangolo/fastapi/pull/5996) by [@tiangolo](https://github.com/tiangolo). ## 0.91.0 From 52ca6cb95b7a4d21052ad69fb1d8aa6280e12e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 14 Feb 2023 10:17:08 +0100 Subject: [PATCH 0801/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c1ba01e8b..a46d44f82 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,14 @@ ## Latest Changes +🚨 This is a security fix. Please upgrade as soon as possible. + +### Upgrades + * ⬆️ Upgrade Starlette to 0.25.0. PR [#5996](https://github.com/tiangolo/fastapi/pull/5996) by [@tiangolo](https://github.com/tiangolo). + * This solves a vulnerability that could allow denial of service attacks by using many small multipart fields/files (parts), consuming high CPU and memory. + * Only applications using forms (e.g. file uploads) could be affected. + * For most cases, upgrading won't have any breaking changes. ## 0.91.0 From 6879082b3668edd213d035b6e9a90a4bccf32e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 14 Feb 2023 10:17:53 +0100 Subject: [PATCH 0802/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?92.0?= 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 a46d44f82..7a2cdc35a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.92.0 + 🚨 This is a security fix. Please upgrade as soon as possible. ### Upgrades diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f26dc09a0..33875c7fa 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.91.0" +__version__ = "0.92.0" from starlette import status as status From 4f4035262cc81c7c895e764d4770f027b7e4d554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 16 Feb 2023 19:50:21 +0100 Subject: [PATCH 0803/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20and=20?= =?UTF-8?q?re-enable=20installing=20Typer-CLI=20(#6008)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d5e34c19..3e651ae36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,8 +81,7 @@ doc = [ "mkdocs-material >=8.1.4,<9.0.0", "mdx-include >=1.4.1,<2.0.0", "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", - # TODO: upgrade and enable typer-cli once it supports Click 8.x.x - # "typer-cli >=0.0.12,<0.0.13", + "typer-cli >=0.0.13,<0.0.14", "typer[all] >=0.6.1,<0.8.0", "pyyaml >=5.3.1,<7.0.0", ] From f6f39d8714d8d5a7dd1ddc2a78f13ecfbe29d7d3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 16 Feb 2023 18:51:02 +0000 Subject: [PATCH 0804/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7a2cdc35a..c521b23ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade and re-enable installing Typer-CLI. PR [#6008](https://github.com/tiangolo/fastapi/pull/6008) by [@tiangolo](https://github.com/tiangolo). ## 0.92.0 From a270ab0c3f13ff731d4e7bae8d9bfe1030ef9c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 21 Feb 2023 11:23:37 +0100 Subject: [PATCH 0805/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20analyt?= =?UTF-8?q?ics=20(#6025)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 2 +- docs/de/mkdocs.yml | 2 +- docs/en/mkdocs.yml | 2 +- docs/es/mkdocs.yml | 2 +- docs/fa/mkdocs.yml | 2 +- docs/fr/mkdocs.yml | 2 +- docs/he/mkdocs.yml | 2 +- docs/id/mkdocs.yml | 2 +- docs/it/mkdocs.yml | 2 +- docs/ja/mkdocs.yml | 2 +- docs/ko/mkdocs.yml | 2 +- docs/nl/mkdocs.yml | 2 +- docs/pl/mkdocs.yml | 2 +- docs/pt/mkdocs.yml | 2 +- docs/ru/mkdocs.yml | 2 +- docs/sq/mkdocs.yml | 2 +- docs/sv/mkdocs.yml | 2 +- docs/tr/mkdocs.yml | 2 +- docs/uk/mkdocs.yml | 2 +- docs/zh/mkdocs.yml | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index d549f37a3..bd70b4afa 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8c3c42b5f..66eaca26c 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -81,7 +81,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 69ad29624..f4ced989a 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -187,7 +187,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index d1d6215b6..343472bc0 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -90,7 +90,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 7c2fe5eab..7a74f5a54 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 7dce4b127..b74c177c5 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -104,7 +104,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 3279099b5..8078094ba 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index abb31252f..9562d5dd8 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 532b5bc52..4ca10d49d 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 5bbcce605..2b5644585 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -124,7 +124,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 3dfc208c8..8fa7efd99 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -92,7 +92,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 6d46939f9..e1e509182 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 1cd129420..2f1593097 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -83,7 +83,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index c598c00e7..21a3c2950 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -117,7 +117,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index da03d258a..ff3be48ed 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -93,7 +93,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index b07f3bc63..98194cbc4 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 3332d232d..c364c7fa5 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5904f71f9..54e198fef 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -85,7 +85,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 711328771..05ff1a8b7 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -80,7 +80,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index f4c3c0ec1..3661c470e 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -137,7 +137,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi From 7b3727e03e84ca202d450ba3d702d5cd37025d60 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 21 Feb 2023 10:24:13 +0000 Subject: [PATCH 0806/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c521b23ef..02fe6dd61 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade analytics. PR [#6025](https://github.com/tiangolo/fastapi/pull/6025) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade and re-enable installing Typer-CLI. PR [#6008](https://github.com/tiangolo/fastapi/pull/6008) by [@tiangolo](https://github.com/tiangolo). ## 0.92.0 From 35d93d59d82f2b2073d492ac335553f1d7c3cc55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 1 Mar 2023 14:22:00 +0100 Subject: [PATCH 0807/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20FastA?= =?UTF-8?q?PI=20Experts=20to=20use=20only=20discussions=20now=20that=20que?= =?UTF-8?q?stions=20are=20migrated=20(#9165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/app/main.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 05cbc71e5..2a0cdfc35 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -496,21 +496,25 @@ def get_discussions_experts(settings: Settings): def get_experts(settings: Settings): - ( - issues_commentors, - issues_last_month_commentors, - issues_authors, - ) = get_issues_experts(settings=settings) + # Migrated to only use GitHub Discussions + # ( + # issues_commentors, + # issues_last_month_commentors, + # issues_authors, + # ) = get_issues_experts(settings=settings) ( discussions_commentors, discussions_last_month_commentors, discussions_authors, ) = get_discussions_experts(settings=settings) - commentors = issues_commentors + discussions_commentors - last_month_commentors = ( - issues_last_month_commentors + discussions_last_month_commentors - ) - authors = {**issues_authors, **discussions_authors} + # commentors = issues_commentors + discussions_commentors + commentors = discussions_commentors + # last_month_commentors = ( + # issues_last_month_commentors + discussions_last_month_commentors + # ) + last_month_commentors = discussions_last_month_commentors + # authors = {**issues_authors, **discussions_authors} + authors = {**discussions_authors} return commentors, last_month_commentors, authors From be54d44b0c886524f10f87a744c13bd553d2e6ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 1 Mar 2023 13:22:41 +0000 Subject: [PATCH 0808/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 02fe6dd61..172cf716e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Refactor FastAPI Experts to use only discussions now that questions are migrated. PR [#9165](https://github.com/tiangolo/fastapi/pull/9165) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#6025](https://github.com/tiangolo/fastapi/pull/6025) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade and re-enable installing Typer-CLI. PR [#6008](https://github.com/tiangolo/fastapi/pull/6008) by [@tiangolo](https://github.com/tiangolo). From a8bde44029fff92d33eb7a58994ac161e26788e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 2 Mar 2023 14:06:57 +0100 Subject: [PATCH 0809/1881] =?UTF-8?q?=F0=9F=92=9A=20Fix/workaround=20GitHu?= =?UTF-8?q?b=20Actions=20in=20Docker=20with=20git=20for=20FastAPI=20People?= =?UTF-8?q?=20(#9169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/people.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 4b47b4072..cca1329e7 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -15,6 +15,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + # Ref: https://github.com/actions/runner/issues/2033 + - name: Fix git safe.directory in container + run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 From 97effae92d3db30a69ea15450be0854a6cb3d412 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 2 Mar 2023 13:07:40 +0000 Subject: [PATCH 0810/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 172cf716e..2623f5afe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Fix/workaround GitHub Actions in Docker with git for FastAPI People. PR [#9169](https://github.com/tiangolo/fastapi/pull/9169) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor FastAPI Experts to use only discussions now that questions are migrated. PR [#9165](https://github.com/tiangolo/fastapi/pull/9165) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#6025](https://github.com/tiangolo/fastapi/pull/6025) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade and re-enable installing Typer-CLI. PR [#6008](https://github.com/tiangolo/fastapi/pull/6008) by [@tiangolo](https://github.com/tiangolo). From e9326de1619ecff41fe15b4e84714168ea68aaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 2 Mar 2023 16:11:17 +0100 Subject: [PATCH 0811/1881] =?UTF-8?q?=F0=9F=94=8A=20Log=20GraphQL=20errors?= =?UTF-8?q?=20in=20FastAPI=20People,=20because=20it=20returns=20200,=20wit?= =?UTF-8?q?h=20a=20payload=20with=20an=20error=20(#9171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/app/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 2a0cdfc35..2bf59f25e 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -381,6 +381,10 @@ def get_graphql_response( logging.error(response.text) raise RuntimeError(response.text) data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(response.text) + raise RuntimeError(response.text) return data From 7674da89d3ca82926eab6516f839e6f2f69efe2c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 2 Mar 2023 15:11:54 +0000 Subject: [PATCH 0812/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2623f5afe..b7bfe4496 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔊 Log GraphQL errors in FastAPI People, because it returns 200, with a payload with an error. PR [#9171](https://github.com/tiangolo/fastapi/pull/9171) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix/workaround GitHub Actions in Docker with git for FastAPI People. PR [#9169](https://github.com/tiangolo/fastapi/pull/9169) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor FastAPI Experts to use only discussions now that questions are migrated. PR [#9165](https://github.com/tiangolo/fastapi/pull/9165) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade analytics. PR [#6025](https://github.com/tiangolo/fastapi/pull/6025) by [@tiangolo](https://github.com/tiangolo). From 87842ac0db824a34785fa1815e73006e8162bbac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 4 Mar 2023 08:29:30 +0100 Subject: [PATCH 0813/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#9181)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 167 ++++++---------- docs/en/data/people.yml | 322 +++++++++++++++---------------- 2 files changed, 219 insertions(+), 270 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index c7ce38f59..2a8573f19 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai +- - login: armand-sauzay + avatarUrl: https://avatars.githubusercontent.com/u/35524799?u=56e3e944bfe62770d1709c09552d2efc6d285ca6&v=4 + url: https://github.com/armand-sauzay - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi @@ -35,21 +38,18 @@ sponsors: - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry +- - login: InesIvanova + avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 + url: https://github.com/InesIvanova - - login: vyos avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 url: https://github.com/vyos - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare - - login: Striveworks - avatarUrl: https://avatars.githubusercontent.com/u/45523576?v=4 - url: https://github.com/Striveworks - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP @@ -59,6 +59,9 @@ sponsors: - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore + - login: ianshan0915 + avatarUrl: https://avatars.githubusercontent.com/u/5893101?u=a178d247d882578b1d1ef214b2494e52eb28634c&v=4 + url: https://github.com/ianshan0915 - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie @@ -119,9 +122,6 @@ sponsors: - login: pamelafox avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 url: https://github.com/pamelafox - - login: deserat - avatarUrl: https://avatars.githubusercontent.com/u/299332?v=4 - url: https://github.com/deserat - login: ericof avatarUrl: https://avatars.githubusercontent.com/u/306014?u=cf7c8733620397e6584a451505581c01c5d842d7&v=4 url: https://github.com/ericof @@ -137,18 +137,15 @@ sponsors: - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner + - login: iobruno + avatarUrl: https://avatars.githubusercontent.com/u/901651?u=460bc34ac298dca9870aafe3a1560a2ae789bc4a&v=4 + url: https://github.com/iobruno - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith - - login: ltieman - avatarUrl: https://avatars.githubusercontent.com/u/1084689?u=e69b17de17cb3ca141a17daa7ccbe173ceb1eb17&v=4 - url: https://github.com/ltieman - login: mrkmcknz avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 url: https://github.com/mrkmcknz - - login: theonlynexus - avatarUrl: https://avatars.githubusercontent.com/u/1515004?v=4 - url: https://github.com/theonlynexus - login: coffeewasmyidea avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 url: https://github.com/coffeewasmyidea @@ -156,11 +153,8 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4 url: https://github.com/jonakoudijs - login: corleyma - avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=aed2ff652294a87d666b1c3f6dbe98104db76d26&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=c61f9a4bbc45a45f5d855f93e5f6e0fc8b32c468&v=4 url: https://github.com/corleyma - - login: madisonmay - avatarUrl: https://avatars.githubusercontent.com/u/2645393?u=f22b93c6ea345a4d26a90a3834dfc7f0789fcb63&v=4 - url: https://github.com/madisonmay - login: andre1sk avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4 url: https://github.com/andre1sk @@ -182,15 +176,9 @@ sponsors: - login: anomaly avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 url: https://github.com/anomaly - - login: peterHoburg - avatarUrl: https://avatars.githubusercontent.com/u/3860655?u=f55f47eb2d6a9b495e806ac5a044e3ae01ccc1fa&v=4 - url: https://github.com/peterHoburg - login: jgreys avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=c66ae617d614f6c886f1f1c1799d22100b3c848d&v=4 url: https://github.com/jgreys - - login: gorhack - avatarUrl: https://avatars.githubusercontent.com/u/4141690?u=ec119ebc4bdf00a7bc84657a71aa17834f4f27f3&v=4 - url: https://github.com/gorhack - login: jaredtrog avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 url: https://github.com/jaredtrog @@ -203,6 +191,9 @@ sponsors: - login: ternaus avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=fabc8d75c921b3380126adb5a931c5da6e7db04f&v=4 url: https://github.com/ternaus + - login: eseglem + avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 + url: https://github.com/eseglem - login: Yaleesa avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 url: https://github.com/Yaleesa @@ -243,8 +234,14 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 url: https://github.com/khadrawy - login: pablonnaoji - avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=afc15bd5a4ba9c5c7206bbb1bcaeef606a0932e0&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=7480e0eaf959e9c5dfe3a05286f2ea4588c0a3c6&v=4 url: https://github.com/pablonnaoji + - login: mjohnsey + avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 + url: https://github.com/mjohnsey + - login: abdalla19977 + avatarUrl: https://avatars.githubusercontent.com/u/17257234?v=4 + url: https://github.com/abdalla19977 - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck @@ -254,6 +251,9 @@ sponsors: - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu + - login: Pablongo24 + avatarUrl: https://avatars.githubusercontent.com/u/24843427?u=78a6798469889d7a0690449fc667c39e13d5c6a9&v=4 + url: https://github.com/Pablongo24 - login: Joeriksson avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4 url: https://github.com/Joeriksson @@ -284,9 +284,12 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: AbdulwahabDev - avatarUrl: https://avatars.githubusercontent.com/u/34792253?u=9d27cbb6e196c95d747abf002df7fe0539764265&v=4 - url: https://github.com/AbdulwahabDev + - login: askurihin + avatarUrl: https://avatars.githubusercontent.com/u/37978981?v=4 + url: https://github.com/askurihin + - login: arleybri18 + avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 + url: https://github.com/arleybri18 - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -300,11 +303,14 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf - login: dudikbender - avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=494f85229115076121b3639a3806bbac1c6ae7f6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - login: thisistheplace avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 url: https://github.com/thisistheplace + - login: kyjoconn + avatarUrl: https://avatars.githubusercontent.com/u/58443406?u=a3e9c2acfb7ba62edda9334aba61cf027f41f789&v=4 + url: https://github.com/kyjoconn - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge @@ -317,9 +323,6 @@ sponsors: - login: predictionmachine avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4 url: https://github.com/predictionmachine - - login: minsau - avatarUrl: https://avatars.githubusercontent.com/u/64386242?u=7e45f24b2958caf946fa3546ea33bacf5cd886f8&v=4 - url: https://github.com/minsau - login: daverin avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 url: https://github.com/daverin @@ -329,16 +332,19 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: programvx - avatarUrl: https://avatars.githubusercontent.com/u/96057906?v=4 - url: https://github.com/programvx + - login: osawa-koki + avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 + url: https://github.com/osawa-koki - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h - login: Dagmaara avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 url: https://github.com/Dagmaara -- - login: linux-china +- - login: pawamoy + avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 + url: https://github.com/pawamoy + - login: linux-china avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4 url: https://github.com/linux-china - login: ddanier @@ -353,12 +359,6 @@ sponsors: - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson - - login: hhatto - avatarUrl: https://avatars.githubusercontent.com/u/150309?u=3e8f63c27bf996bfc68464b0ce3f7a3e40e6ea7f&v=4 - url: https://github.com/hhatto - - login: slafs - avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 - url: https://github.com/slafs - login: adamghill avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4 url: https://github.com/adamghill @@ -380,12 +380,6 @@ sponsors: - login: janfilips avatarUrl: https://avatars.githubusercontent.com/u/870699?u=50de77b93d3a0b06887e672d4e8c7b9d643085aa&v=4 url: https://github.com/janfilips - - login: woodrad - avatarUrl: https://avatars.githubusercontent.com/u/1410765?u=86707076bb03d143b3b11afc1743d2aa496bd8bf&v=4 - url: https://github.com/woodrad - - login: Pytlicek - avatarUrl: https://avatars.githubusercontent.com/u/1430522?u=01b1f2f7671ce3131e0877d08e2e3f8bdbb0a38a&v=4 - url: https://github.com/Pytlicek - login: allen0125 avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4 url: https://github.com/allen0125 @@ -398,9 +392,6 @@ sponsors: - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: Debakel - avatarUrl: https://avatars.githubusercontent.com/u/2857237?u=07df6d11c8feef9306d071cb1c1005a2dd596585&v=4 - url: https://github.com/Debakel - login: paul121 avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4 url: https://github.com/paul121 @@ -410,12 +401,6 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: jonathanhle - avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 - url: https://github.com/jonathanhle - - login: pawamoy - avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 - url: https://github.com/pawamoy - login: nikeee avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=63f8eee593f25138e0f1032ef442e9ad24907d4c&v=4 url: https://github.com/nikeee @@ -431,11 +416,14 @@ sponsors: - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 url: https://github.com/Baghdady92 + - login: KentShikama + avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 + url: https://github.com/KentShikama - login: holec avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4 url: https://github.com/holec - login: mattwelke - avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=5d963ead289969257190b133250653bd99df06ba&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7719209?v=4 url: https://github.com/mattwelke - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 @@ -443,9 +431,6 @@ sponsors: - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 - - login: yenchenLiu - avatarUrl: https://avatars.githubusercontent.com/u/9199638?u=8cdf5ae507448430d90f6f3518d1665a23afe99b&v=4 - url: https://github.com/yenchenLiu - login: xncbf avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 url: https://github.com/xncbf @@ -470,12 +455,9 @@ sponsors: - login: giuliano-oliveira avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 url: https://github.com/giuliano-oliveira - - login: logan-connolly - avatarUrl: https://avatars.githubusercontent.com/u/16244943?u=8ae66dfbba936463cc8aa0dd7a6d2b4c0cc757eb&v=4 - url: https://github.com/logan-connolly - - login: harripj - avatarUrl: https://avatars.githubusercontent.com/u/16853829?u=14db1ad132af9ec340f3f1da564620a73b6e4981&v=4 - url: https://github.com/harripj + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b2923ac17fe6e2a7c9ea14800351ddb92f79b100&v=4 + url: https://github.com/TheR1D - login: cdsre avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 url: https://github.com/cdsre @@ -485,15 +467,9 @@ sponsors: - login: paulowiz avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4 url: https://github.com/paulowiz - - login: yannicschroeer - avatarUrl: https://avatars.githubusercontent.com/u/22749683?u=91515328b5418a4e7289a83f0dcec3573f1a6500&v=4 - url: https://github.com/yannicschroeer - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic - - login: fstau - avatarUrl: https://avatars.githubusercontent.com/u/24669867?u=60e7c8c09f8dafabee8fc3edcd6f9e19abbff918&v=4 - url: https://github.com/fstau - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 @@ -534,26 +510,14 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 url: https://github.com/ww-daniel-mora - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=34cba2eaca6a52072498e06bccebe141694fe1d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: ilias-ant - avatarUrl: https://avatars.githubusercontent.com/u/42189572?u=a84d169eb6f6bbcb85434c2bed0b4a6d4d13c10e&v=4 - url: https://github.com/ilias-ant - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=2a812c1a2ec58227ed01778837f255143de9df97&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=5265858add14a6822bd145f7547323cf078563e6&v=4 url: https://github.com/arrrrrmin - - login: MauriceKuenicke - avatarUrl: https://avatars.githubusercontent.com/u/47433175?u=37455bc95c7851db296ac42626f0cacb77ca2443&v=4 - url: https://github.com/MauriceKuenicke - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby - - login: akanz1 - avatarUrl: https://avatars.githubusercontent.com/u/51492342?u=2280f57134118714645e16b535c1a37adf6b369b&v=4 - url: https://github.com/akanz1 - - login: shidenko97 - avatarUrl: https://avatars.githubusercontent.com/u/54946990?u=3fdc0caea36af9217dacf1cc7760c7ed9d67dcfe&v=4 - url: https://github.com/shidenko97 - login: data-djinn avatarUrl: https://avatars.githubusercontent.com/u/56449985?u=42146e140806908d49bd59ccc96f222abf587886&v=4 url: https://github.com/data-djinn @@ -572,27 +536,12 @@ sponsors: - login: realabja avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 url: https://github.com/realabja - - login: pondDevThai - avatarUrl: https://avatars.githubusercontent.com/u/71592181?u=08af9a59bccfd8f6b101de1005aa9822007d0a44&v=4 - url: https://github.com/pondDevThai - - login: lukzmu - avatarUrl: https://avatars.githubusercontent.com/u/80778518?u=f636ad03cab8e8de15183fa81e768bfad3f515d0&v=4 - url: https://github.com/lukzmu -- - login: chrislemke - avatarUrl: https://avatars.githubusercontent.com/u/11752694?u=70ceb6ee7c51d9a52302ab9220ffbf09eaa9c2a4&v=4 - url: https://github.com/chrislemke - - login: gabrielmbmb - avatarUrl: https://avatars.githubusercontent.com/u/29572918?u=6d1e00b5d558e96718312ff910a2318f47cc3145&v=4 - url: https://github.com/gabrielmbmb + - login: garydsong + avatarUrl: https://avatars.githubusercontent.com/u/105745865?u=03cc1aa9c978be0020e5a1ce1ecca323dd6c8d65&v=4 + url: https://github.com/garydsong +- - login: Leon0824 + avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 + url: https://github.com/Leon0824 - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - - login: buabaj - avatarUrl: https://avatars.githubusercontent.com/u/49881677?u=a85952891036eb448f86eb847902f25badd5f9f7&v=4 - url: https://github.com/buabaj - - login: SoulPancake - avatarUrl: https://avatars.githubusercontent.com/u/70265851?u=9cdd82f2835da7d6a56a2e29e1369d5bf251e8f2&v=4 - url: https://github.com/SoulPancake - - login: junah201 - avatarUrl: https://avatars.githubusercontent.com/u/75025529?u=2451c256e888fa2a06bcfc0646d09b87ddb6a945&v=4 - url: https://github.com/junah201 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 7e917abd0..412f4517a 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,162 +1,158 @@ maintainers: - login: tiangolo - answers: 1956 - prs: 372 + answers: 1827 + prs: 384 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 400 + count: 376 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 262 + count: 237 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu -- login: ycd - count: 224 - avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 - url: https://github.com/ycd - login: Mause - count: 223 + count: 220 avatarUrl: https://avatars.githubusercontent.com/u/1405026?v=4 url: https://github.com/Mause +- login: ycd + count: 217 + avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 + url: https://github.com/ycd - login: JarroVGIT - count: 196 + count: 192 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 - count: 166 + count: 151 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 - login: phy25 - count: 130 + count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 118 + count: 116 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: jgould22 - count: 95 + count: 101 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: raphaelauv - count: 79 + count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv - login: ArcLightSlavik - count: 74 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik - login: ghandic - count: 72 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic - login: falkben - count: 59 + count: 57 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 url: https://github.com/falkben - login: sm-Fifteen - count: 52 + count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: insomnes - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: acidjunk - count: 44 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk -- login: adriangb - count: 44 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 - url: https://github.com/adriangb +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: frankie567 - count: 41 + count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 +- login: acidjunk + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk +- login: odiseo0 + count: 42 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + url: https://github.com/odiseo0 +- login: adriangb + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 + url: https://github.com/adriangb - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: odiseo0 - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: chbndrhnns - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: prostomarkeloff - count: 33 - avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 - url: https://github.com/prostomarkeloff - login: yinziyan1206 - count: 33 + count: 34 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 +- login: chbndrhnns + count: 34 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla -- login: wshayes - count: 29 - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes +- login: prostomarkeloff + count: 28 + avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 + url: https://github.com/prostomarkeloff - login: dbanty count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty +- login: wshayes + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 + url: https://github.com/wshayes - login: SirTelemak - count: 24 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: acnebs - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 - url: https://github.com/acnebs -- login: nsidnev - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev -- login: chris-allnutt +- login: caeser1996 count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 - url: https://github.com/chris-allnutt + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf -- login: Hultner - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 - url: https://github.com/Hultner +- login: nsidnev + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 + url: https://github.com/nsidnev +- login: acnebs + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 + url: https://github.com/acnebs +- login: chris-allnutt + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 + url: https://github.com/chris-allnutt - login: retnikt - count: 19 + count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet -- login: jorgerpo - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 - url: https://github.com/jorgerpo - login: nkhitrov count: 17 avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 @@ -165,75 +161,79 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 url: https://github.com/harunyasar -- login: waynerv - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: dstlny - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 - url: https://github.com/dstlny +- login: Hultner + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 + url: https://github.com/Hultner - login: jonatasoli count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli -- login: hellocoldworld +- login: dstlny + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 + url: https://github.com/dstlny +- login: jorgerpo count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 - url: https://github.com/hellocoldworld -- login: mbroton + avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 + url: https://github.com/jorgerpo +- login: ghost count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost - login: simondale00 count: 15 avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4 url: https://github.com/simondale00 -- login: haizaar +- login: hellocoldworld + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 + url: https://github.com/hellocoldworld +- login: waynerv + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 + url: https://github.com/waynerv +- login: mbroton count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/58201?u=dd40d99a3e1935d0b768f122bfe2258d6ea53b2b&v=4 - url: https://github.com/haizaar -- login: n8sty - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty + avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 + url: https://github.com/mbroton last_month_active: -- login: jgould22 +- login: mr-st0rm count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: yinziyan1206 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: Kludex - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: moadennagi - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/16942283?v=4 - url: https://github.com/moadennagi -- login: iudeen - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: anthonycorletti - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 - url: https://github.com/anthonycorletti -- login: ThirVondukr - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/50728601?u=56010d6430583b2096a96f0946501156cdb79c75&v=4 - url: https://github.com/ThirVondukr + avatarUrl: https://avatars.githubusercontent.com/u/48455163?u=6b83550e4e70bea57cd2fdb41e717aeab7f64a91&v=4 + url: https://github.com/mr-st0rm +- login: caeser1996 + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: ebottos94 - count: 4 + count: 6 avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 url: https://github.com/ebottos94 -- login: odiseo0 +- login: jgould22 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 +- login: Kludex + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex +- login: clemens-tolboom + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/371014?v=4 + url: https://github.com/clemens-tolboom +- login: williamjamir + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/5083518?u=b76ca8e08b906a86fa195fb817dd94e8d9d3d8f6&v=4 + url: https://github.com/williamjamir +- login: nymous count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous +- login: frankie567 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 + url: https://github.com/frankie567 top_contributors: - login: waynerv count: 25 @@ -243,6 +243,10 @@ top_contributors: count: 22 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: Kludex + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: jaystone776 count: 17 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 @@ -251,10 +255,6 @@ top_contributors: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu -- login: Kludex - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 @@ -283,6 +283,10 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo +- login: batlopes + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 + url: https://github.com/batlopes - login: wshayes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -301,12 +305,12 @@ top_contributors: url: https://github.com/ComicShrimp - login: NinaHwang count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 url: https://github.com/NinaHwang -- login: batlopes +- login: Xewus count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 - url: https://github.com/batlopes + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -335,21 +339,17 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas -- login: Xewus - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 - url: https://github.com/Xewus top_reviewers: - login: Kludex - count: 110 + count: 111 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 72 + count: 75 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 70 + count: 71 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -404,24 +404,24 @@ top_reviewers: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu +- login: LorhanSohaky + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky +- login: rjNemo + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: hard-coders count: 20 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders -- login: LorhanSohaky - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky - login: 0417taehyun count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun -- login: rjNemo - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo - login: odiseo0 - count: 18 + count: 19 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 url: https://github.com/odiseo0 - login: Smlep @@ -452,26 +452,30 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: Ryandaydev + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 + url: https://github.com/Ryandaydev +- login: Xewus + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk +- login: peidrao + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4 + url: https://github.com/peidrao - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu -- login: Ryandaydev - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 - url: https://github.com/Ryandaydev - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 url: https://github.com/solomein-sv -- login: Xewus - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=4bdd4a0300530a504987db27488ba79c37f2fb18&v=4 - url: https://github.com/Xewus - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -488,10 +492,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: peidrao +- login: r0b2g1t count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4 - url: https://github.com/peidrao + avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 + url: https://github.com/r0b2g1t - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -532,7 +536,3 @@ top_reviewers: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 url: https://github.com/rogerbrinkmann -- login: NinaHwang - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=1741703bd6c8f491503354b363a86e879b4c1cab&v=4 - url: https://github.com/NinaHwang From e8fd74e7377a28f23061e35d1f74038809316d4a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 07:30:07 +0000 Subject: [PATCH 0814/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b7bfe4496..7e0817a66 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 [#9181](https://github.com/tiangolo/fastapi/pull/9181) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔊 Log GraphQL errors in FastAPI People, because it returns 200, with a payload with an error. PR [#9171](https://github.com/tiangolo/fastapi/pull/9171) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix/workaround GitHub Actions in Docker with git for FastAPI People. PR [#9169](https://github.com/tiangolo/fastapi/pull/9169) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor FastAPI Experts to use only discussions now that questions are migrated. PR [#9165](https://github.com/tiangolo/fastapi/pull/9165) by [@tiangolo](https://github.com/tiangolo). From ff64772dd1421d5796cdab6092c5fef364fc12f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Mar 2023 08:34:39 +0100 Subject: [PATCH 0815/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors-badg?= =?UTF-8?q?es=20(#9182)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 3a7436658..70a7548e4 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -15,3 +15,4 @@ logins: - Doist - nihpo - svix + - armand-sauzay From 4b95025d44f6c77e6077a43b1e72af7b15c0e4a4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 07:35:11 +0000 Subject: [PATCH 0816/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7e0817a66..b04e9fe8f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9181](https://github.com/tiangolo/fastapi/pull/9181) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔊 Log GraphQL errors in FastAPI People, because it returns 200, with a payload with an error. PR [#9171](https://github.com/tiangolo/fastapi/pull/9171) by [@tiangolo](https://github.com/tiangolo). * 💚 Fix/workaround GitHub Actions in Docker with git for FastAPI People. PR [#9169](https://github.com/tiangolo/fastapi/pull/9169) by [@tiangolo](https://github.com/tiangolo). From bd219c2bbf2b48f97b42cd0a13c2da8230e52c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Mar 2023 11:39:28 +0100 Subject: [PATCH 0817/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20translations?= =?UTF-8?q?=20bot=20to=20use=20Discussions,=20and=20notify=20when=20a=20PR?= =?UTF-8?q?=20is=20done=20(#9183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/notify-translations/app/main.py | 421 +++++++++++++++--- .../notify-translations/app/translations.yml | 21 - .github/workflows/notify-translations.yml | 1 + 3 files changed, 368 insertions(+), 75 deletions(-) delete mode 100644 .github/actions/notify-translations/app/translations.yml diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index d4ba0ecfc..de2f5bb9b 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -1,10 +1,11 @@ import logging import random +import sys import time from pathlib import Path -from typing import Dict, Union +from typing import Any, Dict, List, Union, cast -import yaml +import httpx from github import Github from pydantic import BaseModel, BaseSettings, SecretStr @@ -13,12 +14,172 @@ lang_all_label = "lang-all" approved_label = "approved-2" translations_path = Path(__file__).parent / "translations.yml" +github_graphql_url = "https://api.github.com/graphql" +questions_translations_category_id = "DIC_kwDOCZduT84CT5P9" + +all_discussions_query = """ +query Q($category_id: ID) { + repository(name: "fastapi", owner: "tiangolo") { + discussions(categoryId: $category_id, first: 100) { + nodes { + title + id + number + labels(first: 10) { + edges { + node { + id + name + } + } + } + } + } + } +} +""" + +translation_discussion_query = """ +query Q($after: String, $discussion_number: Int!) { + repository(name: "fastapi", owner: "tiangolo") { + discussion(number: $discussion_number) { + comments(first: 100, after: $after) { + edges { + cursor + node { + id + url + body + } + } + } + } + } +} +""" + +add_comment_mutation = """ +mutation Q($discussion_id: ID!, $body: String!) { + addDiscussionComment(input: {discussionId: $discussion_id, body: $body}) { + comment { + id + url + body + } + } +} +""" + +update_comment_mutation = """ +mutation Q($comment_id: ID!, $body: String!) { + updateDiscussionComment(input: {commentId: $comment_id, body: $body}) { + comment { + id + url + body + } + } +} +""" + + +class Comment(BaseModel): + id: str + url: str + body: str + + +class UpdateDiscussionComment(BaseModel): + comment: Comment + + +class UpdateCommentData(BaseModel): + updateDiscussionComment: UpdateDiscussionComment + + +class UpdateCommentResponse(BaseModel): + data: UpdateCommentData + + +class AddDiscussionComment(BaseModel): + comment: Comment + + +class AddCommentData(BaseModel): + addDiscussionComment: AddDiscussionComment + + +class AddCommentResponse(BaseModel): + data: AddCommentData + + +class CommentsEdge(BaseModel): + node: Comment + cursor: str + + +class Comments(BaseModel): + edges: List[CommentsEdge] + + +class CommentsDiscussion(BaseModel): + comments: Comments + + +class CommentsRepository(BaseModel): + discussion: CommentsDiscussion + + +class CommentsData(BaseModel): + repository: CommentsRepository + + +class CommentsResponse(BaseModel): + data: CommentsData + + +class AllDiscussionsLabelNode(BaseModel): + id: str + name: str + + +class AllDiscussionsLabelsEdge(BaseModel): + node: AllDiscussionsLabelNode + + +class AllDiscussionsDiscussionLabels(BaseModel): + edges: List[AllDiscussionsLabelsEdge] + + +class AllDiscussionsDiscussionNode(BaseModel): + title: str + id: str + number: int + labels: AllDiscussionsDiscussionLabels + + +class AllDiscussionsDiscussions(BaseModel): + nodes: List[AllDiscussionsDiscussionNode] + + +class AllDiscussionsRepository(BaseModel): + discussions: AllDiscussionsDiscussions + + +class AllDiscussionsData(BaseModel): + repository: AllDiscussionsRepository + + +class AllDiscussionsResponse(BaseModel): + data: AllDiscussionsData + class Settings(BaseSettings): github_repository: str input_token: SecretStr github_event_path: Path github_event_name: Union[str, None] = None + httpx_timeout: int = 30 input_debug: Union[bool, None] = False @@ -30,6 +191,113 @@ class PartialGitHubEvent(BaseModel): pull_request: PartialGitHubEventIssue +def get_graphql_response( + *, + settings: Settings, + query: str, + after: Union[str, None] = None, + category_id: Union[str, None] = None, + discussion_number: Union[int, None] = None, + discussion_id: Union[str, None] = None, + comment_id: Union[str, None] = None, + body: Union[str, None] = None, +) -> Dict[str, Any]: + headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} + # some fields are only used by one query, but GraphQL allows unused variables, so + # keep them here for simplicity + variables = { + "after": after, + "category_id": category_id, + "discussion_number": discussion_number, + "discussion_id": discussion_id, + "comment_id": comment_id, + "body": body, + } + response = httpx.post( + github_graphql_url, + headers=headers, + timeout=settings.httpx_timeout, + json={"query": query, "variables": variables, "operationName": "Q"}, + ) + if response.status_code != 200: + logging.error( + f"Response was not 200, after: {after}, category_id: {category_id}" + ) + logging.error(response.text) + raise RuntimeError(response.text) + data = response.json() + if "errors" in data: + logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(response.text) + raise RuntimeError(response.text) + return cast(Dict[str, Any], data) + + +def get_graphql_translation_discussions(*, settings: Settings): + data = get_graphql_response( + settings=settings, + query=all_discussions_query, + category_id=questions_translations_category_id, + ) + graphql_response = AllDiscussionsResponse.parse_obj(data) + return graphql_response.data.repository.discussions.nodes + + +def get_graphql_translation_discussion_comments_edges( + *, settings: Settings, discussion_number: int, after: Union[str, None] = None +): + data = get_graphql_response( + settings=settings, + query=translation_discussion_query, + discussion_number=discussion_number, + after=after, + ) + graphql_response = CommentsResponse.parse_obj(data) + return graphql_response.data.repository.discussion.comments.edges + + +def get_graphql_translation_discussion_comments( + *, settings: Settings, discussion_number: int +): + comment_nodes: List[Comment] = [] + discussion_edges = get_graphql_translation_discussion_comments_edges( + settings=settings, discussion_number=discussion_number + ) + + while discussion_edges: + for discussion_edge in discussion_edges: + comment_nodes.append(discussion_edge.node) + last_edge = discussion_edges[-1] + discussion_edges = get_graphql_translation_discussion_comments_edges( + settings=settings, + discussion_number=discussion_number, + after=last_edge.cursor, + ) + return comment_nodes + + +def create_comment(*, settings: Settings, discussion_id: str, body: str): + data = get_graphql_response( + settings=settings, + query=add_comment_mutation, + discussion_id=discussion_id, + body=body, + ) + response = AddCommentResponse.parse_obj(data) + return response.data.addDiscussionComment.comment + + +def update_comment(*, settings: Settings, comment_id: str, body: str): + data = get_graphql_response( + settings=settings, + query=update_comment_mutation, + comment_id=comment_id, + body=body, + ) + response = UpdateCommentResponse.parse_obj(data) + return response.data.updateDiscussionComment.comment + + if __name__ == "__main__": settings = Settings() if settings.input_debug: @@ -45,60 +313,105 @@ if __name__ == "__main__": ) contents = settings.github_event_path.read_text() 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}") + + # Avoid race conditions with multiple labels 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}, with anti-race condition sleep time: {sleep_time}" + logging.info( + f"Sleeping for {sleep_time} seconds to avoid " + "race conditions and multiple comments" ) - if pr.state == "open": - logging.debug(f"PR is open: {pr.number}") - label_strs = {label.name for label in pr.get_labels()} - if lang_all_label in label_strs and awaiting_label in label_strs: - logging.info( - f"This PR seems to be a language translation and awaiting reviews: {pr.number}" - ) - if approved_label in label_strs: - message = ( - f"It seems this PR already has the approved label: {pr.number}" - ) - logging.error(message) - raise RuntimeError(message) - langs = [] - for label in label_strs: - if label.startswith("lang-") and not label == lang_all_label: - langs.append(label[5:]) - 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}" - ) - issue = repo.get_issue(num) - message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} 🎉" - already_notified = False - 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}" - ) - issue.create_comment(message) - else: - logging.info( - f"Issue: {num} was already notified of PR: {pr.number}" - ) - else: + time.sleep(sleep_time) + + # Get PR + logging.debug(f"Processing PR: #{github_event.pull_request.number}") + pr = repo.get_pull(github_event.pull_request.number) + label_strs = {label.name for label in pr.get_labels()} + langs = [] + for label in label_strs: + if label.startswith("lang-") and not label == lang_all_label: + langs.append(label[5:]) + logging.info(f"PR #{pr.number} has labels: {label_strs}") + if not langs or lang_all_label not in label_strs: + logging.info(f"PR #{pr.number} doesn't seem to be a translation PR, skipping") + sys.exit(0) + + # Generate translation map, lang ID to discussion + discussions = get_graphql_translation_discussions(settings=settings) + lang_to_discussion_map: Dict[str, AllDiscussionsDiscussionNode] = {} + for discussion in discussions: + for edge in discussion.labels.edges: + label = edge.node.name + if label.startswith("lang-") and not label == lang_all_label: + lang = label[5:] + lang_to_discussion_map[lang] = discussion + logging.debug(f"Using translations map: {lang_to_discussion_map}") + + # Messages to create or check + new_translation_message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login} 🎉" + done_translation_message = f"Good news everyone! 😉 ~There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}~ 🎉 Good job! This is done. 🍰" + + # Normally only one language, but still + for lang in langs: + if lang not in lang_to_discussion_map: + log_message = f"Could not find discussion for language: {lang}" + logging.error(log_message) + raise RuntimeError(log_message) + discussion = lang_to_discussion_map[lang] logging.info( - f"Changing labels in a closed PR doesn't trigger comments, PR: {pr.number}" + f"Found a translation discussion for language: {lang} in discussion: #{discussion.number}" ) + + already_notified_comment: Union[Comment, None] = None + already_done_comment: Union[Comment, None] = None + + logging.info( + f"Checking current comments in discussion: #{discussion.number} to see if already notified about this PR: #{pr.number}" + ) + comments = get_graphql_translation_discussion_comments( + settings=settings, discussion_number=discussion.number + ) + for comment in comments: + if new_translation_message in comment.body: + already_notified_comment = comment + elif done_translation_message in comment.body: + already_done_comment = comment + logging.info( + f"Already notified comment: {already_notified_comment}, already done comment: {already_done_comment}" + ) + + if pr.state == "open" and awaiting_label in label_strs: + logging.info( + f"This PR seems to be a language translation and awaiting reviews: #{pr.number}" + ) + if already_notified_comment: + logging.info( + f"This PR #{pr.number} was already notified in comment: {already_notified_comment.url}" + ) + else: + logging.info( + f"Writing notification comment about PR #{pr.number} in Discussion: #{discussion.number}" + ) + comment = create_comment( + settings=settings, + discussion_id=discussion.id, + body=new_translation_message, + ) + logging.info(f"Notified in comment: {comment.url}") + elif pr.state == "closed" or approved_label in label_strs: + logging.info(f"Already approved or closed PR #{pr.number}") + if already_done_comment: + logging.info( + f"This PR #{pr.number} was already marked as done in comment: {already_done_comment.url}" + ) + elif already_notified_comment: + updated_comment = update_comment( + settings=settings, + comment_id=already_notified_comment.id, + body=done_translation_message, + ) + logging.info(f"Marked as done in comment: {updated_comment.url}") + else: + logging.info( + f"There doesn't seem to be anything to be done about PR #{pr.number}" + ) logging.info("Finished") diff --git a/.github/actions/notify-translations/app/translations.yml b/.github/actions/notify-translations/app/translations.yml deleted file mode 100644 index 4338e1326..000000000 --- a/.github/actions/notify-translations/app/translations.yml +++ /dev/null @@ -1,21 +0,0 @@ -pt: 1211 -es: 1218 -zh: 1228 -ru: 1362 -it: 1556 -ja: 1572 -uk: 1748 -tr: 1892 -fr: 1972 -ko: 2017 -fa: 2041 -pl: 3169 -de: 3716 -id: 3717 -az: 3994 -nl: 4701 -uz: 4883 -sv: 5146 -he: 5157 -ta: 5434 -ar: 3349 diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 2fcb7595e..fdd24414c 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -4,6 +4,7 @@ on: pull_request_target: types: - labeled + - closed jobs: notify-translations: From 83050bead610ad35765a8682b13d7e168a1dfd94 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 10:40:08 +0000 Subject: [PATCH 0818/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b04e9fe8f..412e1e129 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9181](https://github.com/tiangolo/fastapi/pull/9181) by [@github-actions[bot]](https://github.com/apps/github-actions). * 🔊 Log GraphQL errors in FastAPI People, because it returns 200, with a payload with an error. PR [#9171](https://github.com/tiangolo/fastapi/pull/9171) by [@tiangolo](https://github.com/tiangolo). From c5f343a4fd87719276962f41b7910bbb04a571cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 4 Mar 2023 12:44:30 +0100 Subject: [PATCH 0819/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20translation?= =?UTF-8?q?=20bot=20messages=20(#9206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index de2f5bb9b..494fe6ad8 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -347,8 +347,8 @@ if __name__ == "__main__": logging.debug(f"Using translations map: {lang_to_discussion_map}") # Messages to create or check - new_translation_message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login} 🎉" - done_translation_message = f"Good news everyone! 😉 ~There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}~ 🎉 Good job! This is done. 🍰" + new_translation_message = f"Good news everyone! 😉 There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}. 🎉 This requires 2 approvals from native speakers to be merged. 🤓" + done_translation_message = f"~There's a new translation PR to be reviewed: #{pr.number} by @{pr.user.login}~ Good job! This is done. 🍰☕" # Normally only one language, but still for lang in langs: From 03bb7c166c600b50938ab4b069803bdbca51ecc2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 11:45:13 +0000 Subject: [PATCH 0820/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 412e1e129..ccf3a2684 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). * 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9181](https://github.com/tiangolo/fastapi/pull/9181) by [@github-actions[bot]](https://github.com/apps/github-actions). From 30a9d68232f13c4816b78530b15d7c6d89cc532f Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sat, 4 Mar 2023 13:02:09 +0100 Subject: [PATCH 0821/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`deployment/manually.md`=20(#3693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sam Courtemanche Co-authored-by: Ruidy Co-authored-by: Ruidy Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/fr/docs/deployment/manually.md | 153 ++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 154 insertions(+) create mode 100644 docs/fr/docs/deployment/manually.md diff --git a/docs/fr/docs/deployment/manually.md b/docs/fr/docs/deployment/manually.md new file mode 100644 index 000000000..c53e2db67 --- /dev/null +++ b/docs/fr/docs/deployment/manually.md @@ -0,0 +1,153 @@ +# Exécuter un serveur manuellement - Uvicorn + +La principale chose dont vous avez besoin pour exécuter une application **FastAPI** sur une machine serveur distante est un programme serveur ASGI tel que **Uvicorn**. + +Il existe 3 principales alternatives : + +* Uvicorn : un serveur ASGI haute performance. +* Hypercorn : un serveur + ASGI compatible avec HTTP/2 et Trio entre autres fonctionnalités. +* Daphne : le serveur ASGI + conçu pour Django Channels. + +## Machine serveur et programme serveur + +Il y a un petit détail sur les noms à garder à l'esprit. 💡 + +Le mot "**serveur**" est couramment utilisé pour désigner à la fois l'ordinateur distant/cloud (la machine physique ou virtuelle) et également le programme qui s'exécute sur cette machine (par exemple, Uvicorn). + +Gardez cela à l'esprit lorsque vous lisez "serveur" en général, cela pourrait faire référence à l'une de ces deux choses. + +Lorsqu'on se réfère à la machine distante, il est courant de l'appeler **serveur**, mais aussi **machine**, **VM** (machine virtuelle), **nœud**. Tout cela fait référence à un type de machine distante, exécutant Linux, en règle générale, sur laquelle vous exécutez des programmes. + + +## Installer le programme serveur + +Vous pouvez installer un serveur compatible ASGI avec : + +=== "Uvicorn" + + * Uvicorn, un serveur ASGI rapide comme l'éclair, basé sur uvloop et httptools. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip "Astuce" + En ajoutant `standard`, Uvicorn va installer et utiliser quelques dépendances supplémentaires recommandées. + + Cela inclut `uvloop`, le remplaçant performant de `asyncio`, qui fournit le gros gain de performance en matière de concurrence. + +=== "Hypercorn" + + * Hypercorn, un serveur ASGI également compatible avec HTTP/2. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...ou tout autre serveur ASGI. + +## Exécutez le programme serveur + +Vous pouvez ensuite exécuter votre application de la même manière que vous l'avez fait dans les tutoriels, mais sans l'option `--reload`, par exemple : + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning + N'oubliez pas de supprimer l'option `--reload` si vous l'utilisiez. + + L'option `--reload` consomme beaucoup plus de ressources, est plus instable, etc. + + Cela aide beaucoup pendant le **développement**, mais vous **ne devriez pas** l'utiliser en **production**. + +## Hypercorn avec Trio + +Starlette et **FastAPI** sont basés sur +AnyIO, qui les rend +compatibles avec asyncio, de la bibliothèque standard Python et +Trio. + +Néanmoins, Uvicorn n'est actuellement compatible qu'avec asyncio, et il utilise normalement `uvloop`, le remplaçant hautes performances de `asyncio`. + +Mais si vous souhaitez utiliser directement **Trio**, vous pouvez utiliser **Hypercorn** car il le prend en charge. ✨ + +### Installer Hypercorn avec Trio + +Vous devez d'abord installer Hypercorn avec le support Trio : + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### Exécuter avec Trio + +Ensuite, vous pouvez passer l'option de ligne de commande `--worker-class` avec la valeur `trio` : + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +Et cela démarrera Hypercorn avec votre application en utilisant Trio comme backend. + +Vous pouvez désormais utiliser Trio en interne dans votre application. Ou mieux encore, vous pouvez utiliser AnyIO pour que votre code reste compatible avec Trio et asyncio. 🎉 + +## Concepts de déploiement + +Ces exemples lancent le programme serveur (e.g. Uvicorn), démarrant **un seul processus**, sur toutes les IPs (`0.0. +0.0`) sur un port prédéfini (par example, `80`). + +C'est l'idée de base. Mais vous vous préoccuperez probablement de certains concepts supplémentaires, tels que ... : + +* la sécurité - HTTPS +* l'exécution au démarrage +* les redémarrages +* la réplication (le nombre de processus en cours d'exécution) +* la mémoire +* les étapes précédant le démarrage + +Je vous en dirai plus sur chacun de ces concepts, sur la façon de les aborder, et donnerai quelques exemples concrets avec des stratégies pour les traiter dans les prochains chapitres. 🚀 diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index b74c177c5..294ef3421 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -77,6 +77,7 @@ nav: - deployment/https.md - deployment/deta.md - deployment/docker.md + - deployment/manually.md - project-generation.md - alternatives.md - history-design-future.md From 9ef46a229970e34aaee9e8403bfea8f14ca2992f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 12:02:42 +0000 Subject: [PATCH 0822/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ccf3a2684..a8788318f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). * 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). * 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). From 4d099250f689a13a82dcc82348ac72d1aef386ba Mon Sep 17 00:00:00 2001 From: Aayush Chhabra Date: Sat, 4 Mar 2023 17:47:21 +0530 Subject: [PATCH 0823/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/tutorial/bigger-applications.md`,=20"codes"=20to=20"code?= =?UTF-8?q?"=20(#5990)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typo in docs: it should be code instead of codes --- docs/en/docs/tutorial/bigger-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index d201953df..1de05a00a 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -189,7 +189,7 @@ The end result is that the item paths are now: ### Import the dependencies -This codes lives in the module `app.routers.items`, the file `app/routers/items.py`. +This code lives in the module `app.routers.items`, the file `app/routers/items.py`. And we need to get the dependency function from the module `app.dependencies`, the file `app/dependencies.py`. From e570371003874f406231395b8fb30c106e4e0930 Mon Sep 17 00:00:00 2001 From: eykamp Date: Sat, 4 Mar 2023 04:42:55 -0800 Subject: [PATCH 0824/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20formatting=20in=20`?= =?UTF-8?q?docs/en/docs/tutorial/metadata.md`=20for=20`ReDoc`=20(#6005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index 78f17031a..cf13e7470 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -101,7 +101,7 @@ You can configure the two documentation user interfaces included: * **Swagger UI**: served at `/docs`. * You can set its URL with the parameter `docs_url`. * You can disable it by setting `docs_url=None`. -* ReDoc: served at `/redoc`. +* **ReDoc**: served at `/redoc`. * You can set its URL with the parameter `redoc_url`. * You can disable it by setting `redoc_url=None`. From 8625189351f37a0f843cbea59686142df0a0f4b8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 12:43:32 +0000 Subject: [PATCH 0825/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a8788318f..9fe7a0f26 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). * 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). * 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). * 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). From 83012a9cf65948d4f1bb121ea1d5e00e3b312854 Mon Sep 17 00:00:00 2001 From: har8 Date: Sat, 4 Mar 2023 16:51:37 +0400 Subject: [PATCH 0826/1881] =?UTF-8?q?=F0=9F=8C=90=20Initiate=20Armenian=20?= =?UTF-8?q?translation=20setup=20(#5844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 3 + docs/de/mkdocs.yml | 3 + docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/mkdocs.yml | 3 + docs/hy/docs/index.md | 467 +++++++++++++++++++++++++++++++++++ docs/hy/mkdocs.yml | 148 +++++++++++ docs/hy/overrides/.gitignore | 0 docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 23 files changed, 675 insertions(+) create mode 100644 docs/hy/docs/index.md create mode 100644 docs/hy/mkdocs.yml create mode 100644 docs/hy/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index bd70b4afa..713892599 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 66eaca26c..b70bb54ad 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -112,6 +113,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index f4ced989a..4b48ce4c9 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -218,6 +219,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 343472bc0..004cc3fc9 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -121,6 +122,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 7a74f5a54..5bc1c2f57 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 294ef3421..059e7d2b8 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -136,6 +137,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 8078094ba..85db5aead 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/hy/docs/index.md b/docs/hy/docs/index.md new file mode 100644 index 000000000..cc82b33cf --- /dev/null +++ b/docs/hy/docs/index.md @@ -0,0 +1,467 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

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

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Other sponsors + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.7+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.7+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* **GraphQL** integration with Strawberry and other libraries. +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * extremely easy tests based on HTTPX and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* httpx - Required if you want to use the `TestClient`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install "fastapi[all]"`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml new file mode 100644 index 000000000..bc64e78f2 --- /dev/null +++ b/docs/hy/mkdocs.yml @@ -0,0 +1,148 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/hy/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: hy +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - hy: /hy/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +- attr_list +- md_in_html +extra: + analytics: + provider: google + property: UA-133183413-1 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /hy/ + name: hy + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/hy/overrides/.gitignore b/docs/hy/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 9562d5dd8..e5116e6aa 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 4ca10d49d..6a01763c1 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 2b5644585..cf3f55c20 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -155,6 +156,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 8fa7efd99..e9ec4a596 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -123,6 +124,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index e1e509182..fe8338823 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 2f1593097..261ec6730 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -114,6 +115,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 21a3c2950..39c9fc594 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -148,6 +149,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index ff3be48ed..ceb03a910 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -124,6 +125,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 98194cbc4..13469dd14 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index c364c7fa5..8ae7e2e04 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 54e198fef..51a604c7e 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -116,6 +117,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 05ff1a8b7..c82b13a8f 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -111,6 +112,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 3661c470e..6275808ec 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -45,6 +45,7 @@ nav: - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -168,6 +169,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ From 9b83a00c40e72ce2f06e8681ab2eb42ad7d510bf Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 4 Mar 2023 12:52:10 +0000 Subject: [PATCH 0827/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9fe7a0f26..21a685d7c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). * ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). * 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). * 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). From c5f72f02cdd6f572994caa5828ad705df9128c76 Mon Sep 17 00:00:00 2001 From: Cedric Fraboulet <62244267+frabc@users.noreply.github.com> Date: Mon, 6 Mar 2023 17:26:49 +0100 Subject: [PATCH 0828/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/debugging.md`=20(#9175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fr/docs/tutorial/debugging.md | 112 +++++++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 113 insertions(+) create mode 100644 docs/fr/docs/tutorial/debugging.md diff --git a/docs/fr/docs/tutorial/debugging.md b/docs/fr/docs/tutorial/debugging.md new file mode 100644 index 000000000..e58872d30 --- /dev/null +++ b/docs/fr/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# Débogage + +Vous pouvez connecter le débogueur dans votre éditeur, par exemple avec Visual Studio Code ou PyCharm. + +## Faites appel à `uvicorn` + +Dans votre application FastAPI, importez et exécutez directement `uvicorn` : + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### À propos de `__name__ == "__main__"` + +Le but principal de `__name__ == "__main__"` est d'avoir du code qui est exécuté lorsque votre fichier est appelé avec : + +
+ +```console +$ python myapp.py +``` + +
+ +mais qui n'est pas appelé lorsqu'un autre fichier l'importe, comme dans : + +```Python +from myapp import app +``` + +#### Pour davantage de détails + +Imaginons que votre fichier s'appelle `myapp.py`. + +Si vous l'exécutez avec : + +
+ +```console +$ python myapp.py +``` + +
+ +alors la variable interne `__name__` de votre fichier, créée automatiquement par Python, aura pour valeur la chaîne de caractères `"__main__"`. + +Ainsi, la section : + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +va s'exécuter. + +--- + +Cela ne se produira pas si vous importez ce module (fichier). + +Par exemple, si vous avez un autre fichier `importer.py` qui contient : + +```Python +from myapp import app + +# Code supplémentaire +``` + +dans ce cas, la variable automatique `__name__` à l'intérieur de `myapp.py` n'aura pas la valeur `"__main__"`. + +Ainsi, la ligne : + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +ne sera pas exécutée. + +!!! info +Pour plus d'informations, consultez la documentation officielle de Python. + +## Exécutez votre code avec votre débogueur + +Parce que vous exécutez le serveur Uvicorn directement depuis votre code, vous pouvez appeler votre programme Python (votre application FastAPI) directement depuis le débogueur. + +--- + +Par exemple, dans Visual Studio Code, vous pouvez : + +- Cliquer sur l'onglet "Debug" de la barre d'activités de Visual Studio Code. +- "Add configuration...". +- Sélectionnez "Python". +- Lancez le débogueur avec l'option "`Python: Current File (Integrated Terminal)`". + +Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. + +Voici à quoi cela pourrait ressembler : + + + +--- + +Si vous utilisez Pycharm, vous pouvez : + +- Ouvrir le menu "Run". +- Sélectionnez l'option "Debug...". +- Un menu contextuel s'affiche alors. +- Sélectionnez le fichier à déboguer (dans ce cas, `main.py`). + +Il démarrera alors le serveur avec votre code **FastAPI**, s'arrêtera à vos points d'arrêt, etc. + +Voici à quoi cela pourrait ressembler : + + diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 059e7d2b8..28fc795f9 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -68,6 +68,7 @@ nav: - tutorial/query-params.md - tutorial/body.md - tutorial/background-tasks.md + - tutorial/debugging.md - Guide utilisateur avancé: - advanced/additional-status-codes.md - advanced/additional-responses.md From 40c2c44ff92e5962184f42d1db2956fcf852e9d4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 6 Mar 2023 16:27:29 +0000 Subject: [PATCH 0829/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 21a685d7c..8c7f85e1d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). * 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). * ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). * 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). From 31e148ba8e48cb3f2588d889571a6ac5a7dd1c2f Mon Sep 17 00:00:00 2001 From: Axel Date: Mon, 6 Mar 2023 17:28:40 +0100 Subject: [PATCH 0830/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/advanced/path-operation-advanced-con?= =?UTF-8?q?figuration.md`=20(#9221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Maurin Co-authored-by: Cedric Fraboulet <62244267+frabc@users.noreply.github.com> --- .../path-operation-advanced-configuration.md | 168 ++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 169 insertions(+) create mode 100644 docs/fr/docs/advanced/path-operation-advanced-configuration.md diff --git a/docs/fr/docs/advanced/path-operation-advanced-configuration.md b/docs/fr/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 000000000..ace9f19f9 --- /dev/null +++ b/docs/fr/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,168 @@ +# Configuration avancée des paramètres de chemin + +## ID d'opération OpenAPI + +!!! Attention + Si vous n'êtes pas un "expert" en OpenAPI, vous n'en avez probablement pas besoin. + +Dans OpenAPI, les chemins sont des ressources, tels que /users/ ou /items/, exposées par votre API, et les opérations sont les méthodes HTTP utilisées pour manipuler ces chemins, telles que GET, POST ou DELETE. Les operationId sont des chaînes uniques facultatives utilisées pour identifier une opération d'un chemin. Vous pouvez définir l'OpenAPI `operationId` à utiliser dans votre *opération de chemin* avec le paramètre `operation_id`. + +Vous devez vous assurer qu'il est unique pour chaque opération. + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!} +``` + +### Utilisation du nom *path operation function* comme operationId + +Si vous souhaitez utiliser les noms de fonction de vos API comme `operationId`, vous pouvez les parcourir tous et remplacer chaque `operation_id` de l'*opération de chemin* en utilisant leur `APIRoute.name`. + +Vous devriez le faire après avoir ajouté toutes vos *paramètres de chemin*. + +```Python hl_lines="2 12-21 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!} +``` + +!!! Astuce + Si vous appelez manuellement `app.openapi()`, vous devez mettre à jour les `operationId` avant. + +!!! Attention + Pour faire cela, vous devez vous assurer que chacun de vos *chemin* ait un nom unique. + + Même s'ils se trouvent dans des modules différents (fichiers Python). + +## Exclusion d'OpenAPI + +Pour exclure un *chemin* du schéma OpenAPI généré (et donc des systèmes de documentation automatiques), utilisez le paramètre `include_in_schema` et assignez-lui la valeur `False` : + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} +``` + +## Description avancée de docstring + +Vous pouvez limiter le texte utilisé de la docstring d'une *fonction de chemin* qui sera affiché sur OpenAPI. + +L'ajout d'un `\f` (un caractère d'échappement "form feed") va permettre à **FastAPI** de tronquer la sortie utilisée pour OpenAPI à ce stade. + +Il n'apparaîtra pas dans la documentation, mais d'autres outils (tel que Sphinx) pourront utiliser le reste. + +```Python hl_lines="19-29" +{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} +``` + +## Réponses supplémentaires + +Vous avez probablement vu comment déclarer le `response_model` et le `status_code` pour une *opération de chemin*. + +Cela définit les métadonnées sur la réponse principale d'une *opération de chemin*. + +Vous pouvez également déclarer des réponses supplémentaires avec leurs modèles, codes de statut, etc. + +Il y a un chapitre entier ici dans la documentation à ce sujet, vous pouvez le lire sur [Réponses supplémentaires dans OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +## OpenAPI supplémentaire + +Lorsque vous déclarez un *chemin* dans votre application, **FastAPI** génère automatiquement les métadonnées concernant ce *chemin* à inclure dans le schéma OpenAPI. + +!!! note "Détails techniques" + La spécification OpenAPI appelle ces métaonnées des Objets d'opération. + +Il contient toutes les informations sur le *chemin* et est utilisé pour générer automatiquement la documentation. + +Il inclut les `tags`, `parameters`, `requestBody`, `responses`, etc. + +Ce schéma OpenAPI spécifique aux *operations* est normalement généré automatiquement par **FastAPI**, mais vous pouvez également l'étendre. + +!!! Astuce + Si vous avez seulement besoin de déclarer des réponses supplémentaires, un moyen plus pratique de le faire est d'utiliser les [réponses supplémentaires dans OpenAPI](./additional-responses.md){.internal-link target=_blank}. + +Vous pouvez étendre le schéma OpenAPI pour une *opération de chemin* en utilisant le paramètre `openapi_extra`. + +### Extensions OpenAPI + +Cet `openapi_extra` peut être utile, par exemple, pour déclarer [OpenAPI Extensions](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions) : + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial005.py!} +``` + +Si vous ouvrez la documentation automatique de l'API, votre extension apparaîtra au bas du *chemin* spécifique. + + + +Et dans le fichier openapi généré (`/openapi.json`), vous verrez également votre extension dans le cadre du *chemin* spécifique : + +```JSON hl_lines="22" +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### Personnalisation du Schéma OpenAPI pour un chemin + +Le dictionnaire contenu dans la variable `openapi_extra` sera fusionné avec le schéma OpenAPI généré automatiquement pour l'*opération de chemin*. + +Ainsi, vous pouvez ajouter des données supplémentaires au schéma généré automatiquement. + +Par exemple, vous pouvez décider de lire et de valider la requête avec votre propre code, sans utiliser les fonctionnalités automatiques de validation proposée par Pydantic, mais vous pouvez toujours définir la requête dans le schéma OpenAPI. + +Vous pouvez le faire avec `openapi_extra` : + +```Python hl_lines="20-37 39-40" +{!../../../docs_src/path_operation_advanced_configuration/tutorial006.py !} +``` + +Dans cet exemple, nous n'avons déclaré aucun modèle Pydantic. En fait, le corps de la requête n'est même pas parsé en tant que JSON, il est lu directement en tant que `bytes`, et la fonction `magic_data_reader()` serait chargé de l'analyser d'une manière ou d'une autre. + +Néanmoins, nous pouvons déclarer le schéma attendu pour le corps de la requête. + +### Type de contenu OpenAPI personnalisé + +En utilisant cette même astuce, vous pouvez utiliser un modèle Pydantic pour définir le schéma JSON qui est ensuite inclus dans la section de schéma OpenAPI personnalisée pour le *chemin* concerné. + +Et vous pouvez le faire même si le type de données dans la requête n'est pas au format JSON. + +Dans cet exemple, nous n'utilisons pas les fonctionnalités de FastAPI pour extraire le schéma JSON des modèles Pydantic ni la validation automatique pour JSON. En fait, nous déclarons le type de contenu de la requête en tant que YAML, et non JSON : + +```Python hl_lines="17-22 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +Néanmoins, bien que nous n'utilisions pas la fonctionnalité par défaut, nous utilisons toujours un modèle Pydantic pour générer manuellement le schéma JSON pour les données que nous souhaitons recevoir en YAML. + +Ensuite, nous utilisons directement la requête et extrayons son contenu en tant qu'octets. Cela signifie que FastAPI n'essaiera même pas d'analyser le payload de la requête en tant que JSON. + +Et nous analysons directement ce contenu YAML, puis nous utilisons à nouveau le même modèle Pydantic pour valider le contenu YAML : + +```Python hl_lines="26-33" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +!!! Astuce + Ici, nous réutilisons le même modèle Pydantic. + + Mais nous aurions pu tout aussi bien pu le valider d'une autre manière. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 28fc795f9..f5288a8fa 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -70,6 +70,7 @@ nav: - tutorial/background-tasks.md - tutorial/debugging.md - Guide utilisateur avancé: + - advanced/path-operation-advanced-configuration.md - advanced/additional-status-codes.md - advanced/additional-responses.md - async.md From 2f1b856fe611f2f15d38a04850ed9f25da719178 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 6 Mar 2023 16:29:17 +0000 Subject: [PATCH 0831/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8c7f85e1d..8c5d62787 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`. PR [#9221](https://github.com/tiangolo/fastapi/pull/9221) by [@axel584](https://github.com/axel584). * 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). * 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). * ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). From 639cf3440a450885dc44383017284a3567be7298 Mon Sep 17 00:00:00 2001 From: gusty1g Date: Tue, 7 Mar 2023 01:01:37 +0530 Subject: [PATCH 0832/1881] =?UTF-8?q?=F0=9F=8C=90=20Tamil=20translations?= =?UTF-8?q?=20-=20initial=20setup=20(#5564)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 3 + docs/de/mkdocs.yml | 3 + docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/mkdocs.yml | 3 + docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + docs/ta/mkdocs.yml | 148 +++++++++++++++++++++++++++++++++++ docs/ta/overrides/.gitignore | 0 docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 22 files changed, 208 insertions(+) create mode 100644 docs/ta/mkdocs.yml create mode 100644 docs/ta/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 713892599..7f4a490d8 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index b70bb54ad..f6e0a6b01 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -135,6 +136,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 4b48ce4c9..a5d77acbf 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -241,6 +242,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 004cc3fc9..a89aeb21a 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -144,6 +145,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 5bc1c2f57..f77f82f69 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index f5288a8fa..19182f381 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -161,6 +162,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 85db5aead..c5689395f 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index e5116e6aa..7b7875ef7 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 6a01763c1..9393c3663 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index cf3f55c20..3703398af 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -178,6 +179,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index e9ec4a596..29b684371 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -146,6 +147,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index fe8338823..d9b1bc1b8 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 261ec6730..8d0d20239 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -137,6 +138,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 39c9fc594..2a8302715 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -171,6 +172,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index ceb03a910..daacad71c 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -147,6 +148,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 13469dd14..f24c7c503 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 8ae7e2e04..574cc5abd 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml new file mode 100644 index 000000000..e31821e4b --- /dev/null +++ b/docs/ta/mkdocs.yml @@ -0,0 +1,148 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/ta/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: en +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - ta: /ta/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +- attr_list +- md_in_html +extra: + analytics: + provider: google + property: UA-133183413-1 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /ta/ + name: ta - தமிழ் + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/ta/overrides/.gitignore b/docs/ta/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 51a604c7e..19dcf2099 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -139,6 +140,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index c82b13a8f..b8152e821 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -134,6 +135,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 6275808ec..d25881c43 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -56,6 +56,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -191,6 +192,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ From 66e03c816b8185d6ee8fc6d10fbb410bcb385105 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 6 Mar 2023 19:32:19 +0000 Subject: [PATCH 0833/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8c5d62787..e8e173400 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Tamil translations - initial setup. PR [#5564](https://github.com/tiangolo/fastapi/pull/5564) by [@gusty1g](https://github.com/gusty1g). * 🌐 Add French translation for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`. PR [#9221](https://github.com/tiangolo/fastapi/pull/9221) by [@axel584](https://github.com/axel584). * 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). * 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). From cc9a73c3f83fab4f1d9fcb19dfe5b562869d932c Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Tue, 7 Mar 2023 09:46:00 -0600 Subject: [PATCH 0834/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`li?= =?UTF-8?q?fespan`=20async=20context=20managers=20(superseding=20`startup`?= =?UTF-8?q?=20and=20`shutdown`=20events)=20(#2944)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mike Shantz Co-authored-by: Jonathan Plasse <13716151+JonathanPlasse@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/events.md | 125 +++++++++++++++++- docs_src/events/tutorial003.py | 28 ++++ fastapi/applications.py | 3 + fastapi/routing.py | 3 + tests/test_router_events.py | 97 ++++++++------ .../test_events/test_tutorial003.py | 86 ++++++++++++ 6 files changed, 298 insertions(+), 44 deletions(-) create mode 100644 docs_src/events/tutorial003.py create mode 100644 tests/test_tutorial/test_events/test_tutorial003.py diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index 7cd2998f0..556bbde71 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -1,13 +1,108 @@ -# Events: startup - shutdown +# Lifespan Events + +You can define logic (code) that should be executed before the application **starts up**. This means that this code will be executed **once**, **before** the application **starts receiving requests**. + +The same way, you can define logic (code) that should be executed when the application is **shutting down**. In this case, this code will be executed **once**, **after** having handled possibly **many requests**. + +Because this code is executed before the application **starts** taking requests, and right after it **finishes** handling requests, it covers the whole application **lifespan** (the word "lifespan" will be important in a second 😉). + +This can be very useful for setting up **resources** that you need to use for the whole app, and that are **shared** among requests, and/or that you need to **clean up** afterwards. For example, a database connection pool, or loading a shared machine learning model. + +## Use Case + +Let's start with an example **use case** and then see how to solve it with this. + +Let's imagine that you have some **machine learning models** that you want to use to handle requests. 🤖 + +The same models are shared among requests, so, it's not one model per request, or one per user or something similar. + +Let's imagine that loading the model can **take quite some time**, because it has to read a lot of **data from disk**. So you don't want to do it for every request. + +You could load it at the top level of the module/file, but that would also mean that it would **load the model** even if you are just running a simple automated test, then that test would be **slow** because it would have to wait for the model to load before being able to run an independent part of the code. + +That's what we'll solve, let's load the model before the requests are handled, but only right before the application starts receiving requests, not while the code is being loaded. + +## Lifespan + +You can define this *startup* and *shutdown* logic using the `lifespan` parameter of the `FastAPI` app, and a "context manager" (I'll show you what that is in a second). + +Let's start with an example and then see it in detail. + +We create an async function `lifespan()` with `yield` like this: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +Here we are simulating the expensive *startup* operation of loading the model by putting the (fake) model function in the dictionary with machine learning models before the `yield`. This code will be executed **before** the application **starts taking requests**, during the *startup*. + +And then, right after the `yield`, we unload the model. This code will be executed **after** the application **finishes handling requests**, right before the *shutdown*. This could, for example, release resources like memory or a GPU. + +!!! tip + The `shutdown` would happen when you are **stopping** the application. + + Maybe you need to start a new version, or you just got tired of running it. 🤷 + +### Lifespan function + +The first thing to notice, is that we are defining an async function with `yield`. This is very similar to Dependencies with `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +The first part of the function, before the `yield`, will be executed **before** the application starts. + +And the part after the `yield` will be executed **after** the application has finished. + +### Async Context Manager + +If you check, the function is decorated with an `@asynccontextmanager`. + +That converts the function into something called an "**async context manager**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +A **context manager** in Python is something that you can use in a `with` statement, for example, `open()` can be used as a context manager: + +```Python +with open("file.txt") as file: + file.read() +``` + +In recent versions of Python, there's also an **async context manager**. You would use it with `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +When you create a context manager or an async context manager like above, what it does is that, before entering the `with` block, it will execute the code before the `yield`, and after exiting the `with` block, it will execute the code after the `yield`. + +In our code example above, we don't use it directly, but we pass it to FastAPI for it to use it. + +The `lifespan` parameter of the `FastAPI` app takes an **async context manager**, so we can pass our new `lifespan` async context manager to it. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## Alternative Events (deprecated) + +!!! warning + The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above. + + You can probably skip this part. + +There's an alternative way to define this logic to be executed during *startup* and during *shutdown*. You can define event handlers (functions) that need to be executed before the application starts up, or when the application is shutting down. These functions can be declared with `async def` or normal `def`. -!!! warning - Only event handlers for the main application will be executed, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. - -## `startup` event +### `startup` event To add a function that should be run before the application starts, declare it with the event `"startup"`: @@ -21,7 +116,7 @@ You can add more than one event handler function. And your application won't start receiving requests until all the `startup` event handlers have completed. -## `shutdown` event +### `shutdown` event To add a function that should be run when the application is shutting down, declare it with the event `"shutdown"`: @@ -45,3 +140,21 @@ Here, the `shutdown` event handler function will write a text line `"Application !!! info You can read more about these event handlers in Starlette's Events' docs. + +### `startup` and `shutdown` together + +There's a high chance that the logic for your *startup* and *shutdown* is connected, you might want to start something and then finish it, acquire a resource and then release it, etc. + +Doing that in separated functions that don't share logic or variables together is more difficult as you would need to store values in global variables or similar tricks. + +Because of that, it's now recommended to instead use the `lifespan` as explained above. + +## Technical Details + +Just a technical detail for the curious nerds. 🤓 + +Underneath, in the ASGI technical specification, this is part of the Lifespan Protocol, and it defines events called `startup` and `shutdown`. + +## Sub Applications + +🚨 Have in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs_src/events/tutorial003.py b/docs_src/events/tutorial003.py new file mode 100644 index 000000000..2b650590b --- /dev/null +++ b/docs_src/events/tutorial003.py @@ -0,0 +1,28 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = {} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/predict") +async def predict(x: float): + result = ml_models["answer_to_everything"](x) + return {"result": result} diff --git a/fastapi/applications.py b/fastapi/applications.py index 204bd46b3..e864c4907 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,6 +1,7 @@ from enum import Enum from typing import ( Any, + AsyncContextManager, Awaitable, Callable, Coroutine, @@ -71,6 +72,7 @@ class FastAPI(Starlette): ] = None, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, + lifespan: Optional[Callable[["FastAPI"], AsyncContextManager[Any]]] = None, terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, @@ -125,6 +127,7 @@ class FastAPI(Starlette): dependency_overrides_provider=self, on_startup=on_startup, on_shutdown=on_shutdown, + lifespan=lifespan, default_response_class=default_response_class, dependencies=dependencies, callbacks=callbacks, diff --git a/fastapi/routing.py b/fastapi/routing.py index 7ab6275b6..5a618e4de 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -7,6 +7,7 @@ from contextlib import AsyncExitStack from enum import Enum, IntEnum from typing import ( Any, + AsyncContextManager, Callable, Coroutine, Dict, @@ -492,6 +493,7 @@ class APIRouter(routing.Router): route_class: Type[APIRoute] = APIRoute, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, + lifespan: Optional[Callable[[Any], AsyncContextManager[Any]]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default( @@ -504,6 +506,7 @@ class APIRouter(routing.Router): default=default, on_startup=on_startup, on_shutdown=on_shutdown, + lifespan=lifespan, ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 5ff1fdf9f..ba6b76382 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -1,3 +1,7 @@ +from contextlib import asynccontextmanager +from typing import AsyncGenerator, Dict + +import pytest from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -12,57 +16,49 @@ class State(BaseModel): sub_router_shutdown: bool = False -state = State() - -app = FastAPI() +@pytest.fixture +def state() -> State: + return State() -@app.on_event("startup") -def app_startup(): - state.app_startup = True +def test_router_events(state: State) -> None: + app = FastAPI() + @app.get("/") + def main() -> Dict[str, str]: + return {"message": "Hello World"} -@app.on_event("shutdown") -def app_shutdown(): - state.app_shutdown = True + @app.on_event("startup") + def app_startup() -> None: + state.app_startup = True + @app.on_event("shutdown") + def app_shutdown() -> None: + state.app_shutdown = True -router = APIRouter() + router = APIRouter() + @router.on_event("startup") + def router_startup() -> None: + state.router_startup = True -@router.on_event("startup") -def router_startup(): - state.router_startup = True + @router.on_event("shutdown") + def router_shutdown() -> None: + state.router_shutdown = True + sub_router = APIRouter() -@router.on_event("shutdown") -def router_shutdown(): - state.router_shutdown = True + @sub_router.on_event("startup") + def sub_router_startup() -> None: + state.sub_router_startup = True + @sub_router.on_event("shutdown") + def sub_router_shutdown() -> None: + state.sub_router_shutdown = True -sub_router = APIRouter() + router.include_router(sub_router) + app.include_router(router) - -@sub_router.on_event("startup") -def sub_router_startup(): - state.sub_router_startup = True - - -@sub_router.on_event("shutdown") -def sub_router_shutdown(): - state.sub_router_shutdown = True - - -@sub_router.get("/") -def main(): - return {"message": "Hello World"} - - -router.include_router(sub_router) -app.include_router(router) - - -def test_router_events(): assert state.app_startup is False assert state.router_startup is False assert state.sub_router_startup is False @@ -85,3 +81,28 @@ def test_router_events(): assert state.app_shutdown is True assert state.router_shutdown is True assert state.sub_router_shutdown is True + + +def test_app_lifespan_state(state: State) -> None: + @asynccontextmanager + async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + state.app_startup = True + yield + state.app_shutdown = True + + app = FastAPI(lifespan=lifespan) + + @app.get("/") + def main() -> Dict[str, str]: + return {"message": "Hello World"} + + assert state.app_startup is False + assert state.app_shutdown is False + with TestClient(app) as client: + assert state.app_startup is True + assert state.app_shutdown is False + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Hello World"} + assert state.app_startup is True + assert state.app_shutdown is True diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py new file mode 100644 index 000000000..56b493954 --- /dev/null +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -0,0 +1,86 @@ +from fastapi.testclient import TestClient + +from docs_src.events.tutorial003 import ( + app, + fake_answer_to_everything_ml_model, + ml_models, +) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/predict": { + "get": { + "summary": "Predict", + "operationId": "predict_predict_get", + "parameters": [ + { + "required": True, + "schema": {"title": "X", "type": "number"}, + "name": "x", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_events(): + assert not ml_models, "ml_models should be empty" + with TestClient(app) as client: + assert ml_models["answer_to_everything"] == fake_answer_to_everything_ml_model + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + response = client.get("/predict", params={"x": 2}) + assert response.status_code == 200, response.text + assert response.json() == {"result": 84.0} + assert not ml_models, "ml_models should be empty" From e33f30a607c36cb8e4f4f1b773b884ed1dbcc0d6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 Mar 2023 15:46:37 +0000 Subject: [PATCH 0835/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e8e173400..7136e1dca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `lifespan` async context managers (superseding `startup` and `shutdown` events). PR [#2944](https://github.com/tiangolo/fastapi/pull/2944) by [@uSpike](https://github.com/uSpike). * 🌐 Tamil translations - initial setup. PR [#5564](https://github.com/tiangolo/fastapi/pull/5564) by [@gusty1g](https://github.com/gusty1g). * 🌐 Add French translation for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`. PR [#9221](https://github.com/tiangolo/fastapi/pull/9221) by [@axel584](https://github.com/axel584). * 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). From b9bb441b2397bb003f344a3091244728c3401e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Mar 2023 17:04:40 +0100 Subject: [PATCH 0836/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 54 +++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 7136e1dca..6f3da51ad 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,13 +2,63 @@ ## Latest Changes -* ✨ Add support for `lifespan` async context managers (superseding `startup` and `shutdown` events). PR [#2944](https://github.com/tiangolo/fastapi/pull/2944) by [@uSpike](https://github.com/uSpike). +### Features + +* ✨ Add support for `lifespan` async context managers (superseding `startup` and `shutdown` events). Initial PR [#2944](https://github.com/tiangolo/fastapi/pull/2944) by [@uSpike](https://github.com/uSpike). + +Now, instead of using independent `startup` and `shutdown` events, you can define that logic in a single function with `yield` decorated with `@asynccontextmanager` (an async context manager). + +For example: + +```Python +from contextlib import asynccontextmanager + +from fastapi import FastAPI + + +def fake_answer_to_everything_ml_model(x: float): + return x * 42 + + +ml_models = {} + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Load the ML model + ml_models["answer_to_everything"] = fake_answer_to_everything_ml_model + yield + # Clean up the ML models and release the resources + ml_models.clear() + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/predict") +async def predict(x: float): + result = ml_models["answer_to_everything"](x) + return {"result": result} +``` + +**Note**: This is the recommended way going forward, instead of using `startup` and `shutdown` events. + +Read more about it in the new docs: [Advanced User Guide: Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + +### Docs + +* ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). + +### Translations + * 🌐 Tamil translations - initial setup. PR [#5564](https://github.com/tiangolo/fastapi/pull/5564) by [@gusty1g](https://github.com/gusty1g). * 🌐 Add French translation for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`. PR [#9221](https://github.com/tiangolo/fastapi/pull/9221) by [@axel584](https://github.com/axel584). * 🌐 Add French translation for `docs/tutorial/debugging.md`. PR [#9175](https://github.com/tiangolo/fastapi/pull/9175) by [@frabc](https://github.com/frabc). * 🌐 Initiate Armenian translation setup. PR [#5844](https://github.com/tiangolo/fastapi/pull/5844) by [@har8](https://github.com/har8). -* ✏ Fix formatting in `docs/en/docs/tutorial/metadata.md` for `ReDoc`. PR [#6005](https://github.com/tiangolo/fastapi/pull/6005) by [@eykamp](https://github.com/eykamp). * 🌐 Add French translation for `deployment/manually.md`. PR [#3693](https://github.com/tiangolo/fastapi/pull/3693) by [@rjNemo](https://github.com/rjNemo). + +### Internal + * 👷 Update translation bot messages. PR [#9206](https://github.com/tiangolo/fastapi/pull/9206) by [@tiangolo](https://github.com/tiangolo). * 👷 Update translations bot to use Discussions, and notify when a PR is done. PR [#9183](https://github.com/tiangolo/fastapi/pull/9183) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors-badges. PR [#9182](https://github.com/tiangolo/fastapi/pull/9182) by [@tiangolo](https://github.com/tiangolo). From 25382d2d194bc3050d09e040889d30f5d64a5d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Mar 2023 17:06:47 +0100 Subject: [PATCH 0837/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?93.0?= 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 6f3da51ad..b1c5318d5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.93.0 + ### Features * ✨ Add support for `lifespan` async context managers (superseding `startup` and `shutdown` events). Initial PR [#2944](https://github.com/tiangolo/fastapi/pull/2944) by [@uSpike](https://github.com/uSpike). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 33875c7fa..afa5405c5 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.92.0" +__version__ = "0.93.0" from starlette import status as status From 8a4cfa52afce69f734e4844b1c06e394c891f795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Mar 2023 19:24:04 +0100 Subject: [PATCH 0838/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20version,=20support=20new=20`lifespan`=20with=20state=20(#?= =?UTF-8?q?9239)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/events.md | 8 +++++--- fastapi/applications.py | 5 ++--- fastapi/routing.py | 5 ++--- pyproject.toml | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index 556bbde71..6b7de4130 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -138,9 +138,6 @@ Here, the `shutdown` event handler function will write a text line `"Application So, we declare the event handler function with standard `def` instead of `async def`. -!!! info - You can read more about these event handlers in Starlette's Events' docs. - ### `startup` and `shutdown` together There's a high chance that the logic for your *startup* and *shutdown* is connected, you might want to start something and then finish it, acquire a resource and then release it, etc. @@ -155,6 +152,11 @@ Just a technical detail for the curious nerds. 🤓 Underneath, in the ASGI technical specification, this is part of the Lifespan Protocol, and it defines events called `startup` and `shutdown`. +!!! info + You can read more about the Starlette `lifespan` handlers in Starlette's Lifespan' docs. + + Including how to handle lifespan state that can be used in other areas of your code. + ## Sub Applications 🚨 Have in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. diff --git a/fastapi/applications.py b/fastapi/applications.py index e864c4907..330525936 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1,7 +1,6 @@ from enum import Enum from typing import ( Any, - AsyncContextManager, Awaitable, Callable, Coroutine, @@ -42,7 +41,7 @@ from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute -from starlette.types import ASGIApp, Receive, Scope, Send +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send class FastAPI(Starlette): @@ -72,7 +71,7 @@ class FastAPI(Starlette): ] = None, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - lifespan: Optional[Callable[["FastAPI"], AsyncContextManager[Any]]] = None, + lifespan: Optional[Lifespan] = None, terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index 5a618e4de..7e48c8c3e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -7,7 +7,6 @@ from contextlib import AsyncExitStack from enum import Enum, IntEnum from typing import ( Any, - AsyncContextManager, Callable, Coroutine, Dict, @@ -58,7 +57,7 @@ from starlette.routing import ( websocket_session, ) from starlette.status import WS_1008_POLICY_VIOLATION -from starlette.types import ASGIApp, Scope +from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket @@ -493,7 +492,7 @@ class APIRouter(routing.Router): route_class: Type[APIRoute] = APIRoute, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - lifespan: Optional[Callable[[Any], AsyncContextManager[Any]]] = None, + lifespan: Optional[Lifespan] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default( diff --git a/pyproject.toml b/pyproject.toml index 3e651ae36..5b9d00276 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.25.0,<0.26.0", + "starlette>=0.26.0,<0.27.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From d783463ebd6ac9f309fc8a756a8778618f0db164 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:24:42 +0000 Subject: [PATCH 0839/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b1c5318d5..94fdc06f8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). ## 0.93.0 From 1fea9c5626dd40511106820bd676732f4619f7af Mon Sep 17 00:00:00 2001 From: Steven Eubank <47563310+smeubank@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:27:10 +0100 Subject: [PATCH 0840/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20Sentry=20link?= =?UTF-8?q?=20in=20docs=20(#9218)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 3bf49e392..9219f1d2c 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -92,7 +92,7 @@ There are many other ASGI middlewares. For example: -* Sentry +* Sentry * Uvicorn's `ProxyHeadersMiddleware` * MessagePack From fd3bfe9f505c149fb3aa7ece22a112eb87216846 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:27:50 +0000 Subject: [PATCH 0841/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 94fdc06f8..31cd287f0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). * ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). ## 0.93.0 From 42daed222e7106f08baa95e3cd7ba4ea864fa228 Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Fri, 10 Mar 2023 13:31:36 -0500 Subject: [PATCH 0842/1881] =?UTF-8?q?=E2=AC=86=20Upgrade=20python-multipar?= =?UTF-8?q?t=20to=20support=200.0.6=20(#9212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b9d00276..6748f1d4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ test = [ "databases[sqlite] >=0.3.2,<0.7.0", "orjson >=3.2.1,<4.0.0", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", - "python-multipart >=0.0.5,<0.0.6", + "python-multipart >=0.0.5,<0.0.7", "flask >=1.1.2,<3.0.0", "anyio[trio] >=3.2.1,<4.0.0", "python-jose[cryptography] >=3.3.0,<4.0.0", From d5b0cc9f5869dcb693136c17c87949aaac589f83 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:32:12 +0000 Subject: [PATCH 0843/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 31cd287f0..d40fd2624 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). * 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). * ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). From d1f3753e5e728b96aa27996499b30fa7f617c0a1 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Fri, 10 Mar 2023 21:42:25 +0300 Subject: [PATCH 0844/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/history-design-future.md`=20(#5986)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/history-design-future.md | 77 +++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 78 insertions(+) create mode 100644 docs/ru/docs/history-design-future.md diff --git a/docs/ru/docs/history-design-future.md b/docs/ru/docs/history-design-future.md new file mode 100644 index 000000000..2a5e428b1 --- /dev/null +++ b/docs/ru/docs/history-design-future.md @@ -0,0 +1,77 @@ +# История создания и дальнейшее развитие + +Однажды, один из пользователей **FastAPI** задал вопрос: + +> Какова история этого проекта? Создаётся впечатление, что он явился из ниоткуда и завоевал мир за несколько недель [...] + +Что ж, вот небольшая часть истории проекта. + +## Альтернативы + +В течение нескольких лет я, возглавляя различные команды разработчиков, создавал довольно сложные API для машинного обучения, распределённых систем, асинхронных задач, баз данных NoSQL и т.д. + +В рамках работы над этими проектами я исследовал, проверял и использовал многие фреймворки. + +Во многом история **FastAPI** - история его предшественников. + +Как написано в разделе [Альтернативы](alternatives.md){.internal-link target=_blank}: + +
+ +**FastAPI** не существовал бы, если б не было более ранних работ других людей. + +Они создали большое количество инструментов, которые и вдохновили меня на создание **FastAPI**. + +Я всячески избегал создания нового фреймворка в течение нескольких лет. Сначала я пытался собрать все нужные возможности, которые ныне есть в **FastAPI**, используя множество различных фреймворков, плагинов и инструментов. + +Но в какой-то момент не осталось другого выбора, кроме как создать что-то, что предоставляло бы все эти возможности сразу. Взять самые лучшие идеи из предыдущих инструментов и, используя введённые в Python подсказки типов (которых не было до версии 3.6), объединить их. + +
+ +## Исследования + +Благодаря опыту использования существующих альтернатив, мы с коллегами изучили их основные идеи и скомбинировали собранные знания наилучшим образом. + +Например, стало ясно, что необходимо брать за основу стандартные подсказки типов Python, а самым лучшим подходом является использование уже существующих стандартов. + +Итак, прежде чем приступить к написанию **FastAPI**, я потратил несколько месяцев на изучение OpenAPI, JSON Schema, OAuth2, и т.п. для понимания их взаимосвязей, совпадений и различий. + +## Дизайн + +Затем я потратил некоторое время на придумывание "API" разработчика, который я хотел иметь как пользователь (как разработчик, использующий FastAPI). + +Я проверил несколько идей на самых популярных редакторах кода среди Python-разработчиков: PyCharm, VS Code, Jedi. + +Данные по редакторам я взял из опроса Python-разработчиков, который охватываает около 80% пользователей. + +Это означает, что **FastAPI** был специально проверен на редакторах, используемых 80% Python-разработчиками. И поскольку большинство других редакторов, как правило, работают аналогичным образом, все его преимущества должны работать практически для всех редакторов. + +Таким образом, я смог найти наилучшие способы сократить дублирование кода, обеспечить повсеместное автодополнение, проверку типов и ошибок и т.д. + +И все это, чтобы все пользователи могли получать наилучший опыт разработки. + +## Зависимости + +Протестировав несколько вариантов, я решил, что в качестве основы буду использовать **Pydantic** и его преимущества. + +По моим предложениям был изменён код этого фреймворка, чтобы сделать его полностью совместимым с JSON Schema, поддержать различные способы определения ограничений и улучшить помощь редакторов (проверки типов, автозаполнение). + +В то же время, я принимал участие в разработке **Starlette**, ещё один из основных компонентов FastAPI. + +## Разработка + +К тому времени, когда я начал создавать **FastAPI**, большинство необходимых деталей уже существовало, дизайн был определён, зависимости и прочие инструменты были готовы, а знания о стандартах и спецификациях были четкими и свежими. + +## Будущее + +Сейчас уже ясно, что **FastAPI** со своими идеями стал полезен многим людям. + +При сравнении с альтернативами, выбор падает на него, поскольку он лучше подходит для множества вариантов использования. + +Многие разработчики и команды уже используют **FastAPI** в своих проектах (включая меня и мою команду). + +Но, тем не менее, грядёт добавление ещё многих улучшений и возможностей. + +У **FastAPI** великое будущее. + +И [ваш вклад в это](help-fastapi.md){.internal-link target=_blank} - очень ценнен. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index daacad71c..4b9727872 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -71,6 +71,7 @@ nav: - Развёртывание: - deployment/index.md - deployment/versions.md +- history-design-future.md - external-links.md - contributing.md markdown_extensions: From c26db94a90e73241f73dd39a381affe9df5c451e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:43:10 +0000 Subject: [PATCH 0845/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d40fd2624..cfda5aece 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). * ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). * 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). * ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). From 78b8a9b6ec6b70c9564ef7fa616da587592f3aa7 Mon Sep 17 00:00:00 2001 From: Yasser Tahiri Date: Fri, 10 Mar 2023 22:47:38 +0400 Subject: [PATCH 0846/1881] =?UTF-8?q?=E2=9E=95=20Add=20`pydantic`=20to=20P?= =?UTF-8?q?yPI=20classifiers=20(#5914)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 6748f1d4a..f18549396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,8 @@ classifiers = [ "Environment :: Web Environment", "Framework :: AsyncIO", "Framework :: FastAPI", + "Framework :: Pydantic", + "Framework :: Pydantic :: 1", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", From 253d58bc5ce120db452e2a5a4c04218938cc18a0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:48:20 +0000 Subject: [PATCH 0847/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cfda5aece..bcfa3ce2d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). * ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). * 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). From f04b61bd1633e5e4418edfa359ab6a0320520735 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:49:18 +0100 Subject: [PATCH 0848/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#5709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .pre-commit-config.yaml | 8 ++++---- docs_src/body_multiple_params/tutorial004.py | 2 +- docs_src/body_multiple_params/tutorial004_py310.py | 2 +- docs_src/path_params_numeric_validations/tutorial006.py | 2 +- fastapi/dependencies/utils.py | 2 +- fastapi/routing.py | 1 - fastapi/security/api_key.py | 6 +++--- fastapi/security/oauth2.py | 2 +- fastapi/security/open_id_connect_url.py | 2 +- .../test_request_files/test_tutorial002_py39.py | 1 - .../test_request_files/test_tutorial003_py39.py | 1 - 11 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 01cd6ea0f..25e797d24 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ default_language_version: python: python3.10 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-added-large-files - id: check-toml @@ -14,14 +14,14 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.2.2 + rev: v3.3.1 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.138 + rev: v0.0.254 hooks: - id: ruff args: @@ -38,7 +38,7 @@ repos: name: isort (pyi) types: [pyi] - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.1.0 hooks: - id: black ci: diff --git a/docs_src/body_multiple_params/tutorial004.py b/docs_src/body_multiple_params/tutorial004.py index beea7d1e3..8ce4c7a97 100644 --- a/docs_src/body_multiple_params/tutorial004.py +++ b/docs_src/body_multiple_params/tutorial004.py @@ -25,7 +25,7 @@ async def update_item( item: Item, user: User, importance: int = Body(gt=0), - q: Union[str, None] = None + q: Union[str, None] = None, ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: diff --git a/docs_src/body_multiple_params/tutorial004_py310.py b/docs_src/body_multiple_params/tutorial004_py310.py index 6d495d408..14acbc3b4 100644 --- a/docs_src/body_multiple_params/tutorial004_py310.py +++ b/docs_src/body_multiple_params/tutorial004_py310.py @@ -23,7 +23,7 @@ async def update_item( item: Item, user: User, importance: int = Body(gt=0), - q: str | None = None + q: str | None = None, ): results = {"item_id": item_id, "item": item, "user": user, "importance": importance} if q: diff --git a/docs_src/path_params_numeric_validations/tutorial006.py b/docs_src/path_params_numeric_validations/tutorial006.py index 85bd6e8b4..0ea32694a 100644 --- a/docs_src/path_params_numeric_validations/tutorial006.py +++ b/docs_src/path_params_numeric_validations/tutorial006.py @@ -8,7 +8,7 @@ async def read_items( *, item_id: int = Path(title="The ID of the item to get", ge=0, le=1000), q: str, - size: float = Query(gt=0, lt=10.5) + size: float = Query(gt=0, lt=10.5), ): results = {"item_id": item_id} if q: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 32e171f18..a982b071a 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -696,7 +696,7 @@ async def request_body_to_args( fn: Callable[[], Coroutine[Any, Any, Any]] ) -> None: result = await fn() - results.append(result) + results.append(result) # noqa: B023 async with anyio.create_task_group() as tg: for sub_value in value: diff --git a/fastapi/routing.py b/fastapi/routing.py index 7e48c8c3e..c227ea6ba 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1250,7 +1250,6 @@ class APIRouter(routing.Router): generate_unique_id ), ) -> Callable[[DecoratedCallable], DecoratedCallable]: - return self.api_route( path=path, response_model=response_model, diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 24ddbf482..61730187a 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -18,7 +18,7 @@ class APIKeyQuery(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.query}, name=name, description=description @@ -45,7 +45,7 @@ class APIKeyHeader(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.header}, name=name, description=description @@ -72,7 +72,7 @@ class APIKeyCookie(APIKeyBase): name: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.cookie}, name=name, description=description diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index eb6b4277c..dc75dc9fe 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -119,7 +119,7 @@ class OAuth2(SecurityBase): flows: Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]] = OAuthFlowsModel(), scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model = OAuth2Model(flows=flows, description=description) self.scheme_name = scheme_name or self.__class__.__name__ diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 393614f7c..4e65f1f6c 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -14,7 +14,7 @@ class OpenIdConnect(SecurityBase): openIdConnectUrl: str, scheme_name: Optional[str] = None, description: Optional[str] = None, - auto_error: bool = True + auto_error: bool = True, ): self.model = OpenIdConnectModel( openIdConnectUrl=openIdConnectUrl, description=description diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index de4127057..633795dee 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -150,7 +150,6 @@ def get_app(): @pytest.fixture(name="client") def get_client(app: FastAPI): - client = TestClient(app) return client diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py index 56aeb54cd..474da8ba0 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py @@ -152,7 +152,6 @@ def get_app(): @pytest.fixture(name="client") def get_client(app: FastAPI): - client = TestClient(app) return client From 59f91db1d28c0bb0fd23ef476e95d16757e5a46c Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:49:54 +0000 Subject: [PATCH 0849/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bcfa3ce2d..a7a2a11a2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). * ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). From c8a07078cfe43fdb7da35fe8fc14ce9ff3f143db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:50:08 +0100 Subject: [PATCH 0850/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.24.3=20to=202.26.0=20(#6034)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 2af56e2bc..cf0db59ab 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -16,7 +16,7 @@ jobs: rm -rf ./site mkdir ./site - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.24.3 + uses: dawidd6/action-download-artifact@v2.26.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index d6206d697..421720433 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.24.3 + - uses: dawidd6/action-download-artifact@v2.26.0 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From 3e3278ed3f4a6f021a801d411459e5271f425e73 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:51:19 +0000 Subject: [PATCH 0851/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a7a2a11a2..b750e4acc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.24.3 to 2.26.0. PR [#6034](https://github.com/tiangolo/fastapi/pull/6034) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). * 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). From 39813aa9b0e382fafda794339e7f59cb3b3bfc63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:57:07 +0100 Subject: [PATCH 0852/1881] =?UTF-8?q?=E2=AC=86=20Bump=20types-ujson=20from?= =?UTF-8?q?=205.6.0.0=20to=205.7.0.1=20(#6027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 f18549396..470d56723 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ test = [ "passlib[bcrypt] >=1.7.2,<2.0.0", # types - "types-ujson ==5.6.0.0", + "types-ujson ==5.7.0.1", "types-orjson ==3.6.2", ] doc = [ From 8e4c96c703b3fc31d8b4cfcf34c81e31d52e9f0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Mar 2023 19:57:21 +0100 Subject: [PATCH 0853/1881] =?UTF-8?q?=E2=AC=86=20Bump=20black=20from=2022.?= =?UTF-8?q?10.0=20to=2023.1.0=20(#5953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 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 470d56723..2ae582722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ test = [ "coverage[toml] >= 6.5.0,< 8.0", "mypy ==0.982", "ruff ==0.0.138", - "black == 22.10.0", + "black == 23.1.0", "isort >=5.0.6,<6.0.0", "httpx >=0.23.0,<0.24.0", "email_validator >=1.1.1,<2.0.0", From 486063146841030ee5604d2f603b5c9575ea3d5a Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:57:42 +0000 Subject: [PATCH 0854/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b750e4acc..4f035ae8c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump types-ujson from 5.6.0.0 to 5.7.0.1. PR [#6027](https://github.com/tiangolo/fastapi/pull/6027) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.3 to 2.26.0. PR [#6034](https://github.com/tiangolo/fastapi/pull/6034) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). From 321e873c95baeaedb0366b340c83017a580c9b9d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 10 Mar 2023 18:57:58 +0000 Subject: [PATCH 0855/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4f035ae8c..f1c942ef5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump black from 22.10.0 to 23.1.0. PR [#5953](https://github.com/tiangolo/fastapi/pull/5953) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump types-ujson from 5.6.0.0 to 5.7.0.1. PR [#6027](https://github.com/tiangolo/fastapi/pull/6027) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.3 to 2.26.0. PR [#6034](https://github.com/tiangolo/fastapi/pull/6034) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 202ee0497a7b9e5239d54b8326a3cfe7f459b78c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Mar 2023 20:00:09 +0100 Subject: [PATCH 0856/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f1c942ef5..9ff7e8793 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,15 +2,26 @@ ## Latest Changes +### Upgrades + +* ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). +* ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). + +### Internal + +* ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). * ⬆ Bump black from 22.10.0 to 23.1.0. PR [#5953](https://github.com/tiangolo/fastapi/pull/5953) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump types-ujson from 5.6.0.0 to 5.7.0.1. PR [#6027](https://github.com/tiangolo/fastapi/pull/6027) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.24.3 to 2.26.0. PR [#6034](https://github.com/tiangolo/fastapi/pull/6034) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#5709](https://github.com/tiangolo/fastapi/pull/5709) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* ➕ Add `pydantic` to PyPI classifiers. PR [#5914](https://github.com/tiangolo/fastapi/pull/5914) by [@yezz123](https://github.com/yezz123). -* 🌐 Add Russian translation for `docs/ru/docs/history-design-future.md`. PR [#5986](https://github.com/tiangolo/fastapi/pull/5986) by [@Xewus](https://github.com/Xewus). -* ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). -* 📝 Update Sentry link in docs. PR [#9218](https://github.com/tiangolo/fastapi/pull/9218) by [@smeubank](https://github.com/smeubank). -* ⬆️ Upgrade Starlette version, support new `lifespan` with state. PR [#9239](https://github.com/tiangolo/fastapi/pull/9239) by [@tiangolo](https://github.com/tiangolo). ## 0.93.0 From 392ffaae43d11838ece62acfa8bb50e5d2f5ca05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 10 Mar 2023 20:00:49 +0100 Subject: [PATCH 0857/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?94.0?= 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 9ff7e8793..192fa7a6b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.94.0 + ### Upgrades * ⬆ Upgrade python-multipart to support 0.0.6. PR [#9212](https://github.com/tiangolo/fastapi/pull/9212) by [@musicinmybrain](https://github.com/musicinmybrain). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index afa5405c5..e71b648f9 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.93.0" +__version__ = "0.94.0" from starlette import status as status From 25aabe05ce68c69d7e66d5988c23bd63902407ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 14 Mar 2023 03:19:04 +0100 Subject: [PATCH 0858/1881] =?UTF-8?q?=F0=9F=8E=A8=20Fix=20types=20for=20li?= =?UTF-8?q?fespan,=20upgrade=20Starlette=20to=200.26.1=20(#9245)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 7 +++++-- fastapi/routing.py | 4 +++- pyproject.toml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 330525936..8b3a74d3c 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -9,6 +9,7 @@ from typing import ( Optional, Sequence, Type, + TypeVar, Union, ) @@ -43,10 +44,12 @@ from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +AppType = TypeVar("AppType", bound="FastAPI") + class FastAPI(Starlette): def __init__( - self, + self: AppType, *, debug: bool = False, routes: Optional[List[BaseRoute]] = None, @@ -71,7 +74,7 @@ class FastAPI(Starlette): ] = None, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - lifespan: Optional[Lifespan] = None, + lifespan: Optional[Lifespan[AppType]] = None, terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, diff --git a/fastapi/routing.py b/fastapi/routing.py index c227ea6ba..06c71bffa 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -492,7 +492,9 @@ class APIRouter(routing.Router): route_class: Type[APIRoute] = APIRoute, on_startup: Optional[Sequence[Callable[[], Any]]] = None, on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - lifespan: Optional[Lifespan] = None, + # the generic to Lifespan[AppType] is the type of the top level application + # which the router cannot know statically, so we use typing.Any + lifespan: Optional[Lifespan[Any]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, generate_unique_id_function: Callable[[APIRoute], str] = Default( diff --git a/pyproject.toml b/pyproject.toml index 2ae582722..b8d006359 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.26.0,<0.27.0", + "starlette>=0.26.1,<0.27.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From 7b7e86a3078e71442fda69a1e487d26b90760747 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 14 Mar 2023 02:19:50 +0000 Subject: [PATCH 0859/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 192fa7a6b..261e2f697 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🎨 Fix types for lifespan, upgrade Starlette to 0.26.1. PR [#9245](https://github.com/tiangolo/fastapi/pull/9245) by [@tiangolo](https://github.com/tiangolo). ## 0.94.0 From ef176c663195489b44030bfe1fb94a317762c8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 14 Mar 2023 03:27:11 +0100 Subject: [PATCH 0860/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?94.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 5 +++++ fastapi/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 261e2f697..118ee1dc7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,11 @@ ## Latest Changes + +## 0.94.1 + +### Fixes + * 🎨 Fix types for lifespan, upgrade Starlette to 0.26.1. PR [#9245](https://github.com/tiangolo/fastapi/pull/9245) by [@tiangolo](https://github.com/tiangolo). ## 0.94.0 diff --git a/fastapi/__init__.py b/fastapi/__init__.py index e71b648f9..05da7b759 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.94.0" +__version__ = "0.94.1" from starlette import status as status From 375513f11494bc3499050ad2a0d378fb6e37ca98 Mon Sep 17 00:00:00 2001 From: Nadav Zingerman <7372858+nzig@users.noreply.github.com> Date: Fri, 17 Mar 2023 22:35:45 +0200 Subject: [PATCH 0861/1881] =?UTF-8?q?=E2=9C=A8Add=20support=20for=20PEP-59?= =?UTF-8?q?3=20`Annotated`=20for=20specifying=20dependencies=20and=20param?= =?UTF-8?q?eters=20(#4871)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs_src/annotated/tutorial001.py | 18 ++ docs_src/annotated/tutorial001_py39.py | 17 ++ docs_src/annotated/tutorial002.py | 21 ++ docs_src/annotated/tutorial002_py39.py | 20 ++ docs_src/annotated/tutorial003.py | 15 + docs_src/annotated/tutorial003_py39.py | 16 + fastapi/dependencies/utils.py | 276 +++++++++++------- fastapi/param_functions.py | 2 +- fastapi/params.py | 9 +- fastapi/utils.py | 23 +- tests/main.py | 7 +- tests/test_ambiguous_params.py | 66 +++++ tests/test_annotated.py | 226 ++++++++++++++ tests/test_application.py | 30 -- tests/test_params_repr.py | 5 +- tests/test_path.py | 1 - .../test_tutorial/test_annotated/__init__.py | 0 .../test_annotated/test_tutorial001.py | 100 +++++++ .../test_annotated/test_tutorial001_py39.py | 107 +++++++ .../test_annotated/test_tutorial002.py | 100 +++++++ .../test_annotated/test_tutorial002_py39.py | 107 +++++++ .../test_annotated/test_tutorial003.py | 138 +++++++++ .../test_annotated/test_tutorial003_py39.py | 145 +++++++++ .../test_dataclasses/__init__.py | 0 24 files changed, 1293 insertions(+), 156 deletions(-) create mode 100644 docs_src/annotated/tutorial001.py create mode 100644 docs_src/annotated/tutorial001_py39.py create mode 100644 docs_src/annotated/tutorial002.py create mode 100644 docs_src/annotated/tutorial002_py39.py create mode 100644 docs_src/annotated/tutorial003.py create mode 100644 docs_src/annotated/tutorial003_py39.py create mode 100644 tests/test_ambiguous_params.py create mode 100644 tests/test_annotated.py create mode 100644 tests/test_tutorial/test_annotated/__init__.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial001.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial002.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial002_py39.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial003.py create mode 100644 tests/test_tutorial/test_annotated/test_tutorial003_py39.py create mode 100644 tests/test_tutorial/test_dataclasses/__init__.py diff --git a/docs_src/annotated/tutorial001.py b/docs_src/annotated/tutorial001.py new file mode 100644 index 000000000..959114b3f --- /dev/null +++ b/docs_src/annotated/tutorial001.py @@ -0,0 +1,18 @@ +from typing import Optional + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonParamsDepends = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonParamsDepends): + return commons diff --git a/docs_src/annotated/tutorial001_py39.py b/docs_src/annotated/tutorial001_py39.py new file mode 100644 index 000000000..b05b89c4e --- /dev/null +++ b/docs_src/annotated/tutorial001_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Optional + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonParamsDepends = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonParamsDepends): + return commons diff --git a/docs_src/annotated/tutorial002.py b/docs_src/annotated/tutorial002.py new file mode 100644 index 000000000..2293fbb1a --- /dev/null +++ b/docs_src/annotated/tutorial002.py @@ -0,0 +1,21 @@ +from typing import Optional + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +class CommonQueryParams: + def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] + + +@app.get("/items/") +async def read_items(commons: CommonQueryParamsDepends): + return commons diff --git a/docs_src/annotated/tutorial002_py39.py b/docs_src/annotated/tutorial002_py39.py new file mode 100644 index 000000000..7fa1a8740 --- /dev/null +++ b/docs_src/annotated/tutorial002_py39.py @@ -0,0 +1,20 @@ +from typing import Annotated, Optional + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +class CommonQueryParams: + def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] + + +@app.get("/items/") +async def read_items(commons: CommonQueryParamsDepends): + return commons diff --git a/docs_src/annotated/tutorial003.py b/docs_src/annotated/tutorial003.py new file mode 100644 index 000000000..353d8b806 --- /dev/null +++ b/docs_src/annotated/tutorial003.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Path +from fastapi.param_functions import Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items(item_id: Annotated[int, Path(gt=0)]): + return {"item_id": item_id} + + +@app.get("/users") +async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): + return {"user_id": user_id} diff --git a/docs_src/annotated/tutorial003_py39.py b/docs_src/annotated/tutorial003_py39.py new file mode 100644 index 000000000..9341b7d4f --- /dev/null +++ b/docs_src/annotated/tutorial003_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path +from fastapi.param_functions import Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items(item_id: Annotated[int, Path(gt=0)]): + return {"item_id": item_id} + + +@app.get("/users") +async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): + return {"user_id": user_id} diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a982b071a..c581348c9 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -48,7 +48,7 @@ from pydantic.fields import ( Undefined, ) from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import evaluate_forwardref +from pydantic.typing import evaluate_forwardref, get_args, get_origin from pydantic.utils import lenient_issubclass from starlette.background import BackgroundTasks from starlette.concurrency import run_in_threadpool @@ -56,6 +56,7 @@ from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket +from typing_extensions import Annotated sequence_shapes = { SHAPE_LIST, @@ -112,18 +113,18 @@ def check_file_field(field: ModelField) -> None: def get_param_sub_dependant( - *, param: inspect.Parameter, path: str, security_scopes: Optional[List[str]] = None + *, + param_name: str, + depends: params.Depends, + path: str, + security_scopes: Optional[List[str]] = None, ) -> Dependant: - depends: params.Depends = param.default - if depends.dependency: - dependency = depends.dependency - else: - dependency = param.annotation + assert depends.dependency return get_sub_dependant( depends=depends, - dependency=dependency, + dependency=depends.dependency, path=path, - name=param.name, + name=param_name, security_scopes=security_scopes, ) @@ -298,122 +299,199 @@ def get_dependant( use_cache=use_cache, ) for param_name, param in signature_params.items(): - if isinstance(param.default, params.Depends): + is_path_param = param_name in path_param_names + type_annotation, depends, param_field = analyze_param( + param_name=param_name, + annotation=param.annotation, + value=param.default, + is_path_param=is_path_param, + ) + if depends is not None: sub_dependant = get_param_sub_dependant( - param=param, path=path, security_scopes=security_scopes + param_name=param_name, + depends=depends, + path=path, + security_scopes=security_scopes, ) dependant.dependencies.append(sub_dependant) continue - if add_non_field_param_to_dependency(param=param, dependant=dependant): + if add_non_field_param_to_dependency( + param_name=param_name, + type_annotation=type_annotation, + dependant=dependant, + ): + assert ( + param_field is None + ), f"Cannot specify multiple FastAPI annotations for {param_name!r}" continue - param_field = get_param_field( - param=param, default_field_info=params.Query, param_name=param_name - ) - if param_name in path_param_names: - assert is_scalar_field( - field=param_field - ), "Path params must be of one of the supported types" - ignore_default = not isinstance(param.default, params.Path) - param_field = get_param_field( - param=param, - param_name=param_name, - default_field_info=params.Path, - force_type=params.ParamTypes.path, - ignore_default=ignore_default, - ) - add_param_to_fields(field=param_field, dependant=dependant) - elif is_scalar_field(field=param_field): - add_param_to_fields(field=param_field, dependant=dependant) - elif isinstance( - param.default, (params.Query, params.Header) - ) and is_scalar_sequence_field(param_field): - add_param_to_fields(field=param_field, dependant=dependant) - else: - field_info = param_field.field_info - assert isinstance( - field_info, params.Body - ), f"Param: {param_field.name} can only be a request body, using Body()" + assert param_field is not None + if is_body_param(param_field=param_field, is_path_param=is_path_param): dependant.body_params.append(param_field) + else: + add_param_to_fields(field=param_field, dependant=dependant) return dependant def add_non_field_param_to_dependency( - *, param: inspect.Parameter, dependant: Dependant + *, param_name: str, type_annotation: Any, dependant: Dependant ) -> Optional[bool]: - if lenient_issubclass(param.annotation, Request): - dependant.request_param_name = param.name + if lenient_issubclass(type_annotation, Request): + dependant.request_param_name = param_name return True - elif lenient_issubclass(param.annotation, WebSocket): - dependant.websocket_param_name = param.name + elif lenient_issubclass(type_annotation, WebSocket): + dependant.websocket_param_name = param_name return True - elif lenient_issubclass(param.annotation, HTTPConnection): - dependant.http_connection_param_name = param.name + elif lenient_issubclass(type_annotation, HTTPConnection): + dependant.http_connection_param_name = param_name return True - elif lenient_issubclass(param.annotation, Response): - dependant.response_param_name = param.name + elif lenient_issubclass(type_annotation, Response): + dependant.response_param_name = param_name return True - elif lenient_issubclass(param.annotation, BackgroundTasks): - dependant.background_tasks_param_name = param.name + elif lenient_issubclass(type_annotation, BackgroundTasks): + dependant.background_tasks_param_name = param_name return True - elif lenient_issubclass(param.annotation, SecurityScopes): - dependant.security_scopes_param_name = param.name + elif lenient_issubclass(type_annotation, SecurityScopes): + dependant.security_scopes_param_name = param_name return True return None -def get_param_field( +def analyze_param( *, - param: inspect.Parameter, param_name: str, - default_field_info: Type[params.Param] = params.Param, - force_type: Optional[params.ParamTypes] = None, - ignore_default: bool = False, -) -> ModelField: - default_value: Any = Undefined - had_schema = False - if not param.default == param.empty and ignore_default is False: - default_value = param.default - if isinstance(default_value, FieldInfo): - had_schema = True - field_info = default_value - default_value = field_info.default - if ( + annotation: Any, + value: Any, + is_path_param: bool, +) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: + field_info = None + used_default_field_info = False + depends = None + type_annotation: Any = Any + if ( + annotation is not inspect.Signature.empty + and get_origin(annotation) is Annotated # type: ignore[comparison-overlap] + ): + annotated_args = get_args(annotation) + type_annotation = annotated_args[0] + fastapi_annotations = [ + arg + for arg in annotated_args[1:] + if isinstance(arg, (FieldInfo, params.Depends)) + ] + assert ( + len(fastapi_annotations) <= 1 + ), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}" + fastapi_annotation = next(iter(fastapi_annotations), None) + if isinstance(fastapi_annotation, FieldInfo): + field_info = fastapi_annotation + assert field_info.default is Undefined or field_info.default is Required, ( + f"`{field_info.__class__.__name__}` default value cannot be set in" + f" `Annotated` for {param_name!r}. Set the default value with `=` instead." + ) + if value is not inspect.Signature.empty: + assert not is_path_param, "Path parameters cannot have default values" + field_info.default = value + else: + field_info.default = Required + elif isinstance(fastapi_annotation, params.Depends): + depends = fastapi_annotation + elif annotation is not inspect.Signature.empty: + type_annotation = annotation + + if isinstance(value, params.Depends): + assert depends is None, ( + "Cannot specify `Depends` in `Annotated` and default value" + f" together for {param_name!r}" + ) + assert field_info is None, ( + "Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a" + f" default value together for {param_name!r}" + ) + depends = value + elif isinstance(value, FieldInfo): + assert field_info is None, ( + "Cannot specify FastAPI annotations in `Annotated` and default value" + f" together for {param_name!r}" + ) + field_info = value + + if depends is not None and depends.dependency is None: + depends.dependency = type_annotation + + if lenient_issubclass( + type_annotation, + (Request, WebSocket, HTTPConnection, Response, BackgroundTasks, SecurityScopes), + ): + assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}" + assert ( + field_info is None + ), f"Cannot specify FastAPI annotation for type {type_annotation!r}" + elif field_info is None and depends is None: + default_value = value if value is not inspect.Signature.empty else Required + if is_path_param: + # We might check here that `default_value is Required`, but the fact is that the same + # parameter might sometimes be a path parameter and sometimes not. See + # `tests/test_infer_param_optionality.py` for an example. + field_info = params.Path() + else: + field_info = params.Query(default=default_value) + used_default_field_info = True + + field = None + if field_info is not None: + if is_path_param: + assert isinstance(field_info, params.Path), ( + f"Cannot use `{field_info.__class__.__name__}` for path param" + f" {param_name!r}" + ) + elif ( isinstance(field_info, params.Param) and getattr(field_info, "in_", None) is None ): - field_info.in_ = default_field_info.in_ - if force_type: - field_info.in_ = force_type # type: ignore - else: - field_info = default_field_info(default=default_value) - required = True - if default_value is Required or ignore_default: - required = True - default_value = None - elif default_value is not Undefined: - required = False - annotation: Any = Any - if not param.annotation == param.empty: - annotation = param.annotation - annotation = get_annotation_from_field_info(annotation, field_info, param_name) - if not field_info.alias and getattr(field_info, "convert_underscores", None): - alias = param.name.replace("_", "-") - else: - alias = field_info.alias or param.name - field = create_response_field( - name=param.name, - type_=annotation, - default=default_value, - alias=alias, - required=required, - field_info=field_info, - ) - if not had_schema and not is_scalar_field(field=field): - field.field_info = params.Body(field_info.default) - if not had_schema and lenient_issubclass(field.type_, UploadFile): - field.field_info = params.File(field_info.default) + field_info.in_ = params.ParamTypes.query + annotation = get_annotation_from_field_info( + annotation if annotation is not inspect.Signature.empty else Any, + field_info, + param_name, + ) + if not field_info.alias and getattr(field_info, "convert_underscores", None): + alias = param_name.replace("_", "-") + else: + alias = field_info.alias or param_name + field = create_response_field( + name=param_name, + type_=annotation, + default=field_info.default, + alias=alias, + required=field_info.default in (Required, Undefined), + field_info=field_info, + ) + if used_default_field_info: + if lenient_issubclass(field.type_, UploadFile): + field.field_info = params.File(field_info.default) + elif not is_scalar_field(field=field): + field.field_info = params.Body(field_info.default) - return field + return type_annotation, depends, field + + +def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: + if is_path_param: + assert is_scalar_field( + field=param_field + ), "Path params must be of one of the supported types" + return False + elif is_scalar_field(field=param_field): + return False + elif isinstance( + param_field.field_info, (params.Query, params.Header) + ) and is_scalar_sequence_field(param_field): + return False + else: + assert isinstance( + param_field.field_info, params.Body + ), f"Param: {param_field.name} can only be a request body, using Body()" + return True def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 1932ef065..75f054e9d 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -5,7 +5,7 @@ from pydantic.fields import Undefined def Path( # noqa: N802 - default: Any = Undefined, + default: Any = ..., *, alias: Optional[str] = None, title: Optional[str] = None, diff --git a/fastapi/params.py b/fastapi/params.py index 5395b98a3..16c5c309a 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -62,7 +62,7 @@ class Path(Param): def __init__( self, - default: Any = Undefined, + default: Any = ..., *, alias: Optional[str] = None, title: Optional[str] = None, @@ -80,9 +80,10 @@ class Path(Param): include_in_schema: bool = True, **extra: Any, ): + assert default is ..., "Path parameters cannot have a default value" self.in_ = self.in_ super().__init__( - default=..., + default=default, alias=alias, title=title, description=description, @@ -279,7 +280,7 @@ class Body(FieldInfo): class Form(Body): def __init__( self, - default: Any, + default: Any = Undefined, *, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, @@ -319,7 +320,7 @@ class Form(Body): class File(Form): def __init__( self, - default: Any, + default: Any = Undefined, *, media_type: str = "multipart/form-data", alias: Optional[str] = None, diff --git a/fastapi/utils.py b/fastapi/utils.py index 391c47d81..d8be53c57 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,4 +1,3 @@ -import functools import re import warnings from dataclasses import is_dataclass @@ -73,19 +72,17 @@ def create_response_field( class_validators = class_validators or {} field_info = field_info or FieldInfo() - response_field = functools.partial( - ModelField, - name=name, - type_=type_, - class_validators=class_validators, - default=default, - required=required, - model_config=model_config, - alias=alias, - ) - try: - return response_field(field_info=field_info) + return ModelField( + name=name, + type_=type_, + class_validators=class_validators, + default=default, + required=required, + model_config=model_config, + alias=alias, + field_info=field_info, + ) except RuntimeError: raise fastapi.exceptions.FastAPIError( "Invalid args for response field! Hint: " diff --git a/tests/main.py b/tests/main.py index fce665704..15760c039 100644 --- a/tests/main.py +++ b/tests/main.py @@ -49,12 +49,7 @@ def get_bool_id(item_id: bool): @app.get("/path/param/{item_id}") -def get_path_param_id(item_id: str = Path()): - return item_id - - -@app.get("/path/param-required/{item_id}") -def get_path_param_required_id(item_id: str = Path()): +def get_path_param_id(item_id: Optional[str] = Path()): return item_id diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py new file mode 100644 index 000000000..42bcc27a1 --- /dev/null +++ b/tests/test_ambiguous_params.py @@ -0,0 +1,66 @@ +import pytest +from fastapi import Depends, FastAPI, Path +from fastapi.param_functions import Query +from typing_extensions import Annotated + +app = FastAPI() + + +def test_no_annotated_defaults(): + with pytest.raises( + AssertionError, match="Path parameters cannot have a default value" + ): + + @app.get("/items/{item_id}/") + async def get_item(item_id: Annotated[int, Path(default=1)]): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match=( + "`Query` default value cannot be set in `Annotated` for 'item_id'. Set the" + " default value with `=` instead." + ), + ): + + @app.get("/") + async def get(item_id: Annotated[int, Query(default=1)]): + pass # pragma: nocover + + +def test_no_multiple_annotations(): + async def dep(): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match="Cannot specify multiple `Annotated` FastAPI arguments for 'foo'", + ): + + @app.get("/") + async def get(foo: Annotated[int, Query(min_length=1), Query()]): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match=( + "Cannot specify `Depends` in `Annotated` and default value" + " together for 'foo'" + ), + ): + + @app.get("/") + async def get2(foo: Annotated[int, Depends(dep)] = Depends(dep)): + pass # pragma: nocover + + with pytest.raises( + AssertionError, + match=( + "Cannot specify a FastAPI annotation in `Annotated` and `Depends` as a" + " default value together for 'foo'" + ), + ): + + @app.get("/") + async def get3(foo: Annotated[int, Query(min_length=1)] = Depends(dep)): + pass # pragma: nocover diff --git a/tests/test_annotated.py b/tests/test_annotated.py new file mode 100644 index 000000000..556019897 --- /dev/null +++ b/tests/test_annotated.py @@ -0,0 +1,226 @@ +import pytest +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/default") +async def default(foo: Annotated[str, Query()] = "foo"): + return {"foo": foo} + + +@app.get("/required") +async def required(foo: Annotated[str, Query(min_length=1)]): + return {"foo": foo} + + +@app.get("/multiple") +async def multiple(foo: Annotated[str, object(), Query(min_length=1)]): + return {"foo": foo} + + +@app.get("/unrelated") +async def unrelated(foo: Annotated[str, object()]): + return {"foo": foo} + + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/default": { + "get": { + "summary": "Default", + "operationId": "default_default_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Foo", "type": "string", "default": "foo"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/required": { + "get": { + "summary": "Required", + "operationId": "required_required_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "minLength": 1, "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/multiple": { + "get": { + "summary": "Multiple", + "operationId": "multiple_multiple_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "minLength": 1, "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/unrelated": { + "get": { + "summary": "Unrelated", + "operationId": "unrelated_unrelated_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} +foo_is_missing = { + "detail": [ + { + "loc": ["query", "foo"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} +foo_is_short = { + "detail": [ + { + "ctx": {"limit_value": 1}, + "loc": ["query", "foo"], + "msg": "ensure this value has at least 1 characters", + "type": "value_error.any_str.min_length", + } + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/default", 200, {"foo": "foo"}), + ("/default?foo=bar", 200, {"foo": "bar"}), + ("/required?foo=bar", 200, {"foo": "bar"}), + ("/required", 422, foo_is_missing), + ("/required?foo=", 422, foo_is_short), + ("/multiple?foo=bar", 200, {"foo": "bar"}), + ("/multiple", 422, foo_is_missing), + ("/multiple?foo=", 422, foo_is_short), + ("/unrelated?foo=bar", 200, {"foo": "bar"}), + ("/unrelated", 422, foo_is_missing), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_application.py b/tests/test_application.py index b7d72f9ad..a4f13e12d 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -225,36 +225,6 @@ openapi_schema = { ], } }, - "/path/param-required/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Required Id", - "operationId": "get_path_param_required_id_path_param_required__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, "/path/param-minlength/{item_id}": { "get": { "responses": { diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index d721257d7..d8dca1ea4 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -19,8 +19,9 @@ def test_param_repr(params): assert repr(Param(params)) == "Param(" + str(params) + ")" -def test_path_repr(params): - assert repr(Path(params)) == "Path(Ellipsis)" +def test_path_repr(): + assert repr(Path()) == "Path(Ellipsis)" + assert repr(Path(...)) == "Path(Ellipsis)" def test_query_repr(params): diff --git a/tests/test_path.py b/tests/test_path.py index d1a58bc66..03b93623a 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -193,7 +193,6 @@ response_less_than_equal_3 = { ("/path/bool/False", 200, False), ("/path/bool/false", 200, False), ("/path/param/foo", 200, "foo"), - ("/path/param-required/foo", 200, "foo"), ("/path/param-minlength/foo", 200, "foo"), ("/path/param-minlength/fo", 422, response_at_least_3), ("/path/param-maxlength/foo", 200, "foo"), diff --git a/tests/test_tutorial/test_annotated/__init__.py b/tests/test_tutorial/test_annotated/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_annotated/test_tutorial001.py b/tests/test_tutorial/test_annotated/test_tutorial001.py new file mode 100644 index 000000000..50c9caca2 --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial001.py @@ -0,0 +1,100 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.annotated.tutorial001 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial001_py39.py b/tests/test_tutorial/test_annotated/test_tutorial001_py39.py new file mode 100644 index 000000000..576f55702 --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial001_py39.py @@ -0,0 +1,107 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.annotated.tutorial001_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial002.py b/tests/test_tutorial/test_annotated/test_tutorial002.py new file mode 100644 index 000000000..60c1233d8 --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial002.py @@ -0,0 +1,100 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.annotated.tutorial002 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial002_py39.py b/tests/test_tutorial/test_annotated/test_tutorial002_py39.py new file mode 100644 index 000000000..77a1f36a0 --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial002_py39.py @@ -0,0 +1,107 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.annotated.tutorial002_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003.py b/tests/test_tutorial/test_annotated/test_tutorial003.py new file mode 100644 index 000000000..caf7ffdf1 --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial003.py @@ -0,0 +1,138 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.annotated.tutorial003 import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "User Id", + "minLength": 1, + "type": "string", + "default": "me", + }, + "name": "user_id", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + +item_id_negative = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items/1", 200, {"item_id": 1}), + ("/items/-1", 422, item_id_negative), + ("/users", 200, {"user_id": "me"}), + ("/users?user_id=foo", 200, {"user_id": "foo"}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003_py39.py b/tests/test_tutorial/test_annotated/test_tutorial003_py39.py new file mode 100644 index 000000000..7c828a0ce --- /dev/null +++ b/tests/test_tutorial/test_annotated/test_tutorial003_py39.py @@ -0,0 +1,145 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "User Id", + "minLength": 1, + "type": "string", + "default": "me", + }, + "name": "user_id", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + +item_id_negative = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.annotated.tutorial003_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items/1", 200, {"item_id": 1}), + ("/items/-1", 422, item_id_negative), + ("/users", 200, {"user_id": "me"}), + ("/users?user_id=foo", 200, {"user_id": "foo"}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client): + response = client.get(path) + assert response.status_code == expected_status, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dataclasses/__init__.py b/tests/test_tutorial/test_dataclasses/__init__.py new file mode 100644 index 000000000..e69de29bb From f63b3ad53e3611108260b57bd846121e7c76334b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 17 Mar 2023 20:36:26 +0000 Subject: [PATCH 0862/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 118ee1dc7..d891a0e63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). ## 0.94.1 From 9eaed2eb3738effed1cc9d2f2187ee52d83b33a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 13:29:59 +0100 Subject: [PATCH 0863/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20all=20docs=20?= =?UTF-8?q?to=20use=20`Annotated`=20as=20the=20main=20recommendation,=20wi?= =?UTF-8?q?th=20new=20examples=20and=20tests=20(#9268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🍱 Add new source examples with Annotated for Query Params and String Validations * 📝 Add new docs with Annotated for Query Params and String Validations * 🚚 Rename incorrectly named tests for Query Params and str validations * ✅ Add new tests with Annotated for Query Params and Sring Validations examples * 🍱 Add new examples with Annotated for Intro to Python Types * 📝 Update Python Types Intro, include Annotated * 🎨 Fix formatting in Query params and string validation, and highlight * 🍱 Add new Annotated source examples for Path Params and Numeric Validations * 📝 Update docs for Path Params and Numeric Validations with Annotated * 🍱 Add new source examples with Annotated for Body - Multiple Params * 📝 Update docs with Annotated for Body - Multiple Parameters * ✅ Add test for new Annotated examples in Body - Multiple Parameters * 🍱 Add new Annotated source examples for Body Fields * 📝 Update docs for Body Fields with new Annotated examples * ✅ Add new tests for new Annotated examples for Body Fields * 🍱 Add new Annotated source examples for Schema Extra (Example Data) * 📝 Update docs for Schema Extra with Annotated * ✅ Add tests for new Annotated examples for Schema Extra * 🍱 Add new Annnotated source examples for Extra Data Types * 📝 Update docs with Annotated for Extra Data Types * ✅ Add tests for new Annotated examples for Extra Data Types * 🍱 Add new Annotated source examples for Cookie Parameters * 📝 Update docs for Cookie Parameters with Annotated examples * ✅ Add tests for new Annotated source examples in Cookie Parameters * 🍱 Add new Annotated examples for Header Params * 📝 Update docs with Annotated examples for Header Parameters * ✅ Add tests for new Annotated examples for Header Params * 🍱 Add new Annotated examples for Form Data * 📝 Update Annotated docs for Form Data * ✅ Add tests for new Annotated examples in Form Data * 🍱 Add new Annotated source examples for Request Files * 📝 Update Annotated docs for Request Files * ✅ Test new Annotated examples for Request Files * 🍱 Add new Annotated source examples for Request Forms and Files * ✅ Add tests for new Anotated examples for Request Forms and Files * 🍱 Add new Annotated source examples for Dependencies and Advanced Dependencies * ✅ Add tests for new Annotated dependencies * 📝 Add new docs for using Annotated with dependencies including type aliases * 📝 Update docs for Classes as Dependencies with Annotated * 📝 Update docs for Sub-dependencies with Annotated * 📝 Update docs for Dependencies in path operation decorators with Annotated * 📝 Update docs for Global Dependencies with Annotated * 📝 Update docs for Dependencies with yield with Annotated * 🎨 Update format in example for dependencies with Annotated * 🍱 Add source examples with Annotated for Security * ✅ Add tests for new Annotated examples for security * 📝 Update docs for Security - First Steps with Annotated * 📝 Update docs for Security: Get Current User with Annotated * 📝 Update docs for Simple OAuth2 with Password and Bearer with Annotated * 📝 Update docs for OAuth2 with Password (and hashing), Bearer with JWT tokens with Annotated * 📝 Update docs for Request Forms and Files with Annotated * 🍱 Add new source examples for Bigger Applications with Annotated * ✅ Add new tests for Bigger Applications with Annotated * 📝 Update docs for Bigger Applications - Multiple Files with Annotated * 🍱 Add source examples for background tasks with Annotated * 📝 Update docs for Background Tasks with Annotated * ✅ Add test for Background Tasks with Anotated * 🍱 Add new source examples for docs for Testing with Annotated * 📝 Update docs for Testing with Annotated * ✅ Add tests for Annotated examples for Testing * 🍱 Add new source examples for Additional Status Codes with Annotated * ✅ Add tests for new Annotated examples for Additional Status Codes * 📝 Update docs for Additional Status Codes with Annotated * 📝 Update docs for Advanced Dependencies with Annotated * 📝 Update docs for OAuth2 scopes with Annotated * 📝 Update docs for HTTP Basic Auth with Annotated * 🍱 Add source examples with Annotated for WebSockets * ✅ Add tests for new Annotated examples for WebSockets * 📝 Update docs for WebSockets with new Annotated examples * 🍱 Add source examples with Annotated for Settings and Environment Variables * 📝 Update docs for Settings and Environment Variables with Annotated * 🍱 Add new source examples for testing dependencies with Annotated * ✅ Add tests for new examples for testing dependencies * 📝 Update docs for testing dependencies with new Annotated examples * ✅ Update and fix marker for Python 3.9 test * 🔧 Update Ruff ignores for source examples in docs * ✅ Fix some tests in the grid for Python 3.9 (incorrectly testing 3.10) * 🔥 Remove source examples and tests for (non existent) docs section about Annotated, as it's covered in all the rest of the docs --- .../docs/advanced/additional-status-codes.md | 38 +- .../en/docs/advanced/advanced-dependencies.md | 92 ++- .../docs/advanced/security/http-basic-auth.md | 69 ++- .../docs/advanced/security/oauth2-scopes.md | 376 +++++++++++- docs/en/docs/advanced/settings.md | 69 ++- docs/en/docs/advanced/testing-dependencies.md | 38 +- docs/en/docs/advanced/websockets.md | 52 +- docs/en/docs/python-types.md | 45 +- docs/en/docs/tutorial/background-tasks.md | 26 +- docs/en/docs/tutorial/bigger-applications.md | 23 +- docs/en/docs/tutorial/body-fields.md | 52 +- docs/en/docs/tutorial/body-multiple-params.md | 106 +++- docs/en/docs/tutorial/cookie-params.md | 52 +- .../dependencies/classes-as-dependencies.md | 293 +++++++++- ...pendencies-in-path-operation-decorators.md | 92 ++- .../dependencies/dependencies-with-yield.md | 46 +- .../dependencies/global-dependencies.md | 24 +- docs/en/docs/tutorial/dependencies/index.md | 119 +++- .../tutorial/dependencies/sub-dependencies.md | 98 +++- docs/en/docs/tutorial/extra-data-types.md | 52 +- docs/en/docs/tutorial/header-params.md | 111 +++- .../path-params-numeric-validations.md | 183 +++++- .../tutorial/query-params-str-validations.md | 544 ++++++++++++++++-- docs/en/docs/tutorial/request-files.md | 165 +++++- .../docs/tutorial/request-forms-and-files.md | 46 +- docs/en/docs/tutorial/request-forms.md | 46 +- docs/en/docs/tutorial/schema-extra-example.md | 46 +- docs/en/docs/tutorial/security/first-steps.md | 70 ++- .../tutorial/security/get-current-user.md | 153 ++++- docs/en/docs/tutorial/security/oauth2-jwt.md | 104 +++- .../docs/tutorial/security/simple-oauth2.md | 134 ++++- docs/en/docs/tutorial/testing.md | 26 +- .../additional_status_codes/tutorial001_an.py | 26 + .../tutorial001_an_py310.py | 25 + .../tutorial001_an_py39.py | 25 + .../tutorial001_py310.py | 23 + docs_src/annotated/tutorial001.py | 18 - docs_src/annotated/tutorial001_py39.py | 17 - docs_src/annotated/tutorial002.py | 21 - docs_src/annotated/tutorial002_py39.py | 20 - docs_src/annotated/tutorial003.py | 15 - docs_src/annotated/tutorial003_py39.py | 16 - .../app_testing/app_b_an}/__init__.py | 0 docs_src/app_testing/app_b_an/main.py | 39 ++ docs_src/app_testing/app_b_an/test_main.py | 65 +++ .../app_testing/app_b_an_py310/__init__.py | 0 docs_src/app_testing/app_b_an_py310/main.py | 38 ++ .../app_testing/app_b_an_py310/test_main.py | 65 +++ .../app_testing/app_b_an_py39/__init__.py | 0 docs_src/app_testing/app_b_an_py39/main.py | 38 ++ .../app_testing/app_b_an_py39/test_main.py | 65 +++ docs_src/background_tasks/tutorial002_an.py | 27 + .../background_tasks/tutorial002_an_py310.py | 26 + .../background_tasks/tutorial002_an_py39.py | 26 + .../bigger_applications/app_an/__init__.py | 0 .../app_an/dependencies.py | 12 + .../app_an/internal/__init__.py | 0 .../app_an/internal/admin.py | 8 + docs_src/bigger_applications/app_an/main.py | 23 + .../app_an/routers/__init__.py | 0 .../app_an/routers/items.py | 38 ++ .../app_an/routers/users.py | 18 + .../app_an_py39/__init__.py | 0 .../app_an_py39/dependencies.py | 13 + .../app_an_py39/internal/__init__.py | 0 .../app_an_py39/internal/admin.py | 8 + .../bigger_applications/app_an_py39/main.py | 23 + .../app_an_py39/routers/__init__.py | 0 .../app_an_py39/routers/items.py | 38 ++ .../app_an_py39/routers/users.py | 18 + docs_src/body_fields/tutorial001_an.py | 22 + docs_src/body_fields/tutorial001_an_py310.py | 21 + docs_src/body_fields/tutorial001_an_py39.py | 21 + .../body_multiple_params/tutorial001_an.py | 28 + .../tutorial001_an_py310.py | 27 + .../tutorial001_an_py39.py | 27 + .../body_multiple_params/tutorial003_an.py | 27 + .../tutorial003_an_py310.py | 26 + .../tutorial003_an_py39.py | 26 + .../body_multiple_params/tutorial004_an.py | 34 ++ .../tutorial004_an_py310.py | 33 ++ .../tutorial004_an_py39.py | 33 ++ .../body_multiple_params/tutorial005_an.py | 20 + .../tutorial005_an_py310.py | 19 + .../tutorial005_an_py39.py | 19 + docs_src/cookie_params/tutorial001_an.py | 11 + .../cookie_params/tutorial001_an_py310.py | 10 + docs_src/cookie_params/tutorial001_an_py39.py | 10 + docs_src/dependencies/tutorial001_02_an.py | 25 + .../dependencies/tutorial001_02_an_py310.py | 22 + .../dependencies/tutorial001_02_an_py39.py | 24 + docs_src/dependencies/tutorial001_an.py | 22 + docs_src/dependencies/tutorial001_an_py310.py | 19 + docs_src/dependencies/tutorial001_an_py39.py | 21 + docs_src/dependencies/tutorial002_an.py | 26 + docs_src/dependencies/tutorial002_an_py310.py | 25 + docs_src/dependencies/tutorial002_an_py39.py | 25 + docs_src/dependencies/tutorial003_an.py | 26 + docs_src/dependencies/tutorial003_an_py310.py | 25 + docs_src/dependencies/tutorial003_an_py39.py | 25 + docs_src/dependencies/tutorial004_an.py | 26 + docs_src/dependencies/tutorial004_an_py310.py | 25 + docs_src/dependencies/tutorial004_an_py39.py | 25 + docs_src/dependencies/tutorial005_an.py | 26 + docs_src/dependencies/tutorial005_an_py310.py | 25 + docs_src/dependencies/tutorial005_an_py39.py | 25 + docs_src/dependencies/tutorial006_an.py | 20 + docs_src/dependencies/tutorial006_an_py39.py | 21 + docs_src/dependencies/tutorial008_an.py | 26 + docs_src/dependencies/tutorial008_an_py39.py | 27 + docs_src/dependencies/tutorial011_an.py | 22 + docs_src/dependencies/tutorial011_an_py39.py | 23 + docs_src/dependencies/tutorial012_an.py | 26 + docs_src/dependencies/tutorial012_an_py39.py | 26 + docs_src/dependency_testing/tutorial001_an.py | 60 ++ .../tutorial001_an_py310.py | 57 ++ .../dependency_testing/tutorial001_an_py39.py | 59 ++ .../dependency_testing/tutorial001_py310.py | 55 ++ docs_src/extra_data_types/tutorial001_an.py | 29 + .../extra_data_types/tutorial001_an_py310.py | 28 + .../extra_data_types/tutorial001_an_py39.py | 28 + docs_src/header_params/tutorial001_an.py | 11 + .../header_params/tutorial001_an_py310.py | 10 + docs_src/header_params/tutorial001_an_py39.py | 10 + docs_src/header_params/tutorial002_an.py | 15 + .../header_params/tutorial002_an_py310.py | 12 + docs_src/header_params/tutorial002_an_py39.py | 14 + docs_src/header_params/tutorial003_an.py | 11 + .../header_params/tutorial003_an_py310.py | 10 + docs_src/header_params/tutorial003_an_py39.py | 10 + .../tutorial001_an.py | 17 + .../tutorial001_an_py310.py | 16 + .../tutorial001_an_py39.py | 16 + .../tutorial002_an.py | 14 + .../tutorial002_an_py39.py | 15 + .../tutorial003_an.py | 14 + .../tutorial003_an_py39.py | 15 + .../tutorial004_an.py | 14 + .../tutorial004_an_py39.py | 15 + .../tutorial005_an.py | 15 + .../tutorial005_an_py39.py | 16 + .../tutorial006_an.py | 17 + .../tutorial006_an_py39.py | 18 + docs_src/python_types/tutorial013.py | 5 + docs_src/python_types/tutorial013_py39.py | 5 + .../tutorial002_an.py | 14 + .../tutorial002_an_py310.py | 13 + .../tutorial003_an.py | 16 + .../tutorial003_an_py310.py | 15 + .../tutorial003_an_py39.py | 15 + .../tutorial004_an.py | 18 + .../tutorial004_an_py310.py | 17 + .../tutorial004_an_py39.py | 17 + .../tutorial005_an.py | 12 + .../tutorial005_an_py39.py | 13 + .../tutorial006_an.py | 12 + .../tutorial006_an_py39.py | 13 + .../tutorial006b_an.py | 12 + .../tutorial006b_an_py39.py | 13 + .../tutorial006c_an.py | 14 + .../tutorial006c_an_py310.py | 13 + .../tutorial006c_an_py39.py | 13 + .../tutorial006d_an.py | 13 + .../tutorial006d_an_py39.py | 14 + .../tutorial007_an.py | 16 + .../tutorial007_an_py310.py | 15 + .../tutorial007_an_py39.py | 15 + .../tutorial008_an.py | 23 + .../tutorial008_an_py310.py | 22 + .../tutorial008_an_py39.py | 22 + .../tutorial009_an.py | 14 + .../tutorial009_an_py310.py | 13 + .../tutorial009_an_py39.py | 13 + .../tutorial010_an.py | 27 + .../tutorial010_an_py310.py | 26 + .../tutorial010_an_py39.py | 26 + .../tutorial011_an.py | 12 + .../tutorial011_an_py310.py | 11 + .../tutorial011_an_py39.py | 11 + .../tutorial012_an.py | 12 + .../tutorial012_an_py39.py | 11 + .../tutorial013_an.py | 10 + .../tutorial013_an_py39.py | 11 + .../tutorial014_an.py | 16 + .../tutorial014_an_py310.py | 15 + .../tutorial014_an_py39.py | 15 + docs_src/request_files/tutorial001_02_an.py | 22 + .../request_files/tutorial001_02_an_py310.py | 21 + .../request_files/tutorial001_02_an_py39.py | 21 + docs_src/request_files/tutorial001_03_an.py | 16 + .../request_files/tutorial001_03_an_py39.py | 17 + docs_src/request_files/tutorial001_an.py | 14 + docs_src/request_files/tutorial001_an_py39.py | 15 + docs_src/request_files/tutorial002_an.py | 34 ++ docs_src/request_files/tutorial002_an_py39.py | 33 ++ docs_src/request_files/tutorial003_an.py | 40 ++ docs_src/request_files/tutorial003_an_py39.py | 39 ++ docs_src/request_forms/tutorial001_an.py | 9 + docs_src/request_forms/tutorial001_an_py39.py | 10 + .../request_forms_and_files/tutorial001_an.py | 17 + .../tutorial001_an_py39.py | 18 + .../schema_extra_example/tutorial003_an.py | 33 ++ .../tutorial003_an_py310.py | 32 ++ .../tutorial003_an_py39.py | 32 ++ .../schema_extra_example/tutorial004_an.py | 55 ++ .../tutorial004_an_py310.py | 54 ++ .../tutorial004_an_py39.py | 54 ++ docs_src/security/tutorial001_an.py | 12 + docs_src/security/tutorial001_an_py39.py | 13 + docs_src/security/tutorial002_an.py | 33 ++ docs_src/security/tutorial002_an_py310.py | 32 ++ docs_src/security/tutorial002_an_py39.py | 32 ++ docs_src/security/tutorial003_an.py | 95 +++ docs_src/security/tutorial003_an_py310.py | 94 +++ docs_src/security/tutorial003_an_py39.py | 94 +++ docs_src/security/tutorial004_an.py | 147 +++++ docs_src/security/tutorial004_an_py310.py | 146 +++++ docs_src/security/tutorial004_an_py39.py | 146 +++++ docs_src/security/tutorial005_an.py | 178 ++++++ docs_src/security/tutorial005_an_py310.py | 177 ++++++ docs_src/security/tutorial005_an_py39.py | 177 ++++++ docs_src/security/tutorial006_an.py | 12 + docs_src/security/tutorial006_an_py39.py | 13 + docs_src/security/tutorial007_an.py | 36 ++ docs_src/security/tutorial007_an_py39.py | 36 ++ docs_src/settings/app02_an/__init__.py | 0 docs_src/settings/app02_an/config.py | 7 + docs_src/settings/app02_an/main.py | 22 + docs_src/settings/app02_an/test_main.py | 23 + docs_src/settings/app02_an_py39/__init__.py | 0 docs_src/settings/app02_an_py39/config.py | 7 + docs_src/settings/app02_an_py39/main.py | 22 + docs_src/settings/app02_an_py39/test_main.py | 23 + docs_src/settings/app03_an/__init__.py | 0 docs_src/settings/app03_an/config.py | 10 + docs_src/settings/app03_an/main.py | 22 + docs_src/settings/app03_an_py39/__init__.py | 0 docs_src/settings/app03_an_py39/config.py | 10 + docs_src/settings/app03_an_py39/main.py | 22 + docs_src/websockets/tutorial002_an.py | 93 +++ docs_src/websockets/tutorial002_an_py310.py | 92 +++ docs_src/websockets/tutorial002_an_py39.py | 92 +++ docs_src/websockets/tutorial002_py310.py | 89 +++ docs_src/websockets/tutorial003_py39.py | 81 +++ pyproject.toml | 6 + .../test_tutorial001_an.py | 17 + .../test_tutorial001_an_py310.py | 26 + .../test_tutorial001_an_py39.py | 26 + .../test_tutorial001_py310.py | 26 + .../test_tutorial002_an.py | 19 + .../test_tutorial002_an_py310.py | 21 + .../test_tutorial002_an_py39.py | 21 + .../test_bigger_applications/test_main_an.py | 491 ++++++++++++++++ .../test_main_an_py39.py | 506 ++++++++++++++++ .../test_body_fields/test_tutorial001_an.py | 169 ++++++ .../test_tutorial001_an_py310.py | 176 ++++++ .../test_tutorial001_an_py39.py | 176 ++++++ .../test_tutorial001_an.py} | 157 ++--- .../test_tutorial001_an_py310.py | 155 +++++ .../test_tutorial001_an_py39.py} | 172 +++--- .../test_tutorial003_an.py | 198 +++++++ .../test_tutorial003_an_py310.py | 206 +++++++ .../test_tutorial003_an_py39.py | 206 +++++++ .../test_tutorial001_an.py} | 46 +- .../test_tutorial001_an_py310.py | 95 +++ .../test_tutorial001_an_py39.py | 95 +++ .../test_dependencies/test_tutorial001_an.py | 149 +++++ .../test_tutorial001_an_py310.py | 157 +++++ .../test_tutorial001_an_py39.py | 157 +++++ .../test_tutorial004_an.py} | 58 +- .../test_tutorial004_an_py310.py} | 65 ++- .../test_tutorial004_an_py39.py} | 61 +- .../test_dependencies/test_tutorial006_an.py | 128 +++++ .../test_tutorial006_an_py39.py | 140 +++++ .../test_dependencies/test_tutorial012_an.py | 209 +++++++ .../test_tutorial012_an_py39.py | 225 ++++++++ .../test_tutorial001_an.py | 138 +++++ .../test_tutorial001_an_py310.py | 146 +++++ .../test_tutorial001_an_py39.py | 146 +++++ .../test_header_params/test_tutorial001_an.py | 88 +++ .../test_tutorial001_an_py310.py | 94 +++ .../test_header_params/test_tutorial002.py | 99 ++++ .../test_header_params/test_tutorial002_an.py | 99 ++++ .../test_tutorial002_an_py310.py | 105 ++++ .../test_tutorial002_an_py39.py | 105 ++++ .../test_tutorial002_py310.py | 105 ++++ .../test_header_params/test_tutorial003.py | 98 ++++ .../test_header_params/test_tutorial003_an.py | 98 ++++ .../test_tutorial003_an_py310.py | 106 ++++ .../test_tutorial003_an_py39.py | 106 ++++ .../test_tutorial003_py310.py | 106 ++++ ...est_tutorial001.py => test_tutorial010.py} | 0 .../test_tutorial010_an.py | 122 ++++ .../test_tutorial010_an_py310.py | 132 +++++ .../test_tutorial010_an_py39.py | 132 +++++ ...001_py310.py => test_tutorial010_py310.py} | 0 .../test_tutorial011_an.py | 95 +++ .../test_tutorial011_an_py310.py | 105 ++++ .../test_tutorial011_an_py39.py | 105 ++++ .../test_tutorial012_an.py | 96 ++++ .../test_tutorial012_an_py39.py | 106 ++++ .../test_tutorial013_an.py | 96 ++++ .../test_tutorial013_an_py39.py | 106 ++++ .../test_tutorial014_an.py | 82 +++ .../test_tutorial014_an_py310.py | 91 +++ .../test_tutorial014_an_py39.py | 91 +++ .../test_tutorial001_02_an.py | 157 +++++ .../test_tutorial001_02_an_py310.py | 169 ++++++ .../test_tutorial001_02_an_py39.py | 169 ++++++ .../test_tutorial001_03_an.py | 159 +++++ .../test_tutorial001_03_an_py39.py | 167 ++++++ .../test_request_files/test_tutorial001_an.py | 184 ++++++ .../test_tutorial001_an_py39.py | 194 +++++++ .../test_request_files/test_tutorial002_an.py | 215 +++++++ .../test_tutorial002_an_py39.py | 234 ++++++++ .../test_request_files/test_tutorial003_an.py | 194 +++++++ .../test_tutorial003_an_py39.py | 222 +++++++ .../test_request_forms/test_tutorial001_an.py | 149 +++++ .../test_tutorial001_an_py39.py | 160 ++++++ .../test_tutorial001_an.py | 192 +++++++ .../test_tutorial001_an_py39.py | 211 +++++++ .../test_tutorial004_an.py | 134 +++++ .../test_tutorial004_an_py310.py | 143 +++++ .../test_tutorial004_an_py39.py | 143 +++++ .../test_security/test_tutorial001_an.py | 59 ++ .../test_security/test_tutorial001_an_py39.py | 70 +++ .../test_security/test_tutorial003_an.py | 176 ++++++ .../test_tutorial003_an_py310.py | 192 +++++++ .../test_security/test_tutorial003_an_py39.py | 192 +++++++ .../test_security/test_tutorial005_an.py | 347 +++++++++++ .../test_tutorial005_an_py310.py | 375 ++++++++++++ .../test_security/test_tutorial005_an_py39.py | 375 ++++++++++++ .../test_security/test_tutorial006_an.py | 67 +++ .../test_security/test_tutorial006_an_py39.py | 79 +++ .../test_testing/test_main_b_an.py | 10 + .../test_testing/test_main_b_an_py310.py | 13 + .../test_testing/test_main_b_an_py39.py | 13 + .../test_tutorial001_an.py | 56 ++ .../test_tutorial001_an_py310.py | 75 +++ .../test_tutorial001_an_py39.py | 75 +++ .../test_tutorial001_py310.py | 75 +++ .../test_websockets/test_tutorial002_an.py | 88 +++ .../test_tutorial002_an_py310.py | 102 ++++ .../test_tutorial002_an_py39.py | 102 ++++ .../test_websockets/test_tutorial002_py310.py | 102 ++++ .../test_websockets/test_tutorial003_py39.py | 50 ++ tests/test_typing_python39.py | 4 +- 347 files changed, 21815 insertions(+), 589 deletions(-) create mode 100644 docs_src/additional_status_codes/tutorial001_an.py create mode 100644 docs_src/additional_status_codes/tutorial001_an_py310.py create mode 100644 docs_src/additional_status_codes/tutorial001_an_py39.py create mode 100644 docs_src/additional_status_codes/tutorial001_py310.py delete mode 100644 docs_src/annotated/tutorial001.py delete mode 100644 docs_src/annotated/tutorial001_py39.py delete mode 100644 docs_src/annotated/tutorial002.py delete mode 100644 docs_src/annotated/tutorial002_py39.py delete mode 100644 docs_src/annotated/tutorial003.py delete mode 100644 docs_src/annotated/tutorial003_py39.py rename {tests/test_tutorial/test_annotated => docs_src/app_testing/app_b_an}/__init__.py (100%) create mode 100644 docs_src/app_testing/app_b_an/main.py create mode 100644 docs_src/app_testing/app_b_an/test_main.py create mode 100644 docs_src/app_testing/app_b_an_py310/__init__.py create mode 100644 docs_src/app_testing/app_b_an_py310/main.py create mode 100644 docs_src/app_testing/app_b_an_py310/test_main.py create mode 100644 docs_src/app_testing/app_b_an_py39/__init__.py create mode 100644 docs_src/app_testing/app_b_an_py39/main.py create mode 100644 docs_src/app_testing/app_b_an_py39/test_main.py create mode 100644 docs_src/background_tasks/tutorial002_an.py create mode 100644 docs_src/background_tasks/tutorial002_an_py310.py create mode 100644 docs_src/background_tasks/tutorial002_an_py39.py create mode 100644 docs_src/bigger_applications/app_an/__init__.py create mode 100644 docs_src/bigger_applications/app_an/dependencies.py create mode 100644 docs_src/bigger_applications/app_an/internal/__init__.py create mode 100644 docs_src/bigger_applications/app_an/internal/admin.py create mode 100644 docs_src/bigger_applications/app_an/main.py create mode 100644 docs_src/bigger_applications/app_an/routers/__init__.py create mode 100644 docs_src/bigger_applications/app_an/routers/items.py create mode 100644 docs_src/bigger_applications/app_an/routers/users.py create mode 100644 docs_src/bigger_applications/app_an_py39/__init__.py create mode 100644 docs_src/bigger_applications/app_an_py39/dependencies.py create mode 100644 docs_src/bigger_applications/app_an_py39/internal/__init__.py create mode 100644 docs_src/bigger_applications/app_an_py39/internal/admin.py create mode 100644 docs_src/bigger_applications/app_an_py39/main.py create mode 100644 docs_src/bigger_applications/app_an_py39/routers/__init__.py create mode 100644 docs_src/bigger_applications/app_an_py39/routers/items.py create mode 100644 docs_src/bigger_applications/app_an_py39/routers/users.py create mode 100644 docs_src/body_fields/tutorial001_an.py create mode 100644 docs_src/body_fields/tutorial001_an_py310.py create mode 100644 docs_src/body_fields/tutorial001_an_py39.py create mode 100644 docs_src/body_multiple_params/tutorial001_an.py create mode 100644 docs_src/body_multiple_params/tutorial001_an_py310.py create mode 100644 docs_src/body_multiple_params/tutorial001_an_py39.py create mode 100644 docs_src/body_multiple_params/tutorial003_an.py create mode 100644 docs_src/body_multiple_params/tutorial003_an_py310.py create mode 100644 docs_src/body_multiple_params/tutorial003_an_py39.py create mode 100644 docs_src/body_multiple_params/tutorial004_an.py create mode 100644 docs_src/body_multiple_params/tutorial004_an_py310.py create mode 100644 docs_src/body_multiple_params/tutorial004_an_py39.py create mode 100644 docs_src/body_multiple_params/tutorial005_an.py create mode 100644 docs_src/body_multiple_params/tutorial005_an_py310.py create mode 100644 docs_src/body_multiple_params/tutorial005_an_py39.py create mode 100644 docs_src/cookie_params/tutorial001_an.py create mode 100644 docs_src/cookie_params/tutorial001_an_py310.py create mode 100644 docs_src/cookie_params/tutorial001_an_py39.py create mode 100644 docs_src/dependencies/tutorial001_02_an.py create mode 100644 docs_src/dependencies/tutorial001_02_an_py310.py create mode 100644 docs_src/dependencies/tutorial001_02_an_py39.py create mode 100644 docs_src/dependencies/tutorial001_an.py create mode 100644 docs_src/dependencies/tutorial001_an_py310.py create mode 100644 docs_src/dependencies/tutorial001_an_py39.py create mode 100644 docs_src/dependencies/tutorial002_an.py create mode 100644 docs_src/dependencies/tutorial002_an_py310.py create mode 100644 docs_src/dependencies/tutorial002_an_py39.py create mode 100644 docs_src/dependencies/tutorial003_an.py create mode 100644 docs_src/dependencies/tutorial003_an_py310.py create mode 100644 docs_src/dependencies/tutorial003_an_py39.py create mode 100644 docs_src/dependencies/tutorial004_an.py create mode 100644 docs_src/dependencies/tutorial004_an_py310.py create mode 100644 docs_src/dependencies/tutorial004_an_py39.py create mode 100644 docs_src/dependencies/tutorial005_an.py create mode 100644 docs_src/dependencies/tutorial005_an_py310.py create mode 100644 docs_src/dependencies/tutorial005_an_py39.py create mode 100644 docs_src/dependencies/tutorial006_an.py create mode 100644 docs_src/dependencies/tutorial006_an_py39.py create mode 100644 docs_src/dependencies/tutorial008_an.py create mode 100644 docs_src/dependencies/tutorial008_an_py39.py create mode 100644 docs_src/dependencies/tutorial011_an.py create mode 100644 docs_src/dependencies/tutorial011_an_py39.py create mode 100644 docs_src/dependencies/tutorial012_an.py create mode 100644 docs_src/dependencies/tutorial012_an_py39.py create mode 100644 docs_src/dependency_testing/tutorial001_an.py create mode 100644 docs_src/dependency_testing/tutorial001_an_py310.py create mode 100644 docs_src/dependency_testing/tutorial001_an_py39.py create mode 100644 docs_src/dependency_testing/tutorial001_py310.py create mode 100644 docs_src/extra_data_types/tutorial001_an.py create mode 100644 docs_src/extra_data_types/tutorial001_an_py310.py create mode 100644 docs_src/extra_data_types/tutorial001_an_py39.py create mode 100644 docs_src/header_params/tutorial001_an.py create mode 100644 docs_src/header_params/tutorial001_an_py310.py create mode 100644 docs_src/header_params/tutorial001_an_py39.py create mode 100644 docs_src/header_params/tutorial002_an.py create mode 100644 docs_src/header_params/tutorial002_an_py310.py create mode 100644 docs_src/header_params/tutorial002_an_py39.py create mode 100644 docs_src/header_params/tutorial003_an.py create mode 100644 docs_src/header_params/tutorial003_an_py310.py create mode 100644 docs_src/header_params/tutorial003_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial001_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial001_an_py310.py create mode 100644 docs_src/path_params_numeric_validations/tutorial001_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial002_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial002_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial003_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial003_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial004_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial004_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial005_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial005_an_py39.py create mode 100644 docs_src/path_params_numeric_validations/tutorial006_an.py create mode 100644 docs_src/path_params_numeric_validations/tutorial006_an_py39.py create mode 100644 docs_src/python_types/tutorial013.py create mode 100644 docs_src/python_types/tutorial013_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial002_an.py create mode 100644 docs_src/query_params_str_validations/tutorial002_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial003_an.py create mode 100644 docs_src/query_params_str_validations/tutorial003_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial003_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial004_an.py create mode 100644 docs_src/query_params_str_validations/tutorial004_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial004_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial005_an.py create mode 100644 docs_src/query_params_str_validations/tutorial005_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial006_an.py create mode 100644 docs_src/query_params_str_validations/tutorial006_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial006b_an.py create mode 100644 docs_src/query_params_str_validations/tutorial006b_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial006c_an.py create mode 100644 docs_src/query_params_str_validations/tutorial006c_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial006c_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial006d_an.py create mode 100644 docs_src/query_params_str_validations/tutorial006d_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial007_an.py create mode 100644 docs_src/query_params_str_validations/tutorial007_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial007_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial008_an.py create mode 100644 docs_src/query_params_str_validations/tutorial008_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial008_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial009_an.py create mode 100644 docs_src/query_params_str_validations/tutorial009_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial009_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial010_an.py create mode 100644 docs_src/query_params_str_validations/tutorial010_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial010_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial011_an.py create mode 100644 docs_src/query_params_str_validations/tutorial011_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial011_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial012_an.py create mode 100644 docs_src/query_params_str_validations/tutorial012_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial013_an.py create mode 100644 docs_src/query_params_str_validations/tutorial013_an_py39.py create mode 100644 docs_src/query_params_str_validations/tutorial014_an.py create mode 100644 docs_src/query_params_str_validations/tutorial014_an_py310.py create mode 100644 docs_src/query_params_str_validations/tutorial014_an_py39.py create mode 100644 docs_src/request_files/tutorial001_02_an.py create mode 100644 docs_src/request_files/tutorial001_02_an_py310.py create mode 100644 docs_src/request_files/tutorial001_02_an_py39.py create mode 100644 docs_src/request_files/tutorial001_03_an.py create mode 100644 docs_src/request_files/tutorial001_03_an_py39.py create mode 100644 docs_src/request_files/tutorial001_an.py create mode 100644 docs_src/request_files/tutorial001_an_py39.py create mode 100644 docs_src/request_files/tutorial002_an.py create mode 100644 docs_src/request_files/tutorial002_an_py39.py create mode 100644 docs_src/request_files/tutorial003_an.py create mode 100644 docs_src/request_files/tutorial003_an_py39.py create mode 100644 docs_src/request_forms/tutorial001_an.py create mode 100644 docs_src/request_forms/tutorial001_an_py39.py create mode 100644 docs_src/request_forms_and_files/tutorial001_an.py create mode 100644 docs_src/request_forms_and_files/tutorial001_an_py39.py create mode 100644 docs_src/schema_extra_example/tutorial003_an.py create mode 100644 docs_src/schema_extra_example/tutorial003_an_py310.py create mode 100644 docs_src/schema_extra_example/tutorial003_an_py39.py create mode 100644 docs_src/schema_extra_example/tutorial004_an.py create mode 100644 docs_src/schema_extra_example/tutorial004_an_py310.py create mode 100644 docs_src/schema_extra_example/tutorial004_an_py39.py create mode 100644 docs_src/security/tutorial001_an.py create mode 100644 docs_src/security/tutorial001_an_py39.py create mode 100644 docs_src/security/tutorial002_an.py create mode 100644 docs_src/security/tutorial002_an_py310.py create mode 100644 docs_src/security/tutorial002_an_py39.py create mode 100644 docs_src/security/tutorial003_an.py create mode 100644 docs_src/security/tutorial003_an_py310.py create mode 100644 docs_src/security/tutorial003_an_py39.py create mode 100644 docs_src/security/tutorial004_an.py create mode 100644 docs_src/security/tutorial004_an_py310.py create mode 100644 docs_src/security/tutorial004_an_py39.py create mode 100644 docs_src/security/tutorial005_an.py create mode 100644 docs_src/security/tutorial005_an_py310.py create mode 100644 docs_src/security/tutorial005_an_py39.py create mode 100644 docs_src/security/tutorial006_an.py create mode 100644 docs_src/security/tutorial006_an_py39.py create mode 100644 docs_src/security/tutorial007_an.py create mode 100644 docs_src/security/tutorial007_an_py39.py create mode 100644 docs_src/settings/app02_an/__init__.py create mode 100644 docs_src/settings/app02_an/config.py create mode 100644 docs_src/settings/app02_an/main.py create mode 100644 docs_src/settings/app02_an/test_main.py create mode 100644 docs_src/settings/app02_an_py39/__init__.py create mode 100644 docs_src/settings/app02_an_py39/config.py create mode 100644 docs_src/settings/app02_an_py39/main.py create mode 100644 docs_src/settings/app02_an_py39/test_main.py create mode 100644 docs_src/settings/app03_an/__init__.py create mode 100644 docs_src/settings/app03_an/config.py create mode 100644 docs_src/settings/app03_an/main.py create mode 100644 docs_src/settings/app03_an_py39/__init__.py create mode 100644 docs_src/settings/app03_an_py39/config.py create mode 100644 docs_src/settings/app03_an_py39/main.py create mode 100644 docs_src/websockets/tutorial002_an.py create mode 100644 docs_src/websockets/tutorial002_an_py310.py create mode 100644 docs_src/websockets/tutorial002_an_py39.py create mode 100644 docs_src/websockets/tutorial002_py310.py create mode 100644 docs_src/websockets/tutorial003_py39.py create mode 100644 tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py create mode 100644 tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py create mode 100644 tests/test_tutorial/test_bigger_applications/test_main_an.py create mode 100644 tests/test_tutorial/test_bigger_applications/test_main_an_py39.py create mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py rename tests/test_tutorial/{test_annotated/test_tutorial003.py => test_body_multiple_params/test_tutorial001_an.py} (54%) create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py rename tests/test_tutorial/{test_annotated/test_tutorial003_py39.py => test_body_multiple_params/test_tutorial001_an_py39.py} (54%) create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py create mode 100644 tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py rename tests/test_tutorial/{test_annotated/test_tutorial002.py => test_cookie_params/test_tutorial001_an.py} (65%) create mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py rename tests/test_tutorial/{test_annotated/test_tutorial001.py => test_dependencies/test_tutorial004_an.py} (69%) rename tests/test_tutorial/{test_annotated/test_tutorial002_py39.py => test_dependencies/test_tutorial004_an_py310.py} (67%) rename tests/test_tutorial/{test_annotated/test_tutorial001_py39.py => test_dependencies/test_tutorial004_an_py39.py} (68%) create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial006_an.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial012_an.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py create mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial002.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial003.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py create mode 100644 tests/test_tutorial/test_header_params/test_tutorial003_py310.py rename tests/test_tutorial/test_query_params_str_validations/{test_tutorial001.py => test_tutorial010.py} (100%) create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py rename tests/test_tutorial/test_query_params_str_validations/{test_tutorial001_py310.py => test_tutorial010_py310.py} (100%) create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py create mode 100644 tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py create mode 100644 tests/test_tutorial/test_request_forms/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py create mode 100644 tests/test_tutorial/test_security/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_security/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_security/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_security/test_tutorial003_an_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial003_an_py39.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_an_py310.py create mode 100644 tests/test_tutorial/test_security/test_tutorial005_an_py39.py create mode 100644 tests/test_tutorial/test_security/test_tutorial006_an.py create mode 100644 tests/test_tutorial/test_security/test_tutorial006_an_py39.py create mode 100644 tests/test_tutorial/test_testing/test_main_b_an.py create mode 100644 tests/test_tutorial/test_testing/test_main_b_an_py310.py create mode 100644 tests/test_tutorial/test_testing/test_main_b_an_py39.py create mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py create mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py create mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py create mode 100644 tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py create mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py create mode 100644 tests/test_tutorial/test_websockets/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_websockets/test_tutorial003_py39.py diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index b61f88b93..d287f861a 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -14,9 +14,41 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: -```Python hl_lines="4 25" -{!../../../docs_src/additional_status_codes/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 26" + {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 23" + {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} + ``` !!! warning When you return a `Response` directly, like in the example above, it will be returned directly. diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 5e79776ca..97621fcf9 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -18,9 +18,26 @@ Not the class itself (which is already a callable), but an instance of that clas To do that, we declare a method `__call__`: -```Python hl_lines="10" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, this `__call__` is what **FastAPI** will use to check for additional parameters and sub-dependencies, and this is what will be called to pass a value to the parameter in your *path operation function* later. @@ -28,9 +45,26 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency: -```Python hl_lines="7" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` In this case, **FastAPI** won't ever touch or care about `__init__`, we will use it directly in our code. @@ -38,9 +72,26 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use We could create an instance of this class with: -```Python hl_lines="16" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` And that way we are able to "parameterize" our dependency, that now has `"bar"` inside of it, as the attribute `checker.fixed_content`. @@ -56,9 +107,26 @@ checker(q="somequery") ...and pass whatever that returns as the value of the dependency in our *path operation function* as the parameter `fixed_content_included`: -```Python hl_lines="20" -{!../../../docs_src/dependencies/tutorial011.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial011.py!} + ``` !!! tip All this might seem contrived. And it might not be very clear how is it useful yet. diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 90c516808..a9ce3b1ef 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -20,9 +20,26 @@ Then, when you type that username and password, the browser sends them in the he * It returns an object of type `HTTPBasicCredentials`: * It contains the `username` and `password` sent. -```Python hl_lines="2 6 10" -{!../../../docs_src/security/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 7 11" + {!> ../../../docs_src/security/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 8 12" + {!> ../../../docs_src/security/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 6 10" + {!> ../../../docs_src/security/tutorial006.py!} + ``` When you try to open the URL for the first time (or click the "Execute" button in the docs) the browser will ask you for your username and password: @@ -42,9 +59,26 @@ To handle that, we first convert the `username` and `password` to `bytes` encodi Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. -```Python hl_lines="1 11-21" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 11-21" + {!> ../../../docs_src/security/tutorial007.py!} + ``` This would be similar to: @@ -108,6 +142,23 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -```Python hl_lines="23-27" -{!../../../docs_src/security/tutorial007.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="23-27" + {!> ../../../docs_src/security/tutorial007.py!} + ``` diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index be51325cd..c216d7f50 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -56,9 +56,50 @@ They are normally used to declare specific security permissions, for example: First, let's quickly see the parts that change from the examples in the main **Tutorial - User Guide** for [OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Now using OAuth2 scopes: -```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` Now let's review those changes step by step. @@ -68,9 +109,50 @@ The first change is that now we are declaring the OAuth2 security scheme with tw The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: -```Python hl_lines="62-65" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="63-66" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="61-64" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` Because we are now declaring those scopes, they will show up in the API docs when you log-in/authorize. @@ -93,9 +175,50 @@ And we return the scopes as part of the JWT token. But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. -```Python hl_lines="153" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="152" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Declare scopes in *path operations* and dependencies @@ -118,9 +241,50 @@ In this case, it requires the scope `me` (it could require more than one scope). We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. -```Python hl_lines="4 139 166" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="4 140 171" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 139 166" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 139 166" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3 138 165" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` !!! info "Technical Details" `Security` is actually a subclass of `Depends`, and it has just one extra parameter that we'll see later. @@ -143,9 +307,50 @@ We also declare a special parameter of type `SecurityScopes`, imported from `fas This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). -```Python hl_lines="8 105" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 106" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 104" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Use the `scopes` @@ -159,9 +364,50 @@ We create an `HTTPException` that we can re-use (`raise`) later at several point In this exception, we include the scopes required (if any) as a string separated by spaces (using `scope_str`). We put that string containing the scopes in the `WWW-Authenticate` header (this is part of the spec). -```Python hl_lines="105 107-115" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="106 108-116" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="104 106-114" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Verify the `username` and data shape @@ -177,9 +423,50 @@ Instead of, for example, a `dict`, or something else, as it could break the appl We also verify that we have a user with that username, and if not, we raise that same exception we created before. -```Python hl_lines="46 116-127" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="47 117-128" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="45 115-126" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Verify the `scopes` @@ -187,9 +474,50 @@ We now verify that all the scopes required, by this dependency and all the depen For this, we use `security_scopes.scopes`, that contains a `list` with all these scopes as `str`. -```Python hl_lines="128-134" -{!../../../docs_src/security/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="129-135" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="127-133" + {!> ../../../docs_src/security/tutorial005_py310.py!} + ``` ## Dependency tree and scopes diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index d5151b585..52dbdf6fa 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -216,9 +216,26 @@ Notice that now we don't create a default instance `settings = Settings()`. Now we create a dependency that returns a new `config.Settings()`. -```Python hl_lines="5 11-12" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="5 11-12" + {!> ../../../docs_src/settings/app02/main.py!} + ``` !!! tip We'll discuss the `@lru_cache()` in a bit. @@ -227,9 +244,26 @@ Now we create a dependency that returns a new `config.Settings()`. And then we can require it from the *path operation function* as a dependency and use it anywhere we need it. -```Python hl_lines="16 18-20" -{!../../../docs_src/settings/app02/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16 18-20" + {!> ../../../docs_src/settings/app02/main.py!} + ``` ### Settings and testing @@ -304,9 +338,26 @@ we would create that object for each request, and we would be reading the `.env` But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ -```Python hl_lines="1 10" -{!../../../docs_src/settings/app03/main.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 10" + {!> ../../../docs_src/settings/app03/main.py!} + ``` Then for any subsequent calls of `get_settings()` in the dependencies for the next requests, instead of executing the internal code of `get_settings()` and creating a new `Settings` object, it will return the same object that was returned on the first call, again and again. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index 7bba82fb7..c30dccd5d 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -28,9 +28,41 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. -```Python hl_lines="28-29 32" -{!../../../docs_src/dependency_testing/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="29-30 33" + {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="26-27 30" + {!> ../../../docs_src/dependency_testing/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="24-25 28" + {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} + ``` !!! tip You can set a dependency override for a dependency used anywhere in your **FastAPI** application. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 3cf840819..8df32f4ca 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -112,9 +112,41 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: -```Python hl_lines="66-77 76-91" -{!../../../docs_src/websockets/tutorial002.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="66-67 79" + {!> ../../../docs_src/websockets/tutorial002_py310.py!} + ``` !!! info As this is a WebSocket it doesn't really make sense to raise an `HTTPException`, instead we raise a `WebSocketException`. @@ -153,9 +185,17 @@ With that you can connect the WebSocket and then send and receive messages: When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example. -```Python hl_lines="81-83" -{!../../../docs_src/websockets/tutorial003.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="79-81" + {!> ../../../docs_src/websockets/tutorial003_py39.py!} + ``` To try it out: diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 3d22ee620..7ddfd41f2 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -1,8 +1,8 @@ # Python Types Intro -Python has support for optional "type hints". +Python has support for optional "type hints" (also called "type annotations"). -These **"type hints"** are a special syntax that allow declaring the type of a variable. +These **"type hints"** or annotations are a special syntax that allow declaring the type of a variable. By declaring types for your variables, editors and tools can give you better support. @@ -422,6 +422,10 @@ And then, again, you get all the editor support: +Notice that this means "`one_person` is an **instance** of the class `Person`". + +It doesn't mean "`one_person` is the **class** called `Person`". + ## Pydantic models Pydantic is a Python library to perform data validation. @@ -464,6 +468,43 @@ You will see a lot more of all this in practice in the [Tutorial - User Guide](t !!! tip Pydantic has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. +## Type Hints with Metadata Annotations + +Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. + +=== "Python 3.7 and above" + + In versions below Python 3.9, you import `Annotated` from `typing_extensions`. + + It will already be installed with **FastAPI**. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013.py!} + ``` + +=== "Python 3.9 and above" + + In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +Python itself doesn't do anything with this `Annotated`. And for editors and other tools, the type is still `str`. + +But you can use this space in `Annotated` to provide **FastAPI** with additional metadata about how you want your application to behave. + +The important thing to remember is that **the first *type parameter*** you pass to `Annotated` is the **actual type**. The rest, is just metadata for other tools. + +For now, you just need to know that `Annotated` exists, and that it's standard Python. 😎 + +Later you will see how **powerful** it can be. + +!!! tip + The fact that this is **standard Python** means that you will still get the **best possible developer experience** in your editor, with the tools you use to analyze and refactor your code, etc. ✨ + + And also that your code will be very compatible with many other Python tools and libraries. 🚀 + ## Type hints in **FastAPI** **FastAPI** takes advantage of these type hints to do several things. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 191a4ca0f..909e2a72b 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -59,11 +59,35 @@ Using `BackgroundTasks` also works with the dependency injection system, you can === "Python 3.6 and above" + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="13 15 22 25" {!> ../../../docs_src/background_tasks/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 1de05a00a..9aeafe29e 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -112,9 +112,26 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -```Python hl_lines="1 4-6" -{!../../../docs_src/bigger_applications/app/dependencies.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1 5-7" + {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 6-8" + {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 4-6" + {!> ../../../docs_src/bigger_applications/app/dependencies.py!} + ``` !!! tip We are using an invented header to simplify this example. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 0cfe576d6..301ee84f2 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -9,11 +9,35 @@ First, you have to import it: === "Python 3.6 and above" ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="2" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` @@ -27,11 +51,35 @@ You can then use `Field` with model attributes: === "Python 3.6 and above" + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11-14" {!> ../../../docs_src/body_fields/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="9-12" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 31dd27fed..1a6b572dc 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -11,11 +11,35 @@ And you can also declare body parameters as optional, by setting the default to === "Python 3.6 and above" ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` @@ -89,11 +113,35 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: === "Python 3.6 and above" + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} @@ -139,13 +187,37 @@ For example: === "Python 3.6 and above" + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="26" + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="25" {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` @@ -168,11 +240,35 @@ as in: === "Python 3.6 and above" + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17" {!> ../../../docs_src/body_multiple_params/tutorial005.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 221cdfffb..2aab8d12d 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -9,11 +9,35 @@ First import `Cookie`: === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` @@ -26,11 +50,35 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/cookie_params/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index fb41ba1f6..4fa05c98e 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -8,12 +8,36 @@ In the previous example, we were returning a `dict` from our dependency ("depend === "Python 3.6 and above" - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -81,11 +105,35 @@ Then, we can change the dependency "dependable" `common_parameters` from above t === "Python 3.6 and above" + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -95,11 +143,35 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6 and above" + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="12" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -109,11 +181,35 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -133,13 +229,38 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. + === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -151,14 +272,25 @@ Now you can declare your dependency using this class. Notice how we write `CommonQueryParams` twice in the above code: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` The last `CommonQueryParams`, in: ```Python -... = Depends(CommonQueryParams) +... Depends(CommonQueryParams) ``` ...is what **FastAPI** will actually use to know what is the dependency. @@ -169,27 +301,73 @@ From it is that FastAPI will extract the declared parameters and that is what Fa In this case, the first `CommonQueryParams`, in: -```Python -commons: CommonQueryParams ... -``` +=== "Python 3.6 and above" -...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `= Depends(CommonQueryParams)` for that). + ```Python + commons: Annotated[CommonQueryParams, ... + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams ... + ``` + +...doesn't have any special meaning for **FastAPI**. FastAPI won't use it for data conversion, validation, etc. (as it is using the `Depends(CommonQueryParams)` for that). You could actually write just: -```Python -commons = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons = Depends(CommonQueryParams) + ``` ..as in: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} @@ -203,9 +381,20 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` **FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself. @@ -213,27 +402,73 @@ For those specific cases, you can do the following: Instead of writing: -```Python -commons: CommonQueryParams = Depends(CommonQueryParams) -``` +=== "Python 3.6 and above" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` ...you write: -```Python -commons: CommonQueryParams = Depends() -``` +=== "Python 3.6 and above" -You declare the dependency as the type of the parameter, and you use `Depends()` as its "default" value (that after the `=`) for that function's parameter, without any parameter in `Depends()`, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + commons: CommonQueryParams = Depends() + ``` + +You declare the dependency as the type of the parameter, and you use `Depends()` without any parameter, instead of having to write the full class *again* inside of `Depends(CommonQueryParams)`. The same example would then look like: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index a1bbcc6c7..d3a11c149 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -14,9 +14,26 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: -```Python hl_lines="17" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. @@ -40,17 +57,51 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: -```Python hl_lines="6 11" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7 12" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6 11" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Raise exceptions These dependencies can `raise` exceptions, the same as normal dependencies: -```Python hl_lines="8 13" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8 13" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ### Return values @@ -58,9 +109,26 @@ And they can return values or not, the values won't be used. So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed: -```Python hl_lines="9 14" -{!../../../docs_src/dependencies/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006.py!} + ``` ## Dependencies for a group of *path operations* diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index a7300f0b7..d0c263f40 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -66,9 +66,26 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: -```Python hl_lines="4 12 20" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6 14 22" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 12 20" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` And all of them can use `yield`. @@ -76,9 +93,26 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. -```Python hl_lines="16-17 24-25" -{!../../../docs_src/dependencies/tutorial008.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18-19 26-27" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="16-17 24-25" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` The same way, you could have dependencies with `yield` and `return` mixed. diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index bcd61d52b..9ad503d8f 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -6,9 +6,27 @@ Similar to the way you can [add `dependencies` to the *path operation decorators In that case, they will be applied to all the *path operations* in the application: -```Python hl_lines="15" -{!../../../docs_src/dependencies/tutorial012.py!} -``` + +=== "Python 3.6 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="15" + {!> ../../../docs_src/dependencies/tutorial012.py!} + ``` And all the ideas in the section about [adding `dependencies` to the *path operation decorators*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} still apply, but in this case, to all of the *path operations* in the app. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 5078c0096..5675b8dfd 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -33,11 +33,35 @@ It is just a function that can take all the same parameters that a *path operati === "Python 3.6 and above" + ```Python hl_lines="9-12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="8-11" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -66,11 +90,35 @@ And then it just returns a `dict` containing those values. === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` @@ -81,11 +129,35 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p === "Python 3.6 and above" + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="15 20" {!> ../../../docs_src/dependencies/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="11 16" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -97,6 +169,8 @@ You only give `Depends` a single parameter. This parameter must be something like a function. +You **don't call it** directly (don't add the parenthesis at the end), you just pass it as a parameter to `Depends()`. + And that function takes parameters in the same way that *path operation functions* do. !!! tip @@ -126,6 +200,45 @@ This way you write shared code once and **FastAPI** takes care of calling it for You just pass it to `Depends` and **FastAPI** knows how to do the rest. +## Share `Annotated` dependencies + +In the examples above, you see that there's a tiny bit of **code duplication**. + +When you need to use the `common_parameters()` dependency, you have to write the whole parameter with the type annotation and `Depends()`: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +But because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places: + +=== "Python 3.6 and above" + + ```Python hl_lines="15 19 24" + {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14 18 23" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="12 16 21" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} + ``` + +!!! tip + This is just standard Python, it's called a "type alias", it's actually not specific to **FastAPI**. + + But because **FastAPI** is based on the Python standards, including `Annotated`, you can use this trick in your code. 😎 + +The dependencies will keep working as expected, and the **best part** is that the **type information will be preserved**, which means that your editor will be able to keep providing you with **autocompletion**, **inline errors**, etc. The same for other tools like `mypy`. + +This will be especially useful when you use it in a **large code base** where you use **the same dependencies** over and over again in **many *path operations***. + ## To `async` or not to `async` As dependencies will also be called by **FastAPI** (the same as your *path operation functions*), the same rules apply while defining your functions. diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index a5b40c9ad..8b70e2602 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -12,11 +12,35 @@ You could create a first dependency ("dependable") like: === "Python 3.6 and above" + ```Python hl_lines="9-10" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="8-9" {!> ../../../docs_src/dependencies/tutorial005.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -32,11 +56,35 @@ Then you can create another dependency function (a "dependable") that at the sam === "Python 3.6 and above" + ```Python hl_lines="14" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="13" {!> ../../../docs_src/dependencies/tutorial005.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -55,11 +103,35 @@ Then we can use the dependency with: === "Python 3.6 and above" + ```Python hl_lines="24" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} + ``` + +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="22" {!> ../../../docs_src/dependencies/tutorial005.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -89,10 +161,22 @@ And it will save the returned value in a ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="1 3 12-16" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1 2 11-15" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` @@ -71,12 +95,36 @@ Note that the parameters inside the function have their natural data type, and y === "Python 3.6 and above" + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="18-19" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="17-18" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 8e294416c..475359134 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -9,11 +9,35 @@ First import `Header`: === "Python 3.6 and above" ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` @@ -26,11 +50,35 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} @@ -62,11 +110,35 @@ If for some reason you need to disable automatic conversion of underscores to hy === "Python 3.6 and above" + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} @@ -87,18 +159,45 @@ For example, to declare a header of `X-Token` that can appear more than once, yo === "Python 3.6 and above" - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} ``` === "Python 3.9 and above" ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003_py39.py!} + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial003_py310.py!} ``` diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index cec54b0fb..c6f158307 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -4,15 +4,39 @@ In the same way that you can declare more validations and metadata for query par ## Import Path -First, import `Path` from `fastapi`: +First, import `Path` from `fastapi`, and import `Annotated`: === "Python 3.6 and above" + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="1" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} @@ -26,11 +50,35 @@ For example, to declare a `title` metadata value for the path parameter `item_id === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} @@ -45,11 +93,14 @@ For example, to declare a `title` metadata value for the path parameter `item_id ## Order the parameters as you need +!!! tip + This is probably not as important or necessary if you use `Annotated`. + Let's say that you want to declare the query parameter `q` as a required `str`. And you don't need to declare anything else for that parameter, so you don't really need to use `Query`. -But you still need to use `Path` for the `item_id` path parameter. +But you still need to use `Path` for the `item_id` path parameter. And you don't want to use `Annotated` for some reason. Python will complain if you put a value with a "default" before a value that doesn't have a "default". @@ -59,13 +110,44 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.6 - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +But have in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` ## Order the parameters as you need, tricks -If you want to declare the `q` query parameter without a `Query` nor any default value, and the path parameter `item_id` using `Path`, and have them in a different order, Python has a little special syntax for that. +!!! tip + This is probably not as important or necessary if you use `Annotated`. + +Here's a **small trick** that can be handy, but you won't need it often. + +If you want to: + +* declare the `q` query parameter without a `Query` nor any default value +* declare the path parameter `item_id` using `Path` +* have them in a different order +* not use `Annotated` + +...Python has a little special syntax for that. Pass `*`, as the first parameter of the function. @@ -75,15 +157,48 @@ Python won't do anything with that `*`, but it will know that all the following {!../../../docs_src/path_params_numeric_validations/tutorial003.py!} ``` +### Better with `Annotated` + +Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and yo probably won't need to use `*`. + +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + ## Number validations: greater than or equal With `Query` and `Path` (and others you'll see later) you can declare number constraints. Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than or `e`qual" to `1`. -```Python hl_lines="8" -{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} + ``` ## Number validations: greater than and less than or equal @@ -92,9 +207,26 @@ The same applies for: * `gt`: `g`reater `t`han * `le`: `l`ess than or `e`qual -```Python hl_lines="9" -{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} + ``` ## Number validations: floats, greater than and less than @@ -106,9 +238,26 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. -```Python hl_lines="11" -{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} + ``` ## Recap diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 060e1d58a..a12b0b41a 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -27,25 +27,103 @@ The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python We are going to enforce that even though `q` is optional, whenever it is provided, **its length doesn't exceed 50 characters**. -### Import `Query` +### Import `Query` and `Annotated` -To achieve that, first import `Query` from `fastapi`: +To achieve that, first import: + +* `Query` from `fastapi` +* `Annotated` from `typing` (or from `typing_extensions` in Python below 3.9) === "Python 3.6 and above" - ```Python hl_lines="3" - {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`. + + It will already be installed with FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="1" - {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -## Use `Query` as the default value +## Use `Annotated` in the type for the `q` parameter -And now use it as the default value of your parameter, setting the parameter `max_length` to 50: +Remember I told you before that `Annotated` can be used to add metadata to your parameters in the [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Now it's the time to use it with FastAPI. 🚀 + +We had this type annotation: + +=== "Python 3.6 and above" + + ```Python + q: Union[str, None] = None + ``` + +=== "Python 3.10 and above" + + ```Python + q: str | None = None + ``` + +What we will do is wrap that with `Annotated`, so it becomes: + +=== "Python 3.6 and above" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + +=== "Python 3.10 and above" + + ```Python + q: Annotated[str | None] = None + ``` + +Both of those versions mean the same thing, `q` is a parameter that can be a `str` or `None`, and by default, it is `None`. + +Now let's jump to the fun stuff. 🎉 + +## Add `Query` to `Annotated` in the `q` parameter + +Now that we have this `Annotated` where we can put more metadata, add `Query` to it, and set the parameter `max_length` to 50: + +=== "Python 3.6 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +Notice that the default value is still `None`, so the parameter is still optional. + +But now, having `Query(max_length=50)` inside of `Annotated`, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have **additional validation** for this value (that's why we do this, to get the additional validation). 😎 + +FastAPI wll now: + +* **Validate** the data making sure that the max length is 50 characters +* Show a **clear error** for the client when the data is not valid +* **Document** the parameter in the OpenAPI schema *path operation* (so it will show up in the **automatic docs UI**) + +## Alternative (old) `Query` as the default value + +Previous versions of FastAPI (before 0.95.0) required you to use `Query` as the default value of your parameter, instead of putting it in `Annotated`, there's a high chance that you will see code using it around, so I'll explain it to you. + +!!! tip + For new code and whenever possible, use `Annotated` as explained above. There are multiple advantages (explained below) and no disadvantages. 🍰 + +This is how you would use `Query()` as the default value of your function parameter, setting the parameter `max_length` to 50: === "Python 3.6 and above" @@ -59,7 +137,7 @@ And now use it as the default value of your parameter, setting the parameter `ma {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -As we have to replace the default value `None` in the function with `Query()`, we can now set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value. +As in this case (without using `Annotated`) we have to replace the default value `None` in the function with `Query()`, we now need to set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value (at least for FastAPI). So: @@ -67,7 +145,7 @@ So: q: Union[str, None] = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: Union[str, None] = None @@ -79,7 +157,7 @@ And in Python 3.10 and above: q: str | None = Query(default=None) ``` -...makes the parameter optional, the same as: +...makes the parameter optional, with a default value of `None`, the same as: ```Python q: str | None = None @@ -112,17 +190,79 @@ q: Union[str, None] = Query(default=None, max_length=50) This will validate the data, show a clear error when the data is not valid, and document the parameter in the OpenAPI schema *path operation*. +### `Query` as the default value or in `Annotated` + +Have in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. + +Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. + +For example, this is not allowed: + +```Python +q: Annotated[str Query(default="rick")] = "morty" +``` + +...because it's not clear if the default value should be `"rick"` or `"morty"`. + +So, you would use (preferably): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...or in older code bases you will find: + +```Python +q: str = Query(default="rick") +``` + +### Advantages of `Annotated` + +**Using `Annotated` is recommended** instead of the default value in function parameters, it is **better** for multiple reasons. 🤓 + +The **default** value of the **function parameter** is the **actual default** value, that's more intuitive with Python in general. 😌 + +You could **call** that same function in **other places** without FastAPI, and it would **work as expected**. If there's a **required** parameter (without a default value), your **editor** will let you know with an error, **Python** will also complain if you run it without passing the required parameter. + +When you don't use `Annotated` and instead use the **(old) default value style**, if you call that function without FastAPI in **other place**, you have to **remember** to pass the arguments to the function for it to work correctly, otherwise the values will be different from what you expect (e.g. `QueryInfo` or something similar instead of `str`). And your editor won't complain, and Python won't complain running that function, only when the operations inside error out. + +Because `Annotated` can have more than one metadata annotation, you could now even use the same function with other tools, like Typer. 🚀 + ## Add more validations You can also add a parameter `min_length`: === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} @@ -134,11 +274,35 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} @@ -156,16 +320,33 @@ But whenever you need them and go and learn them, know that you can already use ## Default values -The same way that you can pass `None` as the value for the `default` parameter, you can pass other values. +You can, of course, use default values other than `None`. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial005.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} + ``` !!! note - Having a default value also makes the parameter optional. + Having a default value of any type, including `None`, makes the parameter optional (not required). ## Make it required @@ -183,23 +364,70 @@ q: Union[str, None] = None But we are now declaring it with `Query`, for example like: -```Python -q: Union[str, None] = Query(default=None, min_length=3) -``` +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "non-Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} + ``` + + !!! tip + Notice that, even though in this case the `Query()` is used as the function parameter default value, we don't pass the `default=None` to `Query()`. + + Still, probably better to use the `Annotated` version. 😉 ### Required with Ellipsis (`...`) -There's an alternative way to explicitly declare that a value is required. You can set the `default` parameter to the literal value `...`: +There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value `...`: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial006b.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} + ``` !!! info If you hadn't seen that `...` before: it is a special single value, it is part of Python and is called "Ellipsis". @@ -212,15 +440,39 @@ This will let **FastAPI** know that this parameter is required. You can declare that a parameter can accept `None`, but that it's still required. This would force clients to send a value, even if the value is `None`. -To do that, you can declare that `None` is a valid type but still use `default=...`: +To do that, you can declare that `None` is a valid type but still use `...` as the default: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} @@ -233,12 +485,29 @@ To do that, you can declare that `None` is a valid type but still use `default=. If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: -```Python hl_lines="2 8" -{!../../../docs_src/query_params_str_validations/tutorial006d.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="2 8" + {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} + ``` !!! tip - Remember that in most of the cases, when something is required, you can simply omit the `default` parameter, so you normally don't have to use `...` nor `Required`. + Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...` nor `Required`. ## Query parameter list / multiple values @@ -248,18 +517,45 @@ For example, to declare a query parameter `q` that can appear multiple times in === "Python 3.6 and above" - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} ``` === "Python 3.9 and above" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} ``` === "Python 3.10 and above" + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} ``` @@ -296,11 +592,29 @@ And you can also define a default `list` of values if none are provided: === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} @@ -327,9 +641,26 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial013.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} + ``` !!! note Have in mind that in this case, FastAPI won't check the contents of the list. @@ -351,25 +682,74 @@ You can add a `title`: === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` + And a `description`: === "Python 3.6 and above" + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="13" {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="12" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} @@ -395,11 +775,35 @@ Then you can declare an `alias`, and that alias is what will be used to find the === "Python 3.6 and above" + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} @@ -415,11 +819,35 @@ Then pass the parameter `deprecated=True` to `Query`: === "Python 3.6 and above" + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="18" {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} @@ -435,11 +863,35 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t === "Python 3.6 and above" + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 783783c58..666de76eb 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -13,17 +13,51 @@ You can define files to be uploaded by the client using `File`. Import `File` and `UploadFile` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` ## Define `File` Parameters Create file parameters the same way you would for `Body` or `Form`: -```Python hl_lines="7" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` !!! info `File` is a class that inherits directly from `Form`. @@ -45,9 +79,26 @@ But there are several cases in which you might benefit from using `UploadFile`. Define a file parameter with a type of `UploadFile`: -```Python hl_lines="12" -{!../../../docs_src/request_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="14" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="12" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` Using `UploadFile` has several advantages over `bytes`: @@ -120,23 +171,65 @@ You can make a file optional by using standard type annotations and setting a de === "Python 3.6 and above" + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.9 and above" + ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="7 14" + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 15" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` + ## `UploadFile` with Additional Metadata You can also use `File()` with `UploadFile`, for example, to set additional metadata: -```Python hl_lines="13" -{!../../../docs_src/request_files/tutorial001_03.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9 15" + {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 13" + {!> ../../../docs_src/request_files/tutorial001_03.py!} + ``` ## Multiple File Uploads @@ -148,11 +241,29 @@ To use that, declare a list of `bytes` or `UploadFile`: === "Python 3.6 and above" + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="10 15" {!> ../../../docs_src/request_files/tutorial002.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} @@ -171,13 +282,31 @@ And the same way as before, you can use `File()` to set additional parameters, e === "Python 3.6 and above" - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} ``` === "Python 3.9 and above" - ```Python hl_lines="16" + ```Python hl_lines="11 18-20" + {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "Python 3.9 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 6844f8c65..9729ab160 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -9,17 +9,51 @@ You can define files and form fields at the same time using `File` and `Form`. ## Import `File` and `Form` -```Python hl_lines="1" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` ## Define `File` and `Form` parameters Create file and form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="8" -{!../../../docs_src/request_forms_and_files/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="10-12" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` The files and form fields will be uploaded as form data and you will receive the files and form fields. diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 26922685a..3f866410a 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -11,17 +11,51 @@ When you need to receive form fields instead of JSON, you can use `Form`. Import `Form` from `fastapi`: -```Python hl_lines="1" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` ## Define `Form` parameters Create form parameters the same way you would for `Body` or `Query`: -```Python hl_lines="7" -{!../../../docs_src/request_forms/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` For example, in one of the ways the OAuth2 specification can be used (called "password flow") it is required to send a `username` and `password` as form fields. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 94347018d..d2aa66843 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -68,11 +68,32 @@ Here we pass an `example` of the data expected in `Body()`: === "Python 3.6 and above" + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="20-25" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" ```Python hl_lines="18-23" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} @@ -99,11 +120,32 @@ Each specific example `dict` in the `examples` can contain: === "Python 3.6 and above" + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="21-47" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" ```Python hl_lines="19-45" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index 45ffaab90..9198db6a6 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -20,9 +20,27 @@ Let's first just use the code and see how it works, and then we'll come back to Copy the example in a file `main.py`: -```Python -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` + ## Run it @@ -116,9 +134,26 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin When we create an instance of the `OAuth2PasswordBearer` class we pass in the `tokenUrl` parameter. This parameter contains the URL that the client (the frontend running in the user's browser) will use to send the `username` and `password` in order to get a token. -```Python hl_lines="6" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="8" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6" + {!> ../../../docs_src/security/tutorial001.py!} + ``` !!! tip Here `tokenUrl="token"` refers to a relative URL `token` that we haven't created yet. As it's a relative URL, it's equivalent to `./token`. @@ -150,9 +185,26 @@ So, it can be used with `Depends`. Now you can pass that `oauth2_scheme` in a dependency with `Depends`. -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` This dependency will provide a `str` that is assigned to the parameter `token` of the *path operation function*. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 13e867216..0076e7d16 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -2,9 +2,26 @@ In the previous chapter the security system (which is based on the dependency injection system) was giving the *path operation function* a `token` as a `str`: -```Python hl_lines="10" -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.6 and above" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` But that is still not that useful. @@ -18,11 +35,35 @@ The same way we use Pydantic to declare bodies, we can use it anywhere else: === "Python 3.6 and above" + ```Python hl_lines="5 13-17" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="5 12-16" {!> ../../../docs_src/security/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="3 10-14" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -40,11 +81,35 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6 and above" + ```Python hl_lines="26" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="25" {!> ../../../docs_src/security/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="23" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -56,11 +121,35 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6 and above" + ```Python hl_lines="20-23 27-28" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="19-22 26-27" {!> ../../../docs_src/security/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="17-20 24-25" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -72,11 +161,35 @@ So now we can use the same `Depends` with our `get_current_user` in the *path op === "Python 3.6 and above" + ```Python hl_lines="32" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="31" {!> ../../../docs_src/security/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="29" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -130,11 +243,35 @@ And all these thousands of *path operations* can be as small as 3 lines: === "Python 3.6 and above" + ```Python hl_lines="31-33" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="30-32" {!> ../../../docs_src/security/tutorial002.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="28-30" {!> ../../../docs_src/security/tutorial002_py310.py!} diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 41f00fd41..0b6390975 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -111,11 +111,35 @@ And another one to authenticate and return a user. === "Python 3.6 and above" + ```Python hl_lines="7 49 56-57 60-61 70-76" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="7 48 55-56 59-60 69-75" {!> ../../../docs_src/security/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="6 47 54-55 58-59 68-74" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -154,11 +178,35 @@ Create a utility function to generate a new access token. === "Python 3.6 and above" + ```Python hl_lines="6 13-15 29-31 79-87" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="6 12-14 28-30 78-86" {!> ../../../docs_src/security/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="5 11-13 27-29 77-85" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -174,11 +222,35 @@ If the token is invalid, return an HTTP error right away. === "Python 3.6 and above" + ```Python hl_lines="90-107" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="89-106" {!> ../../../docs_src/security/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="88-105" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -192,11 +264,35 @@ Create a real JWT access token and return it. === "Python 3.6 and above" + ```Python hl_lines="118-133" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="115-128" {!> ../../../docs_src/security/tutorial004.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="114-127" {!> ../../../docs_src/security/tutorial004_py310.py!} diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 505b223b1..6abf43218 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -51,11 +51,35 @@ First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depe === "Python 3.6 and above" + ```Python hl_lines="4 79" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="4 76" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="2 74" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -100,11 +124,35 @@ For the error, we use the exception `HTTPException`: === "Python 3.6 and above" + ```Python hl_lines="3 80-82" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="3 77-79" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="1 75-77" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -136,11 +184,35 @@ So, the thief won't be able to try to use those same passwords in another system === "Python 3.6 and above" + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="80-83" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="78-81" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -182,11 +254,35 @@ For this simple example, we are going to just be completely insecure and return === "Python 3.6 and above" + ```Python hl_lines="88" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python hl_lines="85" {!> ../../../docs_src/security/tutorial003.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. ```Python hl_lines="83" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -215,13 +311,37 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a === "Python 3.6 and above" - ```Python hl_lines="58-66 69-72 90" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` === "Python 3.10 and above" - ```Python hl_lines="55-64 67-70 88" + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="56-64 67-70 88" {!> ../../../docs_src/security/tutorial003_py310.py!} ``` diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index be07aab37..a932ad3f8 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -113,11 +113,35 @@ Both *path operations* require an `X-Token` header. === "Python 3.6 and above" ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.9 and above" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` === "Python 3.10 and above" + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.6 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "Python 3.10 and above - non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` diff --git a/docs_src/additional_status_codes/tutorial001_an.py b/docs_src/additional_status_codes/tutorial001_an.py new file mode 100644 index 000000000..b5ad6a16b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse +from typing_extensions import Annotated + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py310.py b/docs_src/additional_status_codes/tutorial001_an_py310.py new file mode 100644 index 000000000..1865074a1 --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[str | None, Body()] = None, + size: Annotated[int | None, Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_an_py39.py b/docs_src/additional_status_codes/tutorial001_an_py39.py new file mode 100644 index 000000000..89653dd8a --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: Annotated[Union[str, None], Body()] = None, + size: Annotated[Union[int, None], Body()] = None, +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/additional_status_codes/tutorial001_py310.py b/docs_src/additional_status_codes/tutorial001_py310.py new file mode 100644 index 000000000..38bfa455b --- /dev/null +++ b/docs_src/additional_status_codes/tutorial001_py310.py @@ -0,0 +1,23 @@ +from fastapi import Body, FastAPI, status +from fastapi.responses import JSONResponse + +app = FastAPI() + +items = {"foo": {"name": "Fighters", "size": 6}, "bar": {"name": "Tenders", "size": 3}} + + +@app.put("/items/{item_id}") +async def upsert_item( + item_id: str, + name: str | None = Body(default=None), + size: int | None = Body(default=None), +): + if item_id in items: + item = items[item_id] + item["name"] = name + item["size"] = size + return item + else: + item = {"name": name, "size": size} + items[item_id] = item + return JSONResponse(status_code=status.HTTP_201_CREATED, content=item) diff --git a/docs_src/annotated/tutorial001.py b/docs_src/annotated/tutorial001.py deleted file mode 100644 index 959114b3f..000000000 --- a/docs_src/annotated/tutorial001.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): - return {"q": q, "skip": skip, "limit": limit} - - -CommonParamsDepends = Annotated[dict, Depends(common_parameters)] - - -@app.get("/items/") -async def read_items(commons: CommonParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial001_py39.py b/docs_src/annotated/tutorial001_py39.py deleted file mode 100644 index b05b89c4e..000000000 --- a/docs_src/annotated/tutorial001_py39.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated, Optional - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -async def common_parameters(q: Optional[str] = None, skip: int = 0, limit: int = 100): - return {"q": q, "skip": skip, "limit": limit} - - -CommonParamsDepends = Annotated[dict, Depends(common_parameters)] - - -@app.get("/items/") -async def read_items(commons: CommonParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial002.py b/docs_src/annotated/tutorial002.py deleted file mode 100644 index 2293fbb1a..000000000 --- a/docs_src/annotated/tutorial002.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Optional - -from fastapi import Depends, FastAPI -from typing_extensions import Annotated - -app = FastAPI() - - -class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] - - -@app.get("/items/") -async def read_items(commons: CommonQueryParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial002_py39.py b/docs_src/annotated/tutorial002_py39.py deleted file mode 100644 index 7fa1a8740..000000000 --- a/docs_src/annotated/tutorial002_py39.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Annotated, Optional - -from fastapi import Depends, FastAPI - -app = FastAPI() - - -class CommonQueryParams: - def __init__(self, q: Optional[str] = None, skip: int = 0, limit: int = 100): - self.q = q - self.skip = skip - self.limit = limit - - -CommonQueryParamsDepends = Annotated[CommonQueryParams, Depends()] - - -@app.get("/items/") -async def read_items(commons: CommonQueryParamsDepends): - return commons diff --git a/docs_src/annotated/tutorial003.py b/docs_src/annotated/tutorial003.py deleted file mode 100644 index 353d8b806..000000000 --- a/docs_src/annotated/tutorial003.py +++ /dev/null @@ -1,15 +0,0 @@ -from fastapi import FastAPI, Path -from fastapi.param_functions import Query -from typing_extensions import Annotated - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items(item_id: Annotated[int, Path(gt=0)]): - return {"item_id": item_id} - - -@app.get("/users") -async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): - return {"user_id": user_id} diff --git a/docs_src/annotated/tutorial003_py39.py b/docs_src/annotated/tutorial003_py39.py deleted file mode 100644 index 9341b7d4f..000000000 --- a/docs_src/annotated/tutorial003_py39.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Annotated - -from fastapi import FastAPI, Path -from fastapi.param_functions import Query - -app = FastAPI() - - -@app.get("/items/{item_id}") -async def read_items(item_id: Annotated[int, Path(gt=0)]): - return {"item_id": item_id} - - -@app.get("/users") -async def read_users(user_id: Annotated[str, Query(min_length=1)] = "me"): - return {"user_id": user_id} diff --git a/tests/test_tutorial/test_annotated/__init__.py b/docs_src/app_testing/app_b_an/__init__.py similarity index 100% rename from tests/test_tutorial/test_annotated/__init__.py rename to docs_src/app_testing/app_b_an/__init__.py diff --git a/docs_src/app_testing/app_b_an/main.py b/docs_src/app_testing/app_b_an/main.py new file mode 100644 index 000000000..c63134fc9 --- /dev/null +++ b/docs_src/app_testing/app_b_an/main.py @@ -0,0 +1,39 @@ +from typing import Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an/test_main.py b/docs_src/app_testing/app_b_an/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py310/__init__.py b/docs_src/app_testing/app_b_an_py310/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py310/main.py b/docs_src/app_testing/app_b_an_py310/main.py new file mode 100644 index 000000000..48c27a0b8 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/main.py @@ -0,0 +1,38 @@ +from typing import Annotated + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: str | None = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py310/test_main.py b/docs_src/app_testing/app_b_an_py310/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py310/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_an_py39/__init__.py b/docs_src/app_testing/app_b_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/app_testing/app_b_an_py39/main.py b/docs_src/app_testing/app_b_an_py39/main.py new file mode 100644 index 000000000..935a510b7 --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/main.py @@ -0,0 +1,38 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel + +fake_secret_token = "coneofsilence" + +fake_db = { + "foo": {"id": "foo", "title": "Foo", "description": "There goes my hero"}, + "bar": {"id": "bar", "title": "Bar", "description": "The bartenders"}, +} + +app = FastAPI() + + +class Item(BaseModel): + id: str + title: str + description: Union[str, None] = None + + +@app.get("/items/{item_id}", response_model=Item) +async def read_main(item_id: str, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item_id not in fake_db: + raise HTTPException(status_code=404, detail="Item not found") + return fake_db[item_id] + + +@app.post("/items/", response_model=Item) +async def create_item(item: Item, x_token: Annotated[str, Header()]): + if x_token != fake_secret_token: + raise HTTPException(status_code=400, detail="Invalid X-Token header") + if item.id in fake_db: + raise HTTPException(status_code=400, detail="Item already exists") + fake_db[item.id] = item + return item diff --git a/docs_src/app_testing/app_b_an_py39/test_main.py b/docs_src/app_testing/app_b_an_py39/test_main.py new file mode 100644 index 000000000..d186b8ecb --- /dev/null +++ b/docs_src/app_testing/app_b_an_py39/test_main.py @@ -0,0 +1,65 @@ +from fastapi.testclient import TestClient + +from .main import app + +client = TestClient(app) + + +def test_read_item(): + response = client.get("/items/foo", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 200 + assert response.json() == { + "id": "foo", + "title": "Foo", + "description": "There goes my hero", + } + + +def test_read_item_bad_token(): + response = client.get("/items/foo", headers={"X-Token": "hailhydra"}) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_read_inexistent_item(): + response = client.get("/items/baz", headers={"X-Token": "coneofsilence"}) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_create_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={"id": "foobar", "title": "Foo Bar", "description": "The Foo Barters"}, + ) + assert response.status_code == 200 + assert response.json() == { + "id": "foobar", + "title": "Foo Bar", + "description": "The Foo Barters", + } + + +def test_create_item_bad_token(): + response = client.post( + "/items/", + headers={"X-Token": "hailhydra"}, + json={"id": "bazz", "title": "Bazz", "description": "Drop the bazz"}, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Invalid X-Token header"} + + +def test_create_existing_item(): + response = client.post( + "/items/", + headers={"X-Token": "coneofsilence"}, + json={ + "id": "foo", + "title": "The Foo ID Stealers", + "description": "There goes my stealer", + }, + ) + assert response.status_code == 400 + assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/background_tasks/tutorial002_an.py b/docs_src/background_tasks/tutorial002_an.py new file mode 100644 index 000000000..f63502b09 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import BackgroundTasks, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py310.py b/docs_src/background_tasks/tutorial002_an_py310.py new file mode 100644 index 000000000..1fc78fbc9 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: str | None = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/background_tasks/tutorial002_an_py39.py b/docs_src/background_tasks/tutorial002_an_py39.py new file mode 100644 index 000000000..bfdd14875 --- /dev/null +++ b/docs_src/background_tasks/tutorial002_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import BackgroundTasks, Depends, FastAPI + +app = FastAPI() + + +def write_log(message: str): + with open("log.txt", mode="a") as log: + log.write(message) + + +def get_query(background_tasks: BackgroundTasks, q: Union[str, None] = None): + if q: + message = f"found query: {q}\n" + background_tasks.add_task(write_log, message) + return q + + +@app.post("/send-notification/{email}") +async def send_notification( + email: str, background_tasks: BackgroundTasks, q: Annotated[str, Depends(get_query)] +): + message = f"message to {email}\n" + background_tasks.add_task(write_log, message) + return {"message": "Message sent"} diff --git a/docs_src/bigger_applications/app_an/__init__.py b/docs_src/bigger_applications/app_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/dependencies.py b/docs_src/bigger_applications/app_an/dependencies.py new file mode 100644 index 000000000..1374c54b3 --- /dev/null +++ b/docs_src/bigger_applications/app_an/dependencies.py @@ -0,0 +1,12 @@ +from fastapi import Header, HTTPException +from typing_extensions import Annotated + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an/internal/__init__.py b/docs_src/bigger_applications/app_an/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/internal/admin.py b/docs_src/bigger_applications/app_an/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an/main.py b/docs_src/bigger_applications/app_an/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an/routers/__init__.py b/docs_src/bigger_applications/app_an/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an/routers/items.py b/docs_src/bigger_applications/app_an/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an/routers/users.py b/docs_src/bigger_applications/app_an/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/bigger_applications/app_an_py39/__init__.py b/docs_src/bigger_applications/app_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/dependencies.py b/docs_src/bigger_applications/app_an_py39/dependencies.py new file mode 100644 index 000000000..5c7612aa0 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/dependencies.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Header, HTTPException + + +async def get_token_header(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def get_query_token(token: str): + if token != "jessica": + raise HTTPException(status_code=400, detail="No Jessica token provided") diff --git a/docs_src/bigger_applications/app_an_py39/internal/__init__.py b/docs_src/bigger_applications/app_an_py39/internal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/internal/admin.py b/docs_src/bigger_applications/app_an_py39/internal/admin.py new file mode 100644 index 000000000..99d3da86b --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/internal/admin.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.post("/") +async def update_admin(): + return {"message": "Admin getting schwifty"} diff --git a/docs_src/bigger_applications/app_an_py39/main.py b/docs_src/bigger_applications/app_an_py39/main.py new file mode 100644 index 000000000..ae544a3aa --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/main.py @@ -0,0 +1,23 @@ +from fastapi import Depends, FastAPI + +from .dependencies import get_query_token, get_token_header +from .internal import admin +from .routers import items, users + +app = FastAPI(dependencies=[Depends(get_query_token)]) + + +app.include_router(users.router) +app.include_router(items.router) +app.include_router( + admin.router, + prefix="/admin", + tags=["admin"], + dependencies=[Depends(get_token_header)], + responses={418: {"description": "I'm a teapot"}}, +) + + +@app.get("/") +async def root(): + return {"message": "Hello Bigger Applications!"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/__init__.py b/docs_src/bigger_applications/app_an_py39/routers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/bigger_applications/app_an_py39/routers/items.py b/docs_src/bigger_applications/app_an_py39/routers/items.py new file mode 100644 index 000000000..bde9ff4d5 --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/items.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException + +from ..dependencies import get_token_header + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_token_header)], + responses={404: {"description": "Not found"}}, +) + + +fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}} + + +@router.get("/") +async def read_items(): + return fake_items_db + + +@router.get("/{item_id}") +async def read_item(item_id: str): + if item_id not in fake_items_db: + raise HTTPException(status_code=404, detail="Item not found") + return {"name": fake_items_db[item_id]["name"], "item_id": item_id} + + +@router.put( + "/{item_id}", + tags=["custom"], + responses={403: {"description": "Operation forbidden"}}, +) +async def update_item(item_id: str): + if item_id != "plumbus": + raise HTTPException( + status_code=403, detail="You can only update the item: plumbus" + ) + return {"item_id": item_id, "name": "The great Plumbus"} diff --git a/docs_src/bigger_applications/app_an_py39/routers/users.py b/docs_src/bigger_applications/app_an_py39/routers/users.py new file mode 100644 index 000000000..39b3d7e7c --- /dev/null +++ b/docs_src/bigger_applications/app_an_py39/routers/users.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/users/", tags=["users"]) +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + +@router.get("/users/me", tags=["users"]) +async def read_user_me(): + return {"username": "fakecurrentuser"} + + +@router.get("/users/{username}", tags=["users"]) +async def read_user(username: str): + return {"username": username} diff --git a/docs_src/body_fields/tutorial001_an.py b/docs_src/body_fields/tutorial001_an.py new file mode 100644 index 000000000..15ea1b53d --- /dev/null +++ b/docs_src/body_fields/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py310.py b/docs_src/body_fields/tutorial001_an_py310.py new file mode 100644 index 000000000..c9d99e1c2 --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_fields/tutorial001_an_py39.py b/docs_src/body_fields/tutorial001_an_py39.py new file mode 100644 index 000000000..6ef14470c --- /dev/null +++ b/docs_src/body_fields/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = Field( + default=None, title="The description of the item", max_length=300 + ) + price: float = Field(gt=0, description="The price must be greater than zero") + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial001_an.py b/docs_src/body_multiple_params/tutorial001_an.py new file mode 100644 index 000000000..308eee854 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an.py @@ -0,0 +1,28 @@ +from typing import Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py310.py b/docs_src/body_multiple_params/tutorial001_an_py310.py new file mode 100644 index 000000000..2d79c19c2 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py310.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str | None = None, + item: Item | None = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial001_an_py39.py b/docs_src/body_multiple_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1c0ac3a7b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial001_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: Union[str, None] = None, + item: Union[Item, None] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + if item: + results.update({"item": item}) + return results diff --git a/docs_src/body_multiple_params/tutorial003_an.py b/docs_src/body_multiple_params/tutorial003_an.py new file mode 100644 index 000000000..39ef7340a --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py310.py b/docs_src/body_multiple_params/tutorial003_an_py310.py new file mode 100644 index 000000000..593ff893c --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial003_an_py39.py b/docs_src/body_multiple_params/tutorial003_an_py39.py new file mode 100644 index 000000000..042351e0b --- /dev/null +++ b/docs_src/body_multiple_params/tutorial003_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, item: Item, user: User, importance: Annotated[int, Body()] +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + return results diff --git a/docs_src/body_multiple_params/tutorial004_an.py b/docs_src/body_multiple_params/tutorial004_an.py new file mode 100644 index 000000000..f6830f392 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an.py @@ -0,0 +1,34 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py310.py b/docs_src/body_multiple_params/tutorial004_an_py310.py new file mode 100644 index 000000000..cd5c66fef --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py310.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +class User(BaseModel): + username: str + full_name: str | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: str | None = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial004_an_py39.py b/docs_src/body_multiple_params/tutorial004_an_py39.py new file mode 100644 index 000000000..567427c03 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial004_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +class User(BaseModel): + username: str + full_name: Union[str, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item, + user: User, + importance: Annotated[int, Body(gt=0)], + q: Union[str, None] = None, +): + results = {"item_id": item_id, "item": item, "user": user, "importance": importance} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/body_multiple_params/tutorial005_an.py b/docs_src/body_multiple_params/tutorial005_an.py new file mode 100644 index 000000000..dadde80b5 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an.py @@ -0,0 +1,20 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py310.py b/docs_src/body_multiple_params/tutorial005_an_py310.py new file mode 100644 index 000000000..f97680ba0 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/body_multiple_params/tutorial005_an_py39.py b/docs_src/body_multiple_params/tutorial005_an_py39.py new file mode 100644 index 000000000..9a52425c1 --- /dev/null +++ b/docs_src/body_multiple_params/tutorial005_an_py39.py @@ -0,0 +1,19 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Annotated[Item, Body(embed=True)]): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/cookie_params/tutorial001_an.py b/docs_src/cookie_params/tutorial001_an.py new file mode 100644 index 000000000..6d5931229 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import Cookie, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py310.py b/docs_src/cookie_params/tutorial001_an_py310.py new file mode 100644 index 000000000..69ac683fe --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[str | None, Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/cookie_params/tutorial001_an_py39.py b/docs_src/cookie_params/tutorial001_an_py39.py new file mode 100644 index 000000000..e18d0a332 --- /dev/null +++ b/docs_src/cookie_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import Cookie, FastAPI + +app = FastAPI() + + +@app.get("/items/") +async def read_items(ads_id: Annotated[Union[str, None], Cookie()] = None): + return {"ads_id": ads_id} diff --git a/docs_src/dependencies/tutorial001_02_an.py b/docs_src/dependencies/tutorial001_02_an.py new file mode 100644 index 000000000..455d60c82 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an.py @@ -0,0 +1,25 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py310.py b/docs_src/dependencies/tutorial001_02_an_py310.py new file mode 100644 index 000000000..f59637bb2 --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_02_an_py39.py b/docs_src/dependencies/tutorial001_02_an_py39.py new file mode 100644 index 000000000..df969ae9c --- /dev/null +++ b/docs_src/dependencies/tutorial001_02_an_py39.py @@ -0,0 +1,24 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +CommonsDep = Annotated[dict, Depends(common_parameters)] + + +@app.get("/items/") +async def read_items(commons: CommonsDep): + return commons + + +@app.get("/users/") +async def read_users(commons: CommonsDep): + return commons diff --git a/docs_src/dependencies/tutorial001_an.py b/docs_src/dependencies/tutorial001_an.py new file mode 100644 index 000000000..81e24fe86 --- /dev/null +++ b/docs_src/dependencies/tutorial001_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py310.py b/docs_src/dependencies/tutorial001_an_py310.py new file mode 100644 index 000000000..f2e57ae7b --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py310.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial001_an_py39.py b/docs_src/dependencies/tutorial001_an_py39.py new file mode 100644 index 000000000..5d9fe6ddf --- /dev/null +++ b/docs_src/dependencies/tutorial001_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return commons diff --git a/docs_src/dependencies/tutorial002_an.py b/docs_src/dependencies/tutorial002_an.py new file mode 100644 index 000000000..964ccf66c --- /dev/null +++ b/docs_src/dependencies/tutorial002_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py310.py b/docs_src/dependencies/tutorial002_an_py310.py new file mode 100644 index 000000000..077726312 --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial002_an_py39.py b/docs_src/dependencies/tutorial002_an_py39.py new file mode 100644 index 000000000..844a23c5a --- /dev/null +++ b/docs_src/dependencies/tutorial002_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an.py b/docs_src/dependencies/tutorial003_an.py new file mode 100644 index 000000000..ba8e9f717 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an.py @@ -0,0 +1,26 @@ +from typing import Any, Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py310.py b/docs_src/dependencies/tutorial003_an_py310.py new file mode 100644 index 000000000..44116989b --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial003_an_py39.py b/docs_src/dependencies/tutorial003_an_py39.py new file mode 100644 index 000000000..9e9123ad2 --- /dev/null +++ b/docs_src/dependencies/tutorial003_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Any, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an.py b/docs_src/dependencies/tutorial004_an.py new file mode 100644 index 000000000..78881a354 --- /dev/null +++ b/docs_src/dependencies/tutorial004_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py310.py b/docs_src/dependencies/tutorial004_an_py310.py new file mode 100644 index 000000000..2c009efcc --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial004_an_py39.py b/docs_src/dependencies/tutorial004_an_py39.py new file mode 100644 index 000000000..74268626b --- /dev/null +++ b/docs_src/dependencies/tutorial004_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] + + +class CommonQueryParams: + def __init__(self, q: Union[str, None] = None, skip: int = 0, limit: int = 100): + self.q = q + self.skip = skip + self.limit = limit + + +@app.get("/items/") +async def read_items(commons: Annotated[CommonQueryParams, Depends()]): + response = {} + if commons.q: + response.update({"q": commons.q}) + items = fake_items_db[commons.skip : commons.skip + commons.limit] + response.update({"items": items}) + return response diff --git a/docs_src/dependencies/tutorial005_an.py b/docs_src/dependencies/tutorial005_an.py new file mode 100644 index 000000000..6785099da --- /dev/null +++ b/docs_src/dependencies/tutorial005_an.py @@ -0,0 +1,26 @@ +from typing import Union + +from fastapi import Cookie, Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py310.py b/docs_src/dependencies/tutorial005_an_py310.py new file mode 100644 index 000000000..6c0aa0b36 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py310.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: str | None = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[str | None, Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial005_an_py39.py b/docs_src/dependencies/tutorial005_an_py39.py new file mode 100644 index 000000000..e8887e162 --- /dev/null +++ b/docs_src/dependencies/tutorial005_an_py39.py @@ -0,0 +1,25 @@ +from typing import Annotated, Union + +from fastapi import Cookie, Depends, FastAPI + +app = FastAPI() + + +def query_extractor(q: Union[str, None] = None): + return q + + +def query_or_cookie_extractor( + q: Annotated[str, Depends(query_extractor)], + last_query: Annotated[Union[str, None], Cookie()] = None, +): + if not q: + return last_query + return q + + +@app.get("/items/") +async def read_query( + query_or_default: Annotated[str, Depends(query_or_cookie_extractor)] +): + return {"q_or_cookie": query_or_default} diff --git a/docs_src/dependencies/tutorial006_an.py b/docs_src/dependencies/tutorial006_an.py new file mode 100644 index 000000000..5aaea04d1 --- /dev/null +++ b/docs_src/dependencies/tutorial006_an.py @@ -0,0 +1,20 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial006_an_py39.py b/docs_src/dependencies/tutorial006_an_py39.py new file mode 100644 index 000000000..11976ed6a --- /dev/null +++ b/docs_src/dependencies/tutorial006_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, Header, HTTPException + +app = FastAPI() + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)]) +async def read_items(): + return [{"item": "Foo"}, {"item": "Bar"}] diff --git a/docs_src/dependencies/tutorial008_an.py b/docs_src/dependencies/tutorial008_an.py new file mode 100644 index 000000000..2de86f042 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends +from typing_extensions import Annotated + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial008_an_py39.py b/docs_src/dependencies/tutorial008_an_py39.py new file mode 100644 index 000000000..acc804c32 --- /dev/null +++ b/docs_src/dependencies/tutorial008_an_py39.py @@ -0,0 +1,27 @@ +from typing import Annotated + +from fastapi import Depends + + +async def dependency_a(): + dep_a = generate_dep_a() + try: + yield dep_a + finally: + dep_a.close() + + +async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]): + dep_b = generate_dep_b() + try: + yield dep_b + finally: + dep_b.close(dep_a) + + +async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]): + dep_c = generate_dep_c() + try: + yield dep_c + finally: + dep_c.close(dep_b) diff --git a/docs_src/dependencies/tutorial011_an.py b/docs_src/dependencies/tutorial011_an.py new file mode 100644 index 000000000..6c13d9033 --- /dev/null +++ b/docs_src/dependencies/tutorial011_an.py @@ -0,0 +1,22 @@ +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial011_an_py39.py b/docs_src/dependencies/tutorial011_an_py39.py new file mode 100644 index 000000000..68b7434ec --- /dev/null +++ b/docs_src/dependencies/tutorial011_an_py39.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +class FixedContentQueryChecker: + def __init__(self, fixed_content: str): + self.fixed_content = fixed_content + + def __call__(self, q: str = ""): + if q: + return self.fixed_content in q + return False + + +checker = FixedContentQueryChecker("bar") + + +@app.get("/query-checker/") +async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]): + return {"fixed_content_in_query": fixed_content_included} diff --git a/docs_src/dependencies/tutorial012_an.py b/docs_src/dependencies/tutorial012_an.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependencies/tutorial012_an_py39.py b/docs_src/dependencies/tutorial012_an_py39.py new file mode 100644 index 000000000..7541e6bf4 --- /dev/null +++ b/docs_src/dependencies/tutorial012_an_py39.py @@ -0,0 +1,26 @@ +from fastapi import Depends, FastAPI, Header, HTTPException +from typing_extensions import Annotated + + +async def verify_token(x_token: Annotated[str, Header()]): + if x_token != "fake-super-secret-token": + raise HTTPException(status_code=400, detail="X-Token header invalid") + + +async def verify_key(x_key: Annotated[str, Header()]): + if x_key != "fake-super-secret-key": + raise HTTPException(status_code=400, detail="X-Key header invalid") + return x_key + + +app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)]) + + +@app.get("/items/") +async def read_items(): + return [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@app.get("/users/") +async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] diff --git a/docs_src/dependency_testing/tutorial001_an.py b/docs_src/dependency_testing/tutorial001_an.py new file mode 100644 index 000000000..4c76a87ff --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an.py @@ -0,0 +1,60 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py310.py b/docs_src/dependency_testing/tutorial001_an_py310.py new file mode 100644 index 000000000..f866e7a1b --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py310.py @@ -0,0 +1,57 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_an_py39.py b/docs_src/dependency_testing/tutorial001_an_py39.py new file mode 100644 index 000000000..bccb0cdb1 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_an_py39.py @@ -0,0 +1,59 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters( + q: Union[str, None] = None, skip: int = 0, limit: int = 100 +): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: Annotated[dict, Depends(common_parameters)]): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: Union[str, None] = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/dependency_testing/tutorial001_py310.py b/docs_src/dependency_testing/tutorial001_py310.py new file mode 100644 index 000000000..d1f6d4374 --- /dev/null +++ b/docs_src/dependency_testing/tutorial001_py310.py @@ -0,0 +1,55 @@ +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + +app = FastAPI() + + +async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + +@app.get("/items/") +async def read_items(commons: dict = Depends(common_parameters)): + return {"message": "Hello Items!", "params": commons} + + +@app.get("/users/") +async def read_users(commons: dict = Depends(common_parameters)): + return {"message": "Hello Users!", "params": commons} + + +client = TestClient(app) + + +async def override_dependency(q: str | None = None): + return {"q": q, "skip": 5, "limit": 10} + + +app.dependency_overrides[common_parameters] = override_dependency + + +def test_override_in_items(): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_q(): + response = client.get("/items/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_items_with_params(): + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } diff --git a/docs_src/extra_data_types/tutorial001_an.py b/docs_src/extra_data_types/tutorial001_an.py new file mode 100644 index 000000000..a4c074241 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an.py @@ -0,0 +1,29 @@ +from datetime import datetime, time, timedelta +from typing import Union +from uuid import UUID + +from fastapi import Body, FastAPI +from typing_extensions import Annotated + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py310.py b/docs_src/extra_data_types/tutorial001_an_py310.py new file mode 100644 index 000000000..4f69c40d9 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py310.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[datetime | None, Body()] = None, + end_datetime: Annotated[datetime | None, Body()] = None, + repeat_at: Annotated[time | None, Body()] = None, + process_after: Annotated[timedelta | None, Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/extra_data_types/tutorial001_an_py39.py b/docs_src/extra_data_types/tutorial001_an_py39.py new file mode 100644 index 000000000..630d36ae3 --- /dev/null +++ b/docs_src/extra_data_types/tutorial001_an_py39.py @@ -0,0 +1,28 @@ +from datetime import datetime, time, timedelta +from typing import Annotated, Union +from uuid import UUID + +from fastapi import Body, FastAPI + +app = FastAPI() + + +@app.put("/items/{item_id}") +async def read_items( + item_id: UUID, + start_datetime: Annotated[Union[datetime, None], Body()] = None, + end_datetime: Annotated[Union[datetime, None], Body()] = None, + repeat_at: Annotated[Union[time, None], Body()] = None, + process_after: Annotated[Union[timedelta, None], Body()] = None, +): + start_process = start_datetime + process_after + duration = end_datetime - start_process + return { + "item_id": item_id, + "start_datetime": start_datetime, + "end_datetime": end_datetime, + "repeat_at": repeat_at, + "process_after": process_after, + "start_process": start_process, + "duration": duration, + } diff --git a/docs_src/header_params/tutorial001_an.py b/docs_src/header_params/tutorial001_an.py new file mode 100644 index 000000000..816c00086 --- /dev/null +++ b/docs_src/header_params/tutorial001_an.py @@ -0,0 +1,11 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py310.py b/docs_src/header_params/tutorial001_an_py310.py new file mode 100644 index 000000000..4ba5e1bfb --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[str | None, Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial001_an_py39.py b/docs_src/header_params/tutorial001_an_py39.py new file mode 100644 index 000000000..1fbe3bb99 --- /dev/null +++ b/docs_src/header_params/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(user_agent: Annotated[Union[str, None], Header()] = None): + return {"User-Agent": user_agent} diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py new file mode 100644 index 000000000..65d972d46 --- /dev/null +++ b/docs_src/header_params/tutorial002_an.py @@ -0,0 +1,15 @@ +from typing import Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py310.py b/docs_src/header_params/tutorial002_an_py310.py new file mode 100644 index 000000000..b340647b6 --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py310.py @@ -0,0 +1,12 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[str | None, Header(convert_underscores=False)] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py39.py b/docs_src/header_params/tutorial002_an_py39.py new file mode 100644 index 000000000..7f6a99f9c --- /dev/null +++ b/docs_src/header_params/tutorial002_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + strange_header: Annotated[ + Union[str, None], Header(convert_underscores=False) + ] = None +): + return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial003_an.py b/docs_src/header_params/tutorial003_an.py new file mode 100644 index 000000000..5406fd1f8 --- /dev/null +++ b/docs_src/header_params/tutorial003_an.py @@ -0,0 +1,11 @@ +from typing import List, Union + +from fastapi import FastAPI, Header +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py310.py b/docs_src/header_params/tutorial003_an_py310.py new file mode 100644 index 000000000..0e78cf029 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py310.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[list[str] | None, Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/header_params/tutorial003_an_py39.py b/docs_src/header_params/tutorial003_an_py39.py new file mode 100644 index 000000000..c1dd49961 --- /dev/null +++ b/docs_src/header_params/tutorial003_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated, List, Union + +from fastapi import FastAPI, Header + +app = FastAPI() + + +@app.get("/items/") +async def read_items(x_token: Annotated[Union[List[str], None], Header()] = None): + return {"X-Token values": x_token} diff --git a/docs_src/path_params_numeric_validations/tutorial001_an.py b/docs_src/path_params_numeric_validations/tutorial001_an.py new file mode 100644 index 000000000..621be7b04 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an.py @@ -0,0 +1,17 @@ +from typing import Union + +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py310.py b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py new file mode 100644 index 000000000..c4db263f0 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py310.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[str | None, Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial001_an_py39.py b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py new file mode 100644 index 000000000..b36315a46 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial001_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + q: Annotated[Union[str, None], Query(alias="item-query")] = None, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an.py b/docs_src/path_params_numeric_validations/tutorial002_an.py new file mode 100644 index 000000000..322f8cf0b --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial002_an_py39.py b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py new file mode 100644 index 000000000..cd882abb2 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial002_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + q: str, item_id: Annotated[int, Path(title="The ID of the item to get")] +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an.py b/docs_src/path_params_numeric_validations/tutorial003_an.py new file mode 100644 index 000000000..d0fa8b3db --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial003_an_py39.py b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..1588556e7 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an.py b/docs_src/path_params_numeric_validations/tutorial004_an.py new file mode 100644 index 000000000..ffc50f6c5 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial004_an_py39.py b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..f67f6450e --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial004_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", ge=1)], q: str +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an.py b/docs_src/path_params_numeric_validations/tutorial005_an.py new file mode 100644 index 000000000..433c69129 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI, Path +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial005_an_py39.py b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..571dd583c --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial005_an_py39.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import FastAPI, Path + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get", gt=0, le=1000)], + q: str, +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an.py b/docs_src/path_params_numeric_validations/tutorial006_an.py new file mode 100644 index 000000000..22a143623 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, Path, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/path_params_numeric_validations/tutorial006_an_py39.py b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..804751893 --- /dev/null +++ b/docs_src/path_params_numeric_validations/tutorial006_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_items( + *, + item_id: Annotated[int, Path(title="The ID of the item to get", ge=0, le=1000)], + q: str, + size: Annotated[float, Query(gt=0, lt=10.5)], +): + results = {"item_id": item_id} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/python_types/tutorial013.py b/docs_src/python_types/tutorial013.py new file mode 100644 index 000000000..0ec773519 --- /dev/null +++ b/docs_src/python_types/tutorial013.py @@ -0,0 +1,5 @@ +from typing_extensions import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/python_types/tutorial013_py39.py b/docs_src/python_types/tutorial013_py39.py new file mode 100644 index 000000000..65a0eaa93 --- /dev/null +++ b/docs_src/python_types/tutorial013_py39.py @@ -0,0 +1,5 @@ +from typing import Annotated + + +def say_hello(name: Annotated[str, "this is just metadata"]) -> str: + return f"Hello {name}" diff --git a/docs_src/query_params_str_validations/tutorial002_an.py b/docs_src/query_params_str_validations/tutorial002_an.py new file mode 100644 index 000000000..cb1b38940 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial002_an_py310.py b/docs_src/query_params_str_validations/tutorial002_an_py310.py new file mode 100644 index 000000000..66ee7451b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial002_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(max_length=50)] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an.py b/docs_src/query_params_str_validations/tutorial003_an.py new file mode 100644 index 000000000..a3665f6a8 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py310.py b/docs_src/query_params_str_validations/tutorial003_an_py310.py new file mode 100644 index 000000000..836af04de --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial003_an_py39.py b/docs_src/query_params_str_validations/tutorial003_an_py39.py new file mode 100644 index 000000000..87a426839 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial003_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(min_length=3, max_length=50)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py new file mode 100644 index 000000000..5346b997b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -0,0 +1,18 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py new file mode 100644 index 000000000..8fd375b3d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py new file mode 100644 index 000000000..2fd82db75 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an.py b/docs_src/query_params_str_validations/tutorial005_an.py new file mode 100644 index 000000000..452d4d38d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial005_an_py39.py b/docs_src/query_params_str_validations/tutorial005_an_py39.py new file mode 100644 index 000000000..b1f6046b5 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial005_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = "fixedquery"): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an.py b/docs_src/query_params_str_validations/tutorial006_an.py new file mode 100644 index 000000000..559480d2b --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006_an_py39.py b/docs_src/query_params_str_validations/tutorial006_an_py39.py new file mode 100644 index 000000000..3b4a676d2 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)]): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an.py b/docs_src/query_params_str_validations/tutorial006b_an.py new file mode 100644 index 000000000..ea3b02583 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an.py @@ -0,0 +1,12 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006b_an_py39.py b/docs_src/query_params_str_validations/tutorial006b_an_py39.py new file mode 100644 index 000000000..687a9f544 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006b_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an.py b/docs_src/query_params_str_validations/tutorial006c_an.py new file mode 100644 index 000000000..10bf26a57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py310.py b/docs_src/query_params_str_validations/tutorial006c_an_py310.py new file mode 100644 index 000000000..1ab0a7d53 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006c_an_py39.py b/docs_src/query_params_str_validations/tutorial006c_an_py39.py new file mode 100644 index 000000000..ac1273331 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006c_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(min_length=3)] = ...): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an.py b/docs_src/query_params_str_validations/tutorial006d_an.py new file mode 100644 index 000000000..bc8283e15 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an.py @@ -0,0 +1,13 @@ +from fastapi import FastAPI, Query +from pydantic import Required +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial006d_an_py39.py b/docs_src/query_params_str_validations/tutorial006d_an_py39.py new file mode 100644 index 000000000..035d9e3bd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial006d_an_py39.py @@ -0,0 +1,14 @@ +from typing import Annotated + +from fastapi import FastAPI, Query +from pydantic import Required + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str, Query(min_length=3)] = Required): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an.py b/docs_src/query_params_str_validations/tutorial007_an.py new file mode 100644 index 000000000..3bc85cc0c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py310.py b/docs_src/query_params_str_validations/tutorial007_an_py310.py new file mode 100644 index 000000000..5933911fd --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[str | None, Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial007_an_py39.py b/docs_src/query_params_str_validations/tutorial007_an_py39.py new file mode 100644 index 000000000..dafa1c5c9 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial007_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[Union[str, None], Query(title="Query string", min_length=3)] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py new file mode 100644 index 000000000..5699f1e88 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an.py @@ -0,0 +1,23 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py310.py b/docs_src/query_params_str_validations/tutorial008_an_py310.py new file mode 100644 index 000000000..4aaadf8b4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py310.py @@ -0,0 +1,22 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial008_an_py39.py b/docs_src/query_params_str_validations/tutorial008_an_py39.py new file mode 100644 index 000000000..1c3b36176 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial008_an_py39.py @@ -0,0 +1,22 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an.py b/docs_src/query_params_str_validations/tutorial009_an.py new file mode 100644 index 000000000..2894e2d51 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an.py @@ -0,0 +1,14 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py310.py b/docs_src/query_params_str_validations/tutorial009_an_py310.py new file mode 100644 index 000000000..d11753362 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py310.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[str | None, Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial009_an_py39.py b/docs_src/query_params_str_validations/tutorial009_an_py39.py new file mode 100644 index 000000000..70a89e613 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial009_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[str, None], Query(alias="item-query")] = None): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py new file mode 100644 index 000000000..8995f3f57 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -0,0 +1,27 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py new file mode 100644 index 000000000..cfa81926c --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -0,0 +1,26 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py new file mode 100644 index 000000000..220eaabf4 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -0,0 +1,26 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + Union[str, None], + Query( + alias="item-query", + title="Query string", + description="Query string for the items to search in the database that have a good match", + min_length=3, + max_length=50, + regex="^fixedquery$", + deprecated=True, + ), + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial011_an.py b/docs_src/query_params_str_validations/tutorial011_an.py new file mode 100644 index 000000000..8ed699337 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an.py @@ -0,0 +1,12 @@ +from typing import List, Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[List[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py310.py b/docs_src/query_params_str_validations/tutorial011_an_py310.py new file mode 100644 index 000000000..5dad4bfde --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py310.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str] | None, Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial011_an_py39.py b/docs_src/query_params_str_validations/tutorial011_an_py39.py new file mode 100644 index 000000000..416e3990d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial011_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[Union[list[str], None], Query()] = None): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an.py b/docs_src/query_params_str_validations/tutorial012_an.py new file mode 100644 index 000000000..261af250a --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an.py @@ -0,0 +1,12 @@ +from typing import List + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[List[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial012_an_py39.py b/docs_src/query_params_str_validations/tutorial012_an_py39.py new file mode 100644 index 000000000..9b5a9c2fb --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial012_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list[str], Query()] = ["foo", "bar"]): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an.py b/docs_src/query_params_str_validations/tutorial013_an.py new file mode 100644 index 000000000..f12a25055 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an.py @@ -0,0 +1,10 @@ +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial013_an_py39.py b/docs_src/query_params_str_validations/tutorial013_an_py39.py new file mode 100644 index 000000000..602734145 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial013_an_py39.py @@ -0,0 +1,11 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items(q: Annotated[list, Query()] = []): + query_items = {"q": q} + return query_items diff --git a/docs_src/query_params_str_validations/tutorial014_an.py b/docs_src/query_params_str_validations/tutorial014_an.py new file mode 100644 index 000000000..a9a9c4427 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an.py @@ -0,0 +1,16 @@ +from typing import Union + +from fastapi import FastAPI, Query +from typing_extensions import Annotated + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py310.py b/docs_src/query_params_str_validations/tutorial014_an_py310.py new file mode 100644 index 000000000..5fba54150 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py310.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[str | None, Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/query_params_str_validations/tutorial014_an_py39.py b/docs_src/query_params_str_validations/tutorial014_an_py39.py new file mode 100644 index 000000000..b07985210 --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial014_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + hidden_query: Annotated[Union[str, None], Query(include_in_schema=False)] = None +): + if hidden_query: + return {"hidden_query": hidden_query} + else: + return {"hidden_query": "Not found"} diff --git a/docs_src/request_files/tutorial001_02_an.py b/docs_src/request_files/tutorial001_02_an.py new file mode 100644 index 000000000..5007fef15 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an.py @@ -0,0 +1,22 @@ +from typing import Union + +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py310.py b/docs_src/request_files/tutorial001_02_an_py310.py new file mode 100644 index 000000000..9b80321b4 --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py310.py @@ -0,0 +1,21 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes | None, File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile | None = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_02_an_py39.py b/docs_src/request_files/tutorial001_02_an_py39.py new file mode 100644 index 000000000..bb090ff6c --- /dev/null +++ b/docs_src/request_files/tutorial001_02_an_py39.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[Union[bytes, None], File()] = None): + if not file: + return {"message": "No file sent"} + else: + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: Union[UploadFile, None] = None): + if not file: + return {"message": "No upload file sent"} + else: + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an.py b/docs_src/request_files/tutorial001_03_an.py new file mode 100644 index 000000000..8a6b0a245 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_03_an_py39.py b/docs_src/request_files/tutorial001_03_an_py39.py new file mode 100644 index 000000000..93098a677 --- /dev/null +++ b/docs_src/request_files/tutorial001_03_an_py39.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File(description="A file read as bytes")]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file( + file: Annotated[UploadFile, File(description="A file read as UploadFile")], +): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an.py b/docs_src/request_files/tutorial001_an.py new file mode 100644 index 000000000..ca2f76d5c --- /dev/null +++ b/docs_src/request_files/tutorial001_an.py @@ -0,0 +1,14 @@ +from fastapi import FastAPI, File, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial001_an_py39.py b/docs_src/request_files/tutorial001_an_py39.py new file mode 100644 index 000000000..26a767221 --- /dev/null +++ b/docs_src/request_files/tutorial001_an_py39.py @@ -0,0 +1,15 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + +@app.post("/uploadfile/") +async def create_upload_file(file: UploadFile): + return {"filename": file.filename} diff --git a/docs_src/request_files/tutorial002_an.py b/docs_src/request_files/tutorial002_an.py new file mode 100644 index 000000000..eaa90da2b --- /dev/null +++ b/docs_src/request_files/tutorial002_an.py @@ -0,0 +1,34 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[List[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: List[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial002_an_py39.py b/docs_src/request_files/tutorial002_an_py39.py new file mode 100644 index 000000000..db524ceab --- /dev/null +++ b/docs_src/request_files/tutorial002_an_py39.py @@ -0,0 +1,33 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files(files: Annotated[list[bytes], File()]): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files(files: list[UploadFile]): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an.py b/docs_src/request_files/tutorial003_an.py new file mode 100644 index 000000000..2238e3c94 --- /dev/null +++ b/docs_src/request_files/tutorial003_an.py @@ -0,0 +1,40 @@ +from typing import List + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[List[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + List[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_files/tutorial003_an_py39.py b/docs_src/request_files/tutorial003_an_py39.py new file mode 100644 index 000000000..5a8c5dab5 --- /dev/null +++ b/docs_src/request_files/tutorial003_an_py39.py @@ -0,0 +1,39 @@ +from typing import Annotated + +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import HTMLResponse + +app = FastAPI() + + +@app.post("/files/") +async def create_files( + files: Annotated[list[bytes], File(description="Multiple files as bytes")], +): + return {"file_sizes": [len(file) for file in files]} + + +@app.post("/uploadfiles/") +async def create_upload_files( + files: Annotated[ + list[UploadFile], File(description="Multiple files as UploadFile") + ], +): + return {"filenames": [file.filename for file in files]} + + +@app.get("/") +async def main(): + content = """ + +
+ + +
+
+ + +
+ + """ + return HTMLResponse(content=content) diff --git a/docs_src/request_forms/tutorial001_an.py b/docs_src/request_forms/tutorial001_an.py new file mode 100644 index 000000000..677fbf2db --- /dev/null +++ b/docs_src/request_forms/tutorial001_an.py @@ -0,0 +1,9 @@ +from fastapi import FastAPI, Form +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms/tutorial001_an_py39.py b/docs_src/request_forms/tutorial001_an_py39.py new file mode 100644 index 000000000..8e9d2ea53 --- /dev/null +++ b/docs_src/request_forms/tutorial001_an_py39.py @@ -0,0 +1,10 @@ +from typing import Annotated + +from fastapi import FastAPI, Form + +app = FastAPI() + + +@app.post("/login/") +async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]): + return {"username": username} diff --git a/docs_src/request_forms_and_files/tutorial001_an.py b/docs_src/request_forms_and_files/tutorial001_an.py new file mode 100644 index 000000000..0ea285ac8 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI, File, Form, UploadFile +from typing_extensions import Annotated + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/request_forms_and_files/tutorial001_an_py39.py b/docs_src/request_forms_and_files/tutorial001_an_py39.py new file mode 100644 index 000000000..12cc43e50 --- /dev/null +++ b/docs_src/request_forms_and_files/tutorial001_an_py39.py @@ -0,0 +1,18 @@ +from typing import Annotated + +from fastapi import FastAPI, File, Form, UploadFile + +app = FastAPI() + + +@app.post("/files/") +async def create_file( + file: Annotated[bytes, File()], + fileb: Annotated[UploadFile, File()], + token: Annotated[str, Form()], +): + return { + "file_size": len(file), + "token": token, + "fileb_content_type": fileb.content_type, + } diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py new file mode 100644 index 000000000..1dec555a9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py new file mode 100644 index 000000000..9edaddfb8 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py new file mode 100644 index 000000000..fe08847d9 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + item_id: int, + item: Annotated[ + Item, + Body( + example={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py new file mode 100644 index 000000000..82c9a92ac --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py new file mode 100644 index 000000000..01f1a486c --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py new file mode 100644 index 000000000..d50e8aa5f --- /dev/null +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -0,0 +1,54 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/security/tutorial001_an.py b/docs_src/security/tutorial001_an.py new file mode 100644 index 000000000..dac915b7c --- /dev/null +++ b/docs_src/security/tutorial001_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial001_an_py39.py b/docs_src/security/tutorial001_an_py39.py new file mode 100644 index 000000000..de110402e --- /dev/null +++ b/docs_src/security/tutorial001_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +@app.get("/items/") +async def read_items(token: Annotated[str, Depends(oauth2_scheme)]): + return {"token": token} diff --git a/docs_src/security/tutorial002_an.py b/docs_src/security/tutorial002_an.py new file mode 100644 index 000000000..291b3bf53 --- /dev/null +++ b/docs_src/security/tutorial002_an.py @@ -0,0 +1,33 @@ +from typing import Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py310.py b/docs_src/security/tutorial002_an_py310.py new file mode 100644 index 000000000..c7b761e45 --- /dev/null +++ b/docs_src/security/tutorial002_an_py310.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial002_an_py39.py b/docs_src/security/tutorial002_an_py39.py new file mode 100644 index 000000000..7ff1c470b --- /dev/null +++ b/docs_src/security/tutorial002_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +def fake_decode_token(token): + return User( + username=token + "fakedecoded", email="john@example.com", full_name="John Doe" + ) + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + return user + + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]): + return current_user diff --git a/docs_src/security/tutorial003_an.py b/docs_src/security/tutorial003_an.py new file mode 100644 index 000000000..261cb4857 --- /dev/null +++ b/docs_src/security/tutorial003_an.py @@ -0,0 +1,95 @@ +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel +from typing_extensions import Annotated + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py310.py b/docs_src/security/tutorial003_an_py310.py new file mode 100644 index 000000000..a03f4f8bf --- /dev/null +++ b/docs_src/security/tutorial003_an_py310.py @@ -0,0 +1,94 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial003_an_py39.py b/docs_src/security/tutorial003_an_py39.py new file mode 100644 index 000000000..308dbe798 --- /dev/null +++ b/docs_src/security/tutorial003_an_py39.py @@ -0,0 +1,94 @@ +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from pydantic import BaseModel + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Wonderson", + "email": "alice@example.com", + "hashed_password": "fakehashedsecret2", + "disabled": True, + }, +} + +app = FastAPI() + + +def fake_hash_password(password: str): + return "fakehashed" + password + + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def fake_decode_token(token): + # This doesn't provide any security at all + # Check the next version + user = get_user(fake_users_db, token) + return user + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decode_token(token) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + user_dict = fake_users_db.get(form_data.username) + if not user_dict: + raise HTTPException(status_code=400, detail="Incorrect username or password") + user = UserInDB(**user_dict) + hashed_password = fake_hash_password(form_data.password) + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + + +@app.get("/users/me") +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py new file mode 100644 index 000000000..ca350343d --- /dev/null +++ b/docs_src/security/tutorial004_an.py @@ -0,0 +1,147 @@ +from datetime import datetime, timedelta +from typing import Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py new file mode 100644 index 000000000..8bf5f3b71 --- /dev/null +++ b/docs_src/security/tutorial004_an_py310.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py new file mode 100644 index 000000000..a634e23de --- /dev/null +++ b/docs_src/security/tutorial004_an_py39.py @@ -0,0 +1,146 @@ +from datetime import datetime, timedelta +from typing import Annotated, Union + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + } +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py new file mode 100644 index 000000000..ec4fa1a07 --- /dev/null +++ b/docs_src/security/tutorial005_an.py @@ -0,0 +1,178 @@ +from datetime import datetime, timedelta +from typing import List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError +from typing_extensions import Annotated + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py new file mode 100644 index 000000000..45f3fc0bd --- /dev/null +++ b/docs_src/security/tutorial005_an_py310.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: str | None = None + scopes: list[str] = [] + + +class User(BaseModel): + username: str + email: str | None = None + full_name: str | None = None + disabled: bool | None = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: timedelta | None = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py new file mode 100644 index 000000000..ecb5ed516 --- /dev/null +++ b/docs_src/security/tutorial005_an_py39.py @@ -0,0 +1,177 @@ +from datetime import datetime, timedelta +from typing import Annotated, List, Union + +from fastapi import Depends, FastAPI, HTTPException, Security, status +from fastapi.security import ( + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + SecurityScopes, +) +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel, ValidationError + +# to get a string like this run: +# openssl rand -hex 32 +SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +fake_users_db = { + "johndoe": { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW", + "disabled": False, + }, + "alice": { + "username": "alice", + "full_name": "Alice Chains", + "email": "alicechains@example.com", + "hashed_password": "$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm", + "disabled": True, + }, +} + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + username: Union[str, None] = None + scopes: List[str] = [] + + +class User(BaseModel): + username: str + email: Union[str, None] = None + full_name: Union[str, None] = None + disabled: Union[bool, None] = None + + +class UserInDB(User): + hashed_password: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + +app = FastAPI() + + +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + + +def authenticate_user(fake_db, username: str, password: str): + user = get_user(fake_db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user + + +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +async def get_current_user( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + if security_scopes.scopes: + authenticate_value = f'Bearer scope="{security_scopes.scope_str}"' + else: + authenticate_value = "Bearer" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": authenticate_value}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + token_scopes = payload.get("scopes", []) + token_data = TokenData(scopes=token_scopes, username=username) + except (JWTError, ValidationError): + raise credentials_exception + user = get_user(fake_users_db, username=token_data.username) + if user is None: + raise credentials_exception + for scope in security_scopes.scopes: + if scope not in token_data.scopes: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not enough permissions", + headers={"WWW-Authenticate": authenticate_value}, + ) + return user + + +async def get_current_active_user( + current_user: Annotated[User, Security(get_current_user, scopes=["me"])] +): + if current_user.disabled: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user + + +@app.post("/token", response_model=Token) +async def login_for_access_token( + form_data: Annotated[OAuth2PasswordRequestForm, Depends()] +): + user = authenticate_user(fake_users_db, form_data.username, form_data.password) + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": user.username, "scopes": form_data.scopes}, + expires_delta=access_token_expires, + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@app.get("/users/me/", response_model=User) +async def read_users_me( + current_user: Annotated[User, Depends(get_current_active_user)] +): + return current_user + + +@app.get("/users/me/items/") +async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] +): + return [{"item_id": "Foo", "owner": current_user.username}] + + +@app.get("/status/") +async def read_system_status(current_user: Annotated[User, Depends(get_current_user)]): + return {"status": "ok"} diff --git a/docs_src/security/tutorial006_an.py b/docs_src/security/tutorial006_an.py new file mode 100644 index 000000000..985e4b2ad --- /dev/null +++ b/docs_src/security/tutorial006_an.py @@ -0,0 +1,12 @@ +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial006_an_py39.py b/docs_src/security/tutorial006_an_py39.py new file mode 100644 index 000000000..03c696a4b --- /dev/null +++ b/docs_src/security/tutorial006_an_py39.py @@ -0,0 +1,13 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +@app.get("/users/me") +def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py new file mode 100644 index 000000000..5fb7c8e57 --- /dev/null +++ b/docs_src/security/tutorial007_an.py @@ -0,0 +1,36 @@ +import secrets + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials +from typing_extensions import Annotated + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py new file mode 100644 index 000000000..17177dabf --- /dev/null +++ b/docs_src/security/tutorial007_an_py39.py @@ -0,0 +1,36 @@ +import secrets +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException, status +from fastapi.security import HTTPBasic, HTTPBasicCredentials + +app = FastAPI() + +security = HTTPBasic() + + +def get_current_username( + credentials: Annotated[HTTPBasicCredentials, Depends(security)] +): + current_username_bytes = credentials.username.encode("utf8") + correct_username_bytes = b"stanleyjobson" + is_correct_username = secrets.compare_digest( + current_username_bytes, correct_username_bytes + ) + current_password_bytes = credentials.password.encode("utf8") + correct_password_bytes = b"swordfish" + is_correct_password = secrets.compare_digest( + current_password_bytes, correct_password_bytes + ) + if not (is_correct_username and is_correct_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Basic"}, + ) + return credentials.username + + +@app.get("/users/me") +def read_current_user(username: Annotated[str, Depends(get_current_username)]): + return {"username": username} diff --git a/docs_src/settings/app02_an/__init__.py b/docs_src/settings/app02_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py new file mode 100644 index 000000000..cb679202d --- /dev/null +++ b/docs_src/settings/app02_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an/test_main.py b/docs_src/settings/app02_an/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app02_an_py39/__init__.py b/docs_src/settings/app02_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py new file mode 100644 index 000000000..9a7829135 --- /dev/null +++ b/docs_src/settings/app02_an_py39/config.py @@ -0,0 +1,7 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py new file mode 100644 index 000000000..61be74fcb --- /dev/null +++ b/docs_src/settings/app02_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from .config import Settings + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return Settings() + + +@app.get("/info") +async def info(settings: Annotated[Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app02_an_py39/test_main.py b/docs_src/settings/app02_an_py39/test_main.py new file mode 100644 index 000000000..7a04d7e8e --- /dev/null +++ b/docs_src/settings/app02_an_py39/test_main.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from .config import Settings +from .main import app, get_settings + +client = TestClient(app) + + +def get_settings_override(): + return Settings(admin_email="testing_admin@example.com") + + +app.dependency_overrides[get_settings] = get_settings_override + + +def test_app(): + response = client.get("/info") + data = response.json() + assert data == { + "app_name": "Awesome API", + "admin_email": "testing_admin@example.com", + "items_per_user": 50, + } diff --git a/docs_src/settings/app03_an/__init__.py b/docs_src/settings/app03_an/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py new file mode 100644 index 000000000..c33b98f47 --- /dev/null +++ b/docs_src/settings/app03_an/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends, FastAPI + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/settings/app03_an_py39/__init__.py b/docs_src/settings/app03_an_py39/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an_py39/config.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py new file mode 100644 index 000000000..b89c6b6cf --- /dev/null +++ b/docs_src/settings/app03_an_py39/main.py @@ -0,0 +1,22 @@ +from functools import lru_cache + +from fastapi import Depends, FastAPI +from typing_extensions import Annotated + +from . import config + +app = FastAPI() + + +@lru_cache() +def get_settings(): + return config.Settings() + + +@app.get("/info") +async def info(settings: Annotated[config.Settings, Depends(get_settings)]): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/docs_src/websockets/tutorial002_an.py b/docs_src/websockets/tutorial002_an.py new file mode 100644 index 000000000..c838fbd30 --- /dev/null +++ b/docs_src/websockets/tutorial002_an.py @@ -0,0 +1,93 @@ +from typing import Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse +from typing_extensions import Annotated + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+
    +
+ + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[Union[str, None], Cookie()] = None, + token: Annotated[Union[str, None], Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: Union[int, None] = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_an_py310.py b/docs_src/websockets/tutorial002_an_py310.py new file mode 100644 index 000000000..551539b32 --- /dev/null +++ b/docs_src/websockets/tutorial002_an_py310.py @@ -0,0 +1,92 @@ +from typing import Annotated + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+
    +
+ + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[str | None, Cookie()] = None, + token: Annotated[str | None, Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: int | None = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_an_py39.py b/docs_src/websockets/tutorial002_an_py39.py new file mode 100644 index 000000000..606d355fe --- /dev/null +++ b/docs_src/websockets/tutorial002_an_py39.py @@ -0,0 +1,92 @@ +from typing import Annotated, Union + +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+
    +
+ + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: Annotated[Union[str, None], Cookie()] = None, + token: Annotated[Union[str, None], Query()] = None, +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + *, + websocket: WebSocket, + item_id: str, + q: Union[int, None] = None, + cookie_or_token: Annotated[str, Depends(get_cookie_or_token)], +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial002_py310.py b/docs_src/websockets/tutorial002_py310.py new file mode 100644 index 000000000..51daf58e5 --- /dev/null +++ b/docs_src/websockets/tutorial002_py310.py @@ -0,0 +1,89 @@ +from fastapi import ( + Cookie, + Depends, + FastAPI, + Query, + WebSocket, + WebSocketException, + status, +) +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+
+ + + +
+ + +
+
    +
+ + + +""" + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +async def get_cookie_or_token( + websocket: WebSocket, + session: str | None = Cookie(default=None), + token: str | None = Query(default=None), +): + if session is None and token is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + return session or token + + +@app.websocket("/items/{item_id}/ws") +async def websocket_endpoint( + websocket: WebSocket, + item_id: str, + q: int | None = None, + cookie_or_token: str = Depends(get_cookie_or_token), +): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text( + f"Session cookie or query token value is: {cookie_or_token}" + ) + if q is not None: + await websocket.send_text(f"Query parameter q is: {q}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") diff --git a/docs_src/websockets/tutorial003_py39.py b/docs_src/websockets/tutorial003_py39.py new file mode 100644 index 000000000..316218088 --- /dev/null +++ b/docs_src/websockets/tutorial003_py39.py @@ -0,0 +1,81 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi.responses import HTMLResponse + +app = FastAPI() + +html = """ + + + + Chat + + +

WebSocket Chat

+

Your ID:

+
+ + +
+
    +
+ + + +""" + + +class ConnectionManager: + def __init__(self): + self.active_connections: list[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@app.get("/") +async def get(): + return HTMLResponse(html) + + +@app.websocket("/ws/{client_id}") +async def websocket_endpoint(websocket: WebSocket, client_id: int): + await manager.connect(websocket) + try: + while True: + data = await websocket.receive_text() + await manager.send_personal_message(f"You wrote: {data}", websocket) + await manager.broadcast(f"Client #{client_id} says: {data}") + except WebSocketDisconnect: + manager.disconnect(websocket) + await manager.broadcast(f"Client #{client_id} left the chat") diff --git a/pyproject.toml b/pyproject.toml index b8d006359..6aa095a64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,6 +186,12 @@ ignore = [ "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/dependencies/tutorial008_an.py" = ["F821"] +"docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] +"docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] +"docs_src/query_params_str_validations/tutorial013_an_py39.py" = ["B006"] [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py new file mode 100644 index 000000000..2cb2bb993 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an.py @@ -0,0 +1,17 @@ +from fastapi.testclient import TestClient + +from docs_src.additional_status_codes.tutorial001_an import app + +client = TestClient(app) + + +def test_update(): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +def test_create(): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py new file mode 100644 index 000000000..c7660a392 --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py new file mode 100644 index 000000000..303c5dbae --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_an_py39.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py39 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py new file mode 100644 index 000000000..02f2e188c --- /dev/null +++ b/tests/test_tutorial/test_additional_status_codes/test_tutorial001_py310.py @@ -0,0 +1,26 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.additional_status_codes.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_update(client: TestClient): + response = client.put("/items/foo", json={"name": "Wrestlers"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Wrestlers", "size": None} + + +@needs_py310 +def test_create(client: TestClient): + response = client.put("/items/red", json={"name": "Chillies"}) + assert response.status_code == 201, response.text + assert response.json() == {"name": "Chillies", "size": None} diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py new file mode 100644 index 000000000..af682ecff --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an.py @@ -0,0 +1,19 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from docs_src.background_tasks.tutorial002_an import app + +client = TestClient(app) + + +def test(): + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py new file mode 100644 index 000000000..067b2787e --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py310.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@needs_py310 +def test(): + from docs_src.background_tasks.tutorial002_an_py310 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py new file mode 100644 index 000000000..06b5a2f57 --- /dev/null +++ b/tests/test_tutorial/test_background_tasks/test_tutorial002_an_py39.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@needs_py39 +def test(): + from docs_src.background_tasks.tutorial002_an_py39 import app + + client = TestClient(app) + log = Path("log.txt") + if log.is_file(): + os.remove(log) # pragma: no cover + response = client.post("/send-notification/foo@example.com?q=some-query") + assert response.status_code == 200, response.text + assert response.json() == {"message": "Message sent"} + with open("./log.txt") as f: + assert "found query: some-query\nmessage to foo@example.com" in f.read() diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py new file mode 100644 index 000000000..4b84a31b5 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -0,0 +1,491 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.bigger_applications.app_an.main import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path(path, expected_status, expected_response, headers): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_put_no_header(): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_put_invalid_header(): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_put(): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +def test_put_forbidden(): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +def test_admin(): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +def test_admin_invalid_header(): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py new file mode 100644 index 000000000..1caf5bd49 --- /dev/null +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -0,0 +1,506 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +no_jessica = { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an_py39.main import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response,headers", + [ + ( + "/users?token=jessica", + 200, + [{"username": "Rick"}, {"username": "Morty"}], + {}, + ), + ("/users", 422, no_jessica, {}), + ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), + ("/users/foo", 422, no_jessica, {}), + ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), + ("/users/me", 422, no_jessica, {}), + ( + "/users?token=monica", + 400, + {"detail": "No Jessica token provided"}, + {}, + ), + ( + "/items?token=jessica", + 200, + {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items/plumbus?token=jessica", + 200, + {"name": "Plumbus", "item_id": "plumbus"}, + {"X-Token": "fake-super-secret-token"}, + ), + ( + "/items/bar?token=jessica", + 404, + {"detail": "Item not found"}, + {"X-Token": "fake-super-secret-token"}, + ), + ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), + ( + "/items?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items/bar?token=jessica", + 400, + {"detail": "X-Token header invalid"}, + {"X-Token": "invalid"}, + ), + ( + "/items?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ( + "/items/plumbus?token=jessica", + 422, + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + }, + {}, + ), + ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), + ("/", 422, no_jessica, {}), + ("/openapi.json", 200, openapi_schema, {}), + ], +) +def test_get_path( + path, expected_status, expected_response, headers, client: TestClient +): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py39 +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_put_invalid_header(client: TestClient): + response = client.put("/items/foo", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_put(client: TestClient): + response = client.put( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} + + +@needs_py39 +def test_put_forbidden(client: TestClient): + response = client.put( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 403, response.text + assert response.json() == {"detail": "You can only update the item: plumbus"} + + +@needs_py39 +def test_admin(client: TestClient): + response = client.post( + "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"message": "Admin getting schwifty"} + + +@needs_py39 +def test_admin_invalid_header(client: TestClient): + response = client.post("/admin/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py new file mode 100644 index 000000000..879769147 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -0,0 +1,169 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_fields.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py new file mode 100644 index 000000000..0cd57a187 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py new file mode 100644 index 000000000..26ff26f50 --- /dev/null +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -0,0 +1,176 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +price_not_greater = { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + {"item": {"name": "Foo", "price": 3.0}}, + 200, + { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + }, + ), + ( + "/items/6", + { + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + 200, + { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + }, + ), + ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), + ], +) +def test(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py similarity index 54% rename from tests/test_tutorial/test_annotated/test_tutorial003.py rename to tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index caf7ffdf1..94ba8593a 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial003 import app +from docs_src.body_multiple_params.tutorial001_an import app client = TestClient(app) @@ -10,86 +10,65 @@ openapi_schema = { "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", "parameters": [ { "required": True, "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, "type": "integer", }, "name": "item_id", "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users_get", - "parameters": [ { "required": False, - "schema": { - "title": "User Id", - "minLength": 1, - "type": "string", - "default": "me", - }, - "name": "user_id", + "schema": {"title": "Q", "type": "string"}, + "name": "q", "in": "query", - } + }, ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } }, } - }, + } }, "components": { "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", + "Item": { + "title": "Item", + "required": ["name", "price"], "type": "object", "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, }, }, "ValidationError": { @@ -106,33 +85,63 @@ openapi_schema = { "type": {"title": "Error Type", "type": "string"}, }, }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, } }, } -item_id_negative = { + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { "detail": [ { - "ctx": {"limit_value": 0}, "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", + "msg": "value is not a valid integer", + "type": "type_error.integer", } ] } @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,body,expected_status,expected_response", [ - ("/items/1", 200, {"item_id": 1}), - ("/items/-1", 422, item_id_negative), - ("/users", 200, {"user_id": "me"}), - ("/users?user_id=foo", 200, {"user_id": "foo"}), - ("/openapi.json", 200, openapi_schema), + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), ], ) -def test_get(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status, response.text +def test_post_body(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..cd378ec9c --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -0,0 +1,155 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial003_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py similarity index 54% rename from tests/test_tutorial/test_annotated/test_tutorial003_py39.py rename to tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index 7c828a0ce..b8fe1baaf 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -8,86 +8,65 @@ openapi_schema = { "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", "parameters": [ { "required": True, "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, "type": "integer", }, "name": "item_id", "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users_get", - "parameters": [ { "required": False, - "schema": { - "title": "User Id", - "minLength": 1, - "type": "string", - "default": "me", - }, - "name": "user_id", + "schema": {"title": "Q", "type": "string"}, + "name": "q", "in": "query", - } + }, ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } }, } - }, + } }, "components": { "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", + "Item": { + "title": "Item", + "required": ["name", "price"], "type": "object", "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, }, }, "ValidationError": { @@ -104,42 +83,73 @@ openapi_schema = { "type": {"title": "Error Type", "type": "string"}, }, }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, } }, } -item_id_negative = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial003_py39 import app + from docs_src.body_multiple_params.tutorial001_an_py39 import app client = TestClient(app) return client +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +item_id_not_int = { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] +} + + @needs_py39 @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,body,expected_status,expected_response", [ - ("/items/1", 200, {"item_id": 1}), - ("/items/-1", 422, item_id_negative), - ("/users", 200, {"user_id": "me"}), - ("/users?user_id=foo", 200, {"user_id": "foo"}), - ("/openapi.json", 200, openapi_schema), + ( + "/items/5?q=bar", + {"name": "Foo", "price": 50.5}, + 200, + { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + }, + ), + ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), + ("/items/5", None, 200, {"item_id": 5}), + ("/items/foo", None, 422, item_id_not_int), ], ) -def test_get(path, expected_status, expected_response, client): - response = client.get(path) - assert response.status_code == expected_status, response.text +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py new file mode 100644 index 000000000..788db8b30 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -0,0 +1,198 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.body_multiple_params.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..9003016cd --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..bc014a441 --- /dev/null +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -0,0 +1,206 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +@pytest.mark.parametrize( + "path,body,expected_status,expected_response", + [ + ( + "/items/5", + { + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + 200, + { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + }, + ), + ( + "/items/5", + None, + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ( + "/items/5", + [], + 422, + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + }, + ), + ], +) +def test_post_body(path, body, expected_status, expected_response, client: TestClient): + response = client.put(path, json=body) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial002.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py similarity index 65% rename from tests/test_tutorial/test_annotated/test_tutorial002.py rename to tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index 60c1233d8..fb60ea993 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial002.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -1,9 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial002 import app - -client = TestClient(app) +from docs_src.cookie_params.tutorial001_an import app openapi_schema = { "openapi": "3.0.2", @@ -32,25 +30,13 @@ openapi_schema = { "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } ], } - }, + } }, "components": { "schemas": { @@ -85,16 +71,22 @@ openapi_schema = { @pytest.mark.parametrize( - "path,expected_status,expected_response", + "path,cookies,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), ], ) -def test_get(path, expected_status, expected_response): +def test(path, cookies, expected_status, expected_response): + client = TestClient(app, cookies=cookies) response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..308886085 --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py310 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py new file mode 100644 index 000000000..bbfe5ff9a --- /dev/null +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -0,0 +1,95 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@needs_py39 +@pytest.mark.parametrize( + "path,cookies,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"ads_id": None}), + ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), + ( + "/items", + {"ads_id": "ads_track", "session": "cookiesession"}, + 200, + {"ads_id": "ads_track"}, + ), + ("/items", {"session": "cookiesession"}, 200, {"ads_id": None}), + ], +) +def test(path, cookies, expected_status, expected_response): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app, cookies=cookies) + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..13960addc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -0,0 +1,149 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..4b093af0d --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..6059924cc --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,157 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Skip", "type": "integer", "default": 0}, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer", "default": 100}, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +@pytest.mark.parametrize( + "path,expected_status,expected_response", + [ + ("/items", 200, {"q": None, "skip": 0, "limit": 100}), + ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), + ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), + ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), + ("/users", 200, {"q": None, "skip": 0, "limit": 100}), + ("/openapi.json", 200, openapi_schema), + ], +) +def test_get(path, expected_status, expected_response, client: TestClient): + response = client.get(path) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py similarity index 69% rename from tests/test_tutorial/test_annotated/test_tutorial001.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an.py index 50c9caca2..ef6199b04 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from docs_src.annotated.tutorial001 import app +from docs_src.dependencies.tutorial004_an import app client = TestClient(app) @@ -50,7 +50,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,14 +84,58 @@ openapi_schema = { } +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) def test_get(path, expected_status, expected_response): diff --git a/tests/test_tutorial/test_annotated/test_tutorial002_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py similarity index 67% rename from tests/test_tutorial/test_annotated/test_tutorial002_py39.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index 77a1f36a0..e9736780c 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -1,7 +1,7 @@ import pytest from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py310 openapi_schema = { "openapi": "3.0.2", @@ -48,7 +48,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,24 +84,69 @@ openapi_schema = { @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial002_py39 import app + from docs_src.dependencies.tutorial004_an_py310 import app client = TestClient(app) return client -@needs_py39 +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) -def test_get(path, expected_status, expected_response, client): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_annotated/test_tutorial001_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py similarity index 68% rename from tests/test_tutorial/test_annotated/test_tutorial001_py39.py rename to tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 576f55702..2b346f3b2 100644 --- a/tests/test_tutorial/test_annotated/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -48,7 +48,7 @@ openapi_schema = { }, ], } - }, + } }, "components": { "schemas": { @@ -84,24 +84,69 @@ openapi_schema = { @pytest.fixture(name="client") def get_client(): - from docs_src.annotated.tutorial001_py39 import app + from docs_src.dependencies.tutorial004_an_py39 import app client = TestClient(app) return client +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + @needs_py39 @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/items", 200, {"q": None, "skip": 0, "limit": 100}), - ("/items?q=foo", 200, {"q": "foo", "skip": 0, "limit": 100}), - ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), - ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), - ("/openapi.json", 200, openapi_schema), + ( + "/items", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ] + }, + ), + ( + "/items?q=foo", + 200, + { + "items": [ + {"item_name": "Foo"}, + {"item_name": "Bar"}, + {"item_name": "Baz"}, + ], + "q": "foo", + }, + ), + ( + "/items?q=foo&skip=1", + 200, + {"items": [{"item_name": "Bar"}, {"item_name": "Baz"}], "q": "foo"}, + ), + ( + "/items?q=bar&limit=2", + 200, + {"items": [{"item_name": "Foo"}, {"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?q=bar&skip=1&limit=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), + ( + "/items?limit=1&q=bar&skip=1", + 200, + {"items": [{"item_name": "Bar"}], "q": "bar"}, + ), ], ) -def test_get(path, expected_status, expected_response, client): +def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py new file mode 100644 index 000000000..f33b67d58 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -0,0 +1,128 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py new file mode 100644 index 000000000..171e39a96 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -0,0 +1,140 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial006_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py new file mode 100644 index 000000000..0a6908f72 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -0,0 +1,209 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_get_no_headers_items(): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_no_headers_users(): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +def test_get_invalid_one_header_items(): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_one_users(): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_get_invalid_second_header_items(): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_invalid_second_header_users(): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +def test_get_valid_headers_items(): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +def test_get_valid_headers_users(): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py new file mode 100644 index 000000000..25f54f4c9 --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -0,0 +1,225 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.dependencies.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_get_no_headers_items(client: TestClient): + response = client.get("/items/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_no_headers_users(client: TestClient): + response = client.get("/users/") + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + + +@needs_py39 +def test_get_invalid_one_header_items(client: TestClient): + response = client.get("/items/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_one_users(client: TestClient): + response = client.get("/users/", headers={"X-Token": "invalid"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_items(client: TestClient): + response = client.get( + "/items/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_invalid_second_header_users(client: TestClient): + response = client.get( + "/users/", headers={"X-Token": "fake-super-secret-token", "X-Key": "invalid"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "X-Key header invalid"} + + +@needs_py39 +def test_get_valid_headers_items(client: TestClient): + response = client.get( + "/items/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item": "Portal Gun"}, {"item": "Plumbus"}] + + +@needs_py39 +def test_get_valid_headers_users(client: TestClient): + response = client.get( + "/users/", + headers={ + "X-Token": "fake-super-secret-token", + "X-Key": "fake-super-secret-key", + }, + ) + assert response.status_code == 200, response.text + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py new file mode 100644 index 000000000..d5be16dfb --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -0,0 +1,138 @@ +from fastapi.testclient import TestClient + +from docs_src.extra_data_types.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_extra_types(): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py new file mode 100644 index 000000000..80806b694 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py new file mode 100644 index 000000000..5c7d43394 --- /dev/null +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -0,0 +1,146 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.extra_data_types.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_extra_types(client: TestClient): + item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" + data = { + "start_datetime": "2018-12-22T14:00:00+00:00", + "end_datetime": "2018-12-24T15:00:00+00:00", + "repeat_at": "15:30:00", + "process_after": 300, + } + expected_response = data.copy() + expected_response.update( + { + "start_process": "2018-12-22T14:05:00+00:00", + "duration": 176_100, + "item_id": item_id, + } + ) + response = client.put(f"/items/{item_id}", json=data) + assert response.status_code == 200, response.text + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py new file mode 100644 index 000000000..3c155f786 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial001_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py new file mode 100644 index 000000000..1f86f2a5d --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -0,0 +1,94 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial001_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"User-Agent": "testclient"}), + ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), + ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py new file mode 100644 index 000000000..b23398287 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002 import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py new file mode 100644 index 000000000..77d236e09 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -0,0 +1,99 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial002_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py new file mode 100644 index 000000000..49b0ef462 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py new file mode 100644 index 000000000..13aaabeb8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py new file mode 100644 index 000000000..6cae3d338 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/openapi.json", None, 200, openapi_schema), + ("/items", None, 200, {"strange_header": None}), + ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), + ( + "/items", + {"strange_header": "FastAPI test"}, + 200, + {"strange_header": "FastAPI test"}, + ), + ( + "/items", + {"strange-header": "Not really underscore"}, + 200, + {"strange_header": None}, + ), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py new file mode 100644 index 000000000..99dd9e25f --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003 import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py new file mode 100644 index 000000000..4477da7a8 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -0,0 +1,98 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.header_params.tutorial003_an import app + +client = TestClient(app) + + +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py new file mode 100644 index 000000000..b52304a2b --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py new file mode 100644 index 000000000..dffdd1622 --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py new file mode 100644 index 000000000..64ef7b22a --- /dev/null +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.header_params.tutorial003_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@pytest.mark.parametrize( + "path,headers,expected_status,expected_response", + [ + ("/items", None, 200, {"X-Token values": None}), + ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), + # TODO: fix this, is it a bug? + # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ], +) +def test(path, headers, expected_status, expected_response, client: TestClient): + response = client.get(path, headers=headers) + assert response.status_code == expected_status + assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + }, + "name": "x-token", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py new file mode 100644 index 000000000..b2b9b5018 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -0,0 +1,122 @@ +import pytest +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial010_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations(q_name, q, expected_status, expected_response): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py new file mode 100644 index 000000000..edbe4d009 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py310 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py new file mode 100644 index 000000000..f51e90247 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -0,0 +1,132 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +regex_error = { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] +} + + +@needs_py39 +@pytest.mark.parametrize( + "q_name,q,expected_status,expected_response", + [ + (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ( + "item-query", + "fixedquery", + 200, + {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, + ), + ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), + ("item-query", "nonregexquery", 422, regex_error), + ], +) +def test_query_params_str_validations( + q_name, q, expected_status, expected_response, client: TestClient +): + url = "/items/" + if q_name and q: + url = f"{url}?{q_name}={q}" + response = client.get(url) + assert response.status_code == expected_status + assert response.json() == expected_response diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py similarity index 100% rename from tests/test_tutorial/test_query_params_str_validations/test_tutorial001_py310.py rename to tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py new file mode 100644 index 000000000..25a11f2ca --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -0,0 +1,95 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial011_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py new file mode 100644 index 000000000..99aaf3948 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py310 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py new file mode 100644 index 000000000..902add851 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -0,0 +1,105 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial011_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": None} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py new file mode 100644 index 000000000..e57a2178f --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial012_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_default_query_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_multi_query_values(): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py new file mode 100644 index 000000000..140b74790 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial012_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_default_query_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=baz&q=foobar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["baz", "foobar"]} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py new file mode 100644 index 000000000..fc684b557 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -0,0 +1,96 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial013_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_multi_query_values(): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +def test_query_no_values(): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py new file mode 100644 index 000000000..9d3f255e0 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial013_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_multi_query_values(client: TestClient): + url = "/items/?q=foo&q=bar" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": ["foo", "bar"]} + + +@needs_py39 +def test_query_no_values(client: TestClient): + url = "/items/" + response = client.get(url) + assert response.status_code == 200, response.text + assert response.json() == {"q": []} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py new file mode 100644 index 000000000..ba5bf7c50 --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -0,0 +1,82 @@ +from fastapi.testclient import TestClient + +from docs_src.query_params_str_validations.tutorial014_an import app + +client = TestClient(app) + + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_hidden_query(): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +def test_no_hidden_query(): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py new file mode 100644 index 000000000..69e176bab --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py new file mode 100644 index 000000000..2adfddfef --- /dev/null +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -0,0 +1,91 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial014_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_hidden_query(client: TestClient): + response = client.get("/items?hidden_query=somevalue") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "somevalue"} + + +@needs_py310 +def test_no_hidden_query(client: TestClient): + response = client.get("/items") + assert response.status_code == 200, response.text + assert response.json() == {"hidden_query": "Not found"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py new file mode 100644 index 000000000..50b05fa4b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -0,0 +1,157 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_02_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +def test_post_uploadfile_no_body(): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py new file mode 100644 index 000000000..a5796b74c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py310 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py310 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py310 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py new file mode 100644 index 000000000..57175f736 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -0,0 +1,169 @@ +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_02_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No file sent"} + + +@needs_py39 +def test_post_uploadfile_no_body(client: TestClient): + response = client.post("/uploadfile/") + assert response.status_code == 200, response.text + assert response.json() == {"message": "No upload file sent"} + + +@needs_py39 +def test_post_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path: Path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py new file mode 100644 index 000000000..e83fc68bb --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -0,0 +1,159 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_03_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py new file mode 100644 index 000000000..7808262a7 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -0,0 +1,167 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_03_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py new file mode 100644 index 000000000..739f04b43 --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -0,0 +1,184 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +def test_post_large_file(tmp_path): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..091a9362b --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -0,0 +1,194 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_files.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": 14} + + +@needs_py39 +def test_post_large_file(tmp_path, client: TestClient): + default_pydantic_max_size = 2**16 + path = tmp_path / "test.txt" + path.write_bytes(b"x" * (default_pydantic_max_size + 1)) + + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"file_size": default_pydantic_max_size + 1} + + +@needs_py39 +def test_post_upload_file(tmp_path, client: TestClient): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + with path.open("rb") as file: + response = client.post("/uploadfile/", files={"file": file}) + assert response.status_code == 200, response.text + assert response.json() == {"filename": "test.txt"} diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py new file mode 100644 index 000000000..8a99c657c --- /dev/null +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -0,0 +1,215 @@ +from fastapi.testclient import TestClient + +from docs_src.request_files.tutorial002_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Files", + "operationId": "create_files_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_files_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfiles/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload Files", + "operationId": "create_upload_files_uploadfiles__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" + } + } + }, + "required": True, + }, + } + }, + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Main", + "operationId": "main__get", + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_files_uploadfiles__post": { + "title": "Body_create_upload_files_uploadfiles__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "Body_create_files_files__post": { + "title": "Body_create_files_files__post", + "required": ["files"], + "type": "object", + "properties": { + "files": { + "title": "Files", + "type": "array", + "items": {"type": "string", "format": "binary"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] +} + + +def test_post_form_no_body(): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_body_json(): + response = client.post("/files/", json={"file": "Foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +def test_post_files(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +def test_post_upload_file(tmp_path): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +def test_get_root(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/files/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"file_sizes": [14, 15]} + + +@needs_py39 +def test_post_upload_file(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + path2 = tmp_path / "test2.txt" + path2.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file, path2.open("rb") as file2: + response = client.post( + "/uploadfiles/", + files=( + ("files", ("test.txt", file)), + ("files", ("test2.txt", file2)), + ), + ) + assert response.status_code == 200, response.text + assert response.json() == {"filenames": ["test.txt", "test2.txt"]} + + +@needs_py39 +def test_get_root(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +def test_post_files_and_token(tmp_path): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py new file mode 100644 index 000000000..fa2ebc77d --- /dev/null +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -0,0 +1,211 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file", "fileb", "token"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"}, + "fileb": {"title": "Fileb", "type": "string", "format": "binary"}, + "token": {"title": "Token", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, +} + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an_py39 import app + + return app + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +file_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +token_required = { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + +# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} + +file_and_token_required = { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] +} + + +@needs_py39 +def test_post_form_no_body(client: TestClient): + response = client.post("/files/") + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_form_no_file(client: TestClient): + response = client.post("/files/", data={"token": "foo"}) + assert response.status_code == 422, response.text + assert response.json() == file_required + + +@needs_py39 +def test_post_body_json(client: TestClient): + response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) + assert response.status_code == 422, response.text + assert response.json() == file_and_token_required + + +@needs_py39 +def test_post_file_no_token(tmp_path, app: FastAPI): + path = tmp_path / "test.txt" + path.write_bytes(b"") + + client = TestClient(app) + with path.open("rb") as file: + response = client.post("/files/", files={"file": file}) + assert response.status_code == 422, response.text + assert response.json() == token_required + + +@needs_py39 +def test_post_files_and_token(tmp_path, app: FastAPI): + patha = tmp_path / "test.txt" + pathb = tmp_path / "testb.txt" + patha.write_text("") + pathb.write_text("") + + client = TestClient(app) + with patha.open("rb") as filea, pathb.open("rb") as fileb: + response = client.post( + "/files/", + data={"token": "foo"}, + files={"file": filea, "fileb": ("testb.txt", fileb, "text/plain")}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "file_size": 14, + "token": "foo", + "fileb_content_type": "text/plain", + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py new file mode 100644 index 000000000..7694669ce --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -0,0 +1,134 @@ +from fastapi.testclient import TestClient + +from docs_src.schema_extra_example.tutorial004_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +def test_post_body_example(): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py new file mode 100644 index 000000000..c81fbcf52 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py new file mode 100644 index 000000000..395c27b0e --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -0,0 +1,143 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial004_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +# Test required and embedded body parameters with no bodies sent +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py new file mode 100644 index 000000000..fdcb9bfb8 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -0,0 +1,59 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial001_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_no_token(): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +def test_incorrect_token(): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py new file mode 100644 index 000000000..4f8947b90 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -0,0 +1,70 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + } + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial001_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/items") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) + assert response.status_code == 200, response.text + assert response.json() == {"token": "testtoken"} + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py new file mode 100644 index 000000000..b1b9c0fc1 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -0,0 +1,176 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial003_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_inactive_user(): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py new file mode 100644 index 000000000..486f0c4ce --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py new file mode 100644 index 000000000..b6709e2fb --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -0,0 +1,192 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_token_post": { + "title": "Body_login_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "token"}}, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial003_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + assert response.json() == {"access_token": "johndoe", "token_type": "bearer"} + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer johndoe"}) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "hashed_password": "fakehashedsecret", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_inactive_user(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer alice"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py new file mode 100644 index 000000000..b6c2708f0 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -0,0 +1,347 @@ +from fastapi.testclient import TestClient + +from docs_src.security.tutorial005_an import ( + app, + create_access_token, + fake_users_db, + get_password_hash, + verify_password, +) + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token(username="johndoe", password="secret", scope=None): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +def test_login(): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +def test_login_incorrect_password(): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_login_incorrect_username(): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +def test_no_token(): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_token(): + access_token = get_access_token(scope="me") + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +def test_incorrect_token(): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_incorrect_token_type(): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +def test_verify_password(): + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +def test_get_password_hash(): + assert get_password_hash("secretalice") + + +def test_create_access_token(): + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +def test_token_no_sub(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_username(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_no_scope(): + access_token = get_access_token() + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inexistent_user(): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +def test_token_inactive_user(): + access_token = get_access_token( + username="alice", password="secretalice", scope="me" + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +def test_read_items(): + access_token = get_access_token(scope="me items") + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +def test_read_system_status(): + access_token = get_access_token() + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +def test_read_system_status_no_token(): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py new file mode 100644 index 000000000..15a9445b9 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py310 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py310 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py310 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py310 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py310 +def test_verify_password(): + from docs_src.security.tutorial005_an_py310 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py310 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py310 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py310 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py310 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py310 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py310 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py310 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py310 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py310 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py new file mode 100644 index 000000000..989424dd3 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -0,0 +1,375 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/token": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Token"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login For Access Token", + "operationId": "login_for_access_token_token_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_for_access_token_token_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me__get", + "security": [{"OAuth2PasswordBearer": ["me"]}], + } + }, + "/users/me/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Own Items", + "operationId": "read_own_items_users_me_items__get", + "security": [{"OAuth2PasswordBearer": ["items", "me"]}], + } + }, + "/status/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read System Status", + "operationId": "read_system_status_status__get", + "security": [{"OAuth2PasswordBearer": []}], + } + }, + }, + "components": { + "schemas": { + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + "disabled": {"title": "Disabled", "type": "boolean"}, + }, + }, + "Token": { + "title": "Token", + "required": ["access_token", "token_type"], + "type": "object", + "properties": { + "access_token": {"title": "Access Token", "type": "string"}, + "token_type": {"title": "Token Type", "type": "string"}, + }, + }, + "Body_login_for_access_token_token_post": { + "title": "Body_login_for_access_token_token_post", + "required": ["username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "me": "Read information about the current user.", + "items": "Read items.", + }, + "tokenUrl": "token", + } + }, + } + }, + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def get_access_token( + *, username="johndoe", password="secret", scope=None, client: TestClient +): + data = {"username": username, "password": password} + if scope: + data["scope"] = scope + response = client.post("/token", data=data) + content = response.json() + access_token = content.get("access_token") + return access_token + + +@needs_py39 +def test_login(client: TestClient): + response = client.post("/token", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 200, response.text + content = response.json() + assert "access_token" in content + assert content["token_type"] == "bearer" + + +@needs_py39 +def test_login_incorrect_password(client: TestClient): + response = client.post( + "/token", data={"username": "johndoe", "password": "incorrect"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_login_incorrect_username(client: TestClient): + response = client.post("/token", data={"username": "foo", "password": "secret"}) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Incorrect username or password"} + + +@needs_py39 +def test_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_token(client: TestClient): + access_token = get_access_token(scope="me", client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == { + "username": "johndoe", + "full_name": "John Doe", + "email": "johndoe@example.com", + "disabled": False, + } + + +@needs_py39 +def test_incorrect_token(client: TestClient): + response = client.get("/users/me", headers={"Authorization": "Bearer nonexistent"}) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_incorrect_token_type(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Notexistent testtoken"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" + + +@needs_py39 +def test_verify_password(): + from docs_src.security.tutorial005_an_py39 import fake_users_db, verify_password + + assert verify_password("secret", fake_users_db["johndoe"]["hashed_password"]) + + +@needs_py39 +def test_get_password_hash(): + from docs_src.security.tutorial005_an_py39 import get_password_hash + + assert get_password_hash("secretalice") + + +@needs_py39 +def test_create_access_token(): + from docs_src.security.tutorial005_an_py39 import create_access_token + + access_token = create_access_token(data={"data": "foo"}) + assert access_token + + +@needs_py39 +def test_token_no_sub(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiZm9vIn0.9ynBhuYb4e6aW3oJr_K_TBgwcMTDpRToQIE25L57rOE" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_username(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb28ifQ.NnExK_dlNAYyzACrXtXDrcWOgGY2JuPbI4eDaHdfK5Y" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_no_scope(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not enough permissions"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inexistent_user(client: TestClient): + response = client.get( + "/users/me", + headers={ + "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VybmFtZTpib2IifQ.HcfCW67Uda-0gz54ZWTqmtgJnZeNem0Q757eTa9EZuw" + }, + ) + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Could not validate credentials"} + assert response.headers["WWW-Authenticate"] == 'Bearer scope="me"' + + +@needs_py39 +def test_token_inactive_user(client: TestClient): + access_token = get_access_token( + username="alice", password="secretalice", scope="me", client=client + ) + response = client.get( + "/users/me", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Inactive user"} + + +@needs_py39 +def test_read_items(client: TestClient): + access_token = get_access_token(scope="me items", client=client) + response = client.get( + "/users/me/items/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == [{"item_id": "Foo", "owner": "johndoe"}] + + +@needs_py39 +def test_read_system_status(client: TestClient): + access_token = get_access_token(client=client) + response = client.get( + "/status/", headers={"Authorization": f"Bearer {access_token}"} + ) + assert response.status_code == 200, response.text + assert response.json() == {"status": "ok"} + + +@needs_py39 +def test_read_system_status_no_token(client: TestClient): + response = client.get("/status/") + assert response.status_code == 401, response.text + assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py new file mode 100644 index 000000000..1d1668fec --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -0,0 +1,67 @@ +from base64 import b64encode + +from fastapi.testclient import TestClient + +from docs_src.security.tutorial006_an import app + +client = TestClient(app) + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +def test_security_http_basic(): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +def test_security_http_basic_no_credentials(): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +def test_security_http_basic_invalid_credentials(): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_security_http_basic_non_basic_credentials(): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py new file mode 100644 index 000000000..b72b5d864 --- /dev/null +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -0,0 +1,79 @@ +from base64 import b64encode + +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + +openapi_schema = { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, +} + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.security.tutorial006_an import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema + + +@needs_py39 +def test_security_http_basic(client: TestClient): + response = client.get("/users/me", auth=("john", "secret")) + assert response.status_code == 200, response.text + assert response.json() == {"username": "john", "password": "secret"} + + +@needs_py39 +def test_security_http_basic_no_credentials(client: TestClient): + response = client.get("/users/me") + assert response.json() == {"detail": "Not authenticated"} + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + + +@needs_py39 +def test_security_http_basic_invalid_credentials(client: TestClient): + response = client.get( + "/users/me", headers={"Authorization": "Basic notabase64token"} + ) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} + + +@needs_py39 +def test_security_http_basic_non_basic_credentials(client: TestClient): + payload = b64encode(b"johnsecret").decode("ascii") + auth_header = f"Basic {payload}" + response = client.get("/users/me", headers={"Authorization": auth_header}) + assert response.status_code == 401, response.text + assert response.headers["WWW-Authenticate"] == "Basic" + assert response.json() == {"detail": "Invalid authentication credentials"} diff --git a/tests/test_tutorial/test_testing/test_main_b_an.py b/tests/test_tutorial/test_testing/test_main_b_an.py new file mode 100644 index 000000000..b64c5f710 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an.py @@ -0,0 +1,10 @@ +from docs_src.app_testing.app_b_an import test_main + + +def test_app(): + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py310.py b/tests/test_tutorial/test_testing/test_main_b_an_py310.py new file mode 100644 index 000000000..194700b6d --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py310.py @@ -0,0 +1,13 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_app(): + from docs_src.app_testing.app_b_an_py310 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing/test_main_b_an_py39.py b/tests/test_tutorial/test_testing/test_main_b_an_py39.py new file mode 100644 index 000000000..2f8a13623 --- /dev/null +++ b/tests/test_tutorial/test_testing/test_main_b_an_py39.py @@ -0,0 +1,13 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_app(): + from docs_src.app_testing.app_b_an_py39 import test_main + + test_main.test_create_existing_item() + test_main.test_create_item() + test_main.test_create_item_bad_token() + test_main.test_read_inexistent_item() + test_main.test_read_item() + test_main.test_read_item_bad_token() diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py new file mode 100644 index 000000000..fc1f9149a --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an.py @@ -0,0 +1,56 @@ +from docs_src.dependency_testing.tutorial001_an import ( + app, + client, + test_override_in_items, + test_override_in_items_with_params, + test_override_in_items_with_q, +) + + +def test_override_in_items_run(): + test_override_in_items() + + +def test_override_in_items_with_q_run(): + test_override_in_items_with_q() + + +def test_override_in_items_with_params_run(): + test_override_in_items_with_params() + + +def test_override_in_users(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_q(): + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_override_in_users_with_params(): + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +def test_normal_app(): + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py new file mode 100644 index 000000000..a3d27f47f --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py new file mode 100644 index 000000000..f03ed5e07 --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_an_py39.py @@ -0,0 +1,75 @@ +from ...utils import needs_py39 + + +@needs_py39 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import test_override_in_items + + test_override_in_items() + + +@needs_py39 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py39 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_an_py39 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py39 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_an_py39 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py39 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_an_py39 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py new file mode 100644 index 000000000..776b916ff --- /dev/null +++ b/tests/test_tutorial/test_testing_dependencies/test_tutorial001_py310.py @@ -0,0 +1,75 @@ +from ...utils import needs_py310 + + +@needs_py310 +def test_override_in_items_run(): + from docs_src.dependency_testing.tutorial001_py310 import test_override_in_items + + test_override_in_items() + + +@needs_py310 +def test_override_in_items_with_q_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_q, + ) + + test_override_in_items_with_q() + + +@needs_py310 +def test_override_in_items_with_params_run(): + from docs_src.dependency_testing.tutorial001_py310 import ( + test_override_in_items_with_params, + ) + + test_override_in_items_with_params() + + +@needs_py310 +def test_override_in_users(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": None, "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_q(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_override_in_users_with_params(): + from docs_src.dependency_testing.tutorial001_py310 import client + + response = client.get("/users/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Users!", + "params": {"q": "foo", "skip": 5, "limit": 10}, + } + + +@needs_py310 +def test_normal_app(): + from docs_src.dependency_testing.tutorial001_py310 import app, client + + app.dependency_overrides = None + response = client.get("/items/?q=foo&skip=100&limit=200") + assert response.status_code == 200, response.text + assert response.json() == { + "message": "Hello Items!", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an.py b/tests/test_tutorial/test_websockets/test_tutorial002_an.py new file mode 100644 index 000000000..ec78d70d3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an.py @@ -0,0 +1,88 @@ +import pytest +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from docs_src.websockets.tutorial002_an import app + + +def test_main(): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +def test_websocket_with_cookie(): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +def test_websocket_with_header(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +def test_websocket_with_header_and_query(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +def test_websocket_no_credentials(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +def test_websocket_invalid_data(): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py new file mode 100644 index 000000000..23b4bcb78 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py new file mode 100644 index 000000000..2d77f05b3 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_an_py39.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_an_py39 import app + + return app + + +@needs_py39 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py39 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py39 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py39 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py39 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py39 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial002_py310.py b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py new file mode 100644 index 000000000..03bc27bdf --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial002_py310.py @@ -0,0 +1,102 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from fastapi.websockets import WebSocketDisconnect + +from ...utils import needs_py310 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial002_py310 import app + + return app + + +@needs_py310 +def test_main(app: FastAPI): + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200, response.text + assert b"" in response.content + + +@needs_py310 +def test_websocket_with_cookie(app: FastAPI): + client = TestClient(app, cookies={"session": "fakesession"}) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: fakesession" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: foo" + + +@needs_py310 +def test_websocket_with_header(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/bar/ws?token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: bar" + + +@needs_py310 +def test_websocket_with_header_and_query(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/2/ws?q=3&token=some-token") as websocket: + message = "Message one" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + message = "Message two" + websocket.send_text(message) + data = websocket.receive_text() + assert data == "Session cookie or query token value is: some-token" + data = websocket.receive_text() + assert data == "Query parameter q is: 3" + data = websocket.receive_text() + assert data == f"Message text was: {message}, for item ID: 2" + + +@needs_py310 +def test_websocket_no_credentials(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover + + +@needs_py310 +def test_websocket_invalid_data(app: FastAPI): + client = TestClient(app) + with pytest.raises(WebSocketDisconnect): + with client.websocket_connect("/items/foo/ws?q=bar&token=some-token"): + pytest.fail( + "did not raise WebSocketDisconnect on __enter__" + ) # pragma: no cover diff --git a/tests/test_tutorial/test_websockets/test_tutorial003_py39.py b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py new file mode 100644 index 000000000..06c4a9279 --- /dev/null +++ b/tests/test_tutorial/test_websockets/test_tutorial003_py39.py @@ -0,0 +1,50 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="app") +def get_app(): + from docs_src.websockets.tutorial003_py39 import app + + return app + + +@pytest.fixture(name="html") +def get_html(): + from docs_src.websockets.tutorial003_py39 import html + + return html + + +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + + return client + + +@needs_py39 +def test_get(client: TestClient, html: str): + response = client.get("/") + assert response.text == html + + +@needs_py39 +def test_websocket_handle_disconnection(client: TestClient): + with client.websocket_connect("/ws/1234") as connection, client.websocket_connect( + "/ws/5678" + ) as connection_two: + connection.send_text("Hello from 1234") + data1 = connection.receive_text() + assert data1 == "You wrote: Hello from 1234" + data2 = connection_two.receive_text() + client1_says = "Client #1234 says: Hello from 1234" + assert data2 == client1_says + data1 = connection.receive_text() + assert data1 == client1_says + connection_two.close() + data1 = connection.receive_text() + assert data1 == "Client #5678 left the chat" diff --git a/tests/test_typing_python39.py b/tests/test_typing_python39.py index b1ea635e7..26775efd4 100644 --- a/tests/test_typing_python39.py +++ b/tests/test_typing_python39.py @@ -1,10 +1,10 @@ from fastapi import FastAPI from fastapi.testclient import TestClient -from .utils import needs_py39 +from .utils import needs_py310 -@needs_py39 +@needs_py310 def test_typing(): types = { list[int]: [1, 2, 3], From 166d348ea6e68a34422b945289e31962c92ddf8f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Mar 2023 12:30:38 +0000 Subject: [PATCH 0864/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d891a0e63..6f164acf3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update all docs to use `Annotated` as the main recommendation, with new examples and tests. PR [#9268](https://github.com/tiangolo/fastapi/pull/9268) by [@tiangolo](https://github.com/tiangolo). * ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). ## 0.94.1 From 69673548bc8d95c1b7558d033a0ddef94cfce71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 17:16:02 +0100 Subject: [PATCH 0865/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20order=20of=20?= =?UTF-8?q?examples,=20latest=20Python=20version=20first,=20and=20simplify?= =?UTF-8?q?=20version=20tab=20names=20(#9269)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Simplify names in Python versions in tabs in docs * 📝 Update docs for Types Intro, explain Python 3.6+, Python 3.9+, Python 3.10+ * 📝 Re-order all Python examples, show latest Python versions first --- .../docs/advanced/additional-status-codes.md | 38 +- .../en/docs/advanced/advanced-dependencies.md | 64 +-- docs/en/docs/advanced/generate-clients.md | 43 +- .../docs/advanced/security/http-basic-auth.md | 48 +- .../docs/advanced/security/oauth2-scopes.md | 331 +++++------ docs/en/docs/advanced/settings.md | 48 +- docs/en/docs/advanced/testing-dependencies.md | 38 +- docs/en/docs/advanced/websockets.md | 54 +- docs/en/docs/python-types.md | 176 +++--- docs/en/docs/tutorial/background-tasks.md | 38 +- docs/en/docs/tutorial/bigger-applications.md | 16 +- docs/en/docs/tutorial/body-fields.md | 76 +-- docs/en/docs/tutorial/body-multiple-params.md | 170 +++--- docs/en/docs/tutorial/body-nested-models.md | 140 ++--- docs/en/docs/tutorial/body-updates.md | 56 +- docs/en/docs/tutorial/body.md | 84 +-- docs/en/docs/tutorial/cookie-params.md | 76 +-- .../dependencies/classes-as-dependencies.md | 311 +++++----- ...pendencies-in-path-operation-decorators.md | 64 +-- .../dependencies/dependencies-with-yield.md | 32 +- .../dependencies/global-dependencies.md | 17 +- docs/en/docs/tutorial/dependencies/index.md | 128 ++--- .../tutorial/dependencies/sub-dependencies.md | 120 ++-- docs/en/docs/tutorial/encoder.md | 14 +- docs/en/docs/tutorial/extra-data-types.md | 78 +-- docs/en/docs/tutorial/extra-models.md | 70 +-- docs/en/docs/tutorial/header-params.md | 156 ++--- .../tutorial/path-operation-configuration.md | 70 +-- .../path-params-numeric-validations.md | 156 ++--- .../tutorial/query-params-str-validations.md | 539 +++++++++--------- docs/en/docs/tutorial/query-params.md | 56 +- docs/en/docs/tutorial/request-files.md | 157 +++-- .../docs/tutorial/request-forms-and-files.md | 32 +- docs/en/docs/tutorial/request-forms.md | 32 +- docs/en/docs/tutorial/response-model.md | 192 +++---- docs/en/docs/tutorial/schema-extra-example.md | 108 ++-- docs/en/docs/tutorial/security/first-steps.md | 48 +- .../tutorial/security/get-current-user.md | 206 +++---- docs/en/docs/tutorial/security/oauth2-jwt.md | 156 ++--- .../docs/tutorial/security/simple-oauth2.md | 200 +++---- docs/en/docs/tutorial/sql-databases.md | 136 ++--- docs/en/docs/tutorial/testing.md | 38 +- docs/ja/docs/tutorial/testing.md | 14 +- docs/pt/docs/tutorial/body-multiple-params.md | 60 +- docs/pt/docs/tutorial/encoder.md | 14 +- docs/pt/docs/tutorial/header-params.md | 56 +- .../path-params-numeric-validations.md | 28 +- docs/pt/docs/tutorial/query-params.md | 55 +- docs/ru/docs/tutorial/background-tasks.md | 14 +- docs/ru/docs/tutorial/body-fields.md | 28 +- docs/ru/docs/tutorial/cookie-params.md | 28 +- .../dependencies/classes-as-dependencies.md | 92 +-- docs/zh/docs/tutorial/encoder.md | 14 +- docs/zh/docs/tutorial/request-files.md | 42 +- docs/zh/docs/tutorial/sql-databases.md | 136 ++--- 55 files changed, 2598 insertions(+), 2595 deletions(-) diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index d287f861a..b0d8d7bd5 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -14,34 +14,25 @@ But you also want it to accept new items. And when the items didn't exist before To achieve that, import `JSONResponse`, and return your content there directly, setting the `status_code` that you want: -=== "Python 3.6 and above" - - ```Python hl_lines="4 26" - {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="4 25" - {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 25" {!> ../../../docs_src/additional_status_codes/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="4 25" - {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="4 26" + {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -50,6 +41,15 @@ To achieve that, import `JSONResponse`, and return your content there directly, {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + ``` + !!! warning When you return a `Response` directly, like in the example above, it will be returned directly. diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 97621fcf9..9a25d2c64 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -18,19 +18,19 @@ Not the class itself (which is already a callable), but an instance of that clas To do that, we declare a method `__call__`: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/dependencies/tutorial011_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="12" {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -45,19 +45,19 @@ In this case, this `__call__` is what **FastAPI** will use to check for addition And now, we can use `__init__` to declare the parameters of the instance that we can use to "parameterize" the dependency: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/dependencies/tutorial011_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -72,19 +72,19 @@ In this case, **FastAPI** won't ever touch or care about `__init__`, we will use We could create an instance of this class with: -=== "Python 3.6 and above" - - ```Python hl_lines="17" - {!> ../../../docs_src/dependencies/tutorial011_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18" {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -107,19 +107,19 @@ checker(q="somequery") ...and pass whatever that returns as the value of the dependency in our *path operation function* as the parameter `fixed_content_included`: -=== "Python 3.6 and above" - - ```Python hl_lines="21" - {!> ../../../docs_src/dependencies/tutorial011_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="22" {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="21" + {!> ../../../docs_src/dependencies/tutorial011_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index f31a248d0..f62c0b57c 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -16,18 +16,18 @@ If you are building a **frontend**, a very interesting alternative is ../../../docs_src/generate_clients/tutorial001.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7-9 12-13 16-17 21" {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`. ### API Docs @@ -128,19 +128,18 @@ In many cases your FastAPI app will be bigger, and you will probably use tags to For example, you could have a section for **items** and another section for **users**, and they could be separated by tags: - -=== "Python 3.6 and above" - - ```Python hl_lines="23 28 36" - {!> ../../../docs_src/generate_clients/tutorial002.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21 26 34" {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + ### Generate a TypeScript Client with Tags If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags. @@ -186,18 +185,18 @@ For example, here it is using the first tag (you will probably have only one tag You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter: -=== "Python 3.6 and above" - - ```Python hl_lines="8-9 12" - {!> ../../../docs_src/generate_clients/tutorial003.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6-7 10" {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + ### Generate a TypeScript Client with Custom Operation IDs Now if you generate the client again, you will see that it has the improved method names: diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index a9ce3b1ef..f7776e73d 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -20,19 +20,19 @@ Then, when you type that username and password, the browser sends them in the he * It returns an object of type `HTTPBasicCredentials`: * It contains the `username` and `password` sent. -=== "Python 3.6 and above" - - ```Python hl_lines="2 7 11" - {!> ../../../docs_src/security/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="4 8 12" {!> ../../../docs_src/security/tutorial006_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="2 7 11" + {!> ../../../docs_src/security/tutorial006_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -59,19 +59,19 @@ To handle that, we first convert the `username` and `password` to `bytes` encodi Then we can use `secrets.compare_digest()` to ensure that `credentials.username` is `"stanleyjobson"`, and that `credentials.password` is `"swordfish"`. -=== "Python 3.6 and above" - - ```Python hl_lines="1 12-24" - {!> ../../../docs_src/security/tutorial007_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1 12-24" {!> ../../../docs_src/security/tutorial007_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1 12-24" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -142,19 +142,19 @@ That way, using `secrets.compare_digest()` in your application code, it will be After detecting that the credentials are incorrect, return an `HTTPException` with a status code 401 (the same returned when no credentials are provided) and add the header `WWW-Authenticate` to make the browser show the login prompt again: -=== "Python 3.6 and above" - - ```Python hl_lines="26-30" - {!> ../../../docs_src/security/tutorial007_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="26-30" {!> ../../../docs_src/security/tutorial007_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="26-30" + {!> ../../../docs_src/security/tutorial007_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index c216d7f50..57757ec6c 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -56,34 +56,34 @@ They are normally used to declare specific security permissions, for example: First, let's quickly see the parts that change from the examples in the main **Tutorial - User Guide** for [OAuth2 with Password (and hashing), Bearer with JWT tokens](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. Now using OAuth2 scopes: -=== "Python 3.6 and above" - - ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 8 12 46 64 105 107-115 121-124 128-134 139 155" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -92,13 +92,13 @@ First, let's quickly see the parts that change from the examples in the main **T {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + {!> ../../../docs_src/security/tutorial005.py!} ``` Now let's review those changes step by step. @@ -109,34 +109,35 @@ The first change is that now we are declaring the OAuth2 security scheme with tw The `scopes` parameter receives a `dict` with each scope as a key and the description as the value: -=== "Python 3.6 and above" - - ```Python hl_lines="63-66" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="62-65" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="62-65" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="63-66" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="62-65" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="61-64" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" + +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -145,13 +146,13 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="61-64" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="62-65" + {!> ../../../docs_src/security/tutorial005.py!} ``` Because we are now declaring those scopes, they will show up in the API docs when you log-in/authorize. @@ -175,34 +176,34 @@ And we return the scopes as part of the JWT token. But in your application, for security, you should make sure you only add the scopes that the user is actually able to have, or the ones you have predefined. -=== "Python 3.6 and above" - - ```Python hl_lines="156" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="155" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="155" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="155" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="156" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="153" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="152" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -211,13 +212,13 @@ And we return the scopes as part of the JWT token. {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="152" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="153" + {!> ../../../docs_src/security/tutorial005.py!} ``` ## Declare scopes in *path operations* and dependencies @@ -241,34 +242,34 @@ In this case, it requires the scope `me` (it could require more than one scope). We are doing it here to demonstrate how **FastAPI** handles scopes declared at different levels. -=== "Python 3.6 and above" - - ```Python hl_lines="4 140 171" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="4 139 170" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 139 170" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="4 139 170" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4 140 171" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="4 139 166" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="3 138 165" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -277,13 +278,13 @@ In this case, it requires the scope `me` (it could require more than one scope). {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="3 138 165" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="4 139 166" + {!> ../../../docs_src/security/tutorial005.py!} ``` !!! info "Technical Details" @@ -307,34 +308,34 @@ We also declare a special parameter of type `SecurityScopes`, imported from `fas This `SecurityScopes` class is similar to `Request` (`Request` was used to get the request object directly). -=== "Python 3.6 and above" - - ```Python hl_lines="8 106" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="8 105" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8 105" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8 106" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="8 105" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="7 104" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -343,13 +344,13 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="7 104" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="8 105" + {!> ../../../docs_src/security/tutorial005.py!} ``` ## Use the `scopes` @@ -364,34 +365,34 @@ We create an `HTTPException` that we can re-use (`raise`) later at several point In this exception, we include the scopes required (if any) as a string separated by spaces (using `scope_str`). We put that string containing the scopes in the `WWW-Authenticate` header (this is part of the spec). -=== "Python 3.6 and above" - - ```Python hl_lines="106 108-116" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="105 107-115" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="105 107-115" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="106 108-116" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="105 107-115" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="104 106-114" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -400,13 +401,13 @@ In this exception, we include the scopes required (if any) as a string separated {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="104 106-114" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="105 107-115" + {!> ../../../docs_src/security/tutorial005.py!} ``` ## Verify the `username` and data shape @@ -423,43 +424,25 @@ Instead of, for example, a `dict`, or something else, as it could break the appl We also verify that we have a user with that username, and if not, we raise that same exception we created before. -=== "Python 3.6 and above" - - ```Python hl_lines="47 117-128" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="46 116-127" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="46 116-127" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="46 116-127" - {!> ../../../docs_src/security/tutorial005.py!} + {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.6+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="46 116-127" - {!> ../../../docs_src/security/tutorial005_py39.py!} + ```Python hl_lines="47 117-128" + {!> ../../../docs_src/security/tutorial005_an.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -468,40 +451,58 @@ We also verify that we have a user with that username, and if not, we raise that {!> ../../../docs_src/security/tutorial005_py310.py!} ``` +=== "Python 3.9+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005_py39.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="46 116-127" + {!> ../../../docs_src/security/tutorial005.py!} + ``` + ## Verify the `scopes` We now verify that all the scopes required, by this dependency and all the dependants (including *path operations*), are included in the scopes provided in the token received, otherwise raise an `HTTPException`. For this, we use `security_scopes.scopes`, that contains a `list` with all these scopes as `str`. -=== "Python 3.6 and above" - - ```Python hl_lines="129-135" - {!> ../../../docs_src/security/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="128-134" - {!> ../../../docs_src/security/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="128-134" {!> ../../../docs_src/security/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="129-135" + {!> ../../../docs_src/security/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="128-134" - {!> ../../../docs_src/security/tutorial005.py!} + ```Python hl_lines="127-133" + {!> ../../../docs_src/security/tutorial005_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -510,13 +511,13 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="127-133" - {!> ../../../docs_src/security/tutorial005_py310.py!} + ```Python hl_lines="128-134" + {!> ../../../docs_src/security/tutorial005.py!} ``` ## Dependency tree and scopes diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 52dbdf6fa..a29485a21 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -216,19 +216,19 @@ Notice that now we don't create a default instance `settings = Settings()`. Now we create a dependency that returns a new `config.Settings()`. -=== "Python 3.6 and above" - - ```Python hl_lines="6 12-13" - {!> ../../../docs_src/settings/app02_an/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6 12-13" {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -244,19 +244,19 @@ Now we create a dependency that returns a new `config.Settings()`. And then we can require it from the *path operation function* as a dependency and use it anywhere we need it. -=== "Python 3.6 and above" - - ```Python hl_lines="17 19-21" - {!> ../../../docs_src/settings/app02_an/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="17 19-21" {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -338,19 +338,19 @@ we would create that object for each request, and we would be reading the `.env` But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ -=== "Python 3.6 and above" - - ```Python hl_lines="1 11" - {!> ../../../docs_src/settings/app03_an/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1 11" {!> ../../../docs_src/settings/app03_an_py39/main.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index c30dccd5d..fecc14d5f 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -28,34 +28,25 @@ To override a dependency for testing, you put as a key the original dependency ( And then **FastAPI** will call that override instead of the original dependency. -=== "Python 3.6 and above" - - ```Python hl_lines="29-30 33" - {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="28-29 32" - {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="26-27 30" {!> ../../../docs_src/dependency_testing/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="28-29 32" - {!> ../../../docs_src/dependency_testing/tutorial001.py!} + {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="29-30 33" + {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -64,6 +55,15 @@ And then **FastAPI** will call that override instead of the original dependency. {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="28-29 32" + {!> ../../../docs_src/dependency_testing/tutorial001.py!} + ``` + !!! tip You can set a dependency override for a dependency used anywhere in your **FastAPI** application. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 8df32f4ca..3cdd45736 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -112,34 +112,25 @@ In WebSocket endpoints you can import from `fastapi` and use: They work the same way as for other FastAPI endpoints/*path operations*: -=== "Python 3.6 and above" - - ```Python hl_lines="69-70 83" - {!> ../../../docs_src/websockets/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="68-69 82" - {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="68-69 82" {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="68-69 81" - {!> ../../../docs_src/websockets/tutorial002.py!} + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -148,6 +139,15 @@ They work the same way as for other FastAPI endpoints/*path operations*: {!> ../../../docs_src/websockets/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` + !!! info As this is a WebSocket it doesn't really make sense to raise an `HTTPException`, instead we raise a `WebSocketException`. @@ -185,18 +185,18 @@ With that you can connect the WebSocket and then send and receive messages: When a WebSocket connection is closed, the `await websocket.receive_text()` will raise a `WebSocketDisconnect` exception, which you can then catch and handle like in this example. -=== "Python 3.6 and above" - - ```Python hl_lines="81-83" - {!> ../../../docs_src/websockets/tutorial003.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="79-81" {!> ../../../docs_src/websockets/tutorial003_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` + To try it out: * Open the app with several browser tabs. diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 7ddfd41f2..693613a36 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -158,13 +158,31 @@ The syntax using `typing` is **compatible** with all versions, from Python 3.6 t As Python advances, **newer versions** come with improved support for these type annotations and in many cases you won't even need to import and use the `typing` module to declare the type annotations. -If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. See some examples below. +If you can choose a more recent version of Python for your project, you will be able to take advantage of that extra simplicity. + +In all the docs there are examples compatible with each version of Python (when there's a difference). + +For example "**Python 3.6+**" means it's compatible with Python 3.6 or above (including 3.7, 3.8, 3.9, 3.10, etc). And "**Python 3.9+**" means it's compatible with Python 3.9 or above (including 3.10, etc). + +If you can use the **latest versions of Python**, use the examples for the latest version, those will have the **best and simplest syntax**, for example, "**Python 3.10+**". #### List For example, let's define a variable to be a `list` of `str`. -=== "Python 3.6 and above" +=== "Python 3.9+" + + Declare the variable, with the same colon (`:`) syntax. + + As the type, put `list`. + + As the list is a type that contains some internal types, you put them in square brackets: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +=== "Python 3.6+" From `typing`, import `List` (with a capital `L`): @@ -182,18 +200,6 @@ For example, let's define a variable to be a `list` of `str`. {!> ../../../docs_src/python_types/tutorial006.py!} ``` -=== "Python 3.9 and above" - - Declare the variable, with the same colon (`:`) syntax. - - As the type, put `list`. - - As the list is a type that contains some internal types, you put them in square brackets: - - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial006_py39.py!} - ``` - !!! info Those internal types in the square brackets are called "type parameters". @@ -218,18 +224,18 @@ And still, the editor knows it is a `str`, and provides support for that. You would do the same to declare `tuple`s and `set`s: -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial007.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial007_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + This means: * The variable `items_t` is a `tuple` with 3 items, an `int`, another `int`, and a `str`. @@ -243,18 +249,18 @@ The first type parameter is for the keys of the `dict`. The second type parameter is for the values of the `dict`: -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + This means: * The variable `prices` is a `dict`: @@ -267,20 +273,20 @@ You can declare that a variable can be any of **several types**, for example, an In Python 3.6 and above (including Python 3.10) you can use the `Union` type from `typing` and put inside the square brackets the possible types to accept. -In Python 3.10 there's also an **alternative syntax** where you can put the possible types separated by a vertical bar (`|`). +In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). -=== "Python 3.6 and above" - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial008b.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/python_types/tutorial008b_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + In both cases this means that `item` could be an `int` or a `str`. #### Possibly `None` @@ -299,24 +305,24 @@ Using `Optional[str]` instead of just `str` will let the editor help you detecti This also means that in Python 3.10, you can use `Something | None`: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +=== "Python 3.6+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.6 and above - alternative" +=== "Python 3.6+ alternative" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009b.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="1" - {!> ../../../docs_src/python_types/tutorial009_py310.py!} - ``` - #### Using `Union` or `Optional` If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: @@ -360,32 +366,7 @@ And then you won't have to worry about names like `Optional` and `Union`. 😎 These types that take type parameters in square brackets are called **Generic types** or **Generics**, for example: -=== "Python 3.6 and above" - - * `List` - * `Tuple` - * `Set` - * `Dict` - * `Union` - * `Optional` - * ...and others. - -=== "Python 3.9 and above" - - You can use the same builtin types as generics (with square brackets and types inside): - - * `list` - * `tuple` - * `set` - * `dict` - - And the same as with Python 3.6, from the `typing` module: - - * `Union` - * `Optional` - * ...and others. - -=== "Python 3.10 and above" +=== "Python 3.10+" You can use the same builtin types as generics (with square brackets and types inside): @@ -400,7 +381,32 @@ These types that take type parameters in square brackets are called **Generic ty * `Optional` (the same as with Python 3.6) * ...and others. - In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types. + In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types, that's a lot better and simpler. + +=== "Python 3.9+" + + You can use the same builtin types as generics (with square brackets and types inside): + + * `list` + * `tuple` + * `set` + * `dict` + + And the same as with Python 3.6, from the `typing` module: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.6+" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...and others. ### Classes as types @@ -440,22 +446,22 @@ And you get all the editor support with that resulting object. An example from the official Pydantic docs: -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/python_types/tutorial011.py!} + {!> ../../../docs_src/python_types/tutorial011_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/python_types/tutorial011_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/python_types/tutorial011_py310.py!} + {!> ../../../docs_src/python_types/tutorial011.py!} ``` !!! info @@ -472,7 +478,15 @@ You will see a lot more of all this in practice in the [Tutorial - User Guide](t Python also has a feature that allows putting **additional metadata** in these type hints using `Annotated`. -=== "Python 3.7 and above" +=== "Python 3.9+" + + In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +=== "Python 3.6+" In versions below Python 3.9, you import `Annotated` from `typing_extensions`. @@ -482,14 +496,6 @@ Python also has a feature that allows putting **additional metadata** in these t {!> ../../../docs_src/python_types/tutorial013.py!} ``` -=== "Python 3.9 and above" - - In Python 3.9, `Annotated` is part of the standard library, so you can import it from `typing`. - - ```Python hl_lines="1 4" - {!> ../../../docs_src/python_types/tutorial013_py39.py!} - ``` - Python itself doesn't do anything with this `Annotated`. And for editors and other tools, the type is still `str`. But you can use this space in `Annotated` to provide **FastAPI** with additional metadata about how you want your application to behave. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 909e2a72b..582a7e098 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -57,34 +57,25 @@ Using `BackgroundTasks` also works with the dependency injection system, you can **FastAPI** knows what to do in each case and how to re-use the same object, so that all the background tasks are merged together and are run in the background afterwards: -=== "Python 3.6 and above" - - ```Python hl_lines="14 16 23 26" - {!> ../../../docs_src/background_tasks/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="13 15 22 25" {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -93,6 +84,15 @@ Using `BackgroundTasks` also works with the dependency injection system, you can {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + In this example, the messages will be written to the `log.txt` file *after* the response is sent. If there was a query in the request, it will be written to the log in a background task. diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 9aeafe29e..b8e3e5807 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -112,19 +112,19 @@ So we put them in their own `dependencies` module (`app/dependencies.py`). We will now use a simple dependency to read a custom `X-Token` header: -=== "Python 3.6 and above" - - ```Python hl_lines="1 5-7" - {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3 6-8" {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1 5-7" + {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 301ee84f2..484d694ea 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -6,34 +6,25 @@ The same way you can declare additional validation and metadata in *path operati First, you have to import it: -=== "Python 3.6 and above" - - ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4" {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -42,6 +33,15 @@ First, you have to import it: {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + !!! warning Notice that `Field` is imported directly from `pydantic`, not from `fastapi` as are all the rest (`Query`, `Path`, `Body`, etc). @@ -49,34 +49,25 @@ First, you have to import it: You can then use `Field` with model attributes: -=== "Python 3.6 and above" - - ```Python hl_lines="12-15" - {!> ../../../docs_src/body_fields/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="11-14" - {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="11-14" {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="11-14" - {!> ../../../docs_src/body_fields/tutorial001.py!} + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -85,6 +76,15 @@ You can then use `Field` with model attributes: {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + `Field` works the same way as `Query`, `Path` and `Body`, it has all the same parameters, etc. !!! note "Technical Details" diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 1a6b572dc..3358c6772 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -8,34 +8,25 @@ First, of course, you can mix `Path`, `Query` and request body parameter declara And you can also declare body parameters as optional, by setting the default to `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="18-20" - {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="18-20" {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -44,6 +35,15 @@ And you can also declare body parameters as optional, by setting the default to {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + !!! note Notice that, in this case, the `item` that would be taken from the body is optional. As it has a `None` default value. @@ -62,18 +62,18 @@ In the previous example, the *path operations* would expect a JSON body with the But you can also declare multiple body parameters, e.g. `item` and `user`: -=== "Python 3.6 and above" - - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + In this case, **FastAPI** will notice that there are more than one body parameters in the function (two parameters that are Pydantic models). So, it will then use the parameter names as keys (field names) in the body, and expect a body like: @@ -111,34 +111,25 @@ If you declare it as is, because it is a singular value, **FastAPI** will assume But you can instruct **FastAPI** to treat it as another body key using `Body`: -=== "Python 3.6 and above" - - ```Python hl_lines="24" - {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="23" - {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="23" {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -147,6 +138,15 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + In this case, **FastAPI** will expect a body like: ```JSON @@ -185,34 +185,25 @@ q: str | None = None For example: -=== "Python 3.6 and above" - - ```Python hl_lines="28" - {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="27" {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -221,6 +212,15 @@ For example: {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + !!! info `Body` also has all the same extra validation and metadata parameters as `Query`,`Path` and others you will see later. @@ -238,34 +238,25 @@ item: Item = Body(embed=True) as in: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -274,6 +265,15 @@ as in: {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + In this case **FastAPI** will expect a body like: ```JSON hl_lines="2" diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index d51f171d6..ffa0c0d0e 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -6,18 +6,18 @@ With **FastAPI**, you can define, validate, document, and use arbitrarily deeply You can define an attribute to be a subtype. For example, a Python `list`: -=== "Python 3.6 and above" - - ```Python hl_lines="14" - {!> ../../../docs_src/body_nested_models/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="12" {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + This will make `tags` be a list, although it doesn't declare the type of the elements of the list. ## List fields with type parameter @@ -61,22 +61,22 @@ Use that same standard syntax for model attributes with internal types. So, in our example, we can make `tags` be specifically a "list of strings": -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="14" - {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="12" - {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` ## Set types @@ -87,22 +87,22 @@ And Python has a special data type for sets of unique items, the `set`. Then we can declare `tags` as a set of strings: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="1 14" - {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="12" - {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} ``` With this, even if you receive a request with duplicate data, it will be converted to a set of unique items. @@ -125,44 +125,44 @@ All that, arbitrarily nested. For example, we can define an `Image` model: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9-11" - {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9-11" {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7-9" - {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} ``` ### Use the submodel as a type And then we can use it as the type of an attribute: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="18" - {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} ``` This would mean that **FastAPI** would expect a body similar to: @@ -196,22 +196,22 @@ To see all the options you have, checkout the docs for ../../../docs_src/body_nested_models/tutorial005.py!} + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="4 10" {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="2 8" - {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} ``` The string will be checked to be a valid URL, and documented in JSON Schema / OpenAPI as such. @@ -220,22 +220,22 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op You can also use Pydantic models as subtypes of `list`, `set`, etc: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20" - {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="18" - {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} ``` This will expect (convert, validate, document, etc) a JSON body like: @@ -271,22 +271,22 @@ This will expect (convert, validate, document, etc) a JSON body like: You can define arbitrarily deeply nested models: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9 14 20 23 27" - {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9 14 20 23 27" {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7 12 18 21 25" - {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} ``` !!! info @@ -308,18 +308,18 @@ images: list[Image] as in: -=== "Python 3.6 and above" - - ```Python hl_lines="15" - {!> ../../../docs_src/body_nested_models/tutorial008.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="13" {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + ## Editor support everywhere And you get editor support everywhere. @@ -348,18 +348,18 @@ That's what we are going to see here. In this case, you would accept any `dict` as long as it has `int` keys with `float` values: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/body_nested_models/tutorial009.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + !!! tip Have in mind that JSON only supports `str` as keys. diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 7d8675060..a32948db1 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -6,22 +6,22 @@ To update an item you can use the ../../../docs_src/body_updates/tutorial001.py!} + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="30-35" {!> ../../../docs_src/body_updates/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="28-33" - {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001.py!} ``` `PUT` is used to receive data that should replace the existing data. @@ -67,22 +67,22 @@ That would generate a `dict` with only the data that was set when creating the ` Then you can use this to generate a `dict` with only the data that was set (sent in the request), omitting default values: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="34" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="34" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="32" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` ### Using Pydantic's `update` parameter @@ -91,22 +91,22 @@ Now, you can create a copy of the existing model using `.copy()`, and pass the ` Like `stored_item_model.copy(update=update_data)`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="35" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="35" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="33" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` ### Partial updates recap @@ -124,22 +124,22 @@ In summary, to apply partial updates you would: * Save the data to your DB. * Return the updated model. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="30-37" - {!> ../../../docs_src/body_updates/tutorial002.py!} + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="30-37" {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="28-35" - {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} ``` !!! tip diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 509005936..172b91fdf 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -19,36 +19,36 @@ To declare a **request** body, you use ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="2" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + ## Create your data model Then you declare your data model as a class that inherits from `BaseModel`. Use standard Python types for all the attributes: -=== "Python 3.6 and above" - - ```Python hl_lines="7-11" - {!> ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="5-9" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + The same as when declaring query parameters, when a model attribute has a default value, it is not required. Otherwise, it is required. Use `None` to make it just optional. For example, this model above declares a JSON "`object`" (or Python `dict`) like: @@ -75,18 +75,18 @@ For example, this model above declares a JSON "`object`" (or Python `dict`) like To add it to your *path operation*, declare it the same way you declared path and query parameters: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/body/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + ...and declare its type as the model you created, `Item`. ## Results @@ -149,54 +149,54 @@ But you would get the same editor support with ../../../docs_src/body/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/body/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + ## Request body + path parameters You can declare path parameters and request body at the same time. **FastAPI** will recognize that the function parameters that match path parameters should be **taken from the path**, and that function parameters that are declared to be Pydantic models should be **taken from the request body**. -=== "Python 3.6 and above" - - ```Python hl_lines="17-18" - {!> ../../../docs_src/body/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="15-16" {!> ../../../docs_src/body/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + ## Request body + path + query parameters You can also declare **body**, **path** and **query** parameters, all at the same time. **FastAPI** will recognize each of them and take the data from the correct place. -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/body/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/body/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + The function parameters will be recognized as follows: * If the parameter is also declared in the **path**, it will be used as a path parameter. diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 2aab8d12d..169c546f0 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -6,34 +6,25 @@ You can define Cookie parameters the same way you define `Query` and `Path` para First import `Cookie`: -=== "Python 3.6 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -42,40 +33,40 @@ First import `Cookie`: {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + ## Declare `Cookie` parameters Then declare the cookie parameters using the same structure as with `Path` and `Query`. The first value is the default value, you can pass all the extra validation or annotation parameters: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/cookie_params/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/cookie_params/tutorial001.py!} + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -84,6 +75,15 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + !!! note "Technical Details" `Cookie` is a "sister" class of `Path` and `Query`. It also inherits from the same common `Param` class. diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 4fa05c98e..832e23997 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,34 +6,25 @@ Before diving deeper into the **Dependency Injection** system, let's upgrade the In the previous example, we were returning a `dict` from our dependency ("dependable"): -=== "Python 3.6 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/dependencies/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="11" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -42,6 +33,15 @@ In the previous example, we were returning a `dict` from our dependency ("depend {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + But then we get a `dict` in the parameter `commons` of the *path operation function*. And we know that editors can't provide a lot of support (like completion) for `dict`s, because they can't know their keys and value types. @@ -103,34 +103,25 @@ That also applies to callables with no parameters at all. The same as it would b Then, we can change the dependency "dependable" `common_parameters` from above to the class `CommonQueryParams`: -=== "Python 3.6 and above" - - ```Python hl_lines="12-16" - {!> ../../../docs_src/dependencies/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -139,36 +130,36 @@ Then, we can change the dependency "dependable" `common_parameters` from above t {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + Pay attention to the `__init__` method used to create the instance of the class: -=== "Python 3.6 and above" - - ```Python hl_lines="13" - {!> ../../../docs_src/dependencies/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="12" {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="12" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -177,36 +168,36 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + ...it has the same parameters as our previous `common_parameters`: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/dependencies/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -215,6 +206,15 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + Those parameters are what **FastAPI** will use to "solve" the dependency. In both cases, it will have: @@ -229,35 +229,25 @@ In both cases the data will be converted, validated, documented on the OpenAPI s Now you can declare your dependency using this class. - -=== "Python 3.6 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/dependencies/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -266,19 +256,22 @@ Now you can declare your dependency using this class. {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + **FastAPI** calls the `CommonQueryParams` class. This creates an "instance" of that class and the instance will be passed as the parameter `commons` to your function. ## Type annotation vs `Depends` Notice how we write `CommonQueryParams` twice in the above code: -=== "Python 3.6 and above" - - ```Python - commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] - ``` - -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -287,6 +280,12 @@ Notice how we write `CommonQueryParams` twice in the above code: commons: CommonQueryParams = Depends(CommonQueryParams) ``` +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + The last `CommonQueryParams`, in: ```Python @@ -301,13 +300,13 @@ From it is that FastAPI will extract the declared parameters and that is what Fa In this case, the first `CommonQueryParams`, in: -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python commons: Annotated[CommonQueryParams, ... ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -320,13 +319,13 @@ In this case, the first `CommonQueryParams`, in: You could actually write just: -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python commons: Annotated[Any, Depends(CommonQueryParams)] ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -337,34 +336,25 @@ You could actually write just: ..as in: -=== "Python 3.6 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/dependencies/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -373,6 +363,15 @@ You could actually write just: {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + But declaring the type is encouraged as that way your editor will know what will be passed as the parameter `commons`, and then it can help you with code completion, type checks, etc: @@ -381,13 +380,7 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -=== "Python 3.6 and above" - - ```Python - commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] - ``` - -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -396,19 +389,25 @@ But you see that we are having some code repetition here, writing `CommonQueryPa commons: CommonQueryParams = Depends(CommonQueryParams) ``` +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + **FastAPI** provides a shortcut for these cases, in where the dependency is *specifically* a class that **FastAPI** will "call" to create an instance of the class itself. For those specific cases, you can do the following: Instead of writing: -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -419,13 +418,13 @@ Instead of writing: ...you write: -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python commons: Annotated[CommonQueryParams, Depends()] ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -438,34 +437,25 @@ You declare the dependency as the type of the parameter, and you use `Depends()` The same example would then look like: -=== "Python 3.6 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/dependencies/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -474,6 +464,15 @@ The same example would then look like: {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + ...and **FastAPI** will know what to do. !!! tip diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index d3a11c149..aef9bf5e1 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -14,19 +14,19 @@ The *path operation decorator* receives an optional argument `dependencies`. It should be a `list` of `Depends()`: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/dependencies/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -57,19 +57,19 @@ You can use the same dependency *functions* you use normally. They can declare request requirements (like headers) or other sub-dependencies: -=== "Python 3.6 and above" - - ```Python hl_lines="7 12" - {!> ../../../docs_src/dependencies/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="8 13" {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="7 12" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -82,19 +82,19 @@ They can declare request requirements (like headers) or other sub-dependencies: These dependencies can `raise` exceptions, the same as normal dependencies: -=== "Python 3.6 and above" - - ```Python hl_lines="9 14" - {!> ../../../docs_src/dependencies/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10 15" {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9 14" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -109,19 +109,19 @@ And they can return values or not, the values won't be used. So, you can re-use a normal dependency (that returns a value) you already use somewhere else, and even though the value won't be used, the dependency will be executed: -=== "Python 3.6 and above" - - ```Python hl_lines="10 15" - {!> ../../../docs_src/dependencies/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="11 16" {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/dependencies/tutorial006_an.py!} + ``` + +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index d0c263f40..721fee162 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -66,19 +66,19 @@ You can have sub-dependencies and "trees" of sub-dependencies of any size and sh For example, `dependency_c` can have a dependency on `dependency_b`, and `dependency_b` on `dependency_a`: -=== "Python 3.6 and above" - - ```Python hl_lines="5 13 21" - {!> ../../../docs_src/dependencies/tutorial008_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6 14 22" {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -93,19 +93,19 @@ In this case `dependency_c`, to execute its exit code, needs the value from `dep And, in turn, `dependency_b` needs the value from `dependency_a` (here named `dep_a`) to be available for its exit code. -=== "Python 3.6 and above" - - ```Python hl_lines="17-18 25-26" - {!> ../../../docs_src/dependencies/tutorial008_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18-19 26-27" {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index 9ad503d8f..f148388ee 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -6,20 +6,19 @@ Similar to the way you can [add `dependencies` to the *path operation decorators In that case, they will be applied to all the *path operations* in the application: - -=== "Python 3.6 and above" - - ```Python hl_lines="16" - {!> ../../../docs_src/dependencies/tutorial012_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="16" {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 5675b8dfd..7ae32f8b8 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -31,34 +31,25 @@ Let's first focus on the dependency. It is just a function that can take all the same parameters that a *path operation function* can take: -=== "Python 3.6 and above" - - ```Python hl_lines="9-12" - {!> ../../../docs_src/dependencies/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="8-11" - {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8-9" {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="8-11" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -67,6 +58,15 @@ It is just a function that can take all the same parameters that a *path operati {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + That's it. **2 lines**. @@ -87,34 +87,25 @@ And then it just returns a `dict` containing those values. ### Import `Depends` -=== "Python 3.6 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="3" {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="3" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -123,38 +114,38 @@ And then it just returns a `dict` containing those values. {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + ### Declare the dependency, in the "dependant" The same way you use `Body`, `Query`, etc. with your *path operation function* parameters, use `Depends` with a new parameter: -=== "Python 3.6 and above" - - ```Python hl_lines="16 21" - {!> ../../../docs_src/dependencies/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="15 20" - {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="13 18" {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="15 20" - {!> ../../../docs_src/dependencies/tutorial001.py!} + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -163,6 +154,15 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + Although you use `Depends` in the parameters of your function the same way you use `Body`, `Query`, etc, `Depends` works a bit differently. You only give `Depends` a single parameter. @@ -212,22 +212,22 @@ commons: Annotated[dict, Depends(common_parameters)] But because we are using `Annotated`, we can store that `Annotated` value in a variable and use it in multiple places: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="15 19 24" - {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} + ```Python hl_lines="12 16 21" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14 18 23" {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="12 16 21" - {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} + ```Python hl_lines="15 19 24" + {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} ``` !!! tip diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 8b70e2602..27af5970d 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -10,34 +10,25 @@ They can be as **deep** as you need them to be. You could create a first dependency ("dependable") like: -=== "Python 3.6 and above" - - ```Python hl_lines="9-10" - {!> ../../../docs_src/dependencies/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="8-9" - {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8-9" {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.6 - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="8-9" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.10 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9-10" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -46,6 +37,15 @@ You could create a first dependency ("dependable") like: {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.6 non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + It declares an optional query parameter `q` as a `str`, and then it just returns it. This is quite simple (not very useful), but will help us focus on how the sub-dependencies work. @@ -54,34 +54,25 @@ This is quite simple (not very useful), but will help us focus on how the sub-de Then you can create another dependency function (a "dependable") that at the same time declares a dependency of its own (so it is a "dependant" too): -=== "Python 3.6 and above" - - ```Python hl_lines="14" - {!> ../../../docs_src/dependencies/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="13" - {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="13" {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.6 - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="13" - {!> ../../../docs_src/dependencies/tutorial005.py!} + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.10 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -90,6 +81,15 @@ Then you can create another dependency function (a "dependable") that at the sam {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.6 non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + Let's focus on the parameters declared: * Even though this function is a dependency ("dependable") itself, it also declares another dependency (it "depends" on something else). @@ -101,34 +101,25 @@ Let's focus on the parameters declared: Then we can use the dependency with: -=== "Python 3.6 and above" - - ```Python hl_lines="24" - {!> ../../../docs_src/dependencies/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="23" - {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="23" {!> ../../../docs_src/dependencies/tutorial005_an_py310.py!} ``` -=== "Python 3.6 - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="22" - {!> ../../../docs_src/dependencies/tutorial005.py!} + ```Python hl_lines="23" + {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.10 - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/dependencies/tutorial005_an.py!} + ``` + +=== "Python 3.10 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -137,6 +128,15 @@ Then we can use the dependency with: {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` +=== "Python 3.6 non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + !!! info Notice that we are only declaring one dependency in the *path operation function*, the `query_or_cookie_extractor`. @@ -161,14 +161,14 @@ And it will save the returned value in a ../../../docs_src/encoder/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 21" {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + In this example, it would convert the Pydantic model to a `dict`, and the `datetime` to a `str`. The result of calling it is something that can be encoded with the Python standard `json.dumps()`. diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 440f00d18..0d969b41d 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -55,34 +55,25 @@ Here are some of the additional data types you can use: Here's an example *path operation* with parameters using some of the above types. -=== "Python 3.6 and above" - - ```Python hl_lines="1 3 13-17" - {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="1 3 12-16" - {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1 3 12-16" {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="1 2 12-16" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -91,36 +82,36 @@ Here's an example *path operation* with parameters using some of the above types {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + Note that the parameters inside the function have their natural data type, and you can, for example, perform normal date manipulations, like: -=== "Python 3.6 and above" - - ```Python hl_lines="19-20" - {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="18-19" - {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="18-19" {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="18-19" - {!> ../../../docs_src/extra_data_types/tutorial001.py!} + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -128,3 +119,12 @@ Note that the parameters inside the function have their natural data type, and y ```Python hl_lines="17-18" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 72fc74894..e91e879e4 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -17,18 +17,18 @@ This is especially the case for user models, because: Here's a general idea of how the models could look like with their password fields and the places where they are used: -=== "Python 3.6 and above" - - ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" - {!> ../../../docs_src/extra_models/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" {!> ../../../docs_src/extra_models/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + ### About `**user_in.dict()` #### Pydantic's `.dict()` @@ -158,18 +158,18 @@ All the data conversion, validation, documentation, etc. will still work as norm That way, we can declare just the differences between the models (with plaintext `password`, with `hashed_password` and without password): -=== "Python 3.6 and above" - - ```Python hl_lines="9 15-16 19-20 23-24" - {!> ../../../docs_src/extra_models/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 13-14 17-18 21-22" {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + ## `Union` or `anyOf` You can declare a response to be the `Union` of two types, that means, that the response would be any of the two. @@ -181,18 +181,18 @@ To do that, use the standard Python type hint `Union`, include the most specific type first, followed by the less specific type. In the example below, the more specific `PlaneItem` comes before `CarItem` in `Union[PlaneItem, CarItem]`. -=== "Python 3.6 and above" - - ```Python hl_lines="1 14-15 18-20 33" - {!> ../../../docs_src/extra_models/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1 14-15 18-20 33" {!> ../../../docs_src/extra_models/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + ### `Union` in Python 3.10 In this example we pass `Union[PlaneItem, CarItem]` as the value of the argument `response_model`. @@ -213,18 +213,18 @@ The same way, you can declare responses of lists of objects. For that, use the standard Python `typing.List` (or just `list` in Python 3.9 and above): -=== "Python 3.6 and above" - - ```Python hl_lines="1 20" - {!> ../../../docs_src/extra_models/tutorial004.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18" {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + ## Response with arbitrary `dict` You can also declare a response using a plain arbitrary `dict`, declaring just the type of the keys and values, without using a Pydantic model. @@ -233,18 +233,18 @@ This is useful if you don't know the valid field/attribute names (that would be In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above): -=== "Python 3.6 and above" - - ```Python hl_lines="1 8" - {!> ../../../docs_src/extra_models/tutorial005.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="6" {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + ## Recap Use multiple Pydantic models and inherit freely for each case. diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 475359134..47ebbefcf 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -6,34 +6,25 @@ You can define Header parameters the same way you define `Query`, `Path` and `Co First import `Header`: -=== "Python 3.6 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -42,40 +33,40 @@ First import `Header`: {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + ## Declare `Header` parameters Then declare the header parameters using the same structure as with `Path`, `Query` and `Cookie`. The first value is the default value, you can pass all the extra validation or annotation parameters: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -84,6 +75,15 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + !!! note "Technical Details" `Header` is a "sister" class of `Path`, `Query` and `Cookie`. It also inherits from the same common `Param` class. @@ -108,34 +108,25 @@ So, you can use `user_agent` as you normally would in Python code, instead of ne If for some reason you need to disable automatic conversion of underscores to hyphens, set the parameter `convert_underscores` of `Header` to `False`: -=== "Python 3.6 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/header_params/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial002.py!} + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -144,6 +135,15 @@ If for some reason you need to disable automatic conversion of underscores to hy {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + !!! warning Before setting `convert_underscores` to `False`, bear in mind that some HTTP proxies and servers disallow the usage of headers with underscores. @@ -157,34 +157,34 @@ You will receive all the values from the duplicate header as a Python `list`. For example, to declare a header of `X-Token` that can appear more than once, you can write: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -193,13 +193,13 @@ For example, to declare a header of `X-Token` that can appear more than once, yo {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="7" - {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} ``` If you communicate with that *path operation* sending two HTTP headers like: diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 884a762e2..7d4d4bcca 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -13,22 +13,22 @@ You can pass directly the `int` code, like `404`. But if you don't remember what each number code is for, you can use the shortcut constants in `status`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3 17" - {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3 17" {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="1 15" - {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} ``` That status code will be used in the response and will be added to the OpenAPI schema. @@ -42,22 +42,22 @@ That status code will be used in the response and will be added to the OpenAPI s You can add tags to your *path operation*, pass the parameter `tags` with a `list` of `str` (commonly just one `str`): -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="17 22 27" - {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="17 22 27" {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="15 20 25" - {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} ``` They will be added to the OpenAPI schema and used by the automatic documentation interfaces: @@ -80,22 +80,22 @@ In these cases, it could make sense to store the tags in an `Enum`. You can add a `summary` and `description`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="20-21" - {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="20-21" {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="18-19" - {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} ``` ## Description from docstring @@ -104,22 +104,22 @@ As descriptions tend to be long and cover multiple lines, you can declare the *p You can write Markdown in the docstring, it will be interpreted and displayed correctly (taking into account docstring indentation). -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="19-27" - {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="19-27" {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="17-25" - {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} ``` It will be used in the interactive docs: @@ -130,22 +130,22 @@ It will be used in the interactive docs: You can specify the response description with the parameter `response_description`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="21" - {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21" {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="19" - {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} ``` !!! info diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index c6f158307..c2c12f6e9 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -6,34 +6,25 @@ In the same way that you can declare more validations and metadata for query par First, import `Path` from `fastapi`, and import `Annotated`: -=== "Python 3.6 and above" - - ```Python hl_lines="3-4" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="1 3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1 3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -42,40 +33,40 @@ First, import `Path` from `fastapi`, and import `Annotated`: {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + ## Declare metadata You can declare all the same parameters as for `Query`. For example, to declare a `title` metadata value for the path parameter `item_id` you can type: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -84,6 +75,15 @@ For example, to declare a `title` metadata value for the path parameter `item_id {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + !!! note A path parameter is always required as it has to be part of the path. @@ -110,7 +110,7 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -=== "Python 3.6 - non-Annotated" +=== "Python 3.6 non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -121,18 +121,18 @@ So, you can declare your function as: But have in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` + ## Order the parameters as you need, tricks !!! tip @@ -161,37 +161,37 @@ Python won't do anything with that `*`, but it will know that all the following Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and yo probably won't need to use `*`. -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + ## Number validations: greater than or equal With `Query` and `Path` (and others you'll see later) you can declare number constraints. Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than or `e`qual" to `1`. -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -207,19 +207,19 @@ The same applies for: * `gt`: `g`reater `t`han * `le`: `l`ess than or `e`qual -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -238,19 +238,19 @@ So, `0.5` would be a valid value. But `0.0` or `0` would not. And the same for lt. -=== "Python 3.6 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="13" {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index a12b0b41a..3584ca0b3 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -4,18 +4,18 @@ Let's take this application as example: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + The query parameter `q` is of type `Union[str, None]` (or `str | None` in Python 3.10), that means that it's of type `str` but could also be `None`, and indeed, the default value is `None`, so FastAPI will know it's not required. !!! note @@ -34,7 +34,15 @@ To achieve that, first import: * `Query` from `fastapi` * `Annotated` from `typing` (or from `typing_extensions` in Python below 3.9) -=== "Python 3.6 and above" +=== "Python 3.10+" + + In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6+" In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`. @@ -44,14 +52,6 @@ To achieve that, first import: {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` -=== "Python 3.10 and above" - - In Python 3.9 or above, `Annotated` is part of the standard library, so you can import it from `typing`. - - ```Python hl_lines="1 3" - {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} - ``` - ## Use `Annotated` in the type for the `q` parameter Remember I told you before that `Annotated` can be used to add metadata to your parameters in the [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? @@ -60,32 +60,32 @@ Now it's the time to use it with FastAPI. 🚀 We had this type annotation: -=== "Python 3.6 and above" - - ```Python - q: Union[str, None] = None - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python q: str | None = None ``` -What we will do is wrap that with `Annotated`, so it becomes: - -=== "Python 3.6 and above" +=== "Python 3.6+" ```Python - q: Annotated[Union[str, None]] = None + q: Union[str, None] = None ``` -=== "Python 3.10 and above" +What we will do is wrap that with `Annotated`, so it becomes: + +=== "Python 3.10+" ```Python q: Annotated[str | None] = None ``` +=== "Python 3.6+" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + Both of those versions mean the same thing, `q` is a parameter that can be a `str` or `None`, and by default, it is `None`. Now let's jump to the fun stuff. 🎉 @@ -94,18 +94,18 @@ Now let's jump to the fun stuff. 🎉 Now that we have this `Annotated` where we can put more metadata, add `Query` to it, and set the parameter `max_length` to 50: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + Notice that the default value is still `None`, so the parameter is still optional. But now, having `Query(max_length=50)` inside of `Annotated`, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have **additional validation** for this value (that's why we do this, to get the additional validation). 😎 @@ -125,18 +125,18 @@ Previous versions of FastAPI (before 0.95.0) This is how you would use `Query()` as the default value of your function parameter, setting the parameter `max_length` to 50: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + As in this case (without using `Annotated`) we have to replace the default value `None` in the function with `Query()`, we now need to set the default value with the parameter `Query(default=None)`, it serves the same purpose of defining that default value (at least for FastAPI). So: @@ -232,34 +232,25 @@ Because `Annotated` can have more than one metadata annotation, you could now ev You can also add a parameter `min_length`: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -268,38 +259,38 @@ You can also add a parameter `min_length`: {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + ## Add regular expressions You can define a regular expression that the parameter should match: -=== "Python 3.6 and above" - - ```Python hl_lines="12" - {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -308,6 +299,15 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + This specific regular expression checks that the received parameter value: * `^`: starts with the following characters, doesn't have characters before. @@ -324,19 +324,19 @@ You can, of course, use default values other than `None`. Let's say that you want to declare the `q` query parameter to have a `min_length` of `3`, and to have a default value of `"fixedquery"`: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} ``` -=== "non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -378,19 +378,19 @@ But we are now declaring it with `Query`, for example like: So, when you need to declare a value as required while using `Query`, you can simply not declare a default value: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -408,19 +408,19 @@ So, when you need to declare a value as required while using `Query`, you can si There's an alternative way to explicitly declare that a value is required. You can set the default to the literal value `...`: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -442,34 +442,25 @@ You can declare that a parameter can accept `None`, but that it's still required To do that, you can declare that `None` is a valid type but still use `...` as the default: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -478,6 +469,15 @@ To do that, you can declare that `None` is a valid type but still use `...` as t {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + !!! tip Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about Required Optional fields. @@ -485,19 +485,19 @@ To do that, you can declare that `None` is a valid type but still use `...` as t If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: -=== "Python 3.6 and above" - - ```Python hl_lines="2 9" - {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="4 10" {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -515,34 +515,34 @@ When you define a query parameter explicitly with `Query` you can also declare i For example, to declare a query parameter `q` that can appear multiple times in the URL, you can write: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -551,13 +551,13 @@ For example, to declare a query parameter `q` that can appear multiple times in {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. - ```Python hl_lines="7" - {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} ``` Then, with a URL like: @@ -590,28 +590,19 @@ The interactive API docs will update accordingly, to allow multiple values: And you can also define a default `list` of values if none are provided: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -620,6 +611,15 @@ And you can also define a default `list` of values if none are provided: {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + If you go to: ``` @@ -641,19 +641,19 @@ the default of `q` will be: `["foo", "bar"]` and your response will be: You can also use `list` directly instead of `List[str]` (or `list[str]` in Python 3.9+): -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -680,34 +680,25 @@ That information will be included in the generated OpenAPI and used by the docum You can add a `title`: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -716,37 +707,36 @@ You can add a `title`: {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` And a `description`: -=== "Python 3.6 and above" - - ```Python hl_lines="15" - {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="14" - {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="14" {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="13" - {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -755,6 +745,15 @@ And a `description`: {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + ## Alias parameters Imagine that you want the parameter to be `item-query`. @@ -773,34 +772,25 @@ But you still need it to be exactly `item-query`... Then you can declare an `alias`, and that alias is what will be used to find the parameter value: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9" - {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -809,6 +799,15 @@ Then you can declare an `alias`, and that alias is what will be used to find the {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + ## Deprecating parameters Now let's say you don't like this parameter anymore. @@ -817,34 +816,25 @@ You have to leave it there a while because there are clients using it, but you w Then pass the parameter `deprecated=True` to `Query`: -=== "Python 3.6 and above" - - ```Python hl_lines="20" - {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="19" - {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19" {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="18" - {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -853,6 +843,15 @@ Then pass the parameter `deprecated=True` to `Query`: {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + The docs will show it like this: @@ -861,34 +860,25 @@ The docs will show it like this: To exclude a query parameter from the generated OpenAPI schema (and thus, from the automatic documentation systems), set the parameter `include_in_schema` of `Query` to `False`: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="10" - {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -897,6 +887,15 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + ## Recap You can declare additional validations and metadata for your parameters. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index eec55502a..0b74b10f8 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -63,18 +63,18 @@ The parameter values in your function will be: The same way, you can declare optional query parameters, by setting their default to `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + In this case, the function parameter `q` will be optional, and will be `None` by default. !!! check @@ -84,18 +84,18 @@ In this case, the function parameter `q` will be optional, and will be `None` by You can also declare `bool` types, and they will be converted: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + In this case, if you go to: ``` @@ -137,18 +137,18 @@ And you don't have to declare them in any specific order. They will be detected by name: -=== "Python 3.6 and above" - - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="6 8" {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + ## Required query parameters When you declare a default value for non-path parameters (for now, we have only seen query parameters), then it is not required. @@ -203,18 +203,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy And of course, you can define some parameters as required, some as having a default value, and some entirely optional: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/query_params/tutorial006.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + In this case, there are 3 query parameters: * `needy`, a required `str`. diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 666de76eb..bc7474359 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -13,19 +13,19 @@ You can define files to be uploaded by the client using `File`. Import `File` and `UploadFile` from `fastapi`: -=== "Python 3.6 and above" - - ```Python hl_lines="1" - {!> ../../../docs_src/request_files/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3" {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -38,19 +38,19 @@ Import `File` and `UploadFile` from `fastapi`: Create file parameters the same way you would for `Body` or `Form`: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/request_files/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -79,19 +79,19 @@ But there are several cases in which you might benefit from using `UploadFile`. Define a file parameter with a type of `UploadFile`: -=== "Python 3.6 and above" - - ```Python hl_lines="13" - {!> ../../../docs_src/request_files/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="14" {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -169,34 +169,25 @@ The way HTML forms (`
`) sends the data to the server normally uses You can make a file optional by using standard type annotations and setting a default value of `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="10 18" - {!> ../../../docs_src/request_files/tutorial001_02_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9 17" {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -205,24 +196,32 @@ You can make a file optional by using standard type annotations and setting a de {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` ## `UploadFile` with Additional Metadata You can also use `File()` with `UploadFile`, for example, to set additional metadata: -=== "Python 3.6 and above" - - ```Python hl_lines="8 14" - {!> ../../../docs_src/request_files/tutorial001_03_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9 15" {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -239,28 +238,19 @@ They would be associated to the same "form field" sent using "form data". To use that, declare a list of `bytes` or `UploadFile`: -=== "Python 3.6 and above" - - ```Python hl_lines="11 16" - {!> ../../../docs_src/request_files/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10 15" {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="10 15" - {!> ../../../docs_src/request_files/tutorial002.py!} + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -269,6 +259,15 @@ To use that, declare a list of `bytes` or `UploadFile`: {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + You will receive, as declared, a `list` of `bytes` or `UploadFile`s. !!! note "Technical Details" @@ -280,28 +279,19 @@ You will receive, as declared, a `list` of `bytes` or `UploadFile`s. And the same way as before, you can use `File()` to set additional parameters, even for `UploadFile`: -=== "Python 3.6 and above" - - ```Python hl_lines="12 19-21" - {!> ../../../docs_src/request_files/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="11 18-20" {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="11 18" - {!> ../../../docs_src/request_files/tutorial003.py!} + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} ``` -=== "Python 3.9 and above - non-Annotated" +=== "Python 3.9+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -310,6 +300,15 @@ And the same way as before, you can use `File()` to set additional parameters, e {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + ## Recap Use `File`, `bytes`, and `UploadFile` to declare files to be uploaded in the request, sent as form data. diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 9729ab160..9900068fc 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -9,19 +9,19 @@ You can define files and form fields at the same time using `File` and `Form`. ## Import `File` and `Form` -=== "Python 3.6 and above" - - ```Python hl_lines="1" - {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3" {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -34,19 +34,19 @@ You can define files and form fields at the same time using `File` and `Form`. Create file and form parameters the same way you would for `Body` or `Query`: -=== "Python 3.6 and above" - - ```Python hl_lines="9-11" - {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="10-12" {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 3f866410a..c3a0efe39 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -11,19 +11,19 @@ When you need to receive form fields instead of JSON, you can use `Form`. Import `Form` from `fastapi`: -=== "Python 3.6 and above" - - ```Python hl_lines="1" - {!> ../../../docs_src/request_forms/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3" {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -36,19 +36,19 @@ Import `Form` from `fastapi`: Create form parameters the same way you would for `Body` or `Query`: -=== "Python 3.6 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/request_forms/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index cd7a749d4..2181cfb5a 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -4,22 +4,22 @@ You can declare the type used for the response by annotating the *path operation You can use **type annotations** the same way you would for input data in function **parameters**, you can use Pydantic models, lists, dictionaries, scalar values like integers, booleans, etc. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="18 23" - {!> ../../../docs_src/response_model/tutorial001_01.py!} + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="18 23" {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="16 21" - {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} ``` FastAPI will use this return type to: @@ -53,22 +53,22 @@ You can use the `response_model` parameter in any of the *path operations*: * `@app.delete()` * etc. -=== "Python 3.6 and above" +=== "Python 3.10+" ```Python hl_lines="17 22 24-27" - {!> ../../../docs_src/response_model/tutorial001.py!} + {!> ../../../docs_src/response_model/tutorial001_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" ```Python hl_lines="17 22 24-27" - {!> ../../../docs_src/response_model/tutorial001_py310.py!} + {!> ../../../docs_src/response_model/tutorial001.py!} ``` !!! note @@ -95,18 +95,18 @@ You can also use `response_model=None` to disable creating a response model for Here we are declaring a `UserIn` model, it will contain a plaintext password: -=== "Python 3.6 and above" - - ```Python hl_lines="9 11" - {!> ../../../docs_src/response_model/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 9" {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + !!! info To use `EmailStr`, first install `email_validator`. @@ -115,18 +115,18 @@ Here we are declaring a `UserIn` model, it will contain a plaintext password: And we are using this model to declare our input and the same model to declare our output: -=== "Python 3.6 and above" - - ```Python hl_lines="18" - {!> ../../../docs_src/response_model/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="16" {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + Now, whenever a browser is creating a user with a password, the API will return the same password in the response. In this case, it might not be a problem, because it's the same user sending the password. @@ -140,46 +140,46 @@ But if we use the same model for another *path operation*, we could be sending o We can instead create an input model with the plaintext password and an output model without it: -=== "Python 3.6 and above" - - ```Python hl_lines="9 11 16" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + Here, even though our *path operation function* is returning the same input user that contains the password: -=== "Python 3.6 and above" - - ```Python hl_lines="24" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + ...we declared the `response_model` to be our model `UserOut`, that doesn't include the password: -=== "Python 3.6 and above" - - ```Python hl_lines="22" - {!> ../../../docs_src/response_model/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + So, **FastAPI** will take care of filtering out all the data that is not declared in the output model (using Pydantic). ### `response_model` or Return Type @@ -202,18 +202,18 @@ But in most of the cases where we need to do something like this, we want the mo And in those cases, we can use classes and inheritance to take advantage of function **type annotations** to get better support in the editor and tools, and still get the FastAPI **data filtering**. -=== "Python 3.6 and above" - - ```Python hl_lines="9-13 15-16 20" - {!> ../../../docs_src/response_model/tutorial003_01.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7-10 13-14 18" {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + With this, we get tooling support, from editors and mypy as this code is correct in terms of types, but we also get the data filtering from FastAPI. How does this work? Let's check that out. 🤓 @@ -278,18 +278,18 @@ But when you return some other arbitrary object that is not a valid Pydantic typ The same would happen if you had something like a union between different types where one or more of them are not valid Pydantic types, for example this would fail 💥: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/response_model/tutorial003_04.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + ...this fails because the type annotation is not a Pydantic type and is not just a single `Response` class or subclass, it's a union (any of the two) between a `Response` and a `dict`. ### Disable Response Model @@ -300,40 +300,40 @@ But you might want to still keep the return type annotation in the function to g In this case, you can disable the response model generation by setting `response_model=None`: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/response_model/tutorial003_05.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + This will make FastAPI skip the response model generation and that way you can have any return type annotations you need without it affecting your FastAPI application. 🤓 ## Response Model encoding parameters Your response model could have default values, like: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="11 13-14" - {!> ../../../docs_src/response_model/tutorial004.py!} + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="11 13-14" {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="9 11-12" - {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} ``` * `description: Union[str, None] = None` (or `str | None = None` in Python 3.10) has a default of `None`. @@ -348,22 +348,22 @@ For example, if you have models with many optional attributes in a NoSQL databas You can set the *path operation decorator* parameter `response_model_exclude_unset=True`: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="24" - {!> ../../../docs_src/response_model/tutorial004.py!} + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="22" - {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} ``` and those default values won't be included in the response, only the values actually set. @@ -441,18 +441,18 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan This also applies to `response_model_by_alias` that works similarly. -=== "Python 3.6 and above" - - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial005.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="29 35" {!> ../../../docs_src/response_model/tutorial005_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + !!! tip The syntax `{"name", "description"}` creates a `set` with those two values. @@ -462,18 +462,18 @@ This can be used as a quick shortcut if you have only one Pydantic model and wan If you forget to use a `set` and use a `list` or `tuple` instead, FastAPI will still convert it to a `set` and it will work correctly: -=== "Python 3.6 and above" - - ```Python hl_lines="31 37" - {!> ../../../docs_src/response_model/tutorial006.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="29 35" {!> ../../../docs_src/response_model/tutorial006_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + ## Recap Use the *path operation decorator's* parameter `response_model` to define response models and especially to ensure private data is filtered out. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index d2aa66843..705ab5671 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -8,18 +8,18 @@ Here are several ways to do it. You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: -=== "Python 3.6 and above" - - ```Python hl_lines="15-23" - {!> ../../../docs_src/schema_extra_example/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="13-21" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. !!! tip @@ -33,18 +33,18 @@ When using `Field()` with Pydantic models, you can also declare extra info for t You can use this to add `example` for each field: -=== "Python 3.6 and above" - - ```Python hl_lines="4 10-13" - {!> ../../../docs_src/schema_extra_example/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="2 8-11" {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + !!! warning Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. @@ -66,25 +66,31 @@ you can also declare a data `example` or a group of `examples` with additional i Here we pass an `example` of the data expected in `Body()`: -=== "Python 3.6 and above" - - ```Python hl_lines="23-28" - {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="22-27" - {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="22-27" {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -93,12 +99,6 @@ Here we pass an `example` of the data expected in `Body()`: {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` -=== "Python 3.10 and above - non-Annotated" - - ```Python hl_lines="18-23" - {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} - ``` - ### Example in the docs UI With any of the methods above it would look like this in the `/docs`: @@ -118,25 +118,31 @@ Each specific example `dict` in the `examples` can contain: * `value`: This is the actual example shown, e.g. a `dict`. * `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. -=== "Python 3.6 and above" - - ```Python hl_lines="24-50" - {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="23-49" - {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="23-49" {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -145,12 +151,6 @@ Each specific example `dict` in the `examples` can contain: {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` -=== "Python 3.10 and above - non-Annotated" - - ```Python hl_lines="19-45" - {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} - ``` - ### Examples in the docs UI With `examples` added to `Body()` the `/docs` would look like: diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index 9198db6a6..a82809b96 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -20,19 +20,19 @@ Let's first just use the code and see how it works, and then we'll come back to Copy the example in a file `main.py`: -=== "Python 3.6 and above" - - ```Python - {!> ../../../docs_src/security/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/security/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -134,19 +134,19 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin When we create an instance of the `OAuth2PasswordBearer` class we pass in the `tokenUrl` parameter. This parameter contains the URL that the client (the frontend running in the user's browser) will use to send the `username` and `password` in order to get a token. -=== "Python 3.6 and above" - - ```Python hl_lines="7" - {!> ../../../docs_src/security/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="8" {!> ../../../docs_src/security/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -185,19 +185,19 @@ So, it can be used with `Depends`. Now you can pass that `oauth2_scheme` in a dependency with `Depends`. -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/security/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="12" {!> ../../../docs_src/security/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 0076e7d16..3d6358340 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -2,19 +2,19 @@ In the previous chapter the security system (which is based on the dependency injection system) was giving the *path operation function* a `token` as a `str`: -=== "Python 3.6 and above" - - ```Python hl_lines="11" - {!> ../../../docs_src/security/tutorial001_an.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="12" {!> ../../../docs_src/security/tutorial001_an_py39.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -33,34 +33,25 @@ First, let's create a Pydantic user model. The same way we use Pydantic to declare bodies, we can use it anywhere else: -=== "Python 3.6 and above" - - ```Python hl_lines="5 13-17" - {!> ../../../docs_src/security/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="5 12-16" - {!> ../../../docs_src/security/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="5 12-16" {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="5 12-16" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="5 13-17" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -69,6 +60,15 @@ The same way we use Pydantic to declare bodies, we can use it anywhere else: {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Create a `get_current_user` dependency Let's create a dependency `get_current_user`. @@ -79,34 +79,25 @@ Remember that dependencies can have sub-dependencies? The same as we were doing before in the *path operation* directly, our new dependency `get_current_user` will receive a `token` as a `str` from the sub-dependency `oauth2_scheme`: -=== "Python 3.6 and above" - - ```Python hl_lines="26" - {!> ../../../docs_src/security/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="25" - {!> ../../../docs_src/security/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="25" {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="25" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="26" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -115,38 +106,38 @@ The same as we were doing before in the *path operation* directly, our new depen {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Get the user `get_current_user` will use a (fake) utility function we created, that takes a token as a `str` and returns our Pydantic `User` model: -=== "Python 3.6 and above" - - ```Python hl_lines="20-23 27-28" - {!> ../../../docs_src/security/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="19-22 26-27" - {!> ../../../docs_src/security/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="19-22 26-27" {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="19-22 26-27" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="20-23 27-28" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -155,38 +146,38 @@ The same as we were doing before in the *path operation* directly, our new depen {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Inject the current user So now we can use the same `Depends` with our `get_current_user` in the *path operation*: -=== "Python 3.6 and above" - - ```Python hl_lines="32" - {!> ../../../docs_src/security/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="31" - {!> ../../../docs_src/security/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="31" {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="31" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="32" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -195,6 +186,15 @@ So now we can use the same `Depends` with our `get_current_user` in the *path op {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + Notice that we declare the type of `current_user` as the Pydantic model `User`. This will help us inside of the function with all the completion and type checks. @@ -241,34 +241,25 @@ And all of them (or any portion of them that you want) can take the advantage of And all these thousands of *path operations* can be as small as 3 lines: -=== "Python 3.6 and above" - - ```Python hl_lines="31-33" - {!> ../../../docs_src/security/tutorial002_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="30-32" - {!> ../../../docs_src/security/tutorial002_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="30-32" {!> ../../../docs_src/security/tutorial002_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="30-32" - {!> ../../../docs_src/security/tutorial002.py!} + {!> ../../../docs_src/security/tutorial002_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="31-33" + {!> ../../../docs_src/security/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -277,6 +268,15 @@ And all these thousands of *path operations* can be as small as 3 lines: {!> ../../../docs_src/security/tutorial002_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + ## Recap You can now get the current user directly in your *path operation function*. diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 0b6390975..e6c0f1738 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -109,34 +109,25 @@ And another utility to verify if a received password matches the hash stored. And another one to authenticate and return a user. -=== "Python 3.6 and above" - - ```Python hl_lines="7 49 56-57 60-61 70-76" - {!> ../../../docs_src/security/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="7 48 55-56 59-60 69-75" - {!> ../../../docs_src/security/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7 48 55-56 59-60 69-75" {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="7 48 55-56 59-60 69-75" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="7 49 56-57 60-61 70-76" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -145,6 +136,15 @@ And another one to authenticate and return a user. {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + !!! note If you check the new (fake) database `fake_users_db`, you will see how the hashed password looks like now: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. @@ -176,34 +176,25 @@ Define a Pydantic Model that will be used in the token endpoint for the response Create a utility function to generate a new access token. -=== "Python 3.6 and above" - - ```Python hl_lines="6 13-15 29-31 79-87" - {!> ../../../docs_src/security/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="6 12-14 28-30 78-86" - {!> ../../../docs_src/security/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="6 12-14 28-30 78-86" {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="6 12-14 28-30 78-86" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="6 13-15 29-31 79-87" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -212,6 +203,15 @@ Create a utility function to generate a new access token. {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ## Update the dependencies Update `get_current_user` to receive the same token as before, but this time, using JWT tokens. @@ -220,34 +220,25 @@ Decode the received token, verify it, and return the current user. If the token is invalid, return an HTTP error right away. -=== "Python 3.6 and above" - - ```Python hl_lines="90-107" - {!> ../../../docs_src/security/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="89-106" - {!> ../../../docs_src/security/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="89-106" {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python hl_lines="89-106" - {!> ../../../docs_src/security/tutorial004.py!} + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="90-107" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -256,40 +247,40 @@ If the token is invalid, return an HTTP error right away. {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ## Update the `/token` *path operation* Create a `timedelta` with the expiration time of the token. -Create a real JWT access token and return it. +Create a real JWT access token and return it -=== "Python 3.6 and above" - - ```Python hl_lines="118-133" - {!> ../../../docs_src/security/tutorial004_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="117-132" - {!> ../../../docs_src/security/tutorial004_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="117-132" {!> ../../../docs_src/security/tutorial004_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="115-128" - {!> ../../../docs_src/security/tutorial004.py!} + ```Python hl_lines="117-132" + {!> ../../../docs_src/security/tutorial004_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="118-133" + {!> ../../../docs_src/security/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -298,6 +289,15 @@ Create a real JWT access token and return it. {!> ../../../docs_src/security/tutorial004_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="115-128" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + ### Technical details about the JWT "subject" `sub` The JWT specification says that there's a key `sub`, with the subject of the token. diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 6abf43218..9534185c7 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -49,34 +49,25 @@ Now let's use the utilities provided by **FastAPI** to handle this. First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` in the *path operation* for `/token`: -=== "Python 3.6 and above" - - ```Python hl_lines="4 79" - {!> ../../../docs_src/security/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="4 78" - {!> ../../../docs_src/security/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 78" {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="4 76" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="4 79" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -85,6 +76,15 @@ First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depe {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + `OAuth2PasswordRequestForm` is a class dependency that declares a form body with: * The `username`. @@ -122,34 +122,25 @@ If there is no such user, we return an error saying "incorrect username or passw For the error, we use the exception `HTTPException`: -=== "Python 3.6 and above" - - ```Python hl_lines="3 80-82" - {!> ../../../docs_src/security/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="3 79-81" - {!> ../../../docs_src/security/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="3 79-81" {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="3 77-79" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="3 80-82" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -158,6 +149,15 @@ For the error, we use the exception `HTTPException`: {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + ### Check the password At this point we have the user data from our database, but we haven't checked the password. @@ -182,34 +182,25 @@ If your database is stolen, the thief won't have your users' plaintext passwords So, the thief won't be able to try to use those same passwords in another system (as many users use the same password everywhere, this would be dangerous). -=== "Python 3.6 and above" - - ```Python hl_lines="83-86" - {!> ../../../docs_src/security/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="82-85" - {!> ../../../docs_src/security/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="82-85" {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="80-83" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -218,6 +209,15 @@ So, the thief won't be able to try to use those same passwords in another system {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + #### About `**user_dict` `UserInDB(**user_dict)` means: @@ -252,34 +252,25 @@ For this simple example, we are going to just be completely insecure and return But for now, let's focus on the specific details we need. -=== "Python 3.6 and above" - - ```Python hl_lines="88" - {!> ../../../docs_src/security/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="87" - {!> ../../../docs_src/security/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="87" {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="85" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="88" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -288,6 +279,15 @@ For this simple example, we are going to just be completely insecure and return {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + !!! tip By the spec, you should return a JSON with an `access_token` and a `token_type`, the same as in this example. @@ -309,34 +309,25 @@ Both of these dependencies will just return an HTTP error if the user doesn't ex So, in our endpoint, we will only get a user if the user exists, was correctly authenticated, and is active: -=== "Python 3.6 and above" - - ```Python hl_lines="59-67 70-75 95" - {!> ../../../docs_src/security/tutorial003_an.py!} - ``` - -=== "Python 3.9 and above" - - ```Python hl_lines="58-66 69-74 94" - {!> ../../../docs_src/security/tutorial003_an_py39.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="58-66 69-74 94" {!> ../../../docs_src/security/tutorial003_an_py310.py!} ``` -=== "Python 3.6 and above - non-Annotated" +=== "Python 3.9+" - !!! tip - Try to use the main, `Annotated` version better. - - ```Python hl_lines="58-66 69-72 90" - {!> ../../../docs_src/security/tutorial003.py!} + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -345,6 +336,15 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a {!> ../../../docs_src/security/tutorial003_py310.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + !!! info The additional header `WWW-Authenticate` with value `Bearer` we are returning here is also part of the spec. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 5ccaf05ec..fd66c5add 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -262,22 +262,22 @@ So, the user will also have a `password` when creating it. But for security, the `password` won't be in other Pydantic *models*, for example, it won't be sent from the API when reading a user. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="3 6-8 11-12 23-24 27-28" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="1 4-6 9-10 21-22 25-26" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` #### SQLAlchemy style and Pydantic style @@ -306,22 +306,22 @@ The same way, when reading a user, we can now declare that `items` will contain Not only the IDs of those items, but all the data that we defined in the Pydantic *model* for reading items: `Item`. -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="15-17 31-34" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="13-15 29-32" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -335,22 +335,22 @@ This ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="13 17-18 29 34-35" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -481,18 +481,18 @@ And now in the file `sql_app/main.py` let's integrate and use all the other part In a very simplistic way create the database tables: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + #### Alembic Note Normally you would probably initialize your database (create tables, etc) with Alembic. @@ -515,18 +515,18 @@ For that, we will create a new dependency with `yield`, as explained before in t Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in a single request, and then close it once the request is finished. -=== "Python 3.6 and above" - - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="13-18" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. @@ -540,18 +540,18 @@ And then, when using the dependency in a *path operation function*, we declare i This will then give us better editor support inside the *path operation function*, because the editor will know that the `db` parameter is of type `Session`: -=== "Python 3.6 and above" - - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="22 30 36 45 51" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info "Technical Details" The parameter `db` is actually of type `SessionLocal`, but this class (created with `sessionmaker()`) is a "proxy" of a SQLAlchemy `Session`, so, the editor doesn't really know what methods are provided. @@ -561,18 +561,18 @@ This will then give us better editor support inside the *path operation function Now, finally, here's the standard **FastAPI** *path operations* code. -=== "Python 3.6 and above" - - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + We are creating the database session before each request in the dependency with `yield`, and then closing it afterwards. And then we can create the required dependency in the *path operation function*, to get that session directly. @@ -654,22 +654,22 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -680,18 +680,18 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + ## Check it You can copy this code and use it as is. @@ -739,18 +739,18 @@ A "middleware" is basically a function that is always executed for each request, The middleware we'll add (just a function) will create a new SQLAlchemy `SessionLocal` for each request, add it to the request and then close it once the request is finished. -=== "Python 3.6 and above" - - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} - ``` - -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="12-20" {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + !!! info We put the creation of the `SessionLocal()` and handling of the requests in a `try` block. diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index a932ad3f8..9f94183f4 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -110,34 +110,25 @@ It has a `POST` operation that could return several errors. Both *path operations* require an `X-Token` header. -=== "Python 3.6 and above" - - ```Python - {!> ../../../docs_src/app_testing/app_b_an/main.py!} - ``` - -=== "Python 3.9 and above" - - ```Python - {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} ``` -=== "Python 3.6 and above - non-Annotated" - - !!! tip - Try to use the main, `Annotated` version better. +=== "Python 3.9+" ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` -=== "Python 3.10 and above - non-Annotated" +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ non-Annotated" !!! tip Try to use the main, `Annotated` version better. @@ -146,6 +137,15 @@ Both *path operations* require an `X-Token` header. {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` +=== "Python 3.6+ non-Annotated" + + !!! tip + Try to use the main, `Annotated` version better. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + ### Extended testing file You could then update `test_main.py` with the extended tests: diff --git a/docs/ja/docs/tutorial/testing.md b/docs/ja/docs/tutorial/testing.md index 56f5cabac..037e9628f 100644 --- a/docs/ja/docs/tutorial/testing.md +++ b/docs/ja/docs/tutorial/testing.md @@ -74,18 +74,18 @@ これらの *path operation* には `X-Token` ヘッダーが必要です。 -=== "Python 3.6 and above" - - ```Python - {!> ../../../docs_src/app_testing/app_b/main.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + ### 拡張版テストファイル 次に、先程のものに拡張版のテストを加えた、`test_main_b.py` を作成します。 diff --git a/docs/pt/docs/tutorial/body-multiple-params.md b/docs/pt/docs/tutorial/body-multiple-params.md index ac67aa47f..22f5856a6 100644 --- a/docs/pt/docs/tutorial/body-multiple-params.md +++ b/docs/pt/docs/tutorial/body-multiple-params.md @@ -8,18 +8,18 @@ Primeiro, é claro, você pode misturar `Path`, `Query` e declarações de parâ E você também pode declarar parâmetros de corpo como opcionais, definindo o valor padrão com `None`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="19-21" - {!> ../../../docs_src/body_multiple_params/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + !!! nota Repare que, neste caso, o `item` que seria capturado a partir do corpo é opcional. Visto que ele possui `None` como valor padrão. @@ -38,18 +38,18 @@ No exemplo anterior, as *operações de rota* esperariam um JSON no corpo conten Mas você pode também declarar múltiplos parâmetros de corpo, por exemplo, `item` e `user`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="22" - {!> ../../../docs_src/body_multiple_params/tutorial002.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + Neste caso, o **FastAPI** perceberá que existe mais de um parâmetro de corpo na função (dois parâmetros que são modelos Pydantic). Então, ele usará o nome dos parâmetros como chaves (nome dos campos) no corpo, e espera um corpo como: @@ -87,13 +87,13 @@ Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumir Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: -=== "Python 3.6 e superiores" +=== "Python 3.6+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} ``` -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} @@ -137,18 +137,18 @@ q: str | None = None Por exemplo: -=== "Python 3.6 e superiores" - - ```Python hl_lines="27" - {!> ../../../docs_src/body_multiple_params/tutorial004.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="26" {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + !!! info "Informação" `Body` também possui todas as validações adicionais e metadados de parâmetros como em `Query`,`Path` e outras que você verá depois. @@ -166,18 +166,18 @@ item: Item = Body(embed=True) como em: -=== "Python 3.6 e superiores" - - ```Python hl_lines="17" - {!> ../../../docs_src/body_multiple_params/tutorial005.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + Neste caso o **FastAPI** esperará um corpo como: ```JSON hl_lines="2" diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md index bb04e9ca2..bb4483fdc 100644 --- a/docs/pt/docs/tutorial/encoder.md +++ b/docs/pt/docs/tutorial/encoder.md @@ -20,18 +20,18 @@ Você pode usar a função `jsonable_encoder` para resolver isso. A função recebe um objeto, como um modelo Pydantic e retorna uma versão compatível com JSON: -=== "Python 3.6 e acima" - - ```Python hl_lines="5 22" - {!> ../../../docs_src/encoder/tutorial001.py!} - ``` - -=== "Python 3.10 e acima" +=== "Python 3.10+" ```Python hl_lines="4 21" {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + Neste exemplo, ele converteria o modelo Pydantic em um `dict`, e o `datetime` em um `str`. O resultado de chamar a função é algo que pode ser codificado com o padrão do Python `json.dumps()`. diff --git a/docs/pt/docs/tutorial/header-params.md b/docs/pt/docs/tutorial/header-params.md index 94ee784cd..bc8843327 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -6,36 +6,36 @@ Você pode definir parâmetros de Cabeçalho da mesma maneira que define paramê Primeiro importe `Header`: -=== "Python 3.6 and above" - - ```Python hl_lines="3" - {!> ../../../docs_src/header_params/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + ## Declare parâmetros de `Header` Então declare os paramêtros de cabeçalho usando a mesma estrutura que em `Path`, `Query` e `Cookie`. O primeiro valor é o valor padrão, você pode passar todas as validações adicionais ou parâmetros de anotação: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + !!! note "Detalhes Técnicos" `Header` é uma classe "irmã" de `Path`, `Query` e `Cookie`. Ela também herda da mesma classe em comum `Param`. @@ -60,18 +60,18 @@ Portanto, você pode usar `user_agent` como faria normalmente no código Python, Se por algum motivo você precisar desabilitar a conversão automática de sublinhados para hífens, defina o parâmetro `convert_underscores` de `Header` para `False`: -=== "Python 3.6 and above" - - ```Python hl_lines="10" - {!> ../../../docs_src/header_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + !!! warning "Aviso" Antes de definir `convert_underscores` como `False`, lembre-se de que alguns proxies e servidores HTTP não permitem o uso de cabeçalhos com sublinhados. @@ -85,22 +85,22 @@ Você receberá todos os valores do cabeçalho duplicado como uma `list` Python. Por exemplo, para declarar um cabeçalho de `X-Token` que pode aparecer mais de uma vez, você pode escrever: -=== "Python 3.6 and above" +=== "Python 3.10+" - ```Python hl_lines="9" - {!> ../../../docs_src/header_params/tutorial003.py!} + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} ``` -=== "Python 3.9 and above" +=== "Python 3.9+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.10 and above" +=== "Python 3.6+" - ```Python hl_lines="7" - {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} ``` Se você se comunicar com essa *operação de caminho* enviando dois cabeçalhos HTTP como: diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index f478fd190..ec9b74b30 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -6,36 +6,36 @@ Do mesmo modo que você pode declarar mais validações e metadados para parâme Primeiro, importe `Path` de `fastapi`: -=== "Python 3.6 e superiores" - - ```Python hl_lines="3" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + ## Declare metadados Você pode declarar todos os parâmetros da mesma maneira que na `Query`. Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota `item_id` você pode digitar: -=== "Python 3.6 e superiores" - - ```Python hl_lines="10" - {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} - ``` - -=== "Python 3.10 e superiores" +=== "Python 3.10+" ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + !!! note "Nota" Um parâmetro de rota é sempre obrigatório, como se fizesse parte da rota. diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 189724396..3ada4fd21 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -63,18 +63,18 @@ Os valores dos parâmetros na sua função serão: Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo o valor padrão para `None`: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial002.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrão. !!! check "Verificar" @@ -85,18 +85,18 @@ Nesse caso, o parâmetro da função `q` será opcional, e `None` será o padrã Você também pode declarar tipos `bool`, e eles serão convertidos: -=== "Python 3.6 and above" - - ```Python hl_lines="9" - {!> ../../../docs_src/query_params/tutorial003.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + Nesse caso, se você for para: ``` @@ -137,18 +137,18 @@ E você não precisa declarar eles em nenhuma ordem específica. Eles serão detectados pelo nome: -=== "Python 3.6 and above" - - ```Python hl_lines="8 10" - {!> ../../../docs_src/query_params/tutorial004.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="6 8" {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + ## Parâmetros de consulta obrigatórios Quando você declara um valor padrão para parâmetros que não são de rota (até agora, nós vimos apenas parâmetros de consulta), então eles não são obrigatórios. @@ -203,17 +203,18 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy E claro, você pode definir alguns parâmetros como obrigatórios, alguns possuindo um valor padrão, e outros sendo totalmente opcionais: -=== "Python 3.6 and above" +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +=== "Python 3.6+" ```Python hl_lines="10" {!> ../../../docs_src/query_params/tutorial006.py!} ``` -=== "Python 3.10 and above" - - ```Python hl_lines="8" - {!> ../../../docs_src/query_params/tutorial006_py310.py!} - ``` Nesse caso, existem 3 parâmetros de consulta: * `needy`, um `str` obrigatório. diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index e608f6c8f..81efda786 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -57,18 +57,18 @@ **FastAPI** знает, что нужно сделать в каждом случае и как переиспользовать тот же объект `BackgroundTasks`, так чтобы все фоновые задачи собрались и запустились вместе в фоне: -=== "Python 3.6 и выше" - - ```Python hl_lines="13 15 22 25" - {!> ../../../docs_src/background_tasks/tutorial002.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + В этом примере сообщения будут записаны в `log.txt` *после* того, как ответ сервера был отправлен. Если бы в запросе была очередь `q`, она бы первой записалась в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md index e8507c171..674b8bde4 100644 --- a/docs/ru/docs/tutorial/body-fields.md +++ b/docs/ru/docs/tutorial/body-fields.md @@ -6,18 +6,18 @@ Сначала вы должны импортировать его: -=== "Python 3.6 и выше" - - ```Python hl_lines="4" - {!> ../../../docs_src/body_fields/tutorial001.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="2" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + !!! warning "Внимание" Обратите внимание, что функция `Field` импортируется непосредственно из `pydantic`, а не из `fastapi`, как все остальные функции (`Query`, `Path`, `Body` и т.д.). @@ -25,18 +25,18 @@ Вы можете использовать функцию `Field` с атрибутами модели: -=== "Python 3.6 и выше" - - ```Python hl_lines="11-14" - {!> ../../../docs_src/body_fields/tutorial001.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="9-12" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + Функция `Field` работает так же, как `Query`, `Path` и `Body`, у ее такие же параметры и т.д. !!! note "Технические детали" diff --git a/docs/ru/docs/tutorial/cookie-params.md b/docs/ru/docs/tutorial/cookie-params.md index 75e9d9064..a6f2caa26 100644 --- a/docs/ru/docs/tutorial/cookie-params.md +++ b/docs/ru/docs/tutorial/cookie-params.md @@ -6,36 +6,36 @@ Сначала импортируйте `Cookie`: -=== "Python 3.6 и выше" - - ```Python hl_lines="3" - {!> ../../../docs_src/cookie_params/tutorial001.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="1" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + ## Объявление параметров `Cookie` Затем объявляйте параметры cookie, используя ту же структуру, что и с `Path` и `Query`. Первое значение - это значение по умолчанию, вы можете передать все дополнительные параметры проверки или аннотации: -=== "Python 3.6 и выше" - - ```Python hl_lines="9" - {!> ../../../docs_src/cookie_params/tutorial001.py!} - ``` - -=== "Python 3.10 и выше" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + !!! note "Технические детали" `Cookie` - это класс, родственный `Path` и `Query`. Он также наследуется от общего класса `Param`. diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md index 5813272ee..f404820df 100644 --- a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -6,18 +6,18 @@ 在前面的例子中, 我们从依赖项 ("可依赖对象") 中返回了一个 `dict`: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} - ``` - -=== "Python 3.10 以及以上" +=== "Python 3.10+" ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + 但是后面我们在路径操作函数的参数 `commons` 中得到了一个 `dict`。 我们知道编辑器不能为 `dict` 提供很多支持(比如补全),因为编辑器不知道 `dict` 的键和值类型。 @@ -79,46 +79,46 @@ fluffy = Cat(name="Mr Fluffy") 所以,我们可以将上面的依赖项 "可依赖对象" `common_parameters` 更改为类 `CommonQueryParams`: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="11-15" - {!> ../../../docs_src/dependencies/tutorial002.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -注意用于创建类实例的 `__init__` 方法: +=== "Python 3.6+" -=== "Python 3.6 以及 以上" - - ```Python hl_lines="12" + ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 以及 以上" +注意用于创建类实例的 `__init__` 方法: + +=== "Python 3.10+" ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -...它与我们以前的 `common_parameters` 具有相同的参数: +=== "Python 3.6+" -=== "Python 3.6 以及 以上" - - ```Python hl_lines="9" - {!> ../../../docs_src/dependencies/tutorial001.py!} + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} ``` -=== "Python 3.10 以及 以上" +...它与我们以前的 `common_parameters` 具有相同的参数: + +=== "Python 3.10+" ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + 这些参数就是 **FastAPI** 用来 "处理" 依赖项的。 在两个例子下,都有: @@ -133,18 +133,18 @@ fluffy = Cat(name="Mr Fluffy") 现在,您可以使用这个类来声明你的依赖项了。 -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial002.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + **FastAPI** 调用 `CommonQueryParams` 类。这将创建该类的一个 "实例",该实例将作为参数 `commons` 被传递给你的函数。 ## 类型注解 vs `Depends` @@ -183,18 +183,18 @@ commons = Depends(CommonQueryParams) ..就像: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial003.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + 但是声明类型是被鼓励的,因为那样你的编辑器就会知道将传递什么作为参数 `commons` ,然后它可以帮助你完成代码,类型检查,等等: @@ -227,18 +227,18 @@ commons: CommonQueryParams = Depends() 同样的例子看起来像这样: -=== "Python 3.6 以及 以上" - - ```Python hl_lines="19" - {!> ../../../docs_src/dependencies/tutorial004.py!} - ``` - -=== "Python 3.10 以及 以上" +=== "Python 3.10+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + ... **FastAPI** 会知道怎么处理。 !!! tip diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md index cb813940c..76ed846ce 100644 --- a/docs/zh/docs/tutorial/encoder.md +++ b/docs/zh/docs/tutorial/encoder.md @@ -20,18 +20,18 @@ 它接收一个对象,比如Pydantic模型,并会返回一个JSON兼容的版本: -=== "Python 3.6 and above" - - ```Python hl_lines="5 22" - {!> ../../../docs_src/encoder/tutorial001.py!} - ``` - -=== "Python 3.10 and above" +=== "Python 3.10+" ```Python hl_lines="4 21" {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + 在这个例子中,它将Pydantic模型转换为`dict`,并将`datetime`转换为`str`。 调用它的结果后就可以使用Python标准编码中的`json.dumps()`。 diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index e18d6fc9f..03474907e 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -124,18 +124,18 @@ contents = myfile.file.read() 您可以通过使用标准类型注解并将 None 作为默认值的方式将一个文件参数设为可选: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="9 17" - {!> ../../../docs_src/request_files/tutorial001_02.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="7 14" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + ## 带有额外元数据的 `UploadFile` 您也可以将 `File()` 与 `UploadFile` 一起使用,例如,设置额外的元数据: @@ -152,18 +152,18 @@ FastAPI 支持同时上传多个文件。 上传多个文件时,要声明含 `bytes` 或 `UploadFile` 的列表(`List`): -=== "Python 3.6 及以上版本" - - ```Python hl_lines="10 15" - {!> ../../../docs_src/request_files/tutorial002.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + 接收的也是含 `bytes` 或 `UploadFile` 的列表(`list`)。 @@ -177,18 +177,18 @@ FastAPI 支持同时上传多个文件。 和之前的方式一样, 您可以为 `File()` 设置额外参数, 即使是 `UploadFile`: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="18" - {!> ../../../docs_src/request_files/tutorial003.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + ## 小结 本节介绍了如何用 `File` 把上传文件声明为(表单数据的)输入参数。 diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index 6b354c2b6..482588f94 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -246,22 +246,22 @@ connect_args={"check_same_thread": False} 但是为了安全起见,`password`不会出现在其他同类 Pydantic*模型*中,例如用户请求时不应该从 API 返回响应中包含它。 -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="3 6-8 11-12 23-24 27-28" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="1 4-6 9-10 21-22 25-26" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` #### SQLAlchemy 风格和 Pydantic 风格 @@ -290,22 +290,22 @@ name: str 不仅是这些项目的 ID,还有我们在 Pydantic*模型*中定义的用于读取项目的所有数据:`Item`. -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="15-17 31-34" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="13-15 29-32" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -319,22 +319,22 @@ name: str 在`Config`类中,设置属性`orm_mode = True`。 -=== "Python 3.6 及以上版本" +=== "Python 3.10+" - ```Python hl_lines="15 19-20 31 36-37" - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.6+" - ```Python hl_lines="13 17-18 29 34-35" - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` !!! tip @@ -465,18 +465,18 @@ current_user.items 以非常简单的方式创建数据库表: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="9" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="7" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + #### Alembic 注意 通常你可能会使用 Alembic,来进行格式化数据库(创建表等)。 @@ -499,18 +499,18 @@ current_user.items 我们的依赖项将创建一个新的 SQLAlchemy `SessionLocal`,它将在单个请求中使用,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="15-20" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="13-18" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 @@ -524,18 +524,18 @@ current_user.items *这将为我们在路径操作函数*中提供更好的编辑器支持,因为编辑器将知道`db`参数的类型`Session`: -=== "Python 3.6 及以上版本" - - ```Python hl_lines="24 32 38 47 53" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="22 30 36 45 51" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + !!! info "技术细节" 参数`db`实际上是 type `SessionLocal`,但是这个类(用 创建`sessionmaker()`)是 SQLAlchemy 的“代理” `Session`,所以,编辑器并不真正知道提供了哪些方法。 @@ -545,18 +545,18 @@ current_user.items 现在,到了最后,编写标准的**FastAPI** *路径操作*代码。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + 我们在依赖项中的每个请求之前利用`yield`创建数据库会话,然后关闭它。 所以我们就可以在*路径操作函数*中创建需要的依赖,就能直接获取会话。 @@ -638,22 +638,22 @@ def read_user(user_id: int, db: Session = Depends(get_db)): * `sql_app/schemas.py`: -=== "Python 3.6 及以上版本" +=== "Python 3.10+" ```Python - {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} ``` -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.10 及以上版本" +=== "Python 3.6+" ```Python - {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} ``` * `sql_app/crud.py`: @@ -664,18 +664,18 @@ def read_user(user_id: int, db: Session = Depends(get_db)): * `sql_app/main.py`: -=== "Python 3.6 及以上版本" - - ```Python - {!> ../../../docs_src/sql_databases/sql_app/main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + ## 执行项目 您可以复制这些代码并按原样使用它。 @@ -723,18 +723,18 @@ $ uvicorn sql_app.main:app --reload 我们将添加中间件(只是一个函数)将为每个请求创建一个新的 SQLAlchemy`SessionLocal`,将其添加到请求中,然后在请求完成后关闭它。 -=== "Python 3.6 及以上版本" - - ```Python hl_lines="14-22" - {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} - ``` - -=== "Python 3.9 及以上版本" +=== "Python 3.9+" ```Python hl_lines="12-20" {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` +=== "Python 3.6+" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + !!! info 我们将`SessionLocal()`请求的创建和处理放在一个`try`块中。 From 994ea1ad338cb33187246a58f0e7a618d04f6bc9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Mar 2023 16:16:37 +0000 Subject: [PATCH 0866/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6f164acf3..a5ac5fb68 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update order of examples, latest Python version first, and simplify version tab names. PR [#9269](https://github.com/tiangolo/fastapi/pull/9269) by [@tiangolo](https://github.com/tiangolo). * 📝 Update all docs to use `Annotated` as the main recommendation, with new examples and tests. PR [#9268](https://github.com/tiangolo/fastapi/pull/9268) by [@tiangolo](https://github.com/tiangolo). * ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). From fbfd53542e4197c5be4c2a732e004d60eec2c68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 19:46:47 +0100 Subject: [PATCH 0867/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 91 ++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a5ac5fb68..5813ac6b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,9 +2,98 @@ ## Latest Changes +### Highlights + +This release adds support for dependencies and parameters using `Annotated`. ✨ + +This has **several benefits**, one of the main ones is that now the parameters of your functions with `Annotated` would **not be affected** at all. + +If you call those functions in **other places in your code**, the actual **default values** will be kept, your editor will help you notice missing **required arguments**, Python will require you to pass required arguments at **runtime**, you will be able to **use the same functions** for different things and with different libraries (e.g. **Typer** will soon support `Annotated` too, then you could use the same function for an API and a CLI), etc. + +Because `Annotated` is **standard Python**, you still get all the **benefits** from editors and tools, like **autocompletion**, **inline errors**, etc. + +One of the **biggest benefits** is that now you can create `Annotated` dependencies that are then shared by multiple *path operation functions*, this will allow you to **reduce** a lot of **code duplication** in your codebase, while keeping all the support from editors and tools. + +For example, you could have code like this: + +```Python +def get_current_user(token: str): + # authenticate user + return User() + + +@app.get("/items/") +def read_items(user: User = Depends(get_current_user)): + ... + + +@app.post("/items/") +def create_item(*, user: User = Depends(get_current_user), item: Item): + ... + + +@app.get("/items/{item_id}") +def read_item(*, user: User = Depends(get_current_user), item_id: int): + ... + + +@app.delete("/items/{item_id}") +def delete_item(*, user: User = Depends(get_current_user), item_id: int): + ... +``` + +There's a bit of code duplication for the dependency: + +```Python +user: User = Depends(get_current_user) +``` + +...the bigger the codebase, the more noticeable it is. + +Now you can create an annotated dependency once, like this: + +```Python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +And then you can reuse this `Annotated` dependency: + +```Python +CurrentUser = Annotated[User, Depends(get_current_user)] + + +@app.get("/items/") +def read_items(user: CurrentUser): + ... + + +@app.post("/items/") +def create_item(user: CurrentUser, item: Item): + ... + + +@app.get("/items/{item_id}") +def read_item(user: CurrentUser, item_id: int): + ... + + +@app.delete("/items/{item_id}") +def delete_item(user: CurrentUser, item_id: int): + ... +``` + +...and `CurrentUser` has all the typing information as `User`, so your editor will work as expected (autocompletion and everything), and **FastAPI** will be able to understand the dependency defined in `Annotated`. 😎 + +Special thanks to [@nzig](https://github.com/nzig) for the core implementation and to [@adriangb](https://github.com/adriangb) for the inspiration and idea with [Xpresso](https://github.com/adriangb/xpresso)! 🚀 + +### Features + +* ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). + +### Docs + * 📝 Update order of examples, latest Python version first, and simplify version tab names. PR [#9269](https://github.com/tiangolo/fastapi/pull/9269) by [@tiangolo](https://github.com/tiangolo). * 📝 Update all docs to use `Annotated` as the main recommendation, with new examples and tests. PR [#9268](https://github.com/tiangolo/fastapi/pull/9268) by [@tiangolo](https://github.com/tiangolo). -* ✨Add support for PEP-593 `Annotated` for specifying dependencies and parameters. PR [#4871](https://github.com/tiangolo/fastapi/pull/4871) by [@nzig](https://github.com/nzig). ## 0.94.1 From 0bc87ec77cbc53a16fdc164b3b4f472bfacd0d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 20:07:53 +0100 Subject: [PATCH 0868/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20tip=20recommen?= =?UTF-8?q?ding=20`Annotated`=20in=20docs=20(#9270)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Tweak tip recommending Annotated --- .../docs/advanced/additional-status-codes.md | 4 +- .../en/docs/advanced/advanced-dependencies.md | 8 +-- .../docs/advanced/security/http-basic-auth.md | 6 +-- .../docs/advanced/security/oauth2-scopes.md | 48 ++++++++--------- docs/en/docs/advanced/settings.md | 6 +-- docs/en/docs/advanced/testing-dependencies.md | 4 +- docs/en/docs/advanced/websockets.md | 4 +- docs/en/docs/tutorial/background-tasks.md | 4 +- docs/en/docs/tutorial/bigger-applications.md | 2 +- docs/en/docs/tutorial/body-fields.md | 8 +-- docs/en/docs/tutorial/body-multiple-params.md | 16 +++--- docs/en/docs/tutorial/cookie-params.md | 8 +-- .../dependencies/classes-as-dependencies.md | 40 +++++++------- ...pendencies-in-path-operation-decorators.md | 8 +-- .../dependencies/dependencies-with-yield.md | 4 +- .../dependencies/global-dependencies.md | 2 +- docs/en/docs/tutorial/dependencies/index.md | 12 ++--- .../tutorial/dependencies/sub-dependencies.md | 14 ++--- docs/en/docs/tutorial/extra-data-types.md | 8 +-- docs/en/docs/tutorial/header-params.md | 18 +++---- .../path-params-numeric-validations.md | 16 +++--- .../tutorial/query-params-str-validations.md | 52 +++++++++---------- docs/en/docs/tutorial/request-files.md | 20 +++---- .../docs/tutorial/request-forms-and-files.md | 4 +- docs/en/docs/tutorial/request-forms.md | 4 +- docs/en/docs/tutorial/schema-extra-example.md | 4 +- docs/en/docs/tutorial/security/first-steps.md | 6 +-- .../tutorial/security/get-current-user.md | 22 ++++---- docs/en/docs/tutorial/security/oauth2-jwt.md | 16 +++--- .../docs/tutorial/security/simple-oauth2.md | 20 +++---- docs/en/docs/tutorial/testing.md | 4 +- 31 files changed, 196 insertions(+), 196 deletions(-) diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index b0d8d7bd5..416444d3b 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -35,7 +35,7 @@ To achieve that, import `JSONResponse`, and return your content there directly, === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 23" {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} @@ -44,7 +44,7 @@ To achieve that, import `JSONResponse`, and return your content there directly, === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4 25" {!> ../../../docs_src/additional_status_codes/tutorial001.py!} diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 9a25d2c64..402c5d755 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -33,7 +33,7 @@ To do that, we declare a method `__call__`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial011.py!} @@ -60,7 +60,7 @@ And now, we can use `__init__` to declare the parameters of the instance that we === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial011.py!} @@ -87,7 +87,7 @@ We could create an instance of this class with: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="16" {!> ../../../docs_src/dependencies/tutorial011.py!} @@ -122,7 +122,7 @@ checker(q="somequery") === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="20" {!> ../../../docs_src/dependencies/tutorial011.py!} diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index f7776e73d..8177a4b28 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -35,7 +35,7 @@ Then, when you type that username and password, the browser sends them in the he === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 6 10" {!> ../../../docs_src/security/tutorial006.py!} @@ -74,7 +74,7 @@ Then we can use `secrets.compare_digest()` to ensure that `credentials.username` === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 11-21" {!> ../../../docs_src/security/tutorial007.py!} @@ -157,7 +157,7 @@ After detecting that the credentials are incorrect, return an `HTTPException` wi === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="23-27" {!> ../../../docs_src/security/tutorial007.py!} diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index 57757ec6c..41cd61683 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -77,7 +77,7 @@ First, let's quickly see the parts that change from the examples in the main **T === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -86,7 +86,7 @@ First, let's quickly see the parts that change from the examples in the main **T === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -95,7 +95,7 @@ First, let's quickly see the parts that change from the examples in the main **T === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" {!> ../../../docs_src/security/tutorial005.py!} @@ -130,7 +130,7 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="61-64" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -140,7 +140,7 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="62-65" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -149,7 +149,7 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="62-65" {!> ../../../docs_src/security/tutorial005.py!} @@ -197,7 +197,7 @@ And we return the scopes as part of the JWT token. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="152" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -206,7 +206,7 @@ And we return the scopes as part of the JWT token. === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="153" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -215,7 +215,7 @@ And we return the scopes as part of the JWT token. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="153" {!> ../../../docs_src/security/tutorial005.py!} @@ -263,7 +263,7 @@ In this case, it requires the scope `me` (it could require more than one scope). === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3 138 165" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -272,7 +272,7 @@ In this case, it requires the scope `me` (it could require more than one scope). === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4 139 166" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -281,7 +281,7 @@ In this case, it requires the scope `me` (it could require more than one scope). === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4 139 166" {!> ../../../docs_src/security/tutorial005.py!} @@ -329,7 +329,7 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7 104" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -338,7 +338,7 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8 105" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -347,7 +347,7 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8 105" {!> ../../../docs_src/security/tutorial005.py!} @@ -386,7 +386,7 @@ In this exception, we include the scopes required (if any) as a string separated === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="104 106-114" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -395,7 +395,7 @@ In this exception, we include the scopes required (if any) as a string separated === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="105 107-115" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -404,7 +404,7 @@ In this exception, we include the scopes required (if any) as a string separated === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="105 107-115" {!> ../../../docs_src/security/tutorial005.py!} @@ -445,7 +445,7 @@ We also verify that we have a user with that username, and if not, we raise that === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="45 115-126" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -454,7 +454,7 @@ We also verify that we have a user with that username, and if not, we raise that === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="46 116-127" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -463,7 +463,7 @@ We also verify that we have a user with that username, and if not, we raise that === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="46 116-127" {!> ../../../docs_src/security/tutorial005.py!} @@ -496,7 +496,7 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="127-133" {!> ../../../docs_src/security/tutorial005_py310.py!} @@ -505,7 +505,7 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="128-134" {!> ../../../docs_src/security/tutorial005_py39.py!} @@ -514,7 +514,7 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="128-134" {!> ../../../docs_src/security/tutorial005.py!} diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index a29485a21..60ec9c92c 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -231,7 +231,7 @@ Now we create a dependency that returns a new `config.Settings()`. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="5 11-12" {!> ../../../docs_src/settings/app02/main.py!} @@ -259,7 +259,7 @@ And then we can require it from the *path operation function* as a dependency an === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="16 18-20" {!> ../../../docs_src/settings/app02/main.py!} @@ -353,7 +353,7 @@ But as we are using the `@lru_cache()` decorator on top, the `Settings` object w === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 10" {!> ../../../docs_src/settings/app03/main.py!} diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index fecc14d5f..ee48a735d 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -49,7 +49,7 @@ And then **FastAPI** will call that override instead of the original dependency. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="24-25 28" {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} @@ -58,7 +58,7 @@ And then **FastAPI** will call that override instead of the original dependency. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="28-29 32" {!> ../../../docs_src/dependency_testing/tutorial001.py!} diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 3cdd45736..94cf191d2 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -133,7 +133,7 @@ They work the same way as for other FastAPI endpoints/*path operations*: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="66-67 79" {!> ../../../docs_src/websockets/tutorial002_py310.py!} @@ -142,7 +142,7 @@ They work the same way as for other FastAPI endpoints/*path operations*: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="68-69 81" {!> ../../../docs_src/websockets/tutorial002.py!} diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 582a7e098..178297192 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -78,7 +78,7 @@ Using `BackgroundTasks` also works with the dependency injection system, you can === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11 13 20 23" {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} @@ -87,7 +87,7 @@ Using `BackgroundTasks` also works with the dependency injection system, you can === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="13 15 22 25" {!> ../../../docs_src/background_tasks/tutorial002.py!} diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index b8e3e5807..daa7353a2 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -127,7 +127,7 @@ We will now use a simple dependency to read a custom `X-Token` header: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 4-6" {!> ../../../docs_src/bigger_applications/app/dependencies.py!} diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 484d694ea..8966032ff 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -27,7 +27,7 @@ First, you have to import it: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} @@ -36,7 +36,7 @@ First, you have to import it: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4" {!> ../../../docs_src/body_fields/tutorial001.py!} @@ -70,7 +70,7 @@ You can then use `Field` with model attributes: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9-12" {!> ../../../docs_src/body_fields/tutorial001_py310.py!} @@ -79,7 +79,7 @@ You can then use `Field` with model attributes: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11-14" {!> ../../../docs_src/body_fields/tutorial001.py!} diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index 3358c6772..b214092c9 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -29,7 +29,7 @@ And you can also declare body parameters as optional, by setting the default to === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-19" {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} @@ -38,7 +38,7 @@ And you can also declare body parameters as optional, by setting the default to === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19-21" {!> ../../../docs_src/body_multiple_params/tutorial001.py!} @@ -132,7 +132,7 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="20" {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} @@ -141,7 +141,7 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} @@ -206,7 +206,7 @@ For example: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="25" {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} @@ -215,7 +215,7 @@ For example: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="27" {!> ../../../docs_src/body_multiple_params/tutorial004.py!} @@ -259,7 +259,7 @@ as in: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="15" {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} @@ -268,7 +268,7 @@ as in: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/body_multiple_params/tutorial005.py!} diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 169c546f0..111e93458 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -27,7 +27,7 @@ First import `Cookie`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} @@ -36,7 +36,7 @@ First import `Cookie`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001.py!} @@ -69,7 +69,7 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} @@ -78,7 +78,7 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/cookie_params/tutorial001.py!} diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 832e23997..498d935fe 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -27,7 +27,7 @@ In the previous example, we were returning a `dict` from our dependency ("depend === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -36,7 +36,7 @@ In the previous example, we were returning a `dict` from our dependency ("depend === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial001.py!} @@ -124,7 +124,7 @@ Then, we can change the dependency "dependable" `common_parameters` from above t === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9-13" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -133,7 +133,7 @@ Then, we can change the dependency "dependable" `common_parameters` from above t === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} @@ -162,7 +162,7 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -171,7 +171,7 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="12" {!> ../../../docs_src/dependencies/tutorial002.py!} @@ -200,7 +200,7 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -209,7 +209,7 @@ Pay attention to the `__init__` method used to create the instance of the class: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial001.py!} @@ -250,7 +250,7 @@ Now you can declare your dependency using this class. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial002_py310.py!} @@ -259,7 +259,7 @@ Now you can declare your dependency using this class. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial002.py!} @@ -274,7 +274,7 @@ Notice how we write `CommonQueryParams` twice in the above code: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons: CommonQueryParams = Depends(CommonQueryParams) @@ -309,7 +309,7 @@ In this case, the first `CommonQueryParams`, in: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons: CommonQueryParams ... @@ -328,7 +328,7 @@ You could actually write just: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons = Depends(CommonQueryParams) @@ -357,7 +357,7 @@ You could actually write just: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial003_py310.py!} @@ -366,7 +366,7 @@ You could actually write just: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial003.py!} @@ -383,7 +383,7 @@ But you see that we are having some code repetition here, writing `CommonQueryPa === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons: CommonQueryParams = Depends(CommonQueryParams) @@ -410,7 +410,7 @@ Instead of writing: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons: CommonQueryParams = Depends(CommonQueryParams) @@ -427,7 +427,7 @@ Instead of writing: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python commons: CommonQueryParams = Depends() @@ -458,7 +458,7 @@ The same example would then look like: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial004_py310.py!} @@ -467,7 +467,7 @@ The same example would then look like: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial004.py!} diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index aef9bf5e1..935555339 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -29,7 +29,7 @@ It should be a `list` of `Depends()`: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial006.py!} @@ -72,7 +72,7 @@ They can declare request requirements (like headers) or other sub-dependencies: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6 11" {!> ../../../docs_src/dependencies/tutorial006.py!} @@ -97,7 +97,7 @@ These dependencies can `raise` exceptions, the same as normal dependencies: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8 13" {!> ../../../docs_src/dependencies/tutorial006.py!} @@ -124,7 +124,7 @@ So, you can re-use a normal dependency (that returns a value) you already use so === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9 14" {!> ../../../docs_src/dependencies/tutorial006.py!} diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 721fee162..8a5422ac8 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -81,7 +81,7 @@ For example, `dependency_c` can have a dependency on `dependency_b`, and `depend === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4 12 20" {!> ../../../docs_src/dependencies/tutorial008.py!} @@ -108,7 +108,7 @@ And, in turn, `dependency_b` needs the value from `dependency_a` (here named `de === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="16-17 24-25" {!> ../../../docs_src/dependencies/tutorial008.py!} diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index f148388ee..0989b31d4 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -21,7 +21,7 @@ In that case, they will be applied to all the *path operations* in the applicati === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="15" {!> ../../../docs_src/dependencies/tutorial012.py!} diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 7ae32f8b8..80087a4a7 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -52,7 +52,7 @@ It is just a function that can take all the same parameters that a *path operati === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -61,7 +61,7 @@ It is just a function that can take all the same parameters that a *path operati === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8-11" {!> ../../../docs_src/dependencies/tutorial001.py!} @@ -108,7 +108,7 @@ And then it just returns a `dict` containing those values. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -117,7 +117,7 @@ And then it just returns a `dict` containing those values. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3" {!> ../../../docs_src/dependencies/tutorial001.py!} @@ -148,7 +148,7 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11 16" {!> ../../../docs_src/dependencies/tutorial001_py310.py!} @@ -157,7 +157,7 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="15 20" {!> ../../../docs_src/dependencies/tutorial001.py!} diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index 27af5970d..b50de1a46 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -31,7 +31,7 @@ You could create a first dependency ("dependable") like: === "Python 3.10 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6-7" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -40,7 +40,7 @@ You could create a first dependency ("dependable") like: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8-9" {!> ../../../docs_src/dependencies/tutorial005.py!} @@ -75,7 +75,7 @@ Then you can create another dependency function (a "dependable") that at the sam === "Python 3.10 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -84,7 +84,7 @@ Then you can create another dependency function (a "dependable") that at the sam === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="13" {!> ../../../docs_src/dependencies/tutorial005.py!} @@ -122,7 +122,7 @@ Then we can use the dependency with: === "Python 3.10 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19" {!> ../../../docs_src/dependencies/tutorial005_py310.py!} @@ -131,7 +131,7 @@ Then we can use the dependency with: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="22" {!> ../../../docs_src/dependencies/tutorial005.py!} @@ -171,7 +171,7 @@ In an advanced scenario where you know you need the dependency to be called at e === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 0d969b41d..7d6ffbc78 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -76,7 +76,7 @@ Here's an example *path operation* with parameters using some of the above types === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 2 11-15" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} @@ -85,7 +85,7 @@ Here's an example *path operation* with parameters using some of the above types === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 2 12-16" {!> ../../../docs_src/extra_data_types/tutorial001.py!} @@ -114,7 +114,7 @@ Note that the parameters inside the function have their natural data type, and y === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-18" {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} @@ -123,7 +123,7 @@ Note that the parameters inside the function have their natural data type, and y === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="18-19" {!> ../../../docs_src/extra_data_types/tutorial001.py!} diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 47ebbefcf..9e928cdc6 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -27,7 +27,7 @@ First import `Header`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/header_params/tutorial001_py310.py!} @@ -36,7 +36,7 @@ First import `Header`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001.py!} @@ -69,7 +69,7 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial001_py310.py!} @@ -78,7 +78,7 @@ The first value is the default value, you can pass all the extra validation or a === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial001.py!} @@ -129,7 +129,7 @@ If for some reason you need to disable automatic conversion of underscores to hy === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/header_params/tutorial002_py310.py!} @@ -138,7 +138,7 @@ If for some reason you need to disable automatic conversion of underscores to hy === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial002.py!} @@ -178,7 +178,7 @@ For example, to declare a header of `X-Token` that can appear more than once, yo === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/header_params/tutorial003_py310.py!} @@ -187,7 +187,7 @@ For example, to declare a header of `X-Token` that can appear more than once, yo === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003_py39.py!} @@ -196,7 +196,7 @@ For example, to declare a header of `X-Token` that can appear more than once, yo === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003.py!} diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index c2c12f6e9..70ba5ac50 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -27,7 +27,7 @@ First, import `Path` from `fastapi`, and import `Annotated`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} @@ -36,7 +36,7 @@ First, import `Path` from `fastapi`, and import `Annotated`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} @@ -69,7 +69,7 @@ For example, to declare a `title` metadata value for the path parameter `item_id === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} @@ -78,7 +78,7 @@ For example, to declare a `title` metadata value for the path parameter `item_id === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} @@ -113,7 +113,7 @@ So, you can declare your function as: === "Python 3.6 non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} @@ -194,7 +194,7 @@ Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than o === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} @@ -222,7 +222,7 @@ The same applies for: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} @@ -253,7 +253,7 @@ And the same for lt. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11" {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 3584ca0b3..6a5a507b9 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -253,7 +253,7 @@ You can also add a parameter `min_length`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} @@ -262,7 +262,7 @@ You can also add a parameter `min_length`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} @@ -293,7 +293,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} @@ -302,7 +302,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004.py!} @@ -339,7 +339,7 @@ Let's say that you want to declare the `q` query parameter to have a `min_length === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} @@ -393,7 +393,7 @@ So, when you need to declare a value as required while using `Query`, you can si === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} @@ -423,7 +423,7 @@ There's an alternative way to explicitly declare that a value is required. You c === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} @@ -463,7 +463,7 @@ To do that, you can declare that `None` is a valid type but still use `...` as t === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} @@ -472,7 +472,7 @@ To do that, you can declare that `None` is a valid type but still use `...` as t === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} @@ -500,7 +500,7 @@ If you feel uncomfortable using `...`, you can also import and use `Required` fr === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 8" {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} @@ -536,7 +536,7 @@ For example, to declare a query parameter `q` that can appear multiple times in === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} @@ -545,7 +545,7 @@ For example, to declare a query parameter `q` that can appear multiple times in === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} @@ -554,7 +554,7 @@ For example, to declare a query parameter `q` that can appear multiple times in === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} @@ -605,7 +605,7 @@ And you can also define a default `list` of values if none are provided: === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} @@ -614,7 +614,7 @@ And you can also define a default `list` of values if none are provided: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} @@ -656,7 +656,7 @@ You can also use `list` directly instead of `List[str]` (or `list[str]` in Pytho === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} @@ -701,7 +701,7 @@ You can add a `title`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} @@ -710,7 +710,7 @@ You can add a `title`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} @@ -739,7 +739,7 @@ And a `description`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="12" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} @@ -748,7 +748,7 @@ And a `description`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="13" {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} @@ -793,7 +793,7 @@ Then you can declare an `alias`, and that alias is what will be used to find the === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} @@ -802,7 +802,7 @@ Then you can declare an `alias`, and that alias is what will be used to find the === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} @@ -837,7 +837,7 @@ Then pass the parameter `deprecated=True` to `Query`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} @@ -846,7 +846,7 @@ Then pass the parameter `deprecated=True` to `Query`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="18" {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} @@ -881,7 +881,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} @@ -890,7 +890,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index bc7474359..1fe1e7a33 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -28,7 +28,7 @@ Import `File` and `UploadFile` from `fastapi`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/request_files/tutorial001.py!} @@ -53,7 +53,7 @@ Create file parameters the same way you would for `Body` or `Form`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/request_files/tutorial001.py!} @@ -94,7 +94,7 @@ Define a file parameter with a type of `UploadFile`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="12" {!> ../../../docs_src/request_files/tutorial001.py!} @@ -190,7 +190,7 @@ You can make a file optional by using standard type annotations and setting a de === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7 15" {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} @@ -199,7 +199,7 @@ You can make a file optional by using standard type annotations and setting a de === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9 17" {!> ../../../docs_src/request_files/tutorial001_02.py!} @@ -224,7 +224,7 @@ You can also use `File()` with `UploadFile`, for example, to set additional meta === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7 13" {!> ../../../docs_src/request_files/tutorial001_03.py!} @@ -253,7 +253,7 @@ To use that, declare a list of `bytes` or `UploadFile`: === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8 13" {!> ../../../docs_src/request_files/tutorial002_py39.py!} @@ -262,7 +262,7 @@ To use that, declare a list of `bytes` or `UploadFile`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10 15" {!> ../../../docs_src/request_files/tutorial002.py!} @@ -294,7 +294,7 @@ And the same way as before, you can use `File()` to set additional parameters, e === "Python 3.9+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="9 16" {!> ../../../docs_src/request_files/tutorial003_py39.py!} @@ -303,7 +303,7 @@ And the same way as before, you can use `File()` to set additional parameters, e === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="11 18" {!> ../../../docs_src/request_files/tutorial003.py!} diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 9900068fc..1818946c4 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -24,7 +24,7 @@ You can define files and form fields at the same time using `File` and `Form`. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} @@ -49,7 +49,7 @@ Create file and form parameters the same way you would for `Body` or `Query`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="8" {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index c3a0efe39..5d441a614 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -26,7 +26,7 @@ Import `Form` from `fastapi`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1" {!> ../../../docs_src/request_forms/tutorial001.py!} @@ -51,7 +51,7 @@ Create form parameters the same way you would for `Body` or `Query`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7" {!> ../../../docs_src/request_forms/tutorial001.py!} diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 705ab5671..5312254d9 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -93,7 +93,7 @@ Here we pass an `example` of the data expected in `Body()`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="20-25" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} @@ -145,7 +145,7 @@ Each specific example `dict` in the `examples` can contain: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="21-47" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} diff --git a/docs/en/docs/tutorial/security/first-steps.md b/docs/en/docs/tutorial/security/first-steps.md index a82809b96..5765cf2d6 100644 --- a/docs/en/docs/tutorial/security/first-steps.md +++ b/docs/en/docs/tutorial/security/first-steps.md @@ -35,7 +35,7 @@ Copy the example in a file `main.py`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python {!> ../../../docs_src/security/tutorial001.py!} @@ -149,7 +149,7 @@ When we create an instance of the `OAuth2PasswordBearer` class we pass in the `t === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6" {!> ../../../docs_src/security/tutorial001.py!} @@ -200,7 +200,7 @@ Now you can pass that `oauth2_scheme` in a dependency with `Depends`. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/security/tutorial001.py!} diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index 3d6358340..1a8c5d9a8 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -17,7 +17,7 @@ In the previous chapter the security system (which is based on the dependency in === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="10" {!> ../../../docs_src/security/tutorial001.py!} @@ -54,7 +54,7 @@ The same way we use Pydantic to declare bodies, we can use it anywhere else: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3 10-14" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -63,7 +63,7 @@ The same way we use Pydantic to declare bodies, we can use it anywhere else: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="5 12-16" {!> ../../../docs_src/security/tutorial002.py!} @@ -100,7 +100,7 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="23" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -109,7 +109,7 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="25" {!> ../../../docs_src/security/tutorial002.py!} @@ -140,7 +140,7 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="17-20 24-25" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -149,7 +149,7 @@ The same as we were doing before in the *path operation* directly, our new depen === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="19-22 26-27" {!> ../../../docs_src/security/tutorial002.py!} @@ -180,7 +180,7 @@ So now we can use the same `Depends` with our `get_current_user` in the *path op === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="29" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -189,7 +189,7 @@ So now we can use the same `Depends` with our `get_current_user` in the *path op === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="31" {!> ../../../docs_src/security/tutorial002.py!} @@ -262,7 +262,7 @@ And all these thousands of *path operations* can be as small as 3 lines: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="28-30" {!> ../../../docs_src/security/tutorial002_py310.py!} @@ -271,7 +271,7 @@ And all these thousands of *path operations* can be as small as 3 lines: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="30-32" {!> ../../../docs_src/security/tutorial002.py!} diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index e6c0f1738..deb722b96 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -130,7 +130,7 @@ And another one to authenticate and return a user. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6 47 54-55 58-59 68-74" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -139,7 +139,7 @@ And another one to authenticate and return a user. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="7 48 55-56 59-60 69-75" {!> ../../../docs_src/security/tutorial004.py!} @@ -197,7 +197,7 @@ Create a utility function to generate a new access token. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="5 11-13 27-29 77-85" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -206,7 +206,7 @@ Create a utility function to generate a new access token. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="6 12-14 28-30 78-86" {!> ../../../docs_src/security/tutorial004.py!} @@ -241,7 +241,7 @@ If the token is invalid, return an HTTP error right away. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="88-105" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -250,7 +250,7 @@ If the token is invalid, return an HTTP error right away. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="89-106" {!> ../../../docs_src/security/tutorial004.py!} @@ -283,7 +283,7 @@ Create a real JWT access token and return it === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="114-127" {!> ../../../docs_src/security/tutorial004_py310.py!} @@ -292,7 +292,7 @@ Create a real JWT access token and return it === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="115-128" {!> ../../../docs_src/security/tutorial004.py!} diff --git a/docs/en/docs/tutorial/security/simple-oauth2.md b/docs/en/docs/tutorial/security/simple-oauth2.md index 9534185c7..abcf6b667 100644 --- a/docs/en/docs/tutorial/security/simple-oauth2.md +++ b/docs/en/docs/tutorial/security/simple-oauth2.md @@ -70,7 +70,7 @@ First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depe === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="2 74" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -79,7 +79,7 @@ First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depe === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="4 76" {!> ../../../docs_src/security/tutorial003.py!} @@ -143,7 +143,7 @@ For the error, we use the exception `HTTPException`: === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="1 75-77" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -152,7 +152,7 @@ For the error, we use the exception `HTTPException`: === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="3 77-79" {!> ../../../docs_src/security/tutorial003.py!} @@ -203,7 +203,7 @@ So, the thief won't be able to try to use those same passwords in another system === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="78-81" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -212,7 +212,7 @@ So, the thief won't be able to try to use those same passwords in another system === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="80-83" {!> ../../../docs_src/security/tutorial003.py!} @@ -273,7 +273,7 @@ For this simple example, we are going to just be completely insecure and return === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="83" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -282,7 +282,7 @@ For this simple example, we are going to just be completely insecure and return === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="85" {!> ../../../docs_src/security/tutorial003.py!} @@ -330,7 +330,7 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="56-64 67-70 88" {!> ../../../docs_src/security/tutorial003_py310.py!} @@ -339,7 +339,7 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python hl_lines="58-66 69-72 90" {!> ../../../docs_src/security/tutorial003.py!} diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index 9f94183f4..ec133a4d0 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -131,7 +131,7 @@ Both *path operations* require an `X-Token` header. === "Python 3.10+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python {!> ../../../docs_src/app_testing/app_b_py310/main.py!} @@ -140,7 +140,7 @@ Both *path operations* require an `X-Token` header. === "Python 3.6+ non-Annotated" !!! tip - Try to use the main, `Annotated` version better. + Prefer to use the `Annotated` version if possible. ```Python {!> ../../../docs_src/app_testing/app_b/main.py!} From 546392db985eb866edaa37839ed1b3f0f116e5b5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 18 Mar 2023 19:08:30 +0000 Subject: [PATCH 0869/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5813ac6b7..b413d60fe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Tweak tip recommending `Annotated` in docs. PR [#9270](https://github.com/tiangolo/fastapi/pull/9270) by [@tiangolo](https://github.com/tiangolo). ### Highlights This release adds support for dependencies and parameters using `Annotated`. ✨ From bd90bed02a01fb2d43d5cee5078239d71c933f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 20:24:12 +0100 Subject: [PATCH 0870/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b413d60fe..3a196623a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,10 +2,10 @@ ## Latest Changes -* 📝 Tweak tip recommending `Annotated` in docs. PR [#9270](https://github.com/tiangolo/fastapi/pull/9270) by [@tiangolo](https://github.com/tiangolo). + ### Highlights -This release adds support for dependencies and parameters using `Annotated`. ✨ +This release adds support for dependencies and parameters using `Annotated` and recommends its usage. ✨ This has **several benefits**, one of the main ones is that now the parameters of your functions with `Annotated` would **not be affected** at all. @@ -85,6 +85,19 @@ def delete_item(user: CurrentUser, item_id: int): ...and `CurrentUser` has all the typing information as `User`, so your editor will work as expected (autocompletion and everything), and **FastAPI** will be able to understand the dependency defined in `Annotated`. 😎 +Roughly **all the docs** have been rewritten to use `Annotated` as the main way to declare **parameters** and **dependencies**. All the **examples** in the docs now include a version with `Annotated` and a version without it, for each of the specific Python version (when there are small differences/improvements in more recent versions). + +The key updated docs are: + +* Python Types Intro: + * [Type Hints with Metadata Annotations](https://fastapi.tiangolo.com/python-types/#type-hints-with-metadata-annotations). +* Tutorial: + * [Query Parameters and String Validations - Additional validation](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#additional-validation) + * [Advantages of `Annotated`](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#advantages-of-annotated) + * [Path Parameters and Numeric Validations - Order the parameters as you need, tricks](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#order-the-parameters-as-you-need-tricks) + * [Better with `Annotated`](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/#better-with-annotated) + * [Dependencies - First Steps - Share `Annotated` dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/#share-annotated-dependencies) + Special thanks to [@nzig](https://github.com/nzig) for the core implementation and to [@adriangb](https://github.com/adriangb) for the inspiration and idea with [Xpresso](https://github.com/adriangb/xpresso)! 🚀 ### Features @@ -93,6 +106,7 @@ Special thanks to [@nzig](https://github.com/nzig) for the core implementation a ### Docs +* 📝 Tweak tip recommending `Annotated` in docs. PR [#9270](https://github.com/tiangolo/fastapi/pull/9270) by [@tiangolo](https://github.com/tiangolo). * 📝 Update order of examples, latest Python version first, and simplify version tab names. PR [#9269](https://github.com/tiangolo/fastapi/pull/9269) by [@tiangolo](https://github.com/tiangolo). * 📝 Update all docs to use `Annotated` as the main recommendation, with new examples and tests. PR [#9268](https://github.com/tiangolo/fastapi/pull/9268) by [@tiangolo](https://github.com/tiangolo). From 38f0cad517fe90755061228d41c2265e345a6ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 20:37:20 +0100 Subject: [PATCH 0871/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20release=20note?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3a196623a..e5fccd0e4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -85,7 +85,7 @@ def delete_item(user: CurrentUser, item_id: int): ...and `CurrentUser` has all the typing information as `User`, so your editor will work as expected (autocompletion and everything), and **FastAPI** will be able to understand the dependency defined in `Annotated`. 😎 -Roughly **all the docs** have been rewritten to use `Annotated` as the main way to declare **parameters** and **dependencies**. All the **examples** in the docs now include a version with `Annotated` and a version without it, for each of the specific Python version (when there are small differences/improvements in more recent versions). +Roughly **all the docs** have been rewritten to use `Annotated` as the main way to declare **parameters** and **dependencies**. All the **examples** in the docs now include a version with `Annotated` and a version without it, for each of the specific Python versions (when there are small differences/improvements in more recent versions). There were around 23K new lines added between docs, examples, and tests. 🚀 The key updated docs are: From d666ccb62216e45ca78643b52c235ba0d2c53986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Mar 2023 20:37:42 +0100 Subject: [PATCH 0872/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?95.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e5fccd0e4..7a9a09eed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3,6 +3,8 @@ ## Latest Changes +## 0.95.0 + ### Highlights This release adds support for dependencies and parameters using `Annotated` and recommends its usage. ✨ diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 05da7b759..f06bb6454 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.94.1" +__version__ = "0.95.0" from starlette import status as status From d4e85da18bb4dc475a16c20c8abad5133497e89e Mon Sep 17 00:00:00 2001 From: LeeeeT Date: Sat, 1 Apr 2023 12:26:04 +0300 Subject: [PATCH 0873/1881] =?UTF-8?q?=F0=9F=8C=90=20=F0=9F=94=A0=20?= =?UTF-8?q?=F0=9F=93=84=20=F0=9F=90=A2=20Translate=20docs=20to=20Emoji=20?= =?UTF-8?q?=F0=9F=A5=B3=20=F0=9F=8E=89=20=F0=9F=92=A5=20=F0=9F=A4=AF=20?= =?UTF-8?q?=F0=9F=A4=AF=20(#5385)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌐 💬 🩺 🦲 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * 🛠️😊 * ♻️ Rename emoji lang from emj to em, and main docs name as 😉 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Axd1x8a <26704473+FeeeeK@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 3 + docs/de/mkdocs.yml | 3 + docs/em/docs/advanced/additional-responses.md | 240 ++++++ .../docs/advanced/additional-status-codes.md | 37 + .../em/docs/advanced/advanced-dependencies.md | 70 ++ docs/em/docs/advanced/async-sql-databases.md | 162 ++++ docs/em/docs/advanced/async-tests.md | 92 ++ docs/em/docs/advanced/behind-a-proxy.md | 346 ++++++++ docs/em/docs/advanced/conditional-openapi.md | 58 ++ .../docs/advanced/custom-request-and-route.md | 109 +++ docs/em/docs/advanced/custom-response.md | 300 +++++++ docs/em/docs/advanced/dataclasses.md | 98 +++ docs/em/docs/advanced/events.md | 160 ++++ docs/em/docs/advanced/extending-openapi.md | 314 +++++++ docs/em/docs/advanced/generate-clients.md | 267 ++++++ docs/em/docs/advanced/graphql.md | 56 ++ docs/em/docs/advanced/index.md | 24 + docs/em/docs/advanced/middleware.md | 99 +++ docs/em/docs/advanced/nosql-databases.md | 156 ++++ docs/em/docs/advanced/openapi-callbacks.md | 179 ++++ .../path-operation-advanced-configuration.md | 170 ++++ .../advanced/response-change-status-code.md | 33 + docs/em/docs/advanced/response-cookies.md | 49 ++ docs/em/docs/advanced/response-directly.md | 63 ++ docs/em/docs/advanced/response-headers.md | 42 + .../docs/advanced/security/http-basic-auth.md | 113 +++ docs/em/docs/advanced/security/index.md | 16 + .../docs/advanced/security/oauth2-scopes.md | 269 ++++++ docs/em/docs/advanced/settings.md | 382 +++++++++ docs/em/docs/advanced/sql-databases-peewee.md | 529 ++++++++++++ docs/em/docs/advanced/sub-applications.md | 73 ++ docs/em/docs/advanced/templates.md | 77 ++ docs/em/docs/advanced/testing-database.md | 95 +++ docs/em/docs/advanced/testing-dependencies.md | 49 ++ docs/em/docs/advanced/testing-events.md | 7 + docs/em/docs/advanced/testing-websockets.md | 12 + .../docs/advanced/using-request-directly.md | 52 ++ docs/em/docs/advanced/websockets.md | 184 ++++ docs/em/docs/advanced/wsgi.md | 37 + docs/em/docs/alternatives.md | 414 +++++++++ docs/em/docs/async.md | 430 ++++++++++ docs/em/docs/benchmarks.md | 34 + docs/em/docs/contributing.md | 465 +++++++++++ docs/em/docs/deployment/concepts.md | 311 +++++++ docs/em/docs/deployment/deta.md | 258 ++++++ docs/em/docs/deployment/docker.md | 698 ++++++++++++++++ docs/em/docs/deployment/https.md | 190 +++++ docs/em/docs/deployment/index.md | 21 + docs/em/docs/deployment/manually.md | 145 ++++ docs/em/docs/deployment/server-workers.md | 178 ++++ docs/em/docs/deployment/versions.md | 87 ++ docs/em/docs/external-links.md | 91 ++ docs/em/docs/fastapi-people.md | 178 ++++ docs/em/docs/features.md | 200 +++++ docs/em/docs/help-fastapi.md | 265 ++++++ docs/em/docs/history-design-future.md | 79 ++ docs/em/docs/index.md | 469 +++++++++++ docs/em/docs/project-generation.md | 84 ++ docs/em/docs/python-types.md | 490 +++++++++++ docs/em/docs/tutorial/background-tasks.md | 102 +++ docs/em/docs/tutorial/bigger-applications.md | 488 +++++++++++ docs/em/docs/tutorial/body-fields.md | 68 ++ docs/em/docs/tutorial/body-multiple-params.md | 213 +++++ docs/em/docs/tutorial/body-nested-models.md | 382 +++++++++ docs/em/docs/tutorial/body-updates.md | 155 ++++ docs/em/docs/tutorial/body.md | 213 +++++ docs/em/docs/tutorial/cookie-params.md | 49 ++ docs/em/docs/tutorial/cors.md | 84 ++ docs/em/docs/tutorial/debugging.md | 112 +++ .../dependencies/classes-as-dependencies.md | 247 ++++++ ...pendencies-in-path-operation-decorators.md | 71 ++ .../dependencies/dependencies-with-yield.md | 219 +++++ .../dependencies/global-dependencies.md | 17 + docs/em/docs/tutorial/dependencies/index.md | 233 ++++++ .../tutorial/dependencies/sub-dependencies.md | 110 +++ docs/em/docs/tutorial/encoder.md | 42 + docs/em/docs/tutorial/extra-data-types.md | 82 ++ docs/em/docs/tutorial/extra-models.md | 252 ++++++ docs/em/docs/tutorial/first-steps.md | 333 ++++++++ docs/em/docs/tutorial/handling-errors.md | 261 ++++++ docs/em/docs/tutorial/header-params.md | 128 +++ docs/em/docs/tutorial/index.md | 80 ++ docs/em/docs/tutorial/metadata.md | 112 +++ docs/em/docs/tutorial/middleware.md | 61 ++ .../tutorial/path-operation-configuration.md | 179 ++++ .../path-params-numeric-validations.md | 138 +++ docs/em/docs/tutorial/path-params.md | 252 ++++++ .../tutorial/query-params-str-validations.md | 467 +++++++++++ docs/em/docs/tutorial/query-params.md | 225 +++++ docs/em/docs/tutorial/request-files.md | 186 +++++ .../docs/tutorial/request-forms-and-files.md | 35 + docs/em/docs/tutorial/request-forms.md | 58 ++ docs/em/docs/tutorial/response-model.md | 481 +++++++++++ docs/em/docs/tutorial/response-status-code.md | 89 ++ docs/em/docs/tutorial/schema-extra-example.md | 141 ++++ docs/em/docs/tutorial/security/first-steps.md | 182 ++++ .../tutorial/security/get-current-user.md | 151 ++++ docs/em/docs/tutorial/security/index.md | 101 +++ docs/em/docs/tutorial/security/oauth2-jwt.md | 297 +++++++ .../docs/tutorial/security/simple-oauth2.md | 315 +++++++ docs/em/docs/tutorial/sql-databases.md | 786 ++++++++++++++++++ docs/em/docs/tutorial/static-files.md | 39 + docs/em/docs/tutorial/testing.md | 188 +++++ docs/em/mkdocs.yml | 261 ++++++ docs/em/overrides/.gitignore | 0 docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/mkdocs.yml | 3 + docs/hy/mkdocs.yml | 8 +- docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + docs/ta/mkdocs.yml | 8 +- docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 125 files changed, 18865 insertions(+), 2 deletions(-) create mode 100644 docs/em/docs/advanced/additional-responses.md create mode 100644 docs/em/docs/advanced/additional-status-codes.md create mode 100644 docs/em/docs/advanced/advanced-dependencies.md create mode 100644 docs/em/docs/advanced/async-sql-databases.md create mode 100644 docs/em/docs/advanced/async-tests.md create mode 100644 docs/em/docs/advanced/behind-a-proxy.md create mode 100644 docs/em/docs/advanced/conditional-openapi.md create mode 100644 docs/em/docs/advanced/custom-request-and-route.md create mode 100644 docs/em/docs/advanced/custom-response.md create mode 100644 docs/em/docs/advanced/dataclasses.md create mode 100644 docs/em/docs/advanced/events.md create mode 100644 docs/em/docs/advanced/extending-openapi.md create mode 100644 docs/em/docs/advanced/generate-clients.md create mode 100644 docs/em/docs/advanced/graphql.md create mode 100644 docs/em/docs/advanced/index.md create mode 100644 docs/em/docs/advanced/middleware.md create mode 100644 docs/em/docs/advanced/nosql-databases.md create mode 100644 docs/em/docs/advanced/openapi-callbacks.md create mode 100644 docs/em/docs/advanced/path-operation-advanced-configuration.md create mode 100644 docs/em/docs/advanced/response-change-status-code.md create mode 100644 docs/em/docs/advanced/response-cookies.md create mode 100644 docs/em/docs/advanced/response-directly.md create mode 100644 docs/em/docs/advanced/response-headers.md create mode 100644 docs/em/docs/advanced/security/http-basic-auth.md create mode 100644 docs/em/docs/advanced/security/index.md create mode 100644 docs/em/docs/advanced/security/oauth2-scopes.md create mode 100644 docs/em/docs/advanced/settings.md create mode 100644 docs/em/docs/advanced/sql-databases-peewee.md create mode 100644 docs/em/docs/advanced/sub-applications.md create mode 100644 docs/em/docs/advanced/templates.md create mode 100644 docs/em/docs/advanced/testing-database.md create mode 100644 docs/em/docs/advanced/testing-dependencies.md create mode 100644 docs/em/docs/advanced/testing-events.md create mode 100644 docs/em/docs/advanced/testing-websockets.md create mode 100644 docs/em/docs/advanced/using-request-directly.md create mode 100644 docs/em/docs/advanced/websockets.md create mode 100644 docs/em/docs/advanced/wsgi.md create mode 100644 docs/em/docs/alternatives.md create mode 100644 docs/em/docs/async.md create mode 100644 docs/em/docs/benchmarks.md create mode 100644 docs/em/docs/contributing.md create mode 100644 docs/em/docs/deployment/concepts.md create mode 100644 docs/em/docs/deployment/deta.md create mode 100644 docs/em/docs/deployment/docker.md create mode 100644 docs/em/docs/deployment/https.md create mode 100644 docs/em/docs/deployment/index.md create mode 100644 docs/em/docs/deployment/manually.md create mode 100644 docs/em/docs/deployment/server-workers.md create mode 100644 docs/em/docs/deployment/versions.md create mode 100644 docs/em/docs/external-links.md create mode 100644 docs/em/docs/fastapi-people.md create mode 100644 docs/em/docs/features.md create mode 100644 docs/em/docs/help-fastapi.md create mode 100644 docs/em/docs/history-design-future.md create mode 100644 docs/em/docs/index.md create mode 100644 docs/em/docs/project-generation.md create mode 100644 docs/em/docs/python-types.md create mode 100644 docs/em/docs/tutorial/background-tasks.md create mode 100644 docs/em/docs/tutorial/bigger-applications.md create mode 100644 docs/em/docs/tutorial/body-fields.md create mode 100644 docs/em/docs/tutorial/body-multiple-params.md create mode 100644 docs/em/docs/tutorial/body-nested-models.md create mode 100644 docs/em/docs/tutorial/body-updates.md create mode 100644 docs/em/docs/tutorial/body.md create mode 100644 docs/em/docs/tutorial/cookie-params.md create mode 100644 docs/em/docs/tutorial/cors.md create mode 100644 docs/em/docs/tutorial/debugging.md create mode 100644 docs/em/docs/tutorial/dependencies/classes-as-dependencies.md create mode 100644 docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md create mode 100644 docs/em/docs/tutorial/dependencies/dependencies-with-yield.md create mode 100644 docs/em/docs/tutorial/dependencies/global-dependencies.md create mode 100644 docs/em/docs/tutorial/dependencies/index.md create mode 100644 docs/em/docs/tutorial/dependencies/sub-dependencies.md create mode 100644 docs/em/docs/tutorial/encoder.md create mode 100644 docs/em/docs/tutorial/extra-data-types.md create mode 100644 docs/em/docs/tutorial/extra-models.md create mode 100644 docs/em/docs/tutorial/first-steps.md create mode 100644 docs/em/docs/tutorial/handling-errors.md create mode 100644 docs/em/docs/tutorial/header-params.md create mode 100644 docs/em/docs/tutorial/index.md create mode 100644 docs/em/docs/tutorial/metadata.md create mode 100644 docs/em/docs/tutorial/middleware.md create mode 100644 docs/em/docs/tutorial/path-operation-configuration.md create mode 100644 docs/em/docs/tutorial/path-params-numeric-validations.md create mode 100644 docs/em/docs/tutorial/path-params.md create mode 100644 docs/em/docs/tutorial/query-params-str-validations.md create mode 100644 docs/em/docs/tutorial/query-params.md create mode 100644 docs/em/docs/tutorial/request-files.md create mode 100644 docs/em/docs/tutorial/request-forms-and-files.md create mode 100644 docs/em/docs/tutorial/request-forms.md create mode 100644 docs/em/docs/tutorial/response-model.md create mode 100644 docs/em/docs/tutorial/response-status-code.md create mode 100644 docs/em/docs/tutorial/schema-extra-example.md create mode 100644 docs/em/docs/tutorial/security/first-steps.md create mode 100644 docs/em/docs/tutorial/security/get-current-user.md create mode 100644 docs/em/docs/tutorial/security/index.md create mode 100644 docs/em/docs/tutorial/security/oauth2-jwt.md create mode 100644 docs/em/docs/tutorial/security/simple-oauth2.md create mode 100644 docs/em/docs/tutorial/sql-databases.md create mode 100644 docs/em/docs/tutorial/static-files.md create mode 100644 docs/em/docs/tutorial/testing.md create mode 100644 docs/em/mkdocs.yml create mode 100644 docs/em/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 7f4a490d8..7d59451c1 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index f6e0a6b01..87fe74697 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -106,6 +107,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/em/docs/advanced/additional-responses.md b/docs/em/docs/advanced/additional-responses.md new file mode 100644 index 000000000..26963c2e3 --- /dev/null +++ b/docs/em/docs/advanced/additional-responses.md @@ -0,0 +1,240 @@ +# 🌖 📨 🗄 + +!!! warning + 👉 👍 🏧 ❔. + + 🚥 👆 ▶️ ⏮️ **FastAPI**, 👆 💪 🚫 💪 👉. + +👆 💪 📣 🌖 📨, ⏮️ 🌖 👔 📟, 🔉 🆎, 📛, ♒️. + +👈 🌖 📨 🔜 🔌 🗄 🔗, 👫 🔜 😑 🛠️ 🩺. + +✋️ 👈 🌖 📨 👆 ✔️ ⚒ 💭 👆 📨 `Response` 💖 `JSONResponse` 🔗, ⏮️ 👆 👔 📟 & 🎚. + +## 🌖 📨 ⏮️ `model` + +👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔢 `responses`. + +⚫️ 📨 `dict`, 🔑 👔 📟 🔠 📨, 💖 `200`, & 💲 🎏 `dict`Ⓜ ⏮️ ℹ 🔠 👫. + +🔠 👈 📨 `dict`Ⓜ 💪 ✔️ 🔑 `model`, ⚗ Pydantic 🏷, 💖 `response_model`. + +**FastAPI** 🔜 ✊ 👈 🏷, 🏗 🚮 🎻 🔗 & 🔌 ⚫️ ☑ 🥉 🗄. + +🖼, 📣 ➕1️⃣ 📨 ⏮️ 👔 📟 `404` & Pydantic 🏷 `Message`, 👆 💪 ✍: + +```Python hl_lines="18 22" +{!../../../docs_src/additional_responses/tutorial001.py!} +``` + +!!! note + ✔️ 🤯 👈 👆 ✔️ 📨 `JSONResponse` 🔗. + +!!! info + `model` 🔑 🚫 🍕 🗄. + + **FastAPI** 🔜 ✊ Pydantic 🏷 ⚪️➡️ 📤, 🏗 `JSON Schema`, & 🚮 ⚫️ ☑ 🥉. + + ☑ 🥉: + + * 🔑 `content`, 👈 ✔️ 💲 ➕1️⃣ 🎻 🎚 (`dict`) 👈 🔌: + * 🔑 ⏮️ 📻 🆎, ✅ `application/json`, 👈 🔌 💲 ➕1️⃣ 🎻 🎚, 👈 🔌: + * 🔑 `schema`, 👈 ✔️ 💲 🎻 🔗 ⚪️➡️ 🏷, 📥 ☑ 🥉. + * **FastAPI** 🚮 🔗 📥 🌐 🎻 🔗 ➕1️⃣ 🥉 👆 🗄 ↩️ ✅ ⚫️ 🔗. 👉 🌌, 🎏 🈸 & 👩‍💻 💪 ⚙️ 👈 🎻 🔗 🔗, 🚚 👻 📟 ⚡ 🧰, ♒️. + +🏗 📨 🗄 👉 *➡ 🛠️* 🔜: + +```JSON hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} +``` + +🔗 🔗 ➕1️⃣ 🥉 🔘 🗄 🔗: + +```JSON hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + } + } + } +} +``` + +## 🌖 🔉 🆎 👑 📨 + +👆 💪 ⚙️ 👉 🎏 `responses` 🔢 🚮 🎏 🔉 🆎 🎏 👑 📨. + +🖼, 👆 💪 🚮 🌖 📻 🆎 `image/png`, 📣 👈 👆 *➡ 🛠️* 💪 📨 🎻 🎚 (⏮️ 📻 🆎 `application/json`) ⚖️ 🇩🇴 🖼: + +```Python hl_lines="19-24 28" +{!../../../docs_src/additional_responses/tutorial002.py!} +``` + +!!! note + 👀 👈 👆 ✔️ 📨 🖼 ⚙️ `FileResponse` 🔗. + +!!! info + 🚥 👆 ✔ 🎏 📻 🆎 🎯 👆 `responses` 🔢, FastAPI 🔜 🤔 📨 ✔️ 🎏 📻 🆎 👑 📨 🎓 (🔢 `application/json`). + + ✋️ 🚥 👆 ✔️ ✔ 🛃 📨 🎓 ⏮️ `None` 🚮 📻 🆎, FastAPI 🔜 ⚙️ `application/json` 🙆 🌖 📨 👈 ✔️ 👨‍💼 🏷. + +## 🌀 ℹ + +👆 💪 🌀 📨 ℹ ⚪️➡️ 💗 🥉, 🔌 `response_model`, `status_code`, & `responses` 🔢. + +👆 💪 📣 `response_model`, ⚙️ 🔢 👔 📟 `200` (⚖️ 🛃 1️⃣ 🚥 👆 💪), & ⤴️ 📣 🌖 ℹ 👈 🎏 📨 `responses`, 🔗 🗄 🔗. + +**FastAPI** 🔜 🚧 🌖 ℹ ⚪️➡️ `responses`, & 🌀 ⚫️ ⏮️ 🎻 🔗 ⚪️➡️ 👆 🏷. + +🖼, 👆 💪 📣 📨 ⏮️ 👔 📟 `404` 👈 ⚙️ Pydantic 🏷 & ✔️ 🛃 `description`. + +& 📨 ⏮️ 👔 📟 `200` 👈 ⚙️ 👆 `response_model`, ✋️ 🔌 🛃 `example`: + +```Python hl_lines="20-31" +{!../../../docs_src/additional_responses/tutorial003.py!} +``` + +⚫️ 🔜 🌐 🌀 & 🔌 👆 🗄, & 🎦 🛠️ 🩺: + + + +## 🌀 🔢 📨 & 🛃 🕐 + +👆 💪 💚 ✔️ 🔁 📨 👈 ✔ 📚 *➡ 🛠️*, ✋️ 👆 💚 🌀 👫 ⏮️ 🛃 📨 💚 🔠 *➡ 🛠️*. + +📚 💼, 👆 💪 ⚙️ 🐍 ⚒ "🏗" `dict` ⏮️ `**dict_to_unpack`: + +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +📥, `new_dict` 🔜 🔌 🌐 🔑-💲 👫 ⚪️➡️ `old_dict` ➕ 🆕 🔑-💲 👫: + +```Python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` + +👆 💪 ⚙️ 👈 ⚒ 🏤-⚙️ 🔢 📨 👆 *➡ 🛠️* & 🌀 👫 ⏮️ 🌖 🛃 🕐. + +🖼: + +```Python hl_lines="13-17 26" +{!../../../docs_src/additional_responses/tutorial004.py!} +``` + +## 🌖 ℹ 🔃 🗄 📨 + +👀 ⚫️❔ ⚫️❔ 👆 💪 🔌 📨, 👆 💪 ✅ 👉 📄 🗄 🔧: + +* 🗄 📨 🎚, ⚫️ 🔌 `Response Object`. +* 🗄 📨 🎚, 👆 💪 🔌 🕳 ⚪️➡️ 👉 🔗 🔠 📨 🔘 👆 `responses` 🔢. ✅ `description`, `headers`, `content` (🔘 👉 👈 👆 📣 🎏 🔉 🆎 & 🎻 🔗), & `links`. diff --git a/docs/em/docs/advanced/additional-status-codes.md b/docs/em/docs/advanced/additional-status-codes.md new file mode 100644 index 000000000..392579df6 --- /dev/null +++ b/docs/em/docs/advanced/additional-status-codes.md @@ -0,0 +1,37 @@ +# 🌖 👔 📟 + +🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`, 🚮 🎚 👆 📨 ⚪️➡️ 👆 *➡ 🛠️* 🔘 👈 `JSONResponse`. + +⚫️ 🔜 ⚙️ 🔢 👔 📟 ⚖️ 1️⃣ 👆 ⚒ 👆 *➡ 🛠️*. + +## 🌖 👔 📟 + +🚥 👆 💚 📨 🌖 👔 📟 ↖️ ⚪️➡️ 👑 1️⃣, 👆 💪 👈 🛬 `Response` 🔗, 💖 `JSONResponse`, & ⚒ 🌖 👔 📟 🔗. + +🖼, ➡️ 💬 👈 👆 💚 ✔️ *➡ 🛠️* 👈 ✔ ℹ 🏬, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣ "👌" 🕐❔ 🏆. + +✋️ 👆 💚 ⚫️ 🚫 🆕 🏬. & 🕐❔ 🏬 🚫 🔀 ⏭, ⚫️ ✍ 👫, & 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣1️⃣ "✍". + +🏆 👈, 🗄 `JSONResponse`, & 📨 👆 🎚 📤 🔗, ⚒ `status_code` 👈 👆 💚: + +```Python hl_lines="4 25" +{!../../../docs_src/additional_status_codes/tutorial001.py!} +``` + +!!! warning + 🕐❔ 👆 📨 `Response` 🔗, 💖 🖼 🔛, ⚫️ 🔜 📨 🔗. + + ⚫️ 🏆 🚫 🎻 ⏮️ 🏷, ♒️. + + ⚒ 💭 ⚫️ ✔️ 📊 👆 💚 ⚫️ ✔️, & 👈 💲 ☑ 🎻 (🚥 👆 ⚙️ `JSONResponse`). + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `status`. + +## 🗄 & 🛠️ 🩺 + +🚥 👆 📨 🌖 👔 📟 & 📨 🔗, 👫 🏆 🚫 🔌 🗄 🔗 (🛠️ 🩺), ↩️ FastAPI 🚫 ✔️ 🌌 💭 ⏪ ⚫️❔ 👆 🚶 📨. + +✋️ 👆 💪 📄 👈 👆 📟, ⚙️: [🌖 📨](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/advanced-dependencies.md b/docs/em/docs/advanced/advanced-dependencies.md new file mode 100644 index 000000000..fa1554734 --- /dev/null +++ b/docs/em/docs/advanced/advanced-dependencies.md @@ -0,0 +1,70 @@ +# 🏧 🔗 + +## 🔗 🔗 + +🌐 🔗 👥 ✔️ 👀 🔧 🔢 ⚖️ 🎓. + +✋️ 📤 💪 💼 🌐❔ 👆 💚 💪 ⚒ 🔢 🔛 🔗, 🍵 ✔️ 📣 📚 🎏 🔢 ⚖️ 🎓. + +➡️ 🌈 👈 👥 💚 ✔️ 🔗 👈 ✅ 🚥 🔢 🔢 `q` 🔌 🔧 🎚. + +✋️ 👥 💚 💪 🔗 👈 🔧 🎚. + +## "🇧🇲" 👐 + +🐍 📤 🌌 ⚒ 👐 🎓 "🇧🇲". + +🚫 🎓 ⚫️ (❔ ⏪ 🇧🇲), ✋️ 👐 👈 🎓. + +👈, 👥 📣 👩‍🔬 `__call__`: + +```Python hl_lines="10" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +👉 💼, 👉 `__call__` ⚫️❔ **FastAPI** 🔜 ⚙️ ✅ 🌖 🔢 & 🎧-🔗, & 👉 ⚫️❔ 🔜 🤙 🚶‍♀️ 💲 🔢 👆 *➡ 🛠️ 🔢* ⏪. + +## 🔗 👐 + +& 🔜, 👥 💪 ⚙️ `__init__` 📣 🔢 👐 👈 👥 💪 ⚙️ "🔗" 🔗: + +```Python hl_lines="7" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +👉 💼, **FastAPI** 🏆 🚫 ⏱ 👆 ⚖️ 💅 🔃 `__init__`, 👥 🔜 ⚙️ ⚫️ 🔗 👆 📟. + +## ✍ 👐 + +👥 💪 ✍ 👐 👉 🎓 ⏮️: + +```Python hl_lines="16" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +& 👈 🌌 👥 💪 "🔗" 👆 🔗, 👈 🔜 ✔️ `"bar"` 🔘 ⚫️, 🔢 `checker.fixed_content`. + +## ⚙️ 👐 🔗 + +⤴️, 👥 💪 ⚙️ 👉 `checker` `Depends(checker)`, ↩️ `Depends(FixedContentQueryChecker)`, ↩️ 🔗 👐, `checker`, 🚫 🎓 ⚫️. + +& 🕐❔ ❎ 🔗, **FastAPI** 🔜 🤙 👉 `checker` 💖: + +```Python +checker(q="somequery") +``` + +...& 🚶‍♀️ ⚫️❔ 👈 📨 💲 🔗 👆 *➡ 🛠️ 🔢* 🔢 `fixed_content_included`: + +```Python hl_lines="20" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +!!! tip + 🌐 👉 💪 😑 🎭. & ⚫️ 💪 🚫 📶 🆑 ❔ ⚫️ ⚠. + + 👫 🖼 😫 🙅, ✋️ 🎦 ❔ ⚫️ 🌐 👷. + + 📃 🔃 💂‍♂, 📤 🚙 🔢 👈 🛠️ 👉 🎏 🌌. + + 🚥 👆 🤔 🌐 👉, 👆 ⏪ 💭 ❔ 👈 🚙 🧰 💂‍♂ 👷 🔘. diff --git a/docs/em/docs/advanced/async-sql-databases.md b/docs/em/docs/advanced/async-sql-databases.md new file mode 100644 index 000000000..848936de1 --- /dev/null +++ b/docs/em/docs/advanced/async-sql-databases.md @@ -0,0 +1,162 @@ +# 🔁 🗄 (🔗) 💽 + +👆 💪 ⚙️ `encode/databases` ⏮️ **FastAPI** 🔗 💽 ⚙️ `async` & `await`. + +⚫️ 🔗 ⏮️: + +* ✳ +* ✳ +* 🗄 + +👉 🖼, 👥 🔜 ⚙️ **🗄**, ↩️ ⚫️ ⚙️ 👁 📁 & 🐍 ✔️ 🛠️ 🐕‍🦺. , 👆 💪 📁 👉 🖼 & 🏃 ⚫️. + +⏪, 👆 🏭 🈸, 👆 💪 💚 ⚙️ 💽 💽 💖 **✳**. + +!!! tip + 👆 💪 🛠️ 💭 ⚪️➡️ 📄 🔃 🇸🇲 🐜 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}), 💖 ⚙️ 🚙 🔢 🎭 🛠️ 💽, 🔬 👆 **FastAPI** 📟. + + 👉 📄 🚫 ✔ 📚 💭, 🌓 😑 💃. + +## 🗄 & ⚒ 🆙 `SQLAlchemy` + +* 🗄 `SQLAlchemy`. +* ✍ `metadata` 🎚. +* ✍ 🏓 `notes` ⚙️ `metadata` 🎚. + +```Python hl_lines="4 14 16-22" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! tip + 👀 👈 🌐 👉 📟 😁 🇸🇲 🐚. + + `databases` 🚫 🔨 🕳 📥. + +## 🗄 & ⚒ 🆙 `databases` + +* 🗄 `databases`. +* ✍ `DATABASE_URL`. +* ✍ `database` 🎚. + +```Python hl_lines="3 9 12" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! tip + 🚥 👆 🔗 🎏 💽 (✅ ✳), 👆 🔜 💪 🔀 `DATABASE_URL`. + +## ✍ 🏓 + +👉 💼, 👥 🏗 🏓 🎏 🐍 📁, ✋️ 🏭, 👆 🔜 🎲 💚 ✍ 👫 ⏮️ ⚗, 🛠️ ⏮️ 🛠️, ♒️. + +📥, 👉 📄 🔜 🏃 🔗, ▶️️ ⏭ ▶️ 👆 **FastAPI** 🈸. + +* ✍ `engine`. +* ✍ 🌐 🏓 ⚪️➡️ `metadata` 🎚. + +```Python hl_lines="25-28" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +## ✍ 🏷 + +✍ Pydantic 🏷: + +* 🗒 ✍ (`NoteIn`). +* 🗒 📨 (`Note`). + +```Python hl_lines="31-33 36-39" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +🏗 👫 Pydantic 🏷, 🔢 💽 🔜 ✔, 🎻 (🗜), & ✍ (📄). + +, 👆 🔜 💪 👀 ⚫️ 🌐 🎓 🛠️ 🩺. + +## 🔗 & 🔌 + +* ✍ 👆 `FastAPI` 🈸. +* ✍ 🎉 🐕‍🦺 🔗 & 🔌 ⚪️➡️ 💽. + +```Python hl_lines="42 45-47 50-52" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +## ✍ 🗒 + +✍ *➡ 🛠️ 🔢* ✍ 🗒: + +```Python hl_lines="55-58" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! Note + 👀 👈 👥 🔗 ⏮️ 💽 ⚙️ `await`, *➡ 🛠️ 🔢* 📣 ⏮️ `async`. + +### 👀 `response_model=List[Note]` + +⚫️ ⚙️ `typing.List`. + +👈 📄 (& ✔, 🎻, ⛽) 🔢 💽, `list` `Note`Ⓜ. + +## ✍ 🗒 + +✍ *➡ 🛠️ 🔢* ✍ 🗒: + +```Python hl_lines="61-65" +{!../../../docs_src/async_sql_databases/tutorial001.py!} +``` + +!!! Note + 👀 👈 👥 🔗 ⏮️ 💽 ⚙️ `await`, *➡ 🛠️ 🔢* 📣 ⏮️ `async`. + +### 🔃 `{**note.dict(), "id": last_record_id}` + +`note` Pydantic `Note` 🎚. + +`note.dict()` 📨 `dict` ⏮️ 🚮 💽, 🕳 💖: + +```Python +{ + "text": "Some note", + "completed": False, +} +``` + +✋️ ⚫️ 🚫 ✔️ `id` 🏑. + +👥 ✍ 🆕 `dict`, 👈 🔌 🔑-💲 👫 ⚪️➡️ `note.dict()` ⏮️: + +```Python +{**note.dict()} +``` + +`**note.dict()` "unpacks" the key value pairs directly, so, `{**note.dict()}` would be, more or less, a copy of `note.dict()`. + +& ⤴️, 👥 ↔ 👈 📁 `dict`, ❎ ➕1️⃣ 🔑-💲 👫: `"id": last_record_id`: + +```Python +{**note.dict(), "id": last_record_id} +``` + +, 🏁 🏁 📨 🔜 🕳 💖: + +```Python +{ + "id": 1, + "text": "Some note", + "completed": False, +} +``` + +## ✅ ⚫️ + +👆 💪 📁 👉 📟, & 👀 🩺 http://127.0.0.1:8000/docs. + +📤 👆 💪 👀 🌐 👆 🛠️ 📄 & 🔗 ⏮️ ⚫️: + + + +## 🌅 ℹ + +👆 💪 ✍ 🌅 🔃 `encode/databases` 🚮 📂 📃. diff --git a/docs/em/docs/advanced/async-tests.md b/docs/em/docs/advanced/async-tests.md new file mode 100644 index 000000000..df94c6ce7 --- /dev/null +++ b/docs/em/docs/advanced/async-tests.md @@ -0,0 +1,92 @@ +# 🔁 💯 + +👆 ✔️ ⏪ 👀 ❔ 💯 👆 **FastAPI** 🈸 ⚙️ 🚚 `TestClient`. 🆙 🔜, 👆 ✔️ 🕴 👀 ❔ ✍ 🔁 💯, 🍵 ⚙️ `async` 🔢. + +➖ 💪 ⚙️ 🔁 🔢 👆 💯 💪 ⚠, 🖼, 🕐❔ 👆 🔬 👆 💽 🔁. 🌈 👆 💚 💯 📨 📨 👆 FastAPI 🈸 & ⤴️ ✔ 👈 👆 👩‍💻 ⏪ ✍ ☑ 💽 💽, ⏪ ⚙️ 🔁 💽 🗃. + +➡️ 👀 ❔ 👥 💪 ⚒ 👈 👷. + +## pytest.mark.anyio + +🚥 👥 💚 🤙 🔁 🔢 👆 💯, 👆 💯 🔢 ✔️ 🔁. AnyIO 🚚 👌 📁 👉, 👈 ✔ 👥 ✔ 👈 💯 🔢 🤙 🔁. + +## 🇸🇲 + +🚥 👆 **FastAPI** 🈸 ⚙️ 😐 `def` 🔢 ↩️ `async def`, ⚫️ `async` 🈸 🔘. + +`TestClient` 🔨 🎱 🔘 🤙 🔁 FastAPI 🈸 👆 😐 `def` 💯 🔢, ⚙️ 🐩 ✳. ✋️ 👈 🎱 🚫 👷 🚫🔜 🕐❔ 👥 ⚙️ ⚫️ 🔘 🔁 🔢. 🏃 👆 💯 🔁, 👥 💪 🙅‍♂ 📏 ⚙️ `TestClient` 🔘 👆 💯 🔢. + +`TestClient` ⚓️ 🔛 🇸🇲, & ↩️, 👥 💪 ⚙️ ⚫️ 🔗 💯 🛠️. + +## 🖼 + +🙅 🖼, ➡️ 🤔 📁 📊 🎏 1️⃣ 🔬 [🦏 🈸](../tutorial/bigger-applications.md){.internal-link target=_blank} & [🔬](../tutorial/testing.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +📁 `main.py` 🔜 ✔️: + +```Python +{!../../../docs_src/async_tests/main.py!} +``` + +📁 `test_main.py` 🔜 ✔️ 💯 `main.py`, ⚫️ 💪 👀 💖 👉 🔜: + +```Python +{!../../../docs_src/async_tests/test_main.py!} +``` + +## 🏃 ⚫️ + +👆 💪 🏃 👆 💯 🐌 📨: + +
+ +```console +$ pytest + +---> 100% +``` + +
+ +## ℹ + +📑 `@pytest.mark.anyio` 💬 ✳ 👈 👉 💯 🔢 🔜 🤙 🔁: + +```Python hl_lines="7" +{!../../../docs_src/async_tests/test_main.py!} +``` + +!!! tip + 🗒 👈 💯 🔢 🔜 `async def` ↩️ `def` ⏭ 🕐❔ ⚙️ `TestClient`. + +⤴️ 👥 💪 ✍ `AsyncClient` ⏮️ 📱, & 📨 🔁 📨 ⚫️, ⚙️ `await`. + +```Python hl_lines="9-10" +{!../../../docs_src/async_tests/test_main.py!} +``` + +👉 🌓: + +```Python +response = client.get('/') +``` + +...👈 👥 ⚙️ ⚒ 👆 📨 ⏮️ `TestClient`. + +!!! tip + 🗒 👈 👥 ⚙️ 🔁/⌛ ⏮️ 🆕 `AsyncClient` - 📨 🔁. + +## 🎏 🔁 🔢 🤙 + +🔬 🔢 🔜 🔁, 👆 💪 🔜 🤙 (& `await`) 🎏 `async` 🔢 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 👆 💯, ⚫️❔ 👆 🔜 🤙 👫 🙆 🙆 👆 📟. + +!!! tip + 🚥 👆 ⚔ `RuntimeError: Task attached to a different loop` 🕐❔ 🛠️ 🔁 🔢 🤙 👆 💯 (✅ 🕐❔ ⚙️ ✳ MotorClient) 💭 🔗 🎚 👈 💪 🎉 ➰ 🕴 🏞 🔁 🔢, ✅ `'@app.on_event("startup")` ⏲. diff --git a/docs/em/docs/advanced/behind-a-proxy.md b/docs/em/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..12afe638c --- /dev/null +++ b/docs/em/docs/advanced/behind-a-proxy.md @@ -0,0 +1,346 @@ +# ⛅ 🗳 + +⚠, 👆 5️⃣📆 💪 ⚙️ **🗳** 💽 💖 Traefik ⚖️ 👌 ⏮️ 📳 👈 🚮 ➕ ➡ 🔡 👈 🚫 👀 👆 🈸. + +👫 💼 👆 💪 ⚙️ `root_path` 🔗 👆 🈸. + +`root_path` 🛠️ 🚚 🔫 🔧 (👈 FastAPI 🏗 🔛, 🔘 💃). + +`root_path` ⚙️ 🍵 👫 🎯 💼. + +& ⚫️ ⚙️ 🔘 🕐❔ 🗜 🎧-🈸. + +## 🗳 ⏮️ 🎞 ➡ 🔡 + +✔️ 🗳 ⏮️ 🎞 ➡ 🔡, 👉 💼, ⛓ 👈 👆 💪 📣 ➡ `/app` 👆 📟, ✋️ ⤴️, 👆 🚮 🧽 🔛 🔝 (🗳) 👈 🔜 🚮 👆 **FastAPI** 🈸 🔽 ➡ 💖 `/api/v1`. + +👉 💼, ⏮️ ➡ `/app` 🔜 🤙 🍦 `/api/v1/app`. + +✋️ 🌐 👆 📟 ✍ 🤔 📤 `/app`. + +& 🗳 🔜 **"❎"** **➡ 🔡** 🔛 ✈ ⏭ 📶 📨 Uvicorn, 🚧 👆 🈸 🤔 👈 ⚫️ 🍦 `/app`, 👈 👆 🚫 ✔️ ℹ 🌐 👆 📟 🔌 🔡 `/api/v1`. + +🆙 📥, 🌐 🔜 👷 🛎. + +✋️ ⤴️, 🕐❔ 👆 📂 🛠️ 🩺 🎚 (🕸), ⚫️ 🔜 ⌛ 🤚 🗄 🔗 `/openapi.json`, ↩️ `/api/v1/openapi.json`. + +, 🕸 (👈 🏃 🖥) 🔜 🔄 🏆 `/openapi.json` & 🚫🔜 💪 🤚 🗄 🔗. + +↩️ 👥 ✔️ 🗳 ⏮️ ➡ 🔡 `/api/v1` 👆 📱, 🕸 💪 ☕ 🗄 🔗 `/api/v1/openapi.json`. + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +!!! tip + 📢 `0.0.0.0` 🛎 ⚙️ ⛓ 👈 📋 👂 🔛 🌐 📢 💪 👈 🎰/💽. + +🩺 🎚 🔜 💪 🗄 🔗 📣 👈 👉 🛠️ `server` 🔎 `/api/v1` (⛅ 🗳). 🖼: + +```JSON hl_lines="4-8" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +👉 🖼, "🗳" 💪 🕳 💖 **Traefik**. & 💽 🔜 🕳 💖 **Uvicorn**, 🏃‍♂ 👆 FastAPI 🈸. + +### 🚚 `root_path` + +🏆 👉, 👆 💪 ⚙️ 📋 ⏸ 🎛 `--root-path` 💖: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +🚥 👆 ⚙️ Hypercorn, ⚫️ ✔️ 🎛 `--root-path`. + +!!! note "📡 ℹ" + 🔫 🔧 🔬 `root_path` 👉 ⚙️ 💼. + + & `--root-path` 📋 ⏸ 🎛 🚚 👈 `root_path`. + +### ✅ ⏮️ `root_path` + +👆 💪 🤚 ⏮️ `root_path` ⚙️ 👆 🈸 🔠 📨, ⚫️ 🍕 `scope` 📖 (👈 🍕 🔫 🔌). + +📥 👥 ✅ ⚫️ 📧 🎦 🎯. + +```Python hl_lines="8" +{!../../../docs_src/behind_a_proxy/tutorial001.py!} +``` + +⤴️, 🚥 👆 ▶️ Uvicorn ⏮️: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📨 🔜 🕳 💖: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### ⚒ `root_path` FastAPI 📱 + +👐, 🚥 👆 🚫 ✔️ 🌌 🚚 📋 ⏸ 🎛 💖 `--root-path` ⚖️ 🌓, 👆 💪 ⚒ `root_path` 🔢 🕐❔ 🏗 👆 FastAPI 📱: + +```Python hl_lines="3" +{!../../../docs_src/behind_a_proxy/tutorial002.py!} +``` + +🚶‍♀️ `root_path` `FastAPI` 🔜 🌓 🚶‍♀️ `--root-path` 📋 ⏸ 🎛 Uvicorn ⚖️ Hypercorn. + +### 🔃 `root_path` + +✔️ 🤯 👈 💽 (Uvicorn) 🏆 🚫 ⚙️ 👈 `root_path` 🕳 🙆 🌘 🚶‍♀️ ⚫️ 📱. + +✋️ 🚥 👆 🚶 ⏮️ 👆 🖥 http://127.0.0.1:8000/app 👆 🔜 👀 😐 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +, ⚫️ 🏆 🚫 ⌛ 🔐 `http://127.0.0.1:8000/api/v1/app`. + +Uvicorn 🔜 ⌛ 🗳 🔐 Uvicorn `http://127.0.0.1:8000/app`, & ⤴️ ⚫️ 🔜 🗳 🎯 🚮 ➕ `/api/v1` 🔡 🔛 🔝. + +## 🔃 🗳 ⏮️ 🎞 ➡ 🔡 + +✔️ 🤯 👈 🗳 ⏮️ 🎞 ➡ 🔡 🕴 1️⃣ 🌌 🔗 ⚫️. + +🎲 📚 💼 🔢 🔜 👈 🗳 🚫 ✔️ 🏚 ➡ 🔡. + +💼 💖 👈 (🍵 🎞 ➡ 🔡), 🗳 🔜 👂 🔛 🕳 💖 `https://myawesomeapp.com`, & ⤴️ 🚥 🖥 🚶 `https://myawesomeapp.com/api/v1/app` & 👆 💽 (✅ Uvicorn) 👂 🔛 `http://127.0.0.1:8000` 🗳 (🍵 🎞 ➡ 🔡) 🔜 🔐 Uvicorn 🎏 ➡: `http://127.0.0.1:8000/api/v1/app`. + +## 🔬 🌐 ⏮️ Traefik + +👆 💪 💪 🏃 🥼 🌐 ⏮️ 🎞 ➡ 🔡 ⚙️ Traefik. + +⏬ Traefik, ⚫️ 👁 💱, 👆 💪 ⚗ 🗜 📁 & 🏃 ⚫️ 🔗 ⚪️➡️ 📶. + +⤴️ ✍ 📁 `traefik.toml` ⏮️: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +👉 💬 Traefik 👂 🔛 ⛴ 9️⃣9️⃣9️⃣9️⃣ & ⚙️ ➕1️⃣ 📁 `routes.toml`. + +!!! tip + 👥 ⚙️ ⛴ 9️⃣9️⃣9️⃣9️⃣ ↩️ 🐩 🇺🇸🔍 ⛴ 8️⃣0️⃣ 👈 👆 🚫 ✔️ 🏃 ⚫️ ⏮️ 📡 (`sudo`) 😌. + +🔜 ✍ 👈 🎏 📁 `routes.toml`: + +```TOML hl_lines="5 12 20" +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api/v1"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api/v1`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +👉 📁 🔗 Traefik ⚙️ ➡ 🔡 `/api/v1`. + +& ⤴️ ⚫️ 🔜 ❎ 🚮 📨 👆 Uvicorn 🏃‍♂ 🔛 `http://127.0.0.1:8000`. + +🔜 ▶️ Traefik: + +
+ +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +
+ +& 🔜 ▶️ 👆 📱 ⏮️ Uvicorn, ⚙️ `--root-path` 🎛: + +
+ +```console +$ uvicorn main:app --root-path /api/v1 + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### ✅ 📨 + +🔜, 🚥 👆 🚶 📛 ⏮️ ⛴ Uvicorn: http://127.0.0.1:8000/app, 👆 🔜 👀 😐 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +!!! tip + 👀 👈 ✋️ 👆 🔐 ⚫️ `http://127.0.0.1:8000/app` ⚫️ 🎦 `root_path` `/api/v1`, ✊ ⚪️➡️ 🎛 `--root-path`. + +& 🔜 📂 📛 ⏮️ ⛴ Traefik, ✅ ➡ 🔡: http://127.0.0.1:9999/api/v1/app. + +👥 🤚 🎏 📨: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +✋️ 👉 🕰 📛 ⏮️ 🔡 ➡ 🚚 🗳: `/api/v1`. + +↗️, 💭 📥 👈 👱 🔜 🔐 📱 🔘 🗳, ⏬ ⏮️ ➡ 🔡 `/app/v1` "☑" 1️⃣. + +& ⏬ 🍵 ➡ 🔡 (`http://127.0.0.1:8000/app`), 🚚 Uvicorn 🔗, 🔜 🎯 _🗳_ (Traefik) 🔐 ⚫️. + +👈 🎦 ❔ 🗳 (Traefik) ⚙️ ➡ 🔡 & ❔ 💽 (Uvicorn) ⚙️ `root_path` ⚪️➡️ 🎛 `--root-path`. + +### ✅ 🩺 🎚 + +✋️ 📥 🎊 🍕. 👶 + +"🛂" 🌌 🔐 📱 🔜 🔘 🗳 ⏮️ ➡ 🔡 👈 👥 🔬. , 👥 🔜 ⌛, 🚥 👆 🔄 🩺 🎚 🍦 Uvicorn 🔗, 🍵 ➡ 🔡 📛, ⚫️ 🏆 🚫 👷, ↩️ ⚫️ ⌛ 🔐 🔘 🗳. + +👆 💪 ✅ ⚫️ http://127.0.0.1:8000/docs: + + + +✋️ 🚥 👥 🔐 🩺 🎚 "🛂" 📛 ⚙️ 🗳 ⏮️ ⛴ `9999`, `/api/v1/docs`, ⚫️ 👷 ☑ ❗ 👶 + +👆 💪 ✅ ⚫️ http://127.0.0.1:9999/api/v1/docs: + + + +▶️️ 👥 💚 ⚫️. 👶 👶 + +👉 ↩️ FastAPI ⚙️ 👉 `root_path` ✍ 🔢 `server` 🗄 ⏮️ 📛 🚚 `root_path`. + +## 🌖 💽 + +!!! warning + 👉 🌅 🏧 ⚙️ 💼. 💭 🆓 🚶 ⚫️. + +🔢, **FastAPI** 🔜 ✍ `server` 🗄 🔗 ⏮️ 📛 `root_path`. + +✋️ 👆 💪 🚚 🎏 🎛 `servers`, 🖼 🚥 👆 💚 *🎏* 🩺 🎚 🔗 ⏮️ 🏗 & 🏭 🌐. + +🚥 👆 🚶‍♀️ 🛃 📇 `servers` & 📤 `root_path` (↩️ 👆 🛠️ 👨‍❤‍👨 ⛅ 🗳), **FastAPI** 🔜 📩 "💽" ⏮️ 👉 `root_path` ▶️ 📇. + +🖼: + +```Python hl_lines="4-7" +{!../../../docs_src/behind_a_proxy/tutorial003.py!} +``` + +🔜 🏗 🗄 🔗 💖: + +```JSON hl_lines="5-7" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + }, + { + "url": "https://stag.example.com", + "description": "Staging environment" + }, + { + "url": "https://prod.example.com", + "description": "Production environment" + } + ], + "paths": { + // More stuff here + } +} +``` + +!!! tip + 👀 🚘-🏗 💽 ⏮️ `url` 💲 `/api/v1`, ✊ ⚪️➡️ `root_path`. + +🩺 🎚 http://127.0.0.1:9999/api/v1/docs ⚫️ 🔜 👀 💖: + + + +!!! tip + 🩺 🎚 🔜 🔗 ⏮️ 💽 👈 👆 🖊. + +### ❎ 🏧 💽 ⚪️➡️ `root_path` + +🚥 👆 🚫 💚 **FastAPI** 🔌 🏧 💽 ⚙️ `root_path`, 👆 💪 ⚙️ 🔢 `root_path_in_servers=False`: + +```Python hl_lines="9" +{!../../../docs_src/behind_a_proxy/tutorial004.py!} +``` + +& ⤴️ ⚫️ 🏆 🚫 🔌 ⚫️ 🗄 🔗. + +## 🗜 🎧-🈸 + +🚥 👆 💪 🗻 🎧-🈸 (🔬 [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}) ⏪ ⚙️ 🗳 ⏮️ `root_path`, 👆 💪 ⚫️ 🛎, 👆 🔜 ⌛. + +FastAPI 🔜 🔘 ⚙️ `root_path` 🎆, ⚫️ 🔜 👷. 👶 diff --git a/docs/em/docs/advanced/conditional-openapi.md b/docs/em/docs/advanced/conditional-openapi.md new file mode 100644 index 000000000..a17ba4eec --- /dev/null +++ b/docs/em/docs/advanced/conditional-openapi.md @@ -0,0 +1,58 @@ +# 🎲 🗄 + +🚥 👆 💪, 👆 💪 ⚙️ ⚒ & 🌐 🔢 🔗 🗄 ✔ ⚓️ 🔛 🌐, & ❎ ⚫️ 🍕. + +## 🔃 💂‍♂, 🔗, & 🩺 + +🕵‍♂ 👆 🧾 👩‍💻 🔢 🏭 *🚫🔜 🚫* 🌌 🛡 👆 🛠️. + +👈 🚫 🚮 🙆 ➕ 💂‍♂ 👆 🛠️, *➡ 🛠️* 🔜 💪 🌐❔ 👫. + +🚥 📤 💂‍♂ ⚠ 👆 📟, ⚫️ 🔜 🔀. + +🕵‍♂ 🧾 ⚒ ⚫️ 🌅 ⚠ 🤔 ❔ 🔗 ⏮️ 👆 🛠️, & 💪 ⚒ ⚫️ 🌅 ⚠ 👆 ℹ ⚫️ 🏭. ⚫️ 💪 🤔 🎯 📨 💂‍♂ 🔘 🌌. + +🚥 👆 💚 🔐 👆 🛠️, 📤 📚 👍 👜 👆 💪, 🖼: + +* ⚒ 💭 👆 ✔️ 👍 🔬 Pydantic 🏷 👆 📨 💪 & 📨. +* 🔗 🙆 ✔ ✔ & 🔑 ⚙️ 🔗. +* 🙅 🏪 🔢 🔐, 🕴 🔐#️⃣. +* 🛠️ & ⚙️ 👍-💭 🔐 🧰, 💖 🇸🇲 & 🥙 🤝, ♒️. +* 🚮 🌅 🧽 ✔ 🎛 ⏮️ Oauth2️⃣ ↔ 🌐❔ 💪. +* ...♒️. + +👐, 👆 5️⃣📆 ✔️ 📶 🎯 ⚙️ 💼 🌐❔ 👆 🤙 💪 ❎ 🛠️ 🩺 🌐 (✅ 🏭) ⚖️ ⚓️ 🔛 📳 ⚪️➡️ 🌐 🔢. + +## 🎲 🗄 ⚪️➡️ ⚒ & 🇨🇻 { + +👆 💪 💪 ⚙️ 🎏 Pydantic ⚒ 🔗 👆 🏗 🗄 & 🩺 ⚜. + +🖼: + +```Python hl_lines="6 11" +{!../../../docs_src/conditional_openapi/tutorial001.py!} +``` + +📥 👥 📣 ⚒ `openapi_url` ⏮️ 🎏 🔢 `"/openapi.json"`. + +& ⤴️ 👥 ⚙️ ⚫️ 🕐❔ 🏗 `FastAPI` 📱. + +⤴️ 👆 💪 ❎ 🗄 (✅ 🎚 🩺) ⚒ 🌐 🔢 `OPENAPI_URL` 🛁 🎻, 💖: + +
+ +```console +$ OPENAPI_URL= uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +⤴️ 🚥 👆 🚶 📛 `/openapi.json`, `/docs`, ⚖️ `/redoc` 👆 🔜 🤚 `404 Not Found` ❌ 💖: + +```JSON +{ + "detail": "Not Found" +} +``` diff --git a/docs/em/docs/advanced/custom-request-and-route.md b/docs/em/docs/advanced/custom-request-and-route.md new file mode 100644 index 000000000..d6fafa2ea --- /dev/null +++ b/docs/em/docs/advanced/custom-request-and-route.md @@ -0,0 +1,109 @@ +# 🛃 📨 & APIRoute 🎓 + +💼, 👆 5️⃣📆 💚 🔐 ⚛ ⚙️ `Request` & `APIRoute` 🎓. + +🎯, 👉 5️⃣📆 👍 🎛 ⚛ 🛠️. + +🖼, 🚥 👆 💚 ✍ ⚖️ 🔬 📨 💪 ⏭ ⚫️ 🛠️ 👆 🈸. + +!!! danger + 👉 "🏧" ⚒. + + 🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 👉 📄. + +## ⚙️ 💼 + +⚙️ 💼 🔌: + +* 🏭 🚫-🎻 📨 💪 🎻 (✅ `msgpack`). +* 🗜 🗜-🗜 📨 💪. +* 🔁 🚨 🌐 📨 💪. + +## 🚚 🛃 📨 💪 🔢 + +➡️ 👀 ❔ ⚒ ⚙️ 🛃 `Request` 🏿 🗜 🗜 📨. + +& `APIRoute` 🏿 ⚙️ 👈 🛃 📨 🎓. + +### ✍ 🛃 `GzipRequest` 🎓 + +!!! tip + 👉 🧸 🖼 🎦 ❔ ⚫️ 👷, 🚥 👆 💪 🗜 🐕‍🦺, 👆 💪 ⚙️ 🚚 [`GzipMiddleware`](./middleware.md#gzipmiddleware){.internal-link target=_blank}. + +🥇, 👥 ✍ `GzipRequest` 🎓, ❔ 🔜 📁 `Request.body()` 👩‍🔬 🗜 💪 🔍 ☑ 🎚. + +🚥 📤 🙅‍♂ `gzip` 🎚, ⚫️ 🔜 🚫 🔄 🗜 💪. + +👈 🌌, 🎏 🛣 🎓 💪 🍵 🗜 🗜 ⚖️ 🗜 📨. + +```Python hl_lines="8-15" +{!../../../docs_src/custom_request_and_route/tutorial001.py!} +``` + +### ✍ 🛃 `GzipRoute` 🎓 + +⏭, 👥 ✍ 🛃 🏿 `fastapi.routing.APIRoute` 👈 🔜 ⚒ ⚙️ `GzipRequest`. + +👉 🕰, ⚫️ 🔜 📁 👩‍🔬 `APIRoute.get_route_handler()`. + +👉 👩‍🔬 📨 🔢. & 👈 🔢 ⚫️❔ 🔜 📨 📨 & 📨 📨. + +📥 👥 ⚙️ ⚫️ ✍ `GzipRequest` ⚪️➡️ ⏮️ 📨. + +```Python hl_lines="18-26" +{!../../../docs_src/custom_request_and_route/tutorial001.py!} +``` + +!!! note "📡 ℹ" + `Request` ✔️ `request.scope` 🔢, 👈 🐍 `dict` ⚗ 🗃 🔗 📨. + + `Request` ✔️ `request.receive`, 👈 🔢 "📨" 💪 📨. + + `scope` `dict` & `receive` 🔢 👯‍♂️ 🍕 🔫 🔧. + + & 👈 2️⃣ 👜, `scope` & `receive`, ⚫️❔ 💪 ✍ 🆕 `Request` 👐. + + 💡 🌅 🔃 `Request` ✅ 💃 🩺 🔃 📨. + +🕴 👜 🔢 📨 `GzipRequest.get_route_handler` 🔨 🎏 🗜 `Request` `GzipRequest`. + +🔨 👉, 👆 `GzipRequest` 🔜 ✊ 💅 🗜 📊 (🚥 💪) ⏭ 🚶‍♀️ ⚫️ 👆 *➡ 🛠️*. + +⏮️ 👈, 🌐 🏭 ⚛ 🎏. + +✋️ ↩️ 👆 🔀 `GzipRequest.body`, 📨 💪 🔜 🔁 🗜 🕐❔ ⚫️ 📐 **FastAPI** 🕐❔ 💪. + +## 🔐 📨 💪 ⚠ 🐕‍🦺 + +!!! tip + ❎ 👉 🎏 ⚠, ⚫️ 🎲 📚 ⏩ ⚙️ `body` 🛃 🐕‍🦺 `RequestValidationError` ([🚚 ❌](../tutorial/handling-errors.md#use-the-requestvalidationerror-body){.internal-link target=_blank}). + + ✋️ 👉 🖼 ☑ & ⚫️ 🎦 ❔ 🔗 ⏮️ 🔗 🦲. + +👥 💪 ⚙️ 👉 🎏 🎯 🔐 📨 💪 ⚠ 🐕‍🦺. + +🌐 👥 💪 🍵 📨 🔘 `try`/`except` 🍫: + +```Python hl_lines="13 15" +{!../../../docs_src/custom_request_and_route/tutorial002.py!} +``` + +🚥 ⚠ 📉, `Request` 👐 🔜 ↔, 👥 💪 ✍ & ⚒ ⚙️ 📨 💪 🕐❔ 🚚 ❌: + +```Python hl_lines="16-18" +{!../../../docs_src/custom_request_and_route/tutorial002.py!} +``` + +## 🛃 `APIRoute` 🎓 📻 + +👆 💪 ⚒ `route_class` 🔢 `APIRouter`: + +```Python hl_lines="26" +{!../../../docs_src/custom_request_and_route/tutorial003.py!} +``` + +👉 🖼, *➡ 🛠️* 🔽 `router` 🔜 ⚙️ 🛃 `TimedRoute` 🎓, & 🔜 ✔️ ➕ `X-Response-Time` 🎚 📨 ⏮️ 🕰 ⚫️ ✊ 🏗 📨: + +```Python hl_lines="13-20" +{!../../../docs_src/custom_request_and_route/tutorial003.py!} +``` diff --git a/docs/em/docs/advanced/custom-response.md b/docs/em/docs/advanced/custom-response.md new file mode 100644 index 000000000..cf76c01d0 --- /dev/null +++ b/docs/em/docs/advanced/custom-response.md @@ -0,0 +1,300 @@ +# 🛃 📨 - 🕸, 🎏, 📁, 🎏 + +🔢, **FastAPI** 🔜 📨 📨 ⚙️ `JSONResponse`. + +👆 💪 🔐 ⚫️ 🛬 `Response` 🔗 👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. + +✋️ 🚥 👆 📨 `Response` 🔗, 📊 🏆 🚫 🔁 🗜, & 🧾 🏆 🚫 🔁 🏗 (🖼, 🔌 🎯 "📻 🆎", 🇺🇸🔍 🎚 `Content-Type` 🍕 🏗 🗄). + +✋️ 👆 💪 📣 `Response` 👈 👆 💚 ⚙️, *➡ 🛠️ 👨‍🎨*. + +🎚 👈 👆 📨 ⚪️➡️ 👆 *➡ 🛠️ 🔢* 🔜 🚮 🔘 👈 `Response`. + +& 🚥 👈 `Response` ✔️ 🎻 📻 🆎 (`application/json`), 💖 💼 ⏮️ `JSONResponse` & `UJSONResponse`, 💽 👆 📨 🔜 🔁 🗜 (& ⛽) ⏮️ 🙆 Pydantic `response_model` 👈 👆 📣 *➡ 🛠️ 👨‍🎨*. + +!!! note + 🚥 👆 ⚙️ 📨 🎓 ⏮️ 🙅‍♂ 📻 🆎, FastAPI 🔜 ⌛ 👆 📨 ✔️ 🙅‍♂ 🎚, ⚫️ 🔜 🚫 📄 📨 📁 🚮 🏗 🗄 🩺. + +## ⚙️ `ORJSONResponse` + +🖼, 🚥 👆 ✊ 🎭, 👆 💪 ❎ & ⚙️ `orjson` & ⚒ 📨 `ORJSONResponse`. + +🗄 `Response` 🎓 (🎧-🎓) 👆 💚 ⚙️ & 📣 ⚫️ *➡ 🛠️ 👨‍🎨*. + +⭕ 📨, 📨 `Response` 🔗 🌅 ⏩ 🌘 🛬 📖. + +👉 ↩️ 🔢, FastAPI 🔜 ✔ 🔠 🏬 🔘 & ⚒ 💭 ⚫️ 🎻 ⏮️ 🎻, ⚙️ 🎏 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank} 🔬 🔰. 👉 ⚫️❔ ✔ 👆 📨 **❌ 🎚**, 🖼 💽 🏷. + +✋️ 🚥 👆 🎯 👈 🎚 👈 👆 🛬 **🎻 ⏮️ 🎻**, 👆 💪 🚶‍♀️ ⚫️ 🔗 📨 🎓 & ❎ ➕ 🌥 👈 FastAPI 🔜 ✔️ 🚶‍♀️ 👆 📨 🎚 🔘 `jsonable_encoder` ⏭ 🚶‍♀️ ⚫️ 📨 🎓. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001b.py!} +``` + +!!! info + 🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. + + 👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `application/json`. + + & ⚫️ 🔜 📄 ✅ 🗄. + +!!! tip + `ORJSONResponse` ⏳ 🕴 💪 FastAPI, 🚫 💃. + +## 🕸 📨 + +📨 📨 ⏮️ 🕸 🔗 ⚪️➡️ **FastAPI**, ⚙️ `HTMLResponse`. + +* 🗄 `HTMLResponse`. +* 🚶‍♀️ `HTMLResponse` 🔢 `response_class` 👆 *➡ 🛠️ 👨‍🎨*. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial002.py!} +``` + +!!! info + 🔢 `response_class` 🔜 ⚙️ 🔬 "📻 🆎" 📨. + + 👉 💼, 🇺🇸🔍 🎚 `Content-Type` 🔜 ⚒ `text/html`. + + & ⚫️ 🔜 📄 ✅ 🗄. + +### 📨 `Response` + +👀 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}, 👆 💪 🔐 📨 🔗 👆 *➡ 🛠️*, 🛬 ⚫️. + +🎏 🖼 ⚪️➡️ 🔛, 🛬 `HTMLResponse`, 💪 👀 💖: + +```Python hl_lines="2 7 19" +{!../../../docs_src/custom_response/tutorial003.py!} +``` + +!!! warning + `Response` 📨 🔗 👆 *➡ 🛠️ 🔢* 🏆 🚫 📄 🗄 (🖼, `Content-Type` 🏆 🚫 📄) & 🏆 🚫 ⭐ 🏧 🎓 🩺. + +!!! info + ↗️, ☑ `Content-Type` 🎚, 👔 📟, ♒️, 🔜 👟 ⚪️➡️ `Response` 🎚 👆 📨. + +### 📄 🗄 & 🔐 `Response` + +🚥 👆 💚 🔐 📨 ⚪️➡️ 🔘 🔢 ✋️ 🎏 🕰 📄 "📻 🆎" 🗄, 👆 💪 ⚙️ `response_class` 🔢 & 📨 `Response` 🎚. + +`response_class` 🔜 ⤴️ ⚙️ 🕴 📄 🗄 *➡ 🛠️*, ✋️ 👆 `Response` 🔜 ⚙️. + +#### 📨 `HTMLResponse` 🔗 + +🖼, ⚫️ 💪 🕳 💖: + +```Python hl_lines="7 21 23" +{!../../../docs_src/custom_response/tutorial004.py!} +``` + +👉 🖼, 🔢 `generate_html_response()` ⏪ 🏗 & 📨 `Response` ↩️ 🛬 🕸 `str`. + +🛬 🏁 🤙 `generate_html_response()`, 👆 ⏪ 🛬 `Response` 👈 🔜 🔐 🔢 **FastAPI** 🎭. + +✋️ 👆 🚶‍♀️ `HTMLResponse` `response_class` 💁‍♂️, **FastAPI** 🔜 💭 ❔ 📄 ⚫️ 🗄 & 🎓 🩺 🕸 ⏮️ `text/html`: + + + +## 💪 📨 + +📥 💪 📨. + +✔️ 🤯 👈 👆 💪 ⚙️ `Response` 📨 🕳 🙆, ⚖️ ✍ 🛃 🎧-🎓. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### `Response` + +👑 `Response` 🎓, 🌐 🎏 📨 😖 ⚪️➡️ ⚫️. + +👆 💪 📨 ⚫️ 🔗. + +⚫️ 🚫 📄 🔢: + +* `content` - `str` ⚖️ `bytes`. +* `status_code` - `int` 🇺🇸🔍 👔 📟. +* `headers` - `dict` 🎻. +* `media_type` - `str` 🤝 📻 🆎. 🤶 Ⓜ. `"text/html"`. + +FastAPI (🤙 💃) 🔜 🔁 🔌 🎚-📐 🎚. ⚫️ 🔜 🔌 🎚-🆎 🎚, ⚓️ 🔛 = & 🔁 = ✍ 🆎. + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +### `HTMLResponse` + +✊ ✍ ⚖️ 🔢 & 📨 🕸 📨, 👆 ✍ 🔛. + +### `PlainTextResponse` + +✊ ✍ ⚖️ 🔢 & 📨 ✅ ✍ 📨. + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial005.py!} +``` + +### `JSONResponse` + +✊ 💽 & 📨 `application/json` 🗜 📨. + +👉 🔢 📨 ⚙️ **FastAPI**, 👆 ✍ 🔛. + +### `ORJSONResponse` + +⏩ 🎛 🎻 📨 ⚙️ `orjson`, 👆 ✍ 🔛. + +### `UJSONResponse` + +🎛 🎻 📨 ⚙️ `ujson`. + +!!! warning + `ujson` 🌘 💛 🌘 🐍 🏗-🛠️ ❔ ⚫️ 🍵 📐-💼. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001.py!} +``` + +!!! tip + ⚫️ 💪 👈 `ORJSONResponse` 💪 ⏩ 🎛. + +### `RedirectResponse` + +📨 🇺🇸🔍 ❎. ⚙️ 3️⃣0️⃣7️⃣ 👔 📟 (🍕 ❎) 🔢. + +👆 💪 📨 `RedirectResponse` 🔗: + +```Python hl_lines="2 9" +{!../../../docs_src/custom_response/tutorial006.py!} +``` + +--- + +⚖️ 👆 💪 ⚙️ ⚫️ `response_class` 🔢: + + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006b.py!} +``` + +🚥 👆 👈, ⤴️ 👆 💪 📨 📛 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. + +👉 💼, `status_code` ⚙️ 🔜 🔢 1️⃣ `RedirectResponse`, ❔ `307`. + +--- + +👆 💪 ⚙️ `status_code` 🔢 🌀 ⏮️ `response_class` 🔢: + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006c.py!} +``` + +### `StreamingResponse` + +✊ 🔁 🚂 ⚖️ 😐 🚂/🎻 & 🎏 📨 💪. + +```Python hl_lines="2 14" +{!../../../docs_src/custom_response/tutorial007.py!} +``` + +#### ⚙️ `StreamingResponse` ⏮️ 📁-💖 🎚 + +🚥 👆 ✔️ 📁-💖 🎚 (✅ 🎚 📨 `open()`), 👆 💪 ✍ 🚂 🔢 🔁 🤭 👈 📁-💖 🎚. + +👈 🌌, 👆 🚫 ✔️ ✍ ⚫️ 🌐 🥇 💾, & 👆 💪 🚶‍♀️ 👈 🚂 🔢 `StreamingResponse`, & 📨 ⚫️. + +👉 🔌 📚 🗃 🔗 ⏮️ ☁ 💾, 📹 🏭, & 🎏. + +```{ .python .annotate hl_lines="2 10-12 14" } +{!../../../docs_src/custom_response/tutorial008.py!} +``` + +1️⃣. 👉 🚂 🔢. ⚫️ "🚂 🔢" ↩️ ⚫️ 🔌 `yield` 📄 🔘. +2️⃣. ⚙️ `with` 🍫, 👥 ⚒ 💭 👈 📁-💖 🎚 📪 ⏮️ 🚂 🔢 🔨. , ⏮️ ⚫️ 🏁 📨 📨. +3️⃣. 👉 `yield from` 💬 🔢 🔁 🤭 👈 👜 🌟 `file_like`. & ⤴️, 🔠 🍕 🔁, 🌾 👈 🍕 👟 ⚪️➡️ 👉 🚂 🔢. + + , ⚫️ 🚂 🔢 👈 📨 "🏭" 👷 🕳 🙆 🔘. + + 🔨 ⚫️ 👉 🌌, 👥 💪 🚮 ⚫️ `with` 🍫, & 👈 🌌, 🚚 👈 ⚫️ 📪 ⏮️ 🏁. + +!!! tip + 👀 👈 📥 👥 ⚙️ 🐩 `open()` 👈 🚫 🐕‍🦺 `async` & `await`, 👥 📣 ➡ 🛠️ ⏮️ 😐 `def`. + +### `FileResponse` + +🔁 🎏 📁 📨. + +✊ 🎏 ⚒ ❌ 🔗 🌘 🎏 📨 🆎: + +* `path` - 📁 📁 🎏. +* `headers` - 🙆 🛃 🎚 🔌, 📖. +* `media_type` - 🎻 🤝 📻 🆎. 🚥 🔢, 📁 ⚖️ ➡ 🔜 ⚙️ 🔑 📻 🆎. +* `filename` - 🚥 ⚒, 👉 🔜 🔌 📨 `Content-Disposition`. + +📁 📨 🔜 🔌 ☑ `Content-Length`, `Last-Modified` & `ETag` 🎚. + +```Python hl_lines="2 10" +{!../../../docs_src/custom_response/tutorial009.py!} +``` + +👆 💪 ⚙️ `response_class` 🔢: + +```Python hl_lines="2 8 10" +{!../../../docs_src/custom_response/tutorial009b.py!} +``` + +👉 💼, 👆 💪 📨 📁 ➡ 🔗 ⚪️➡️ 👆 *➡ 🛠️* 🔢. + +## 🛃 📨 🎓 + +👆 💪 ✍ 👆 👍 🛃 📨 🎓, 😖 ⚪️➡️ `Response` & ⚙️ ⚫️. + +🖼, ➡️ 💬 👈 👆 💚 ⚙️ `orjson`, ✋️ ⏮️ 🛃 ⚒ 🚫 ⚙️ 🔌 `ORJSONResponse` 🎓. + +➡️ 💬 👆 💚 ⚫️ 📨 🔂 & 📁 🎻, 👆 💚 ⚙️ Orjson 🎛 `orjson.OPT_INDENT_2`. + +👆 💪 ✍ `CustomORJSONResponse`. 👑 👜 👆 ✔️ ✍ `Response.render(content)` 👩‍🔬 👈 📨 🎚 `bytes`: + +```Python hl_lines="9-14 17" +{!../../../docs_src/custom_response/tutorial009c.py!} +``` + +🔜 ↩️ 🛬: + +```json +{"message": "Hello World"} +``` + +...👉 📨 🔜 📨: + +```json +{ + "message": "Hello World" +} +``` + +↗️, 👆 🔜 🎲 🔎 🌅 👍 🌌 ✊ 📈 👉 🌘 ❕ 🎻. 👶 + +## 🔢 📨 🎓 + +🕐❔ 🏗 **FastAPI** 🎓 👐 ⚖️ `APIRouter` 👆 💪 ✔ ❔ 📨 🎓 ⚙️ 🔢. + +🔢 👈 🔬 👉 `default_response_class`. + +🖼 🔛, **FastAPI** 🔜 ⚙️ `ORJSONResponse` 🔢, 🌐 *➡ 🛠️*, ↩️ `JSONResponse`. + +```Python hl_lines="2 4" +{!../../../docs_src/custom_response/tutorial010.py!} +``` + +!!! tip + 👆 💪 🔐 `response_class` *➡ 🛠️* ⏭. + +## 🌖 🧾 + +👆 💪 📣 📻 🆎 & 📚 🎏 ℹ 🗄 ⚙️ `responses`: [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/dataclasses.md b/docs/em/docs/advanced/dataclasses.md new file mode 100644 index 000000000..a4c287106 --- /dev/null +++ b/docs/em/docs/advanced/dataclasses.md @@ -0,0 +1,98 @@ +# ⚙️ 🎻 + +FastAPI 🏗 🔛 🔝 **Pydantic**, & 👤 ✔️ 🌏 👆 ❔ ⚙️ Pydantic 🏷 📣 📨 & 📨. + +✋️ FastAPI 🐕‍🦺 ⚙️ `dataclasses` 🎏 🌌: + +```Python hl_lines="1 7-12 19-20" +{!../../../docs_src/dataclasses/tutorial001.py!} +``` + +👉 🐕‍🦺 👏 **Pydantic**, ⚫️ ✔️ 🔗 🐕‍🦺 `dataclasses`. + +, ⏮️ 📟 🔛 👈 🚫 ⚙️ Pydantic 🎯, FastAPI ⚙️ Pydantic 🗜 📚 🐩 🎻 Pydantic 👍 🍛 🎻. + +& ↗️, ⚫️ 🐕‍🦺 🎏: + +* 💽 🔬 +* 💽 🛠️ +* 💽 🧾, ♒️. + +👉 👷 🎏 🌌 ⏮️ Pydantic 🏷. & ⚫️ 🤙 🏆 🎏 🌌 🔘, ⚙️ Pydantic. + +!!! info + ✔️ 🤯 👈 🎻 💪 🚫 🌐 Pydantic 🏷 💪. + + , 👆 5️⃣📆 💪 ⚙️ Pydantic 🏷. + + ✋️ 🚥 👆 ✔️ 📚 🎻 🤥 🤭, 👉 👌 🎱 ⚙️ 👫 🏋️ 🕸 🛠️ ⚙️ FastAPI. 👶 + +## 🎻 `response_model` + +👆 💪 ⚙️ `dataclasses` `response_model` 🔢: + +```Python hl_lines="1 7-13 19" +{!../../../docs_src/dataclasses/tutorial002.py!} +``` + +🎻 🔜 🔁 🗜 Pydantic 🎻. + +👉 🌌, 🚮 🔗 🔜 🎦 🆙 🛠️ 🩺 👩‍💻 🔢: + + + +## 🎻 🔁 📊 📊 + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ ⚒ 🐦 📊 📊. + +💼, 👆 💪 ✔️ ⚙️ Pydantic ⏬ `dataclasses`. 🖼, 🚥 👆 ✔️ ❌ ⏮️ 🔁 🏗 🛠️ 🧾. + +👈 💼, 👆 💪 🎯 💱 🐩 `dataclasses` ⏮️ `pydantic.dataclasses`, ❔ 💧-♻: + +```{ .python .annotate hl_lines="1 5 8-11 14-17 23-25 28" } +{!../../../docs_src/dataclasses/tutorial003.py!} +``` + +1️⃣. 👥 🗄 `field` ⚪️➡️ 🐩 `dataclasses`. + +2️⃣. `pydantic.dataclasses` 💧-♻ `dataclasses`. + +3️⃣. `Author` 🎻 🔌 📇 `Item` 🎻. + +4️⃣. `Author` 🎻 ⚙️ `response_model` 🔢. + +5️⃣. 👆 💪 ⚙️ 🎏 🐩 🆎 ✍ ⏮️ 🎻 📨 💪. + + 👉 💼, ⚫️ 📇 `Item` 🎻. + +6️⃣. 📥 👥 🛬 📖 👈 🔌 `items` ❔ 📇 🎻. + + FastAPI 🎯 💽 🎻. + +7️⃣. 📥 `response_model` ⚙️ 🆎 ✍ 📇 `Author` 🎻. + + 🔄, 👆 💪 🌀 `dataclasses` ⏮️ 🐩 🆎 ✍. + +8️⃣. 👀 👈 👉 *➡ 🛠️ 🔢* ⚙️ 🥔 `def` ↩️ `async def`. + + 🕧, FastAPI 👆 💪 🌀 `def` & `async def` 💪. + + 🚥 👆 💪 ↗️ 🔃 🕐❔ ⚙️ ❔, ✅ 👅 📄 _"🏃 ❓" _ 🩺 🔃 `async` & `await`. + +9️⃣. 👉 *➡ 🛠️ 🔢* 🚫 🛬 🎻 (👐 ⚫️ 💪), ✋️ 📇 📖 ⏮️ 🔗 💽. + + FastAPI 🔜 ⚙️ `response_model` 🔢 (👈 🔌 🎻) 🗜 📨. + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 🆎 ✍ 📚 🎏 🌀 📨 🏗 📊 📊. + +✅-📟 ✍ 💁‍♂ 🔛 👀 🌅 🎯 ℹ. + +## 💡 🌅 + +👆 💪 🌀 `dataclasses` ⏮️ 🎏 Pydantic 🏷, 😖 ⚪️➡️ 👫, 🔌 👫 👆 👍 🏷, ♒️. + +💡 🌅, ✅ Pydantic 🩺 🔃 🎻. + +## ⏬ + +👉 💪 ↩️ FastAPI ⏬ `0.67.0`. 👶 diff --git a/docs/em/docs/advanced/events.md b/docs/em/docs/advanced/events.md new file mode 100644 index 000000000..671e81b18 --- /dev/null +++ b/docs/em/docs/advanced/events.md @@ -0,0 +1,160 @@ +# 🔆 🎉 + +👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ ⏭ 🈸 **▶️ 🆙**. 👉 ⛓ 👈 👉 📟 🔜 🛠️ **🕐**, **⏭** 🈸 **▶️ 📨 📨**. + +🎏 🌌, 👆 💪 🔬 ⚛ (📟) 👈 🔜 🛠️ 🕐❔ 🈸 **🤫 🔽**. 👉 💼, 👉 📟 🔜 🛠️ **🕐**, **⏮️** ✔️ 🍵 🎲 **📚 📨**. + +↩️ 👉 📟 🛠️ ⏭ 🈸 **▶️** ✊ 📨, & ▶️️ ⏮️ ⚫️ **🏁** 🚚 📨, ⚫️ 📔 🎂 🈸 **🔆** (🔤 "🔆" 🔜 ⚠ 🥈 👶). + +👉 💪 📶 ⚠ ⚒ 🆙 **ℹ** 👈 👆 💪 ⚙️ 🎂 📱, & 👈 **💰** 👪 📨, &/⚖️ 👈 👆 💪 **🧹 🆙** ⏮️. 🖼, 💽 🔗 🎱, ⚖️ 🚚 🔗 🎰 🏫 🏷. + +## ⚙️ 💼 + +➡️ ▶️ ⏮️ 🖼 **⚙️ 💼** & ⤴️ 👀 ❔ ❎ ⚫️ ⏮️ 👉. + +➡️ 🌈 👈 👆 ✔️ **🎰 🏫 🏷** 👈 👆 💚 ⚙️ 🍵 📨. 👶 + +🎏 🏷 🔗 👪 📨,, ⚫️ 🚫 1️⃣ 🏷 📍 📨, ⚖️ 1️⃣ 📍 👩‍💻 ⚖️ 🕳 🎏. + +➡️ 🌈 👈 🚚 🏷 💪 **✊ 🕰**, ↩️ ⚫️ ✔️ ✍ 📚 **💽 ⚪️➡️ 💾**. 👆 🚫 💚 ⚫️ 🔠 📨. + +👆 💪 📐 ⚫️ 🔝 🎚 🕹/📁, ✋️ 👈 🔜 ⛓ 👈 ⚫️ 🔜 **📐 🏷** 🚥 👆 🏃‍♂ 🙅 🏧 💯, ⤴️ 👈 💯 🔜 **🐌** ↩️ ⚫️ 🔜 ✔️ ⌛ 🏷 📐 ⏭ 💆‍♂ 💪 🏃 🔬 🍕 📟. + +👈 ⚫️❔ 👥 🔜 ❎, ➡️ 📐 🏷 ⏭ 📨 🍵, ✋️ 🕴 ▶️️ ⏭ 🈸 ▶️ 📨 📨, 🚫 ⏪ 📟 ➖ 📐. + +## 🔆 + +👆 💪 🔬 👉 *🕴* & *🤫* ⚛ ⚙️ `lifespan` 🔢 `FastAPI` 📱, & "🔑 👨‍💼" (👤 🔜 🎦 👆 ⚫️❔ 👈 🥈). + +➡️ ▶️ ⏮️ 🖼 & ⤴️ 👀 ⚫️ ℹ. + +👥 ✍ 🔁 🔢 `lifespan()` ⏮️ `yield` 💖 👉: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +📥 👥 ⚖ 😥 *🕴* 🛠️ 🚚 🏷 🚮 (❌) 🏷 🔢 📖 ⏮️ 🎰 🏫 🏷 ⏭ `yield`. 👉 📟 🔜 🛠️ **⏭** 🈸 **▶️ ✊ 📨**, ⏮️ *🕴*. + +& ⤴️, ▶️️ ⏮️ `yield`, 👥 🚚 🏷. 👉 📟 🔜 🛠️ **⏮️** 🈸 **🏁 🚚 📨**, ▶️️ ⏭ *🤫*. 👉 💪, 🖼, 🚀 ℹ 💖 💾 ⚖️ 💻. + +!!! tip + `shutdown` 🔜 🔨 🕐❔ 👆 **⛔️** 🈸. + + 🎲 👆 💪 ▶️ 🆕 ⏬, ⚖️ 👆 🤚 🎡 🏃 ⚫️. 🤷 + +### 🔆 🔢 + +🥇 👜 👀, 👈 👥 ⚖ 🔁 🔢 ⏮️ `yield`. 👉 📶 🎏 🔗 ⏮️ `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +🥇 🍕 🔢, ⏭ `yield`, 🔜 🛠️ **⏭** 🈸 ▶️. + +& 🍕 ⏮️ `yield` 🔜 🛠️ **⏮️** 🈸 ✔️ 🏁. + +### 🔁 🔑 👨‍💼 + +🚥 👆 ✅, 🔢 🎀 ⏮️ `@asynccontextmanager`. + +👈 🗜 🔢 🔘 🕳 🤙 "**🔁 🔑 👨‍💼**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +**🔑 👨‍💼** 🐍 🕳 👈 👆 💪 ⚙️ `with` 📄, 🖼, `open()` 💪 ⚙️ 🔑 👨‍💼: + +```Python +with open("file.txt") as file: + file.read() +``` + +⏮️ ⏬ 🐍, 📤 **🔁 🔑 👨‍💼**. 👆 🔜 ⚙️ ⚫️ ⏮️ `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +🕐❔ 👆 ✍ 🔑 👨‍💼 ⚖️ 🔁 🔑 👨‍💼 💖 🔛, ⚫️❔ ⚫️ 🔨 👈, ⏭ 🛬 `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏭ `yield`, & ⏮️ ❎ `with` 🍫, ⚫️ 🔜 🛠️ 📟 ⏮️ `yield`. + +👆 📟 🖼 🔛, 👥 🚫 ⚙️ ⚫️ 🔗, ✋️ 👥 🚶‍♀️ ⚫️ FastAPI ⚫️ ⚙️ ⚫️. + +`lifespan` 🔢 `FastAPI` 📱 ✊ **🔁 🔑 👨‍💼**, 👥 💪 🚶‍♀️ 👆 🆕 `lifespan` 🔁 🔑 👨‍💼 ⚫️. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## 🎛 🎉 (😢) + +!!! warning + 👍 🌌 🍵 *🕴* & *🤫* ⚙️ `lifespan` 🔢 `FastAPI` 📱 🔬 🔛. + + 👆 💪 🎲 🚶 👉 🍕. + +📤 🎛 🌌 🔬 👉 ⚛ 🛠️ ⏮️ *🕴* & ⏮️ *🤫*. + +👆 💪 🔬 🎉 🐕‍🦺 (🔢) 👈 💪 🛠️ ⏭ 🈸 ▶️ 🆙, ⚖️ 🕐❔ 🈸 🤫 🔽. + +👫 🔢 💪 📣 ⏮️ `async def` ⚖️ 😐 `def`. + +### `startup` 🎉 + +🚮 🔢 👈 🔜 🏃 ⏭ 🈸 ▶️, 📣 ⚫️ ⏮️ 🎉 `"startup"`: + +```Python hl_lines="8" +{!../../../docs_src/events/tutorial001.py!} +``` + +👉 💼, `startup` 🎉 🐕‍🦺 🔢 🔜 🔢 🏬 "💽" ( `dict`) ⏮️ 💲. + +👆 💪 🚮 🌅 🌘 1️⃣ 🎉 🐕‍🦺 🔢. + +& 👆 🈸 🏆 🚫 ▶️ 📨 📨 ⏭ 🌐 `startup` 🎉 🐕‍🦺 ✔️ 🏁. + +### `shutdown` 🎉 + +🚮 🔢 👈 🔜 🏃 🕐❔ 🈸 🤫 🔽, 📣 ⚫️ ⏮️ 🎉 `"shutdown"`: + +```Python hl_lines="6" +{!../../../docs_src/events/tutorial002.py!} +``` + +📥, `shutdown` 🎉 🐕‍🦺 🔢 🔜 ✍ ✍ ⏸ `"Application shutdown"` 📁 `log.txt`. + +!!! info + `open()` 🔢, `mode="a"` ⛓ "🎻",, ⏸ 🔜 🚮 ⏮️ ⚫️❔ 🔛 👈 📁, 🍵 📁 ⏮️ 🎚. + +!!! tip + 👀 👈 👉 💼 👥 ⚙️ 🐩 🐍 `open()` 🔢 👈 🔗 ⏮️ 📁. + + , ⚫️ 🔌 👤/🅾 (🔢/🔢), 👈 🚚 "⌛" 👜 ✍ 💾. + + ✋️ `open()` 🚫 ⚙️ `async` & `await`. + + , 👥 📣 🎉 🐕‍🦺 🔢 ⏮️ 🐩 `def` ↩️ `async def`. + +!!! info + 👆 💪 ✍ 🌅 🔃 👫 🎉 🐕‍🦺 💃 🎉' 🩺. + +### `startup` & `shutdown` 👯‍♂️ + +📤 ↕ 🤞 👈 ⚛ 👆 *🕴* & *🤫* 🔗, 👆 💪 💚 ▶️ 🕳 & ⤴️ 🏁 ⚫️, 📎 ℹ & ⤴️ 🚀 ⚫️, ♒️. + +🔨 👈 👽 🔢 👈 🚫 💰 ⚛ ⚖️ 🔢 👯‍♂️ 🌅 ⚠ 👆 🔜 💪 🏪 💲 🌐 🔢 ⚖️ 🎏 🎱. + +↩️ 👈, ⚫️ 🔜 👍 ↩️ ⚙️ `lifespan` 🔬 🔛. + +## 📡 ℹ + +📡 ℹ 😟 🤓. 👶 + +🔘, 🔫 📡 🔧, 👉 🍕 🔆 🛠️, & ⚫️ 🔬 🎉 🤙 `startup` & `shutdown`. + +## 🎧 🈸 + +👶 ✔️ 🤯 👈 👫 🔆 🎉 (🕴 & 🤫) 🔜 🕴 🛠️ 👑 🈸, 🚫 [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/extending-openapi.md b/docs/em/docs/advanced/extending-openapi.md new file mode 100644 index 000000000..496a8d9de --- /dev/null +++ b/docs/em/docs/advanced/extending-openapi.md @@ -0,0 +1,314 @@ +# ↔ 🗄 + +!!! warning + 👉 👍 🏧 ⚒. 👆 🎲 💪 🚶 ⚫️. + + 🚥 👆 📄 🔰 - 👩‍💻 🦮, 👆 💪 🎲 🚶 👉 📄. + + 🚥 👆 ⏪ 💭 👈 👆 💪 🔀 🏗 🗄 🔗, 😣 👂. + +📤 💼 🌐❔ 👆 💪 💪 🔀 🏗 🗄 🔗. + +👉 📄 👆 🔜 👀 ❔. + +## 😐 🛠️ + +😐 (🔢) 🛠️, ⏩. + +`FastAPI` 🈸 (👐) ✔️ `.openapi()` 👩‍🔬 👈 📈 📨 🗄 🔗. + +🍕 🈸 🎚 🏗, *➡ 🛠️* `/openapi.json` (⚖️ ⚫️❔ 👆 ⚒ 👆 `openapi_url`) ®. + +⚫️ 📨 🎻 📨 ⏮️ 🏁 🈸 `.openapi()` 👩‍🔬. + +🔢, ⚫️❔ 👩‍🔬 `.openapi()` 🔨 ✅ 🏠 `.openapi_schema` 👀 🚥 ⚫️ ✔️ 🎚 & 📨 👫. + +🚥 ⚫️ 🚫, ⚫️ 🏗 👫 ⚙️ 🚙 🔢 `fastapi.openapi.utils.get_openapi`. + +& 👈 🔢 `get_openapi()` 📨 🔢: + +* `title`: 🗄 📛, 🎦 🩺. +* `version`: ⏬ 👆 🛠️, ✅ `2.5.0`. +* `openapi_version`: ⏬ 🗄 🔧 ⚙️. 🔢, ⏪: `3.0.2`. +* `description`: 📛 👆 🛠️. +* `routes`: 📇 🛣, 👫 🔠 ® *➡ 🛠️*. 👫 ✊ ⚪️➡️ `app.routes`. + +## 🔑 🔢 + +⚙️ ℹ 🔛, 👆 💪 ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗 & 🔐 🔠 🍕 👈 👆 💪. + +🖼, ➡️ 🚮 📄 🗄 ↔ 🔌 🛃 🔱. + +### 😐 **FastAPI** + +🥇, ✍ 🌐 👆 **FastAPI** 🈸 🛎: + +```Python hl_lines="1 4 7-9" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🏗 🗄 🔗 + +⤴️, ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗, 🔘 `custom_openapi()` 🔢: + +```Python hl_lines="2 15-20" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔀 🗄 🔗 + +🔜 👆 💪 🚮 📄 ↔, ❎ 🛃 `x-logo` `info` "🎚" 🗄 🔗: + +```Python hl_lines="21-23" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 💾 🗄 🔗 + +👆 💪 ⚙️ 🏠 `.openapi_schema` "💾", 🏪 👆 🏗 🔗. + +👈 🌌, 👆 🈸 🏆 🚫 ✔️ 🏗 🔗 🔠 🕰 👩‍💻 📂 👆 🛠️ 🩺. + +⚫️ 🔜 🏗 🕴 🕐, & ⤴️ 🎏 💾 🔗 🔜 ⚙️ ⏭ 📨. + +```Python hl_lines="13-14 24-25" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔐 👩‍🔬 + +🔜 👆 💪 ❎ `.openapi()` 👩‍🔬 ⏮️ 👆 🆕 🔢. + +```Python hl_lines="28" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### ✅ ⚫️ + +🕐 👆 🚶 http://127.0.0.1:8000/redoc 👆 🔜 👀 👈 👆 ⚙️ 👆 🛃 🔱 (👉 🖼, **FastAPI**'Ⓜ 🔱): + + + +## 👤-🕸 🕸 & 🎚 🩺 + +🛠️ 🩺 ⚙️ **🦁 🎚** & **📄**, & 🔠 👈 💪 🕸 & 🎚 📁. + +🔢, 👈 📁 🍦 ⚪️➡️ 💲. + +✋️ ⚫️ 💪 🛃 ⚫️, 👆 💪 ⚒ 🎯 💲, ⚖️ 🍦 📁 👆. + +👈 ⚠, 🖼, 🚥 👆 💪 👆 📱 🚧 👷 ⏪ 📱, 🍵 📂 🕸 🔐, ⚖️ 🇧🇿 🕸. + +📥 👆 🔜 👀 ❔ 🍦 👈 📁 👆, 🎏 FastAPI 📱, & 🔗 🩺 ⚙️ 👫. + +### 🏗 📁 📊 + +➡️ 💬 👆 🏗 📁 📊 👀 💖 👉: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +🔜 ✍ 📁 🏪 📚 🎻 📁. + +👆 🆕 📁 📊 💪 👀 💖 👉: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### ⏬ 📁 + +⏬ 🎻 📁 💪 🩺 & 🚮 👫 🔛 👈 `static/` 📁. + +👆 💪 🎲 ▶️️-🖊 🔠 🔗 & 🖊 🎛 🎏 `Save link as...`. + +**🦁 🎚** ⚙️ 📁: + +* `swagger-ui-bundle.js` +* `swagger-ui.css` + +& **📄** ⚙️ 📁: + +* `redoc.standalone.js` + +⏮️ 👈, 👆 📁 📊 💪 👀 💖: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### 🍦 🎻 📁 + +* 🗄 `StaticFiles`. +* "🗻" `StaticFiles()` 👐 🎯 ➡. + +```Python hl_lines="7 11" +{!../../../docs_src/extending_openapi/tutorial002.py!} +``` + +### 💯 🎻 📁 + +▶️ 👆 🈸 & 🚶 http://127.0.0.1:8000/static/redoc.standalone.js. + +👆 🔜 👀 📶 📏 🕸 📁 **📄**. + +⚫️ 💪 ▶️ ⏮️ 🕳 💖: + +```JavaScript +/*! + * ReDoc - OpenAPI/Swagger-generated API Reference Documentation + * ------------------------------------------------------------- + * Version: "2.0.0-rc.18" + * Repo: https://github.com/Redocly/redoc + */ +!function(e,t){"object"==typeof exports&&"object"==typeof m + +... +``` + +👈 ✔ 👈 👆 💆‍♂ 💪 🍦 🎻 📁 ⚪️➡️ 👆 📱, & 👈 👆 🥉 🎻 📁 🩺 ☑ 🥉. + +🔜 👥 💪 🔗 📱 ⚙️ 📚 🎻 📁 🩺. + +### ❎ 🏧 🩺 + +🥇 🔁 ❎ 🏧 🩺, 📚 ⚙️ 💲 🔢. + +❎ 👫, ⚒ 👫 📛 `None` 🕐❔ 🏗 👆 `FastAPI` 📱: + +```Python hl_lines="9" +{!../../../docs_src/extending_openapi/tutorial002.py!} +``` + +### 🔌 🛃 🩺 + +🔜 👆 💪 ✍ *➡ 🛠️* 🛃 🩺. + +👆 💪 🏤-⚙️ FastAPI 🔗 🔢 ✍ 🕸 📃 🩺, & 🚶‍♀️ 👫 💪 ❌: + +* `openapi_url`: 📛 🌐❔ 🕸 📃 🩺 💪 🤚 🗄 🔗 👆 🛠️. 👆 💪 ⚙️ 📥 🔢 `app.openapi_url`. +* `title`: 📛 👆 🛠️. +* `oauth2_redirect_url`: 👆 💪 ⚙️ `app.swagger_ui_oauth2_redirect_url` 📥 ⚙️ 🔢. +* `swagger_js_url`: 📛 🌐❔ 🕸 👆 🦁 🎚 🩺 💪 🤚 **🕸** 📁. 👉 1️⃣ 👈 👆 👍 📱 🔜 🍦. +* `swagger_css_url`: 📛 🌐❔ 🕸 👆 🦁 🎚 🩺 💪 🤚 **🎚** 📁. 👉 1️⃣ 👈 👆 👍 📱 🔜 🍦. + +& ➡ 📄... + +```Python hl_lines="2-6 14-22 25-27 30-36" +{!../../../docs_src/extending_openapi/tutorial002.py!} +``` + +!!! tip + *➡ 🛠️* `swagger_ui_redirect` 👩‍🎓 🕐❔ 👆 ⚙️ Oauth2️⃣. + + 🚥 👆 🛠️ 👆 🛠️ ⏮️ Oauth2️⃣ 🐕‍🦺, 👆 🔜 💪 🔓 & 👟 🔙 🛠️ 🩺 ⏮️ 📎 🎓. & 🔗 ⏮️ ⚫️ ⚙️ 🎰 Oauth2️⃣ 🤝. + + 🦁 🎚 🔜 🍵 ⚫️ ⛅ 🎑 👆, ✋️ ⚫️ 💪 👉 "❎" 👩‍🎓. + +### ✍ *➡ 🛠️* 💯 ⚫️ + +🔜, 💪 💯 👈 🌐 👷, ✍ *➡ 🛠️*: + +```Python hl_lines="39-41" +{!../../../docs_src/extending_openapi/tutorial002.py!} +``` + +### 💯 ⚫️ + +🔜, 👆 🔜 💪 🔌 👆 📻, 🚶 👆 🩺 http://127.0.0.1:8000/docs, & 🔃 📃. + +& 🍵 🕸, 👆 🔜 💪 👀 🩺 👆 🛠️ & 🔗 ⏮️ ⚫️. + +## 🛠️ 🦁 🎚 + +👆 💪 🔗 ➕ 🦁 🎚 🔢. + +🔗 👫, 🚶‍♀️ `swagger_ui_parameters` ❌ 🕐❔ 🏗 `FastAPI()` 📱 🎚 ⚖️ `get_swagger_ui_html()` 🔢. + +`swagger_ui_parameters` 📨 📖 ⏮️ 📳 🚶‍♀️ 🦁 🎚 🔗. + +FastAPI 🗜 📳 **🎻** ⚒ 👫 🔗 ⏮️ 🕸, 👈 ⚫️❔ 🦁 🎚 💪. + +### ❎ ❕ 🎦 + +🖼, 👆 💪 ❎ ❕ 🎦 🦁 🎚. + +🍵 🔀 ⚒, ❕ 🎦 🛠️ 🔢: + + + +✋️ 👆 💪 ❎ ⚫️ ⚒ `syntaxHighlight` `False`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial003.py!} +``` + +...& ⤴️ 🦁 🎚 🏆 🚫 🎦 ❕ 🎦 🚫🔜: + + + +### 🔀 🎢 + +🎏 🌌 👆 💪 ⚒ ❕ 🎦 🎢 ⏮️ 🔑 `"syntaxHighlight.theme"` (👀 👈 ⚫️ ✔️ ❣ 🖕): + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial004.py!} +``` + +👈 📳 🔜 🔀 ❕ 🎦 🎨 🎢: + + + +### 🔀 🔢 🦁 🎚 🔢 + +FastAPI 🔌 🔢 📳 🔢 ☑ 🌅 ⚙️ 💼. + +⚫️ 🔌 👫 🔢 📳: + +```Python +{!../../../fastapi/openapi/docs.py[ln:7-13]!} +``` + +👆 💪 🔐 🙆 👫 ⚒ 🎏 💲 ❌ `swagger_ui_parameters`. + +🖼, ❎ `deepLinking` 👆 💪 🚶‍♀️ 👉 ⚒ `swagger_ui_parameters`: + +```Python hl_lines="3" +{!../../../docs_src/extending_openapi/tutorial005.py!} +``` + +### 🎏 🦁 🎚 🔢 + +👀 🌐 🎏 💪 📳 👆 💪 ⚙️, ✍ 🛂 🩺 🦁 🎚 🔢. + +### 🕸-🕴 ⚒ + +🦁 🎚 ✔ 🎏 📳 **🕸-🕴** 🎚 (🖼, 🕸 🔢). + +FastAPI 🔌 👫 🕸-🕴 `presets` ⚒: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +👫 **🕸** 🎚, 🚫 🎻, 👆 💪 🚫 🚶‍♀️ 👫 ⚪️➡️ 🐍 📟 🔗. + +🚥 👆 💪 ⚙️ 🕸-🕴 📳 💖 📚, 👆 💪 ⚙️ 1️⃣ 👩‍🔬 🔛. 🔐 🌐 🦁 🎚 *➡ 🛠️* & ❎ ✍ 🙆 🕸 👆 💪. diff --git a/docs/em/docs/advanced/generate-clients.md b/docs/em/docs/advanced/generate-clients.md new file mode 100644 index 000000000..30560c8c6 --- /dev/null +++ b/docs/em/docs/advanced/generate-clients.md @@ -0,0 +1,267 @@ +# 🏗 👩‍💻 + +**FastAPI** ⚓️ 🔛 🗄 🔧, 👆 🤚 🏧 🔗 ⏮️ 📚 🧰, 🔌 🏧 🛠️ 🩺 (🚚 🦁 🎚). + +1️⃣ 🎯 📈 👈 🚫 🎯 ⭐ 👈 👆 💪 **🏗 👩‍💻** (🕣 🤙 **📱** ) 👆 🛠️, 📚 🎏 **🛠️ 🇪🇸**. + +## 🗄 👩‍💻 🚂 + +📤 📚 🧰 🏗 👩‍💻 ⚪️➡️ **🗄**. + +⚠ 🧰 🗄 🚂. + +🚥 👆 🏗 **🕸**, 📶 😌 🎛 🗄-📕-🇦🇪. + +## 🏗 📕 🕸 👩‍💻 + +➡️ ▶️ ⏮️ 🙅 FastAPI 🈸: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ``` + +👀 👈 *➡ 🛠️* 🔬 🏷 👫 ⚙️ 📨 🚀 & 📨 🚀, ⚙️ 🏷 `Item` & `ResponseMessage`. + +### 🛠️ 🩺 + +🚥 👆 🚶 🛠️ 🩺, 👆 🔜 👀 👈 ⚫️ ✔️ **🔗** 📊 📨 📨 & 📨 📨: + + + +👆 💪 👀 👈 🔗 ↩️ 👫 📣 ⏮️ 🏷 📱. + +👈 ℹ 💪 📱 **🗄 🔗**, & ⤴️ 🎦 🛠️ 🩺 (🦁 🎚). + +& 👈 🎏 ℹ ⚪️➡️ 🏷 👈 🔌 🗄 ⚫️❔ 💪 ⚙️ **🏗 👩‍💻 📟**. + +### 🏗 📕 👩‍💻 + +🔜 👈 👥 ✔️ 📱 ⏮️ 🏷, 👥 💪 🏗 👩‍💻 📟 🕸. + +#### ❎ `openapi-typescript-codegen` + +👆 💪 ❎ `openapi-typescript-codegen` 👆 🕸 📟 ⏮️: + +
+ +```console +$ npm install openapi-typescript-codegen --save-dev + +---> 100% +``` + +
+ +#### 🏗 👩‍💻 📟 + +🏗 👩‍💻 📟 👆 💪 ⚙️ 📋 ⏸ 🈸 `openapi` 👈 🔜 🔜 ❎. + +↩️ ⚫️ ❎ 🇧🇿 🏗, 👆 🎲 🚫🔜 💪 🤙 👈 📋 🔗, ✋️ 👆 🔜 🚮 ⚫️ 🔛 👆 `package.json` 📁. + +⚫️ 💪 👀 💖 👉: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +⏮️ ✔️ 👈 ☕ `generate-client` ✍ 📤, 👆 💪 🏃 ⚫️ ⏮️: + +
+ +```console +$ npm run generate-client + +frontend-app@1.0.0 generate-client /home/user/code/frontend-app +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +``` + +
+ +👈 📋 🔜 🏗 📟 `./src/client` & 🔜 ⚙️ `axios` (🕸 🇺🇸🔍 🗃) 🔘. + +### 🔄 👅 👩‍💻 📟 + +🔜 👆 💪 🗄 & ⚙️ 👩‍💻 📟, ⚫️ 💪 👀 💖 👉, 👀 👈 👆 🤚 ✍ 👩‍🔬: + + + +👆 🔜 🤚 ✍ 🚀 📨: + + + +!!! tip + 👀 ✍ `name` & `price`, 👈 🔬 FastAPI 🈸, `Item` 🏷. + +👆 🔜 ✔️ ⏸ ❌ 📊 👈 👆 📨: + + + +📨 🎚 🔜 ✔️ ✍: + + + +## FastAPI 📱 ⏮️ 🔖 + +📚 💼 👆 FastAPI 📱 🔜 🦏, & 👆 🔜 🎲 ⚙️ 🔖 🎏 🎏 👪 *➡ 🛠️*. + +🖼, 👆 💪 ✔️ 📄 **🏬** & ➕1️⃣ 📄 **👩‍💻**, & 👫 💪 👽 🔖: + + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ``` + +### 🏗 📕 👩‍💻 ⏮️ 🔖 + +🚥 👆 🏗 👩‍💻 FastAPI 📱 ⚙️ 🔖, ⚫️ 🔜 🛎 🎏 👩‍💻 📟 ⚓️ 🔛 🔖. + +👉 🌌 👆 🔜 💪 ✔️ 👜 ✔ & 👪 ☑ 👩‍💻 📟: + + + +👉 💼 👆 ✔️: + +* `ItemsService` +* `UsersService` + +### 👩‍💻 👩‍🔬 📛 + +▶️️ 🔜 🏗 👩‍🔬 📛 💖 `createItemItemsPost` 🚫 👀 📶 🧹: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...👈 ↩️ 👩‍💻 🚂 ⚙️ 🗄 🔗 **🛠️ 🆔** 🔠 *➡ 🛠️*. + +🗄 🚚 👈 🔠 🛠️ 🆔 😍 🤭 🌐 *➡ 🛠️*, FastAPI ⚙️ **🔢 📛**, **➡**, & **🇺🇸🔍 👩‍🔬/🛠️** 🏗 👈 🛠️ 🆔, ↩️ 👈 🌌 ⚫️ 💪 ⚒ 💭 👈 🛠️ 🆔 😍. + +✋️ 👤 🔜 🎦 👆 ❔ 📉 👈 ⏭. 👶 + +## 🛃 🛠️ 🆔 & 👍 👩‍🔬 📛 + +👆 💪 **🔀** 🌌 👫 🛠️ 🆔 **🏗** ⚒ 👫 🙅 & ✔️ **🙅 👩‍🔬 📛** 👩‍💻. + +👉 💼 👆 🔜 ✔️ 🚚 👈 🔠 🛠️ 🆔 **😍** 🎏 🌌. + +🖼, 👆 💪 ⚒ 💭 👈 🔠 *➡ 🛠️* ✔️ 🔖, & ⤴️ 🏗 🛠️ 🆔 ⚓️ 🔛 **🔖** & *➡ 🛠️* **📛** (🔢 📛). + +### 🛃 🏗 😍 🆔 🔢 + +FastAPI ⚙️ **😍 🆔** 🔠 *➡ 🛠️*, ⚫️ ⚙️ **🛠️ 🆔** & 📛 🙆 💪 🛃 🏷, 📨 ⚖️ 📨. + +👆 💪 🛃 👈 🔢. ⚫️ ✊ `APIRoute` & 🔢 🎻. + +🖼, 📥 ⚫️ ⚙️ 🥇 🔖 (👆 🔜 🎲 ✔️ 🕴 1️⃣ 🔖) & *➡ 🛠️* 📛 (🔢 📛). + +👆 💪 ⤴️ 🚶‍♀️ 👈 🛃 🔢 **FastAPI** `generate_unique_id_function` 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ``` + +### 🏗 📕 👩‍💻 ⏮️ 🛃 🛠️ 🆔 + +🔜 🚥 👆 🏗 👩‍💻 🔄, 👆 🔜 👀 👈 ⚫️ ✔️ 📉 👩‍🔬 📛: + + + +👆 👀, 👩‍🔬 📛 🔜 ✔️ 🔖 & ⤴️ 🔢 📛, 🔜 👫 🚫 🔌 ℹ ⚪️➡️ 📛 ➡ & 🇺🇸🔍 🛠️. + +### 🗜 🗄 🔧 👩‍💻 🚂 + +🏗 📟 ✔️ **❎ ℹ**. + +👥 ⏪ 💭 👈 👉 👩‍🔬 🔗 **🏬** ↩️ 👈 🔤 `ItemsService` (✊ ⚪️➡️ 🔖), ✋️ 👥 ✔️ 📛 🔡 👩‍🔬 📛 💁‍♂️. 👶 + +👥 🔜 🎲 💚 🚧 ⚫️ 🗄 🏢, 👈 🔜 🚚 👈 🛠️ 🆔 **😍**. + +✋️ 🏗 👩‍💻 👥 💪 **🔀** 🗄 🛠️ 🆔 ▶️️ ⏭ 🏭 👩‍💻, ⚒ 👈 👩‍🔬 📛 👌 & **🧹**. + +👥 💪 ⏬ 🗄 🎻 📁 `openapi.json` & ⤴️ 👥 💪 **❎ 👈 🔡 🔖** ⏮️ ✍ 💖 👉: + +```Python +{!../../../docs_src/generate_clients/tutorial004.py!} +``` + +⏮️ 👈, 🛠️ 🆔 🔜 📁 ⚪️➡️ 👜 💖 `items-get_items` `get_items`, 👈 🌌 👩‍💻 🚂 💪 🏗 🙅 👩‍🔬 📛. + +### 🏗 📕 👩‍💻 ⏮️ 🗜 🗄 + +🔜 🔚 🏁 📁 `openapi.json`, 👆 🔜 🔀 `package.json` ⚙️ 👈 🇧🇿 📁, 🖼: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +⏮️ 🏭 🆕 👩‍💻, 👆 🔜 🔜 ✔️ **🧹 👩‍🔬 📛**, ⏮️ 🌐 **✍**, **⏸ ❌**, ♒️: + + + +## 💰 + +🕐❔ ⚙️ 🔁 🏗 👩‍💻 👆 🔜 **✍** : + +* 👩‍🔬. +* 📨 🚀 💪, 🔢 🔢, ♒️. +* 📨 🚀. + +👆 🔜 ✔️ **⏸ ❌** 🌐. + +& 🕐❔ 👆 ℹ 👩‍💻 📟, & **♻** 🕸, ⚫️ 🔜 ✔️ 🙆 🆕 *➡ 🛠️* 💪 👩‍🔬, 🗝 🕐 ❎, & 🙆 🎏 🔀 🔜 🎨 🔛 🏗 📟. 👶 + +👉 ⛓ 👈 🚥 🕳 🔀 ⚫️ 🔜 **🎨** 🔛 👩‍💻 📟 🔁. & 🚥 👆 **🏗** 👩‍💻 ⚫️ 🔜 ❌ 👅 🚥 👆 ✔️ 🙆 **🔖** 📊 ⚙️. + +, 👆 🔜 **🔍 📚 ❌** 📶 ⏪ 🛠️ 🛵 ↩️ ✔️ ⌛ ❌ 🎦 🆙 👆 🏁 👩‍💻 🏭 & ⤴️ 🔄 ℹ 🌐❔ ⚠. 👶 diff --git a/docs/em/docs/advanced/graphql.md b/docs/em/docs/advanced/graphql.md new file mode 100644 index 000000000..8509643ce --- /dev/null +++ b/docs/em/docs/advanced/graphql.md @@ -0,0 +1,56 @@ +# 🕹 + +**FastAPI** ⚓️ 🔛 **🔫** 🐩, ⚫️ 📶 ⏩ 🛠️ 🙆 **🕹** 🗃 🔗 ⏮️ 🔫. + +👆 💪 🌀 😐 FastAPI *➡ 🛠️* ⏮️ 🕹 🔛 🎏 🈸. + +!!! tip + **🕹** ❎ 📶 🎯 ⚙️ 💼. + + ⚫️ ✔️ **📈** & **⚠** 🕐❔ 🔬 ⚠ **🕸 🔗**. + + ⚒ 💭 👆 🔬 🚥 **💰** 👆 ⚙️ 💼 ⚖ **👐**. 👶 + +## 🕹 🗃 + +📥 **🕹** 🗃 👈 ✔️ **🔫** 🐕‍🦺. 👆 💪 ⚙️ 👫 ⏮️ **FastAPI**: + +* 🍓 👶 + * ⏮️ 🩺 FastAPI +* 👸 + * ⏮️ 🩺 💃 (👈 ✔ FastAPI) +* 🍟 + * ⏮️ 🍟 🔫 🚚 🔫 🛠️ +* + * ⏮️ 💃-Graphene3️⃣ + +## 🕹 ⏮️ 🍓 + +🚥 👆 💪 ⚖️ 💚 👷 ⏮️ **🕹**, **🍓** **👍** 🗃 ⚫️ ✔️ 🔧 🔐 **FastAPI** 🔧, ⚫️ 🌐 ⚓️ 🔛 **🆎 ✍**. + +⚓️ 🔛 👆 ⚙️ 💼, 👆 5️⃣📆 💖 ⚙️ 🎏 🗃, ✋️ 🚥 👆 💭 👤, 👤 🔜 🎲 🤔 👆 🔄 **🍓**. + +📥 🤪 🎮 ❔ 👆 💪 🛠️ 🍓 ⏮️ FastAPI: + +```Python hl_lines="3 22 25-26" +{!../../../docs_src/graphql/tutorial001.py!} +``` + +👆 💪 💡 🌅 🔃 🍓 🍓 🧾. + +& 🩺 🔃 🍓 ⏮️ FastAPI. + +## 🗝 `GraphQLApp` ⚪️➡️ 💃 + +⏮️ ⏬ 💃 🔌 `GraphQLApp` 🎓 🛠️ ⏮️ . + +⚫️ 😢 ⚪️➡️ 💃, ✋️ 🚥 👆 ✔️ 📟 👈 ⚙️ ⚫️, 👆 💪 💪 **↔** 💃-Graphene3️⃣, 👈 📔 🎏 ⚙️ 💼 & ✔️ **🌖 🌓 🔢**. + +!!! tip + 🚥 👆 💪 🕹, 👤 🔜 👍 👆 ✅ 👅 🍓, ⚫️ ⚓️ 🔛 🆎 ✍ ↩️ 🛃 🎓 & 🆎. + +## 💡 🌅 + +👆 💪 💡 🌅 🔃 **🕹** 🛂 🕹 🧾. + +👆 💪 ✍ 🌅 🔃 🔠 👈 🗃 🔬 🔛 👫 🔗. diff --git a/docs/em/docs/advanced/index.md b/docs/em/docs/advanced/index.md new file mode 100644 index 000000000..6a43a09e7 --- /dev/null +++ b/docs/em/docs/advanced/index.md @@ -0,0 +1,24 @@ +# 🏧 👩‍💻 🦮 - 🎶 + +## 🌖 ⚒ + +👑 [🔰 - 👩‍💻 🦮](../tutorial/){.internal-link target=_blank} 🔜 🥃 🤝 👆 🎫 🔘 🌐 👑 ⚒ **FastAPI**. + +⏭ 📄 👆 🔜 👀 🎏 🎛, 📳, & 🌖 ⚒. + +!!! tip + ⏭ 📄 **🚫 🎯 "🏧"**. + + & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. + +## ✍ 🔰 🥇 + +👆 💪 ⚙️ 🏆 ⚒ **FastAPI** ⏮️ 💡 ⚪️➡️ 👑 [🔰 - 👩‍💻 🦮](../tutorial/){.internal-link target=_blank}. + +& ⏭ 📄 🤔 👆 ⏪ ✍ ⚫️, & 🤔 👈 👆 💭 👈 👑 💭. + +## 🏎.🅾 ↗️ + +🚥 👆 🔜 💖 ✊ 🏧-🔰 ↗️ 🔗 👉 📄 🩺, 👆 💪 💚 ✅: 💯-💾 🛠️ ⏮️ FastAPI & ☁ **🏎.🅾**. + +👫 ⏳ 🩸 1️⃣0️⃣ 💯 🌐 💰 🛠️ **FastAPI**. 👶 👶 diff --git a/docs/em/docs/advanced/middleware.md b/docs/em/docs/advanced/middleware.md new file mode 100644 index 000000000..b3e722ed0 --- /dev/null +++ b/docs/em/docs/advanced/middleware.md @@ -0,0 +1,99 @@ +# 🏧 🛠️ + +👑 🔰 👆 ✍ ❔ 🚮 [🛃 🛠️](../tutorial/middleware.md){.internal-link target=_blank} 👆 🈸. + +& ⤴️ 👆 ✍ ❔ 🍵 [⚜ ⏮️ `CORSMiddleware`](../tutorial/cors.md){.internal-link target=_blank}. + +👉 📄 👥 🔜 👀 ❔ ⚙️ 🎏 🛠️. + +## ❎ 🔫 🛠️ + +**FastAPI** ⚓️ 🔛 💃 & 🛠️ 🔫 🔧, 👆 💪 ⚙️ 🙆 🔫 🛠️. + +🛠️ 🚫 ✔️ ⚒ FastAPI ⚖️ 💃 👷, 📏 ⚫️ ⏩ 🔫 🔌. + +🏢, 🔫 🛠️ 🎓 👈 ⌛ 📨 🔫 📱 🥇 ❌. + +, 🧾 🥉-🥳 🔫 🛠️ 👫 🔜 🎲 💬 👆 🕳 💖: + +```Python +from unicorn import UnicornMiddleware + +app = SomeASGIApp() + +new_app = UnicornMiddleware(app, some_config="rainbow") +``` + +✋️ FastAPI (🤙 💃) 🚚 🙅 🌌 ⚫️ 👈 ⚒ 💭 👈 🔗 🛠️ 🍵 💽 ❌ & 🛃 ⚠ 🐕‍🦺 👷 ☑. + +👈, 👆 ⚙️ `app.add_middleware()` (🖼 ⚜). + +```Python +from fastapi import FastAPI +from unicorn import UnicornMiddleware + +app = FastAPI() + +app.add_middleware(UnicornMiddleware, some_config="rainbow") +``` + +`app.add_middleware()` 📨 🛠️ 🎓 🥇 ❌ & 🙆 🌖 ❌ 🚶‍♀️ 🛠️. + +## 🛠️ 🛠️ + +**FastAPI** 🔌 📚 🛠️ ⚠ ⚙️ 💼, 👥 🔜 👀 ⏭ ❔ ⚙️ 👫. + +!!! note "📡 ℹ" + ⏭ 🖼, 👆 💪 ⚙️ `from starlette.middleware.something import SomethingMiddleware`. + + **FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. + +## `HTTPSRedirectMiddleware` + +🛠️ 👈 🌐 📨 📨 🔜 👯‍♂️ `https` ⚖️ `wss`. + +🙆 📨 📨 `http` ⚖️ `ws` 🔜 ❎ 🔐 ⚖ ↩️. + +```Python hl_lines="2 6" +{!../../../docs_src/advanced_middleware/tutorial001.py!} +``` + +## `TrustedHostMiddleware` + +🛠️ 👈 🌐 📨 📨 ✔️ ☑ ⚒ `Host` 🎚, ✔ 💂‍♂ 🛡 🇺🇸🔍 🦠 🎚 👊. + +```Python hl_lines="2 6-8" +{!../../../docs_src/advanced_middleware/tutorial002.py!} +``` + +📄 ❌ 🐕‍🦺: + +* `allowed_hosts` - 📇 🆔 📛 👈 🔜 ✔ 📛. 🃏 🆔 ✅ `*.example.com` 🐕‍🦺 🎀 📁. ✔ 🙆 📛 👯‍♂️ ⚙️ `allowed_hosts=["*"]` ⚖️ 🚫 🛠️. + +🚥 📨 📨 🔨 🚫 ✔ ☑ ⤴️ `400` 📨 🔜 📨. + +## `GZipMiddleware` + +🍵 🗜 📨 🙆 📨 👈 🔌 `"gzip"` `Accept-Encoding` 🎚. + +🛠️ 🔜 🍵 👯‍♂️ 🐩 & 🎥 📨. + +```Python hl_lines="2 6" +{!../../../docs_src/advanced_middleware/tutorial003.py!} +``` + +📄 ❌ 🐕‍🦺: + +* `minimum_size` - 🚫 🗜 📨 👈 🤪 🌘 👉 💯 📐 🔢. 🔢 `500`. + +## 🎏 🛠️ + +📤 📚 🎏 🔫 🛠️. + +🖼: + +* 🔫 +* Uvicorn `ProxyHeadersMiddleware` +* 🇸🇲 + +👀 🎏 💪 🛠️ ✅ 💃 🛠️ 🩺 & 🔫 👌 📇. diff --git a/docs/em/docs/advanced/nosql-databases.md b/docs/em/docs/advanced/nosql-databases.md new file mode 100644 index 000000000..9c828a909 --- /dev/null +++ b/docs/em/docs/advanced/nosql-databases.md @@ -0,0 +1,156 @@ +# ☁ (📎 / 🦏 💽) 💽 + +**FastAPI** 💪 🛠️ ⏮️ 🙆 . + +📥 👥 🔜 👀 🖼 ⚙️ **🗄**, 📄 🧢 ☁ 💽. + +👆 💪 🛠️ ⚫️ 🙆 🎏 ☁ 💽 💖: + +* **✳** +* **👸** +* **✳** +* **🇸🇲** +* **✳**, ♒️. + +!!! tip + 📤 🛂 🏗 🚂 ⏮️ **FastAPI** & **🗄**, 🌐 ⚓️ 🔛 **☁**, 🔌 🕸 & 🌖 🧰: https://github.com/tiangolo/full-stack-fastapi-couchbase + +## 🗄 🗄 🦲 + +🔜, 🚫 💸 🙋 🎂, 🕴 🗄: + +```Python hl_lines="3-5" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🔬 📉 ⚙️ "📄 🆎" + +👥 🔜 ⚙️ ⚫️ ⏪ 🔧 🏑 `type` 👆 📄. + +👉 🚫 ✔ 🗄, ✋️ 👍 💡 👈 🔜 ℹ 👆 ⏮️. + +```Python hl_lines="9" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🚮 🔢 🤚 `Bucket` + +**🗄**, 🥡 ⚒ 📄, 👈 💪 🎏 🆎. + +👫 🛎 🌐 🔗 🎏 🈸. + +🔑 🔗 💽 🌏 🔜 "💽" (🎯 💽, 🚫 💽 💽). + +🔑 **✳** 🔜 "🗃". + +📟, `Bucket` 🎨 👑 🇨🇻 📻 ⏮️ 💽. + +👉 🚙 🔢 🔜: + +* 🔗 **🗄** 🌑 (👈 💪 👁 🎰). + * ⚒ 🔢 ⏲. +* 🔓 🌑. +* 🤚 `Bucket` 👐. + * ⚒ 🔢 ⏲. +* 📨 ⚫️. + +```Python hl_lines="12-21" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## ✍ Pydantic 🏷 + +**🗄** "📄" 🤙 "🎻 🎚", 👥 💪 🏷 👫 ⏮️ Pydantic. + +### `User` 🏷 + +🥇, ➡️ ✍ `User` 🏷: + +```Python hl_lines="24-28" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +👥 🔜 ⚙️ 👉 🏷 👆 *➡ 🛠️ 🔢*,, 👥 🚫 🔌 ⚫️ `hashed_password`. + +### `UserInDB` 🏷 + +🔜, ➡️ ✍ `UserInDB` 🏷. + +👉 🔜 ✔️ 💽 👈 🤙 🏪 💽. + +👥 🚫 ✍ ⚫️ 🏿 Pydantic `BaseModel` ✋️ 🏿 👆 👍 `User`, ↩️ ⚫️ 🔜 ✔️ 🌐 🔢 `User` ➕ 👩‍❤‍👨 🌅: + +```Python hl_lines="31-33" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +!!! note + 👀 👈 👥 ✔️ `hashed_password` & `type` 🏑 👈 🔜 🏪 💽. + + ✋️ ⚫️ 🚫 🍕 🏢 `User` 🏷 (1️⃣ 👥 🔜 📨 *➡ 🛠️*). + +## 🤚 👩‍💻 + +🔜 ✍ 🔢 👈 🔜: + +* ✊ 🆔. +* 🏗 📄 🆔 ⚪️➡️ ⚫️. +* 🤚 📄 ⏮️ 👈 🆔. +* 🚮 🎚 📄 `UserInDB` 🏷. + +🏗 🔢 👈 🕴 💡 🤚 👆 👩‍💻 ⚪️➡️ `username` (⚖️ 🙆 🎏 🔢) 🔬 👆 *➡ 🛠️ 🔢*, 👆 💪 🌖 💪 🏤-⚙️ ⚫️ 💗 🍕 & 🚮 ⚒ 💯 ⚫️: + +```Python hl_lines="36-42" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### Ⓜ-🎻 + +🚥 👆 🚫 😰 ⏮️ `f"userprofile::{username}"`, ⚫️ 🐍 "Ⓜ-🎻". + +🙆 🔢 👈 🚮 🔘 `{}` Ⓜ-🎻 🔜 ↔ / 💉 🎻. + +### `dict` 🏗 + +🚥 👆 🚫 😰 ⏮️ `UserInDB(**result.value)`, ⚫️ ⚙️ `dict` "🏗". + +⚫️ 🔜 ✊ `dict` `result.value`, & ✊ 🔠 🚮 🔑 & 💲 & 🚶‍♀️ 👫 🔑-💲 `UserInDB` 🇨🇻 ❌. + +, 🚥 `dict` 🔌: + +```Python +{ + "username": "johndoe", + "hashed_password": "some_hash", +} +``` + +⚫️ 🔜 🚶‍♀️ `UserInDB` : + +```Python +UserInDB(username="johndoe", hashed_password="some_hash") +``` + +## ✍ 👆 **FastAPI** 📟 + +### ✍ `FastAPI` 📱 + +```Python hl_lines="46" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +### ✍ *➡ 🛠️ 🔢* + +👆 📟 🤙 🗄 & 👥 🚫 ⚙️ 🥼 🐍 await 🐕‍🦺, 👥 🔜 📣 👆 🔢 ⏮️ 😐 `def` ↩️ `async def`. + +, 🗄 👍 🚫 ⚙️ 👁 `Bucket` 🎚 💗 "🧵Ⓜ",, 👥 💪 🤚 🥡 🔗 & 🚶‍♀️ ⚫️ 👆 🚙 🔢: + +```Python hl_lines="49-53" +{!../../../docs_src/nosql_databases/tutorial001.py!} +``` + +## 🌃 + +👆 💪 🛠️ 🙆 🥉 🥳 ☁ 💽, ⚙️ 👫 🐩 📦. + +🎏 ✔ 🙆 🎏 🔢 🧰, ⚙️ ⚖️ 🛠️. diff --git a/docs/em/docs/advanced/openapi-callbacks.md b/docs/em/docs/advanced/openapi-callbacks.md new file mode 100644 index 000000000..630b75ed2 --- /dev/null +++ b/docs/em/docs/advanced/openapi-callbacks.md @@ -0,0 +1,179 @@ +# 🗄 ⏲ + +👆 💪 ✍ 🛠️ ⏮️ *➡ 🛠️* 👈 💪 ⏲ 📨 *🔢 🛠️* ✍ 👱 🙆 (🎲 🎏 👩‍💻 👈 🔜 *⚙️* 👆 🛠️). + +🛠️ 👈 🔨 🕐❔ 👆 🛠️ 📱 🤙 *🔢 🛠️* 📛 "⏲". ↩️ 🖥 👈 🔢 👩‍💻 ✍ 📨 📨 👆 🛠️ & ⤴️ 👆 🛠️ *🤙 🔙*, 📨 📨 *🔢 🛠️* (👈 🎲 ✍ 🎏 👩‍💻). + +👉 💼, 👆 💪 💚 📄 ❔ 👈 🔢 🛠️ *🔜* 👀 💖. ⚫️❔ *➡ 🛠️* ⚫️ 🔜 ✔️, ⚫️❔ 💪 ⚫️ 🔜 ⌛, ⚫️❔ 📨 ⚫️ 🔜 📨, ♒️. + +## 📱 ⏮️ ⏲ + +➡️ 👀 🌐 👉 ⏮️ 🖼. + +🌈 👆 🛠️ 📱 👈 ✔ 🏗 🧾. + +👉 🧾 🔜 ✔️ `id`, `title` (📦), `customer`, & `total`. + +👩‍💻 👆 🛠️ (🔢 👩‍💻) 🔜 ✍ 🧾 👆 🛠️ ⏮️ 🏤 📨. + +⤴️ 👆 🛠️ 🔜 (➡️ 🌈): + +* 📨 🧾 🕴 🔢 👩‍💻. +* 📈 💸. +* 📨 📨 🔙 🛠️ 👩‍💻 (🔢 👩‍💻). + * 👉 🔜 🔨 📨 🏤 📨 (⚪️➡️ *👆 🛠️*) *🔢 🛠️* 🚚 👈 🔢 👩‍💻 (👉 "⏲"). + +## 😐 **FastAPI** 📱 + +➡️ 🥇 👀 ❔ 😐 🛠️ 📱 🔜 👀 💖 ⏭ ❎ ⏲. + +⚫️ 🔜 ✔️ *➡ 🛠️* 👈 🔜 📨 `Invoice` 💪, & 🔢 🔢 `callback_url` 👈 🔜 🔌 📛 ⏲. + +👉 🍕 📶 😐, 🌅 📟 🎲 ⏪ 😰 👆: + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +!!! tip + `callback_url` 🔢 🔢 ⚙️ Pydantic 📛 🆎. + +🕴 🆕 👜 `callbacks=messages_callback_router.routes` ❌ *➡ 🛠️ 👨‍🎨*. 👥 🔜 👀 ⚫️❔ 👈 ⏭. + +## 🔬 ⏲ + +☑ ⏲ 📟 🔜 🪀 🙇 🔛 👆 👍 🛠️ 📱. + +& ⚫️ 🔜 🎲 🪀 📚 ⚪️➡️ 1️⃣ 📱 ⏭. + +⚫️ 💪 1️⃣ ⚖️ 2️⃣ ⏸ 📟, 💖: + +```Python +callback_url = "https://example.com/api/v1/invoices/events/" +httpx.post(callback_url, json={"description": "Invoice paid", "paid": True}) +``` + +✋️ 🎲 🏆 ⚠ 🍕 ⏲ ⚒ 💭 👈 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 🛠️ *🔢 🛠️* ☑, 🛄 💽 👈 *👆 🛠️* 🔜 📨 📨 💪 ⏲, ♒️. + +, ⚫️❔ 👥 🔜 ⏭ 🚮 📟 📄 ❔ 👈 *🔢 🛠️* 🔜 👀 💖 📨 ⏲ ⚪️➡️ *👆 🛠️*. + +👈 🧾 🔜 🎦 🆙 🦁 🎚 `/docs` 👆 🛠️, & ⚫️ 🔜 ➡️ 🔢 👩‍💻 💭 ❔ 🏗 *🔢 🛠️*. + +👉 🖼 🚫 🛠️ ⏲ ⚫️ (👈 💪 ⏸ 📟), 🕴 🧾 🍕. + +!!! tip + ☑ ⏲ 🇺🇸🔍 📨. + + 🕐❔ 🛠️ ⏲ 👆, 👆 💪 ⚙️ 🕳 💖 🇸🇲 ⚖️ 📨. + +## ✍ ⏲ 🧾 📟 + +👉 📟 🏆 🚫 🛠️ 👆 📱, 👥 🕴 💪 ⚫️ *📄* ❔ 👈 *🔢 🛠️* 🔜 👀 💖. + +✋️, 👆 ⏪ 💭 ❔ 💪 ✍ 🏧 🧾 🛠️ ⏮️ **FastAPI**. + +👥 🔜 ⚙️ 👈 🎏 💡 📄 ❔ *🔢 🛠️* 🔜 👀 💖... 🏗 *➡ 🛠️(Ⓜ)* 👈 🔢 🛠️ 🔜 🛠️ (🕐 👆 🛠️ 🔜 🤙). + +!!! tip + 🕐❔ ✍ 📟 📄 ⏲, ⚫️ 💪 ⚠ 🌈 👈 👆 👈 *🔢 👩‍💻*. & 👈 👆 ⏳ 🛠️ *🔢 🛠️*, 🚫 *👆 🛠️*. + + 🍕 🛠️ 👉 ☝ 🎑 ( *🔢 👩‍💻*) 💪 ℹ 👆 💭 💖 ⚫️ 🌅 ⭐ 🌐❔ 🚮 🔢, Pydantic 🏷 💪, 📨, ♒️. 👈 *🔢 🛠️*. + +### ✍ ⏲ `APIRouter` + +🥇 ✍ 🆕 `APIRouter` 👈 🔜 🔌 1️⃣ ⚖️ 🌅 ⏲. + +```Python hl_lines="3 25" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +### ✍ ⏲ *➡ 🛠️* + +✍ ⏲ *➡ 🛠️* ⚙️ 🎏 `APIRouter` 👆 ✍ 🔛. + +⚫️ 🔜 👀 💖 😐 FastAPI *➡ 🛠️*: + +* ⚫️ 🔜 🎲 ✔️ 📄 💪 ⚫️ 🔜 📨, ✅ `body: InvoiceEvent`. +* & ⚫️ 💪 ✔️ 📄 📨 ⚫️ 🔜 📨, ✅ `response_model=InvoiceEventReceived`. + +```Python hl_lines="16-18 21-22 28-32" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +📤 2️⃣ 👑 🔺 ⚪️➡️ 😐 *➡ 🛠️*: + +* ⚫️ 🚫 💪 ✔️ 🙆 ☑ 📟, ↩️ 👆 📱 🔜 🙅 🤙 👉 📟. ⚫️ 🕴 ⚙️ 📄 *🔢 🛠️*. , 🔢 💪 ✔️ `pass`. +* *➡* 💪 🔌 🗄 3️⃣ 🧬 (👀 🌖 🔛) 🌐❔ ⚫️ 💪 ⚙️ 🔢 ⏮️ 🔢 & 🍕 ⏮️ 📨 📨 *👆 🛠️*. + +### ⏲ ➡ 🧬 + +⏲ *➡* 💪 ✔️ 🗄 3️⃣ 🧬 👈 💪 🔌 🍕 ⏮️ 📨 📨 *👆 🛠️*. + +👉 💼, ⚫️ `str`: + +```Python +"{$callback_url}/invoices/{$request.body.id}" +``` + +, 🚥 👆 🛠️ 👩‍💻 (🔢 👩‍💻) 📨 📨 *👆 🛠️* : + +``` +https://yourapi.com/invoices/?callback_url=https://www.external.org/events +``` + +⏮️ 🎻 💪: + +```JSON +{ + "id": "2expen51ve", + "customer": "Mr. Richie Rich", + "total": "9999" +} +``` + +⤴️ *👆 🛠️* 🔜 🛠️ 🧾, & ☝ ⏪, 📨 ⏲ 📨 `callback_url` ( *🔢 🛠️*): + +``` +https://www.external.org/events/invoices/2expen51ve +``` + +⏮️ 🎻 💪 ⚗ 🕳 💖: + +```JSON +{ + "description": "Payment celebration", + "paid": true +} +``` + +& ⚫️ 🔜 ⌛ 📨 ⚪️➡️ 👈 *🔢 🛠️* ⏮️ 🎻 💪 💖: + +```JSON +{ + "ok": true +} +``` + +!!! tip + 👀 ❔ ⏲ 📛 ⚙️ 🔌 📛 📨 🔢 🔢 `callback_url` (`https://www.external.org/events`) & 🧾 `id` ⚪️➡️ 🔘 🎻 💪 (`2expen51ve`). + +### 🚮 ⏲ 📻 + +👉 ☝ 👆 ✔️ *⏲ ➡ 🛠️(Ⓜ)* 💪 (1️⃣(Ⓜ) 👈 *🔢 👩‍💻* 🔜 🛠️ *🔢 🛠️*) ⏲ 📻 👆 ✍ 🔛. + +🔜 ⚙️ 🔢 `callbacks` *👆 🛠️ ➡ 🛠️ 👨‍🎨* 🚶‍♀️ 🔢 `.routes` (👈 🤙 `list` 🛣/*➡ 🛠️*) ⚪️➡️ 👈 ⏲ 📻: + +```Python hl_lines="35" +{!../../../docs_src/openapi_callbacks/tutorial001.py!} +``` + +!!! tip + 👀 👈 👆 🚫 🚶‍♀️ 📻 ⚫️ (`invoices_callback_router`) `callback=`, ✋️ 🔢 `.routes`, `invoices_callback_router.routes`. + +### ✅ 🩺 + +🔜 👆 💪 ▶️ 👆 📱 ⏮️ Uvicorn & 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 👆 🩺 ✅ "⏲" 📄 👆 *➡ 🛠️* 👈 🎦 ❔ *🔢 🛠️* 🔜 👀 💖: + + diff --git a/docs/em/docs/advanced/path-operation-advanced-configuration.md b/docs/em/docs/advanced/path-operation-advanced-configuration.md new file mode 100644 index 000000000..ec7231870 --- /dev/null +++ b/docs/em/docs/advanced/path-operation-advanced-configuration.md @@ -0,0 +1,170 @@ +# ➡ 🛠️ 🏧 📳 + +## 🗄 { + +!!! warning + 🚥 👆 🚫 "🕴" 🗄, 👆 🎲 🚫 💪 👉. + +👆 💪 ⚒ 🗄 `operationId` ⚙️ 👆 *➡ 🛠️* ⏮️ 🔢 `operation_id`. + +👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 😍 🔠 🛠️. + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial001.py!} +``` + +### ⚙️ *➡ 🛠️ 🔢* 📛 { + +🚥 👆 💚 ⚙️ 👆 🔗' 🔢 📛 `operationId`Ⓜ, 👆 💪 🔁 🤭 🌐 👫 & 🔐 🔠 *➡ 🛠️* `operation_id` ⚙️ 👫 `APIRoute.name`. + +👆 🔜 ⚫️ ⏮️ ❎ 🌐 👆 *➡ 🛠️*. + +```Python hl_lines="2 12-21 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial002.py!} +``` + +!!! tip + 🚥 👆 ❎ 🤙 `app.openapi()`, 👆 🔜 ℹ `operationId`Ⓜ ⏭ 👈. + +!!! warning + 🚥 👆 👉, 👆 ✔️ ⚒ 💭 🔠 1️⃣ 👆 *➡ 🛠️ 🔢* ✔️ 😍 📛. + + 🚥 👫 🎏 🕹 (🐍 📁). + +## 🚫 ⚪️➡️ 🗄 + +🚫 *➡ 🛠️* ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚙️ 🔢 `include_in_schema` & ⚒ ⚫️ `False`: + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial003.py!} +``` + +## 🏧 📛 ⚪️➡️ #️⃣ + +👆 💪 📉 ⏸ ⚙️ ⚪️➡️ #️⃣ *➡ 🛠️ 🔢* 🗄. + +❎ `\f` (😖 "📨 🍼" 🦹) 🤕 **FastAPI** 🔁 🔢 ⚙️ 🗄 👉 ☝. + +⚫️ 🏆 🚫 🎦 🆙 🧾, ✋️ 🎏 🧰 (✅ 🐉) 🔜 💪 ⚙️ 🎂. + +```Python hl_lines="19-29" +{!../../../docs_src/path_operation_advanced_configuration/tutorial004.py!} +``` + +## 🌖 📨 + +👆 🎲 ✔️ 👀 ❔ 📣 `response_model` & `status_code` *➡ 🛠️*. + +👈 🔬 🗃 🔃 👑 📨 *➡ 🛠️*. + +👆 💪 📣 🌖 📨 ⏮️ 👫 🏷, 👔 📟, ♒️. + +📤 🎂 📃 📥 🧾 🔃 ⚫️, 👆 💪 ✍ ⚫️ [🌖 📨 🗄](./additional-responses.md){.internal-link target=_blank}. + +## 🗄 ➕ + +🕐❔ 👆 📣 *➡ 🛠️* 👆 🈸, **FastAPI** 🔁 🏗 🔗 🗃 🔃 👈 *➡ 🛠️* 🔌 🗄 🔗. + +!!! note "📡 ℹ" + 🗄 🔧 ⚫️ 🤙 🛠️ 🎚. + +⚫️ ✔️ 🌐 ℹ 🔃 *➡ 🛠️* & ⚙️ 🏗 🏧 🧾. + +⚫️ 🔌 `tags`, `parameters`, `requestBody`, `responses`, ♒️. + +👉 *➡ 🛠️*-🎯 🗄 🔗 🛎 🏗 🔁 **FastAPI**, ✋️ 👆 💪 ↔ ⚫️. + +!!! tip + 👉 🔅 🎚 ↔ ☝. + + 🚥 👆 🕴 💪 📣 🌖 📨, 🌅 🏪 🌌 ⚫️ ⏮️ [🌖 📨 🗄](./additional-responses.md){.internal-link target=_blank}. + +👆 💪 ↔ 🗄 🔗 *➡ 🛠️* ⚙️ 🔢 `openapi_extra`. + +### 🗄 ↔ + +👉 `openapi_extra` 💪 👍, 🖼, 📣 [🗄 ↔](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#specificationExtensions): + +```Python hl_lines="6" +{!../../../docs_src/path_operation_advanced_configuration/tutorial005.py!} +``` + +🚥 👆 📂 🏧 🛠️ 🩺, 👆 ↔ 🔜 🎦 🆙 🔝 🎯 *➡ 🛠️*. + + + +& 🚥 👆 👀 📉 🗄 ( `/openapi.json` 👆 🛠️), 👆 🔜 👀 👆 ↔ 🍕 🎯 *➡ 🛠️* 💁‍♂️: + +```JSON hl_lines="22" +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-aperture-labs-portal": "blue" + } + } + } +} +``` + +### 🛃 🗄 *➡ 🛠️* 🔗 + +📖 `openapi_extra` 🔜 🙇 🔗 ⏮️ 🔁 🏗 🗄 🔗 *➡ 🛠️*. + +, 👆 💪 🚮 🌖 💽 🔁 🏗 🔗. + +🖼, 👆 💪 💭 ✍ & ✔ 📨 ⏮️ 👆 👍 📟, 🍵 ⚙️ 🏧 ⚒ FastAPI ⏮️ Pydantic, ✋️ 👆 💪 💚 🔬 📨 🗄 🔗. + +👆 💪 👈 ⏮️ `openapi_extra`: + +```Python hl_lines="20-37 39-40" +{!../../../docs_src/path_operation_advanced_configuration/tutorial006.py!} +``` + +👉 🖼, 👥 🚫 📣 🙆 Pydantic 🏷. 👐, 📨 💪 🚫 🎻 🎻, ⚫️ ✍ 🔗 `bytes`, & 🔢 `magic_data_reader()` 🔜 🈚 🎻 ⚫️ 🌌. + +👐, 👥 💪 📣 📈 🔗 📨 💪. + +### 🛃 🗄 🎚 🆎 + +⚙️ 👉 🎏 🎱, 👆 💪 ⚙️ Pydantic 🏷 🔬 🎻 🔗 👈 ⤴️ 🔌 🛃 🗄 🔗 📄 *➡ 🛠️*. + +& 👆 💪 👉 🚥 💽 🆎 📨 🚫 🎻. + +🖼, 👉 🈸 👥 🚫 ⚙️ FastAPI 🛠️ 🛠️ ⚗ 🎻 🔗 ⚪️➡️ Pydantic 🏷 🚫 🏧 🔬 🎻. 👐, 👥 📣 📨 🎚 🆎 📁, 🚫 🎻: + +```Python hl_lines="17-22 24" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +👐, 👐 👥 🚫 ⚙️ 🔢 🛠️ 🛠️, 👥 ⚙️ Pydantic 🏷 ❎ 🏗 🎻 🔗 💽 👈 👥 💚 📨 📁. + +⤴️ 👥 ⚙️ 📨 🔗, & ⚗ 💪 `bytes`. 👉 ⛓ 👈 FastAPI 🏆 🚫 🔄 🎻 📨 🚀 🎻. + +& ⤴️ 👆 📟, 👥 🎻 👈 📁 🎚 🔗, & ⤴️ 👥 🔄 ⚙️ 🎏 Pydantic 🏷 ✔ 📁 🎚: + +```Python hl_lines="26-33" +{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} +``` + +!!! tip + 📥 👥 🏤-⚙️ 🎏 Pydantic 🏷. + + ✋️ 🎏 🌌, 👥 💪 ✔️ ✔ ⚫️ 🎏 🌌. diff --git a/docs/em/docs/advanced/response-change-status-code.md b/docs/em/docs/advanced/response-change-status-code.md new file mode 100644 index 000000000..156efcc16 --- /dev/null +++ b/docs/em/docs/advanced/response-change-status-code.md @@ -0,0 +1,33 @@ +# 📨 - 🔀 👔 📟 + +👆 🎲 ✍ ⏭ 👈 👆 💪 ⚒ 🔢 [📨 👔 📟](../tutorial/response-status-code.md){.internal-link target=_blank}. + +✋️ 💼 👆 💪 📨 🎏 👔 📟 🌘 🔢. + +## ⚙️ 💼 + +🖼, 🌈 👈 👆 💚 📨 🇺🇸🔍 👔 📟 "👌" `200` 🔢. + +✋️ 🚥 💽 🚫 🔀, 👆 💚 ✍ ⚫️, & 📨 🇺🇸🔍 👔 📟 "✍" `201`. + +✋️ 👆 💚 💪 ⛽ & 🗜 💽 👆 📨 ⏮️ `response_model`. + +📚 💼, 👆 💪 ⚙️ `Response` 🔢. + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪 & 🎚). + +& ⤴️ 👆 💪 ⚒ `status_code` 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 9 12" +{!../../../docs_src/response_change_status_code/tutorial001.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 👔 📟 (🍪 & 🎚), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 👔 📟 👫. ✋️ ✔️ 🤯 👈 🏁 1️⃣ ⚒ 🔜 🏆. diff --git a/docs/em/docs/advanced/response-cookies.md b/docs/em/docs/advanced/response-cookies.md new file mode 100644 index 000000000..23fffe1dd --- /dev/null +++ b/docs/em/docs/advanced/response-cookies.md @@ -0,0 +1,49 @@ +# 📨 🍪 + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢*. + +& ⤴️ 👆 💪 ⚒ 🍪 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 8-9" +{!../../../docs_src/response_cookies/tutorial002.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🍪 (🎚 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🍪 (& 🎚) 👫. + +## 📨 `Response` 🔗 + +👆 💪 ✍ 🍪 🕐❔ 🛬 `Response` 🔗 👆 📟. + +👈, 👆 💪 ✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank}. + +⤴️ ⚒ 🍪 ⚫️, & ⤴️ 📨 ⚫️: + +```Python hl_lines="10-12" +{!../../../docs_src/response_cookies/tutorial001.py!} +``` + +!!! tip + ✔️ 🤯 👈 🚥 👆 📨 📨 🔗 ↩️ ⚙️ `Response` 🔢, FastAPI 🔜 📨 ⚫️ 🔗. + + , 👆 🔜 ✔️ ⚒ 💭 👆 💽 ☑ 🆎. 🤶 Ⓜ. ⚫️ 🔗 ⏮️ 🎻, 🚥 👆 🛬 `JSONResponse`. + + & 👈 👆 🚫 📨 🙆 📊 👈 🔜 ✔️ ⛽ `response_model`. + +### 🌅 ℹ + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + + & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. + +👀 🌐 💪 🔢 & 🎛, ✅ 🧾 💃. diff --git a/docs/em/docs/advanced/response-directly.md b/docs/em/docs/advanced/response-directly.md new file mode 100644 index 000000000..ba09734fb --- /dev/null +++ b/docs/em/docs/advanced/response-directly.md @@ -0,0 +1,63 @@ +# 📨 📨 🔗 + +🕐❔ 👆 ✍ **FastAPI** *➡ 🛠️* 👆 💪 🛎 📨 🙆 📊 ⚪️➡️ ⚫️: `dict`, `list`, Pydantic 🏷, 💽 🏷, ♒️. + +🔢, **FastAPI** 🔜 🔁 🗜 👈 📨 💲 🎻 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](../tutorial/encoder.md){.internal-link target=_blank}. + +⤴️, ⛅ 🎑, ⚫️ 🔜 🚮 👈 🎻-🔗 💽 (✅ `dict`) 🔘 `JSONResponse` 👈 🔜 ⚙️ 📨 📨 👩‍💻. + +✋️ 👆 💪 📨 `JSONResponse` 🔗 ⚪️➡️ 👆 *➡ 🛠️*. + +⚫️ 💪 ⚠, 🖼, 📨 🛃 🎚 ⚖️ 🍪. + +## 📨 `Response` + +👐, 👆 💪 📨 🙆 `Response` ⚖️ 🙆 🎧-🎓 ⚫️. + +!!! tip + `JSONResponse` ⚫️ 🎧-🎓 `Response`. + +& 🕐❔ 👆 📨 `Response`, **FastAPI** 🔜 🚶‍♀️ ⚫️ 🔗. + +⚫️ 🏆 🚫 🙆 💽 🛠️ ⏮️ Pydantic 🏷, ⚫️ 🏆 🚫 🗜 🎚 🙆 🆎, ♒️. + +👉 🤝 👆 📚 💪. 👆 💪 📨 🙆 📊 🆎, 🔐 🙆 💽 📄 ⚖️ 🔬, ♒️. + +## ⚙️ `jsonable_encoder` `Response` + +↩️ **FastAPI** 🚫 🙆 🔀 `Response` 👆 📨, 👆 ✔️ ⚒ 💭 ⚫️ 🎚 🔜 ⚫️. + +🖼, 👆 🚫🔜 🚮 Pydantic 🏷 `JSONResponse` 🍵 🥇 🏭 ⚫️ `dict` ⏮️ 🌐 📊 🆎 (💖 `datetime`, `UUID`, ♒️) 🗜 🎻-🔗 🆎. + +📚 💼, 👆 💪 ⚙️ `jsonable_encoder` 🗜 👆 📊 ⏭ 🚶‍♀️ ⚫️ 📨: + +```Python hl_lines="6-7 21-22" +{!../../../docs_src/response_directly/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +## 🛬 🛃 `Response` + +🖼 🔛 🎦 🌐 🍕 👆 💪, ✋️ ⚫️ 🚫 📶 ⚠, 👆 💪 ✔️ 📨 `item` 🔗, & **FastAPI** 🔜 🚮 ⚫️ `JSONResponse` 👆, 🏭 ⚫️ `dict`, ♒️. 🌐 👈 🔢. + +🔜, ➡️ 👀 ❔ 👆 💪 ⚙️ 👈 📨 🛃 📨. + +➡️ 💬 👈 👆 💚 📨 📂 📨. + +👆 💪 🚮 👆 📂 🎚 🎻, 🚮 ⚫️ `Response`, & 📨 ⚫️: + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +## 🗒 + +🕐❔ 👆 📨 `Response` 🔗 🚮 📊 🚫 ✔, 🗜 (🎻), 🚫 📄 🔁. + +✋️ 👆 💪 📄 ⚫️ 🔬 [🌖 📨 🗄](additional-responses.md){.internal-link target=_blank}. + +👆 💪 👀 ⏪ 📄 ❔ ⚙️/📣 👉 🛃 `Response`Ⓜ ⏪ ✔️ 🏧 💽 🛠️, 🧾, ♒️. diff --git a/docs/em/docs/advanced/response-headers.md b/docs/em/docs/advanced/response-headers.md new file mode 100644 index 000000000..de798982a --- /dev/null +++ b/docs/em/docs/advanced/response-headers.md @@ -0,0 +1,42 @@ +# 📨 🎚 + +## ⚙️ `Response` 🔢 + +👆 💪 📣 🔢 🆎 `Response` 👆 *➡ 🛠️ 🔢* (👆 💪 🍪). + +& ⤴️ 👆 💪 ⚒ 🎚 👈 *🔀* 📨 🎚. + +```Python hl_lines="1 7-8" +{!../../../docs_src/response_headers/tutorial002.py!} +``` + +& ⤴️ 👆 💪 📨 🙆 🎚 👆 💪, 👆 🛎 🔜 ( `dict`, 💽 🏷, ♒️). + +& 🚥 👆 📣 `response_model`, ⚫️ 🔜 ⚙️ ⛽ & 🗜 🎚 👆 📨. + +**FastAPI** 🔜 ⚙️ 👈 *🔀* 📨 ⚗ 🎚 (🍪 & 👔 📟), & 🔜 🚮 👫 🏁 📨 👈 🔌 💲 👆 📨, ⛽ 🙆 `response_model`. + +👆 💪 📣 `Response` 🔢 🔗, & ⚒ 🎚 (& 🍪) 👫. + +## 📨 `Response` 🔗 + +👆 💪 🚮 🎚 🕐❔ 👆 📨 `Response` 🔗. + +✍ 📨 🔬 [📨 📨 🔗](response-directly.md){.internal-link target=_blank} & 🚶‍♀️ 🎚 🌖 🔢: + +```Python hl_lines="10-12" +{!../../../docs_src/response_headers/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import Response` ⚖️ `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + + & `Response` 💪 ⚙️ 🛎 ⚒ 🎚 & 🍪, **FastAPI** 🚚 ⚫️ `fastapi.Response`. + +## 🛃 🎚 + +✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. + +✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 (✍ 🌅 [⚜ (✖️-🇨🇳 ℹ 🤝)](../tutorial/cors.md){.internal-link target=_blank}), ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. diff --git a/docs/em/docs/advanced/security/http-basic-auth.md b/docs/em/docs/advanced/security/http-basic-auth.md new file mode 100644 index 000000000..33470a726 --- /dev/null +++ b/docs/em/docs/advanced/security/http-basic-auth.md @@ -0,0 +1,113 @@ +# 🇺🇸🔍 🔰 🔐 + +🙅 💼, 👆 💪 ⚙️ 🇺🇸🔍 🔰 🔐. + +🇺🇸🔍 🔰 🔐, 🈸 ⌛ 🎚 👈 🔌 🆔 & 🔐. + +🚥 ⚫️ 🚫 📨 ⚫️, ⚫️ 📨 🇺🇸🔍 4️⃣0️⃣1️⃣ "⛔" ❌. + +& 📨 🎚 `WWW-Authenticate` ⏮️ 💲 `Basic`, & 📦 `realm` 🔢. + +👈 💬 🖥 🎦 🛠️ 📋 🆔 & 🔐. + +⤴️, 🕐❔ 👆 🆎 👈 🆔 & 🔐, 🖥 📨 👫 🎚 🔁. + +## 🙅 🇺🇸🔍 🔰 🔐 + +* 🗄 `HTTPBasic` & `HTTPBasicCredentials`. +* ✍ "`security` ⚖" ⚙️ `HTTPBasic`. +* ⚙️ 👈 `security` ⏮️ 🔗 👆 *➡ 🛠️*. +* ⚫️ 📨 🎚 🆎 `HTTPBasicCredentials`: + * ⚫️ 🔌 `username` & `password` 📨. + +```Python hl_lines="2 6 10" +{!../../../docs_src/security/tutorial006.py!} +``` + +🕐❔ 👆 🔄 📂 📛 🥇 🕰 (⚖️ 🖊 "🛠️" 🔼 🩺) 🖥 🔜 💭 👆 👆 🆔 & 🔐: + + + +## ✅ 🆔 + +📥 🌅 🏁 🖼. + +⚙️ 🔗 ✅ 🚥 🆔 & 🔐 ☑. + +👉, ⚙️ 🐍 🐩 🕹 `secrets` ✅ 🆔 & 🔐. + +`secrets.compare_digest()` 💪 ✊ `bytes` ⚖️ `str` 👈 🕴 🔌 🔠 🦹 (🕐 🇪🇸), 👉 ⛓ ⚫️ 🚫🔜 👷 ⏮️ 🦹 💖 `á`, `Sebastián`. + +🍵 👈, 👥 🥇 🗜 `username` & `password` `bytes` 🔢 👫 ⏮️ 🔠-8️⃣. + +⤴️ 👥 💪 ⚙️ `secrets.compare_digest()` 🚚 👈 `credentials.username` `"stanleyjobson"`, & 👈 `credentials.password` `"swordfish"`. + +```Python hl_lines="1 11-21" +{!../../../docs_src/security/tutorial007.py!} +``` + +👉 🔜 🎏: + +```Python +if not (credentials.username == "stanleyjobson") or not (credentials.password == "swordfish"): + # Return some error + ... +``` + +✋️ ⚙️ `secrets.compare_digest()` ⚫️ 🔜 🔐 🛡 🆎 👊 🤙 "🕰 👊". + +### ⏲ 👊 + +✋️ ⚫️❔ "⏲ 👊"❓ + +➡️ 🌈 👊 🔄 💭 🆔 & 🔐. + +& 👫 📨 📨 ⏮️ 🆔 `johndoe` & 🔐 `love123`. + +⤴️ 🐍 📟 👆 🈸 🔜 🌓 🕳 💖: + +```Python +if "johndoe" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +✋️ ▶️️ 🙍 🐍 🔬 🥇 `j` `johndoe` 🥇 `s` `stanleyjobson`, ⚫️ 🔜 📨 `False`, ↩️ ⚫️ ⏪ 💭 👈 📚 2️⃣ 🎻 🚫 🎏, 💭 👈 "📤 🙅‍♂ 💪 🗑 🌅 📊 ⚖ 🎂 🔤". & 👆 🈸 🔜 💬 "❌ 👩‍💻 ⚖️ 🔐". + +✋️ ⤴️ 👊 🔄 ⏮️ 🆔 `stanleyjobsox` & 🔐 `love123`. + +& 👆 🈸 📟 🔨 🕳 💖: + +```Python +if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": + ... +``` + +🐍 🔜 ✔️ 🔬 🎂 `stanleyjobso` 👯‍♂️ `stanleyjobsox` & `stanleyjobson` ⏭ 🤔 👈 👯‍♂️ 🎻 🚫 🎏. ⚫️ 🔜 ✊ ➕ ⏲ 📨 🔙 "❌ 👩‍💻 ⚖️ 🔐". + +#### 🕰 ❔ ℹ 👊 + +👈 ☝, 👀 👈 💽 ✊ ⏲ 📏 📨 "❌ 👩‍💻 ⚖️ 🔐" 📨, 👊 🔜 💭 👈 👫 🤚 _🕳_ ▶️️, ▶️ 🔤 ▶️️. + +& ⤴️ 👫 💪 🔄 🔄 🤔 👈 ⚫️ 🎲 🕳 🌖 🎏 `stanleyjobsox` 🌘 `johndoe`. + +#### "🕴" 👊 + +↗️, 👊 🔜 🚫 🔄 🌐 👉 ✋, 👫 🔜 ✍ 📋 ⚫️, 🎲 ⏮️ 💯 ⚖️ 💯 💯 📍 🥈. & 🔜 🤚 1️⃣ ➕ ☑ 🔤 🕰. + +✋️ 🔨 👈, ⏲ ⚖️ 📆 👊 🔜 ✔️ 💭 ☑ 🆔 & 🔐, ⏮️ "ℹ" 👆 🈸, ⚙️ 🕰 ✊ ❔. + +#### 🔧 ⚫️ ⏮️ `secrets.compare_digest()` + +✋️ 👆 📟 👥 🤙 ⚙️ `secrets.compare_digest()`. + +📏, ⚫️ 🔜 ✊ 🎏 🕰 🔬 `stanleyjobsox` `stanleyjobson` 🌘 ⚫️ ✊ 🔬 `johndoe` `stanleyjobson`. & 🎏 🔐. + +👈 🌌, ⚙️ `secrets.compare_digest()` 👆 🈸 📟, ⚫️ 🔜 🔒 🛡 👉 🎂 ↔ 💂‍♂ 👊. + +### 📨 ❌ + +⏮️ 🔍 👈 🎓 ❌, 📨 `HTTPException` ⏮️ 👔 📟 4️⃣0️⃣1️⃣ (🎏 📨 🕐❔ 🙅‍♂ 🎓 🚚) & 🚮 🎚 `WWW-Authenticate` ⚒ 🖥 🎦 💳 📋 🔄: + +```Python hl_lines="23-27" +{!../../../docs_src/security/tutorial007.py!} +``` diff --git a/docs/em/docs/advanced/security/index.md b/docs/em/docs/advanced/security/index.md new file mode 100644 index 000000000..20ee85553 --- /dev/null +++ b/docs/em/docs/advanced/security/index.md @@ -0,0 +1,16 @@ +# 🏧 💂‍♂ - 🎶 + +## 🌖 ⚒ + +📤 ➕ ⚒ 🍵 💂‍♂ ↖️ ⚪️➡️ 🕐 📔 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/){.internal-link target=_blank}. + +!!! tip + ⏭ 📄 **🚫 🎯 "🏧"**. + + & ⚫️ 💪 👈 👆 ⚙️ 💼, ⚗ 1️⃣ 👫. + +## ✍ 🔰 🥇 + +⏭ 📄 🤔 👆 ⏪ ✍ 👑 [🔰 - 👩‍💻 🦮: 💂‍♂](../../tutorial/security/){.internal-link target=_blank}. + +👫 🌐 ⚓️ 🔛 🎏 🔧, ✋️ ✔ ➕ 🛠️. diff --git a/docs/em/docs/advanced/security/oauth2-scopes.md b/docs/em/docs/advanced/security/oauth2-scopes.md new file mode 100644 index 000000000..a4684352c --- /dev/null +++ b/docs/em/docs/advanced/security/oauth2-scopes.md @@ -0,0 +1,269 @@ +# Oauth2️⃣ ↔ + +👆 💪 ⚙️ Oauth2️⃣ ↔ 🔗 ⏮️ **FastAPI**, 👫 🛠️ 👷 💎. + +👉 🔜 ✔ 👆 ✔️ 🌖 👌-🧽 ✔ ⚙️, 📄 Oauth2️⃣ 🐩, 🛠️ 🔘 👆 🗄 🈸 (& 🛠️ 🩺). + +Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. 👫 ⚙️ ⚫️ 🚚 🎯 ✔ 👩‍💻 & 🈸. + +🔠 🕰 👆 "🕹 ⏮️" 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, 👈 🈸 ⚙️ Oauth2️⃣ ⏮️ ↔. + +👉 📄 👆 🔜 👀 ❔ 🛠️ 🤝 & ✔ ⏮️ 🎏 Oauth2️⃣ ⏮️ ↔ 👆 **FastAPI** 🈸. + +!!! warning + 👉 🌅 ⚖️ 🌘 🏧 📄. 🚥 👆 ▶️, 👆 💪 🚶 ⚫️. + + 👆 🚫 🎯 💪 Oauth2️⃣ ↔, & 👆 💪 🍵 🤝 & ✔ 👐 👆 💚. + + ✋️ Oauth2️⃣ ⏮️ ↔ 💪 🎆 🛠️ 🔘 👆 🛠️ (⏮️ 🗄) & 👆 🛠️ 🩺. + + 👐, 👆 🛠️ 📚 ↔, ⚖️ 🙆 🎏 💂‍♂/✔ 📄, 👐 👆 💪, 👆 📟. + + 📚 💼, Oauth2️⃣ ⏮️ ↔ 💪 👹. + + ✋️ 🚥 👆 💭 👆 💪 ⚫️, ⚖️ 👆 😟, 🚧 👂. + +## Oauth2️⃣ ↔ & 🗄 + +Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. + +🎚 🔠 👉 🎻 💪 ✔️ 🙆 📁, ✋️ 🔜 🚫 🔌 🚀. + +👫 ↔ 🎨 "✔". + +🗄 (✅ 🛠️ 🩺), 👆 💪 🔬 "💂‍♂ ⚖". + +🕐❔ 1️⃣ 👫 💂‍♂ ⚖ ⚙️ Oauth2️⃣, 👆 💪 📣 & ⚙️ ↔. + +🔠 "↔" 🎻 (🍵 🚀). + +👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: + +* `users:read` ⚖️ `users:write` ⚠ 🖼. +* `instagram_basic` ⚙️ 👱📔 / 👱📔. +* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. + +!!! info + Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. + + ⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. + + 👈 ℹ 🛠️ 🎯. + + Oauth2️⃣ 👫 🎻. + +## 🌐 🎑 + +🥇, ➡️ 🔜 👀 🍕 👈 🔀 ⚪️➡️ 🖼 👑 **🔰 - 👩‍💻 🦮** [Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. 🔜 ⚙️ Oauth2️⃣ ↔: + +```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" +{!../../../docs_src/security/tutorial005.py!} +``` + +🔜 ➡️ 📄 👈 🔀 🔁 🔁. + +## Oauth2️⃣ 💂‍♂ ⚖ + +🥇 🔀 👈 🔜 👥 📣 Oauth2️⃣ 💂‍♂ ⚖ ⏮️ 2️⃣ 💪 ↔, `me` & `items`. + +`scopes` 🔢 📨 `dict` ⏮️ 🔠 ↔ 🔑 & 📛 💲: + +```Python hl_lines="62-65" +{!../../../docs_src/security/tutorial005.py!} +``` + +↩️ 👥 🔜 📣 📚 ↔, 👫 🔜 🎦 🆙 🛠️ 🩺 🕐❔ 👆 🕹-/✔. + +& 👆 🔜 💪 🖊 ❔ ↔ 👆 💚 🤝 🔐: `me` & `items`. + +👉 🎏 🛠️ ⚙️ 🕐❔ 👆 🤝 ✔ ⏪ 🚨 ⏮️ 👱📔, 🇺🇸🔍, 📂, ♒️: + + + +## 🥙 🤝 ⏮️ ↔ + +🔜, 🔀 🤝 *➡ 🛠️* 📨 ↔ 📨. + +👥 ⚙️ 🎏 `OAuth2PasswordRequestForm`. ⚫️ 🔌 🏠 `scopes` ⏮️ `list` `str`, ⏮️ 🔠 ↔ ⚫️ 📨 📨. + +& 👥 📨 ↔ 🍕 🥙 🤝. + +!!! danger + 🦁, 📥 👥 ❎ ↔ 📨 🔗 🤝. + + ✋️ 👆 🈸, 💂‍♂, 👆 🔜 ⚒ 💭 👆 🕴 🚮 ↔ 👈 👩‍💻 🤙 💪 ✔️, ⚖️ 🕐 👆 ✔️ 🔁. + +```Python hl_lines="153" +{!../../../docs_src/security/tutorial005.py!} +``` + +## 📣 ↔ *➡ 🛠️* & 🔗 + +🔜 👥 📣 👈 *➡ 🛠️* `/users/me/items/` 🚚 ↔ `items`. + +👉, 👥 🗄 & ⚙️ `Security` ⚪️➡️ `fastapi`. + +👆 💪 ⚙️ `Security` 📣 🔗 (💖 `Depends`), ✋️ `Security` 📨 🔢 `scopes` ⏮️ 📇 ↔ (🎻). + +👉 💼, 👥 🚶‍♀️ 🔗 🔢 `get_current_active_user` `Security` (🎏 🌌 👥 🔜 ⏮️ `Depends`). + +✋️ 👥 🚶‍♀️ `list` ↔, 👉 💼 ⏮️ 1️⃣ ↔: `items` (⚫️ 💪 ✔️ 🌅). + +& 🔗 🔢 `get_current_active_user` 💪 📣 🎧-🔗, 🚫 🕴 ⏮️ `Depends` ✋️ ⏮️ `Security`. 📣 🚮 👍 🎧-🔗 🔢 (`get_current_user`), & 🌖 ↔ 📄. + +👉 💼, ⚫️ 🚚 ↔ `me` (⚫️ 💪 🚚 🌅 🌘 1️⃣ ↔). + +!!! note + 👆 🚫 🎯 💪 🚮 🎏 ↔ 🎏 🥉. + + 👥 🔨 ⚫️ 📥 🎦 ❔ **FastAPI** 🍵 ↔ 📣 🎏 🎚. + +```Python hl_lines="4 139 166" +{!../../../docs_src/security/tutorial005.py!} +``` + +!!! info "📡 ℹ" + `Security` 🤙 🏿 `Depends`, & ⚫️ ✔️ 1️⃣ ➕ 🔢 👈 👥 🔜 👀 ⏪. + + ✋️ ⚙️ `Security` ↩️ `Depends`, **FastAPI** 🔜 💭 👈 ⚫️ 💪 📣 💂‍♂ ↔, ⚙️ 👫 🔘, & 📄 🛠️ ⏮️ 🗄. + + ✋️ 🕐❔ 👆 🗄 `Query`, `Path`, `Depends`, `Security` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +## ⚙️ `SecurityScopes` + +🔜 ℹ 🔗 `get_current_user`. + +👉 1️⃣ ⚙️ 🔗 🔛. + +📥 👥 ⚙️ 🎏 Oauth2️⃣ ⚖ 👥 ✍ ⏭, 📣 ⚫️ 🔗: `oauth2_scheme`. + +↩️ 👉 🔗 🔢 🚫 ✔️ 🙆 ↔ 📄 ⚫️, 👥 💪 ⚙️ `Depends` ⏮️ `oauth2_scheme`, 👥 🚫 ✔️ ⚙️ `Security` 🕐❔ 👥 🚫 💪 ✔ 💂‍♂ ↔. + +👥 📣 🎁 🔢 🆎 `SecurityScopes`, 🗄 ⚪️➡️ `fastapi.security`. + +👉 `SecurityScopes` 🎓 🎏 `Request` (`Request` ⚙️ 🤚 📨 🎚 🔗). + +```Python hl_lines="8 105" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ⚙️ `scopes` + +🔢 `security_scopes` 🔜 🆎 `SecurityScopes`. + +⚫️ 🔜 ✔️ 🏠 `scopes` ⏮️ 📇 ⚗ 🌐 ↔ ✔ ⚫️ & 🌐 🔗 👈 ⚙️ 👉 🎧-🔗. 👈 ⛓, 🌐 "⚓️"... 👉 💪 🔊 😨, ⚫️ 🔬 🔄 ⏪ 🔛. + +`security_scopes` 🎚 (🎓 `SecurityScopes`) 🚚 `scope_str` 🔢 ⏮️ 👁 🎻, 🔌 👈 ↔ 👽 🚀 (👥 🔜 ⚙️ ⚫️). + +👥 ✍ `HTTPException` 👈 👥 💪 🏤-⚙️ (`raise`) ⏪ 📚 ☝. + +👉 ⚠, 👥 🔌 ↔ 🚚 (🚥 🙆) 🎻 👽 🚀 (⚙️ `scope_str`). 👥 🚮 👈 🎻 ⚗ ↔ `WWW-Authenticate` 🎚 (👉 🍕 🔌). + +```Python hl_lines="105 107-115" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ✔ `username` & 💽 💠 + +👥 ✔ 👈 👥 🤚 `username`, & ⚗ ↔. + +& ⤴️ 👥 ✔ 👈 📊 ⏮️ Pydantic 🏷 (✊ `ValidationError` ⚠), & 🚥 👥 🤚 ❌ 👂 🥙 🤝 ⚖️ ⚖ 📊 ⏮️ Pydantic, 👥 🤚 `HTTPException` 👥 ✍ ⏭. + +👈, 👥 ℹ Pydantic 🏷 `TokenData` ⏮️ 🆕 🏠 `scopes`. + +⚖ 📊 ⏮️ Pydantic 👥 💪 ⚒ 💭 👈 👥 ✔️, 🖼, ⚫️❔ `list` `str` ⏮️ ↔ & `str` ⏮️ `username`. + +↩️, 🖼, `dict`, ⚖️ 🕳 🙆, ⚫️ 💪 💔 🈸 ☝ ⏪, ⚒ ⚫️ 💂‍♂ ⚠. + +👥 ✔ 👈 👥 ✔️ 👩‍💻 ⏮️ 👈 🆔, & 🚥 🚫, 👥 🤚 👈 🎏 ⚠ 👥 ✍ ⏭. + +```Python hl_lines="46 116-127" +{!../../../docs_src/security/tutorial005.py!} +``` + +## ✔ `scopes` + +👥 🔜 ✔ 👈 🌐 ↔ ✔, 👉 🔗 & 🌐 ⚓️ (🔌 *➡ 🛠️*), 🔌 ↔ 🚚 🤝 📨, ⏪ 🤚 `HTTPException`. + +👉, 👥 ⚙️ `security_scopes.scopes`, 👈 🔌 `list` ⏮️ 🌐 👫 ↔ `str`. + +```Python hl_lines="128-134" +{!../../../docs_src/security/tutorial005.py!} +``` + +## 🔗 🌲 & ↔ + +➡️ 📄 🔄 👉 🔗 🌲 & ↔. + +`get_current_active_user` 🔗 ✔️ 🎧-🔗 🔛 `get_current_user`, ↔ `"me"` 📣 `get_current_active_user` 🔜 🔌 📇 ✔ ↔ `security_scopes.scopes` 🚶‍♀️ `get_current_user`. + +*➡ 🛠️* ⚫️ 📣 ↔, `"items"`, 👉 🔜 📇 `security_scopes.scopes` 🚶‍♀️ `get_current_user`. + +📥 ❔ 🔗 🔗 & ↔ 👀 💖: + +* *➡ 🛠️* `read_own_items` ✔️: + * ✔ ↔ `["items"]` ⏮️ 🔗: + * `get_current_active_user`: + * 🔗 🔢 `get_current_active_user` ✔️: + * ✔ ↔ `["me"]` ⏮️ 🔗: + * `get_current_user`: + * 🔗 🔢 `get_current_user` ✔️: + * 🙅‍♂ ↔ ✔ ⚫️. + * 🔗 ⚙️ `oauth2_scheme`. + * `security_scopes` 🔢 🆎 `SecurityScopes`: + * 👉 `security_scopes` 🔢 ✔️ 🏠 `scopes` ⏮️ `list` ⚗ 🌐 👫 ↔ 📣 🔛,: + * `security_scopes.scopes` 🔜 🔌 `["me", "items"]` *➡ 🛠️* `read_own_items`. + * `security_scopes.scopes` 🔜 🔌 `["me"]` *➡ 🛠️* `read_users_me`, ↩️ ⚫️ 📣 🔗 `get_current_active_user`. + * `security_scopes.scopes` 🔜 🔌 `[]` (🕳) *➡ 🛠️* `read_system_status`, ↩️ ⚫️ 🚫 📣 🙆 `Security` ⏮️ `scopes`, & 🚮 🔗, `get_current_user`, 🚫 📣 🙆 `scope` 👯‍♂️. + +!!! tip + ⚠ & "🎱" 👜 📥 👈 `get_current_user` 🔜 ✔️ 🎏 📇 `scopes` ✅ 🔠 *➡ 🛠️*. + + 🌐 ⚓️ 🔛 `scopes` 📣 🔠 *➡ 🛠️* & 🔠 🔗 🔗 🌲 👈 🎯 *➡ 🛠️*. + +## 🌖 ℹ 🔃 `SecurityScopes` + +👆 💪 ⚙️ `SecurityScopes` 🙆 ☝, & 💗 🥉, ⚫️ 🚫 ✔️ "🌱" 🔗. + +⚫️ 🔜 🕧 ✔️ 💂‍♂ ↔ 📣 ⏮️ `Security` 🔗 & 🌐 ⚓️ **👈 🎯** *➡ 🛠️* & **👈 🎯** 🔗 🌲. + +↩️ `SecurityScopes` 🔜 ✔️ 🌐 ↔ 📣 ⚓️, 👆 💪 ⚙️ ⚫️ ✔ 👈 🤝 ✔️ 🚚 ↔ 🇨🇫 🔗 🔢, & ⤴️ 📣 🎏 ↔ 📄 🎏 *➡ 🛠️*. + +👫 🔜 ✅ ➡ 🔠 *➡ 🛠️*. + +## ✅ ⚫️ + +🚥 👆 📂 🛠️ 🩺, 👆 💪 🔓 & ✔ ❔ ↔ 👆 💚 ✔. + + + +🚥 👆 🚫 🖊 🙆 ↔, 👆 🔜 "🔓", ✋️ 🕐❔ 👆 🔄 🔐 `/users/me/` ⚖️ `/users/me/items/` 👆 🔜 🤚 ❌ 💬 👈 👆 🚫 ✔️ 🥃 ✔. 👆 🔜 💪 🔐 `/status/`. + +& 🚥 👆 🖊 ↔ `me` ✋️ 🚫 ↔ `items`, 👆 🔜 💪 🔐 `/users/me/` ✋️ 🚫 `/users/me/items/`. + +👈 ⚫️❔ 🔜 🔨 🥉 🥳 🈸 👈 🔄 🔐 1️⃣ 👫 *➡ 🛠️* ⏮️ 🤝 🚚 👩‍💻, ⚓️ 🔛 ❔ 📚 ✔ 👩‍💻 🤝 🈸. + +## 🔃 🥉 🥳 🛠️ + +👉 🖼 👥 ⚙️ Oauth2️⃣ "🔐" 💧. + +👉 ☑ 🕐❔ 👥 🚨 👆 👍 🈸, 🎲 ⏮️ 👆 👍 🕸. + +↩️ 👥 💪 💙 ⚫️ 📨 `username` & `password`, 👥 🎛 ⚫️. + +✋️ 🚥 👆 🏗 Oauth2️⃣ 🈸 👈 🎏 🔜 🔗 (➡, 🚥 👆 🏗 🤝 🐕‍🦺 🌓 👱📔, 🇺🇸🔍, 📂, ♒️.) 👆 🔜 ⚙️ 1️⃣ 🎏 💧. + +🌅 ⚠ 🔑 💧. + +🏆 🔐 📟 💧, ✋️ 🌖 🏗 🛠️ ⚫️ 🚚 🌅 📶. ⚫️ 🌅 🏗, 📚 🐕‍🦺 🔚 🆙 ✔ 🔑 💧. + +!!! note + ⚫️ ⚠ 👈 🔠 🤝 🐕‍🦺 📛 👫 💧 🎏 🌌, ⚒ ⚫️ 🍕 👫 🏷. + + ✋️ 🔚, 👫 🛠️ 🎏 Oauth2️⃣ 🐩. + +**FastAPI** 🔌 🚙 🌐 👫 Oauth2️⃣ 🤝 💧 `fastapi.security.oauth2`. + +## `Security` 👨‍🎨 `dependencies` + +🎏 🌌 👆 💪 🔬 `list` `Depends` 👨‍🎨 `dependencies` 🔢 (🔬 [🔗 ➡ 🛠️ 👨‍🎨](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), 👆 💪 ⚙️ `Security` ⏮️ `scopes` 📤. diff --git a/docs/em/docs/advanced/settings.md b/docs/em/docs/advanced/settings.md new file mode 100644 index 000000000..bc50bf755 --- /dev/null +++ b/docs/em/docs/advanced/settings.md @@ -0,0 +1,382 @@ +# ⚒ & 🌐 🔢 + +📚 💼 👆 🈸 💪 💪 🔢 ⚒ ⚖️ 📳, 🖼 ㊙ 🔑, 💽 🎓, 🎓 📧 🐕‍🦺, ♒️. + +🏆 👫 ⚒ 🔢 (💪 🔀), 💖 💽 📛. & 📚 💪 🚿, 💖 ㊙. + +👉 🤔 ⚫️ ⚠ 🚚 👫 🌐 🔢 👈 ✍ 🈸. + +## 🌐 🔢 + +!!! tip + 🚥 👆 ⏪ 💭 ⚫️❔ "🌐 🔢" & ❔ ⚙️ 👫, 💭 🆓 🚶 ⏭ 📄 🔛. + +🌐 🔢 (💭 "🇨🇻 {") 🔢 👈 🖖 🏞 🐍 📟, 🏃‍♂ ⚙️, & 💪 ✍ 👆 🐍 📟 (⚖️ 🎏 📋 👍). + +👆 💪 ✍ & ⚙️ 🌐 🔢 🐚, 🍵 💆‍♂ 🐍: + +=== "💾, 🇸🇻, 🚪 🎉" + +
+ + ```console + // You could create an env var MY_NAME with + $ export MY_NAME="Wade Wilson" + + // Then you could use it with other programs, like + $ echo "Hello $MY_NAME" + + Hello Wade Wilson + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + // Create an env var MY_NAME + $ $Env:MY_NAME = "Wade Wilson" + + // Use it with other programs, like + $ echo "Hello $Env:MY_NAME" + + Hello Wade Wilson + ``` + +
+ +### ✍ 🇨🇻 {🐍 + +👆 💪 ✍ 🌐 🔢 🏞 🐍, 📶 (⚖️ ⏮️ 🙆 🎏 👩‍🔬), & ⤴️ ✍ 👫 🐍. + +🖼 👆 💪 ✔️ 📁 `main.py` ⏮️: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +!!! tip + 🥈 ❌ `os.getenv()` 🔢 💲 📨. + + 🚥 🚫 🚚, ⚫️ `None` 🔢, 📥 👥 🚚 `"World"` 🔢 💲 ⚙️. + +⤴️ 👆 💪 🤙 👈 🐍 📋: + +
+ +```console +// Here we don't set the env var yet +$ python main.py + +// As we didn't set the env var, we get the default value + +Hello World from Python + +// But if we create an environment variable first +$ export MY_NAME="Wade Wilson" + +// And then call the program again +$ python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python +``` + +
+ +🌐 🔢 💪 ⚒ 🏞 📟, ✋️ 💪 ✍ 📟, & 🚫 ✔️ 🏪 (💕 `git`) ⏮️ 🎂 📁, ⚫️ ⚠ ⚙️ 👫 📳 ⚖️ ⚒. + +👆 💪 ✍ 🌐 🔢 🕴 🎯 📋 👼, 👈 🕴 💪 👈 📋, & 🕴 🚮 📐. + +👈, ✍ ⚫️ ▶️️ ⏭ 📋 ⚫️, 🔛 🎏 ⏸: + +
+ +```console +// Create an env var MY_NAME in line for this program call +$ MY_NAME="Wade Wilson" python main.py + +// Now it can read the environment variable + +Hello Wade Wilson from Python + +// The env var no longer exists afterwards +$ python main.py + +Hello World from Python +``` + +
+ +!!! tip + 👆 💪 ✍ 🌅 🔃 ⚫️ 1️⃣2️⃣-⚖ 📱: 📁. + +### 🆎 & 🔬 + +👫 🌐 🔢 💪 🕴 🍵 ✍ 🎻, 👫 🔢 🐍 & ✔️ 🔗 ⏮️ 🎏 📋 & 🎂 ⚙️ (& ⏮️ 🎏 🏃‍♂ ⚙️, 💾, 🚪, 🇸🇻). + +👈 ⛓ 👈 🙆 💲 ✍ 🐍 ⚪️➡️ 🌐 🔢 🔜 `str`, & 🙆 🛠️ 🎏 🆎 ⚖️ 🔬 ✔️ 🔨 📟. + +## Pydantic `Settings` + +👐, Pydantic 🚚 👑 🚙 🍵 👫 ⚒ 👟 ⚪️➡️ 🌐 🔢 ⏮️ Pydantic: ⚒ 🧾. + +### ✍ `Settings` 🎚 + +🗄 `BaseSettings` ⚪️➡️ Pydantic & ✍ 🎧-🎓, 📶 🌅 💖 ⏮️ Pydantic 🏷. + +🎏 🌌 ⏮️ Pydantic 🏷, 👆 📣 🎓 🔢 ⏮️ 🆎 ✍, & 🎲 🔢 💲. + +👆 💪 ⚙️ 🌐 🎏 🔬 ⚒ & 🧰 👆 ⚙️ Pydantic 🏷, 💖 🎏 📊 🆎 & 🌖 🔬 ⏮️ `Field()`. + +```Python hl_lines="2 5-8 11" +{!../../../docs_src/settings/tutorial001.py!} +``` + +!!! tip + 🚥 👆 💚 🕳 ⏩ 📁 & 📋, 🚫 ⚙️ 👉 🖼, ⚙️ 🏁 1️⃣ 🔛. + +⤴️, 🕐❔ 👆 ✍ 👐 👈 `Settings` 🎓 (👉 💼, `settings` 🎚), Pydantic 🔜 ✍ 🌐 🔢 💼-😛 🌌,, ↖-💼 🔢 `APP_NAME` 🔜 ✍ 🔢 `app_name`. + +⏭ ⚫️ 🔜 🗜 & ✔ 💽. , 🕐❔ 👆 ⚙️ 👈 `settings` 🎚, 👆 🔜 ✔️ 📊 🆎 👆 📣 (✅ `items_per_user` 🔜 `int`). + +### ⚙️ `settings` + +⤴️ 👆 💪 ⚙️ 🆕 `settings` 🎚 👆 🈸: + +```Python hl_lines="18-20" +{!../../../docs_src/settings/tutorial001.py!} +``` + +### 🏃 💽 + +⏭, 👆 🔜 🏃 💽 🚶‍♀️ 📳 🌐 🔢, 🖼 👆 💪 ⚒ `ADMIN_EMAIL` & `APP_NAME` ⏮️: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +!!! tip + ⚒ 💗 🇨🇻 {👁 📋 🎏 👫 ⏮️ 🚀, & 🚮 👫 🌐 ⏭ 📋. + +& ⤴️ `admin_email` ⚒ 🔜 ⚒ `"deadpool@example.com"`. + +`app_name` 🔜 `"ChimichangApp"`. + +& `items_per_user` 🔜 🚧 🚮 🔢 💲 `50`. + +## ⚒ ➕1️⃣ 🕹 + +👆 💪 🚮 👈 ⚒ ➕1️⃣ 🕹 📁 👆 👀 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. + +🖼, 👆 💪 ✔️ 📁 `config.py` ⏮️: + +```Python +{!../../../docs_src/settings/app01/config.py!} +``` + +& ⤴️ ⚙️ ⚫️ 📁 `main.py`: + +```Python hl_lines="3 11-13" +{!../../../docs_src/settings/app01/main.py!} +``` + +!!! tip + 👆 🔜 💪 📁 `__init__.py` 👆 👀 🔛 [🦏 🈸 - 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}. + +## ⚒ 🔗 + +🍾 ⚫️ 5️⃣📆 ⚠ 🚚 ⚒ ⚪️➡️ 🔗, ↩️ ✔️ 🌐 🎚 ⏮️ `settings` 👈 ⚙️ 🌐. + +👉 💪 ✴️ ⚠ ⏮️ 🔬, ⚫️ 📶 ⏩ 🔐 🔗 ⏮️ 👆 👍 🛃 ⚒. + +### 📁 📁 + +👟 ⚪️➡️ ⏮️ 🖼, 👆 `config.py` 📁 💪 👀 💖: + +```Python hl_lines="10" +{!../../../docs_src/settings/app02/config.py!} +``` + +👀 👈 🔜 👥 🚫 ✍ 🔢 👐 `settings = Settings()`. + +### 👑 📱 📁 + +🔜 👥 ✍ 🔗 👈 📨 🆕 `config.Settings()`. + +```Python hl_lines="5 11-12" +{!../../../docs_src/settings/app02/main.py!} +``` + +!!! tip + 👥 🔜 🔬 `@lru_cache()` 🍖. + + 🔜 👆 💪 🤔 `get_settings()` 😐 🔢. + +& ⤴️ 👥 💪 🚚 ⚫️ ⚪️➡️ *➡ 🛠️ 🔢* 🔗 & ⚙️ ⚫️ 🙆 👥 💪 ⚫️. + +```Python hl_lines="16 18-20" +{!../../../docs_src/settings/app02/main.py!} +``` + +### ⚒ & 🔬 + +⤴️ ⚫️ 🔜 📶 ⏩ 🚚 🎏 ⚒ 🎚 ⏮️ 🔬 🏗 🔗 🔐 `get_settings`: + +```Python hl_lines="9-10 13 21" +{!../../../docs_src/settings/app02/test_main.py!} +``` + +🔗 🔐 👥 ⚒ 🆕 💲 `admin_email` 🕐❔ 🏗 🆕 `Settings` 🎚, & ⤴️ 👥 📨 👈 🆕 🎚. + +⤴️ 👥 💪 💯 👈 ⚫️ ⚙️. + +## 👂 `.env` 📁 + +🚥 👆 ✔️ 📚 ⚒ 👈 🎲 🔀 📚, 🎲 🎏 🌐, ⚫️ 5️⃣📆 ⚠ 🚮 👫 🔛 📁 & ⤴️ ✍ 👫 ⚪️➡️ ⚫️ 🚥 👫 🌐 🔢. + +👉 💡 ⚠ 🥃 👈 ⚫️ ✔️ 📛, 👫 🌐 🔢 🛎 🥉 📁 `.env`, & 📁 🤙 "🇨🇻". + +!!! tip + 📁 ▶️ ⏮️ ❣ (`.`) 🕵‍♂ 📁 🖥-💖 ⚙️, 💖 💾 & 🇸🇻. + + ✋️ 🇨🇻 📁 🚫 🤙 ✔️ ✔️ 👈 ☑ 📁. + +Pydantic ✔️ 🐕‍🦺 👂 ⚪️➡️ 👉 🆎 📁 ⚙️ 🔢 🗃. 👆 💪 ✍ 🌖 Pydantic ⚒: 🇨🇻 (.🇨🇻) 🐕‍🦺. + +!!! tip + 👉 👷, 👆 💪 `pip install python-dotenv`. + +### `.env` 📁 + +👆 💪 ✔️ `.env` 📁 ⏮️: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### ✍ ⚒ ⚪️➡️ `.env` + +& ⤴️ ℹ 👆 `config.py` ⏮️: + +```Python hl_lines="9-10" +{!../../../docs_src/settings/app03/config.py!} +``` + +📥 👥 ✍ 🎓 `Config` 🔘 👆 Pydantic `Settings` 🎓, & ⚒ `env_file` 📁 ⏮️ 🇨🇻 📁 👥 💚 ⚙️. + +!!! tip + `Config` 🎓 ⚙️ Pydantic 📳. 👆 💪 ✍ 🌖 Pydantic 🏷 📁 + +### 🏗 `Settings` 🕴 🕐 ⏮️ `lru_cache` + +👂 📁 ⚪️➡️ 💾 🛎 ⚠ (🐌) 🛠️, 👆 🎲 💚 ⚫️ 🕴 🕐 & ⤴️ 🏤-⚙️ 🎏 ⚒ 🎚, ↩️ 👂 ⚫️ 🔠 📨. + +✋️ 🔠 🕰 👥: + +```Python +Settings() +``` + +🆕 `Settings` 🎚 🔜 ✍, & 🏗 ⚫️ 🔜 ✍ `.env` 📁 🔄. + +🚥 🔗 🔢 💖: + +```Python +def get_settings(): + return Settings() +``` + +👥 🔜 ✍ 👈 🎚 🔠 📨, & 👥 🔜 👂 `.env` 📁 🔠 📨. 👶 👶 + +✋️ 👥 ⚙️ `@lru_cache()` 👨‍🎨 🔛 🔝, `Settings` 🎚 🔜 ✍ 🕴 🕐, 🥇 🕰 ⚫️ 🤙. 👶 👶 + +```Python hl_lines="1 10" +{!../../../docs_src/settings/app03/main.py!} +``` + +⤴️ 🙆 🏁 🤙 `get_settings()` 🔗 ⏭ 📨, ↩️ 🛠️ 🔗 📟 `get_settings()` & 🏗 🆕 `Settings` 🎚, ⚫️ 🔜 📨 🎏 🎚 👈 📨 🔛 🥇 🤙, 🔄 & 🔄. + +#### `lru_cache` 📡 ℹ + +`@lru_cache()` 🔀 🔢 ⚫️ 🎀 📨 🎏 💲 👈 📨 🥇 🕰, ↩️ 💻 ⚫️ 🔄, 🛠️ 📟 🔢 🔠 🕰. + +, 🔢 🔛 ⚫️ 🔜 🛠️ 🕐 🔠 🌀 ❌. & ⤴️ 💲 📨 🔠 👈 🌀 ❌ 🔜 ⚙️ 🔄 & 🔄 🕐❔ 🔢 🤙 ⏮️ ⚫️❔ 🎏 🌀 ❌. + +🖼, 🚥 👆 ✔️ 🔢: + +```Python +@lru_cache() +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +👆 📋 💪 🛠️ 💖 👉: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: execute function code + execute ->> code: return the result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: return stored result + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: return stored result + end +``` + +💼 👆 🔗 `get_settings()`, 🔢 🚫 ✊ 🙆 ❌, ⚫️ 🕧 📨 🎏 💲. + +👈 🌌, ⚫️ 🎭 🌖 🚥 ⚫️ 🌐 🔢. ✋️ ⚫️ ⚙️ 🔗 🔢, ⤴️ 👥 💪 🔐 ⚫️ 💪 🔬. + +`@lru_cache()` 🍕 `functools` ❔ 🍕 🐍 🐩 🗃, 👆 💪 ✍ 🌅 🔃 ⚫️ 🐍 🩺 `@lru_cache()`. + +## 🌃 + +👆 💪 ⚙️ Pydantic ⚒ 🍵 ⚒ ⚖️ 📳 👆 🈸, ⏮️ 🌐 🏋️ Pydantic 🏷. + +* ⚙️ 🔗 👆 💪 📉 🔬. +* 👆 💪 ⚙️ `.env` 📁 ⏮️ ⚫️. +* ⚙️ `@lru_cache()` ➡️ 👆 ❎ 👂 🇨🇻 📁 🔄 & 🔄 🔠 📨, ⏪ 🤝 👆 🔐 ⚫️ ⏮️ 🔬. diff --git a/docs/em/docs/advanced/sql-databases-peewee.md b/docs/em/docs/advanced/sql-databases-peewee.md new file mode 100644 index 000000000..62619fc2c --- /dev/null +++ b/docs/em/docs/advanced/sql-databases-peewee.md @@ -0,0 +1,529 @@ +# 🗄 (🔗) 💽 ⏮️ 🏒 + +!!! warning + 🚥 👆 ▶️, 🔰 [🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank} 👈 ⚙️ 🇸🇲 🔜 🥃. + + 💭 🆓 🚶 👉. + +🚥 👆 ▶️ 🏗 ⚪️➡️ 🖌, 👆 🎲 👻 📆 ⏮️ 🇸🇲 🐜 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}), ⚖️ 🙆 🎏 🔁 🐜. + +🚥 👆 ⏪ ✔️ 📟 🧢 👈 ⚙️ 🏒 🐜, 👆 💪 ✅ 📥 ❔ ⚙️ ⚫️ ⏮️ **FastAPI**. + +!!! warning "🐍 3️⃣.7️⃣ ➕ ✔" + 👆 🔜 💪 🐍 3️⃣.7️⃣ ⚖️ 🔛 🔒 ⚙️ 🏒 ⏮️ FastAPI. + +## 🏒 🔁 + +🏒 🚫 🔧 🔁 🛠️, ⚖️ ⏮️ 👫 🤯. + +🏒 ✔️ 🏋️ 🔑 🔃 🚮 🔢 & 🔃 ❔ ⚫️ 🔜 ⚙️. + +🚥 👆 🛠️ 🈸 ⏮️ 🗝 🚫-🔁 🛠️, & 💪 👷 ⏮️ 🌐 🚮 🔢, **⚫️ 💪 👑 🧰**. + +✋️ 🚥 👆 💪 🔀 🔢, 🐕‍🦺 🌖 🌘 1️⃣ 🔁 💽, 👷 ⏮️ 🔁 🛠️ (💖 FastAPI), ♒️, 👆 🔜 💪 🚮 🏗 ➕ 📟 🔐 👈 🔢. + +👐, ⚫️ 💪 ⚫️, & 📥 👆 🔜 👀 ⚫️❔ ⚫️❔ 📟 👆 ✔️ 🚮 💪 ⚙️ 🏒 ⏮️ FastAPI. + +!!! note "📡 ℹ" + 👆 💪 ✍ 🌅 🔃 🏒 🧍 🔃 🔁 🐍 🩺, , 🇵🇷. + +## 🎏 📱 + +👥 🔜 ✍ 🎏 🈸 🇸🇲 🔰 ([🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank}). + +🌅 📟 🤙 🎏. + +, 👥 🔜 🎯 🕴 🔛 🔺. + +## 📁 📊 + +➡️ 💬 👆 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app` ⏮️ 📊 💖 👉: + +``` +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + └── schemas.py +``` + +👉 🌖 🎏 📊 👥 ✔️ 🇸🇲 🔰. + +🔜 ➡️ 👀 ⚫️❔ 🔠 📁/🕹 🔨. + +## ✍ 🏒 🍕 + +➡️ 🔗 📁 `sql_app/database.py`. + +### 🐩 🏒 📟 + +➡️ 🥇 ✅ 🌐 😐 🏒 📟, ✍ 🏒 💽: + +```Python hl_lines="3 5 22" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +!!! tip + ✔️ 🤯 👈 🚥 👆 💚 ⚙️ 🎏 💽, 💖 ✳, 👆 🚫 🚫 🔀 🎻. 👆 🔜 💪 ⚙️ 🎏 🏒 💽 🎓. + +#### 🗒 + +❌: + +```Python +check_same_thread=False +``` + +🌓 1️⃣ 🇸🇲 🔰: + +```Python +connect_args={"check_same_thread": False} +``` + +...⚫️ 💪 🕴 `SQLite`. + +!!! info "📡 ℹ" + + ⚫️❔ 🎏 📡 ℹ [🗄 (🔗) 💽](../tutorial/sql-databases.md#note){.internal-link target=_blank} ✔. + +### ⚒ 🏒 🔁-🔗 `PeeweeConnectionState` + +👑 ❔ ⏮️ 🏒 & FastAPI 👈 🏒 ⚓️ 🙇 🔛 🐍 `threading.local`, & ⚫️ 🚫 ✔️ 🎯 🌌 🔐 ⚫️ ⚖️ ➡️ 👆 🍵 🔗/🎉 🔗 (🔨 🇸🇲 🔰). + +& `threading.local` 🚫 🔗 ⏮️ 🆕 🔁 ⚒ 🏛 🐍. + +!!! note "📡 ℹ" + `threading.local` ⚙️ ✔️ "🎱" 🔢 👈 ✔️ 🎏 💲 🔠 🧵. + + 👉 ⚠ 🗝 🛠️ 🏗 ✔️ 1️⃣ 👁 🧵 📍 📨, 🙅‍♂ 🌖, 🙅‍♂ 🌘. + + ⚙️ 👉, 🔠 📨 🔜 ✔️ 🚮 👍 💽 🔗/🎉, ❔ ☑ 🏁 🥅. + + ✋️ FastAPI, ⚙️ 🆕 🔁 ⚒, 💪 🍵 🌅 🌘 1️⃣ 📨 🔛 🎏 🧵. & 🎏 🕰, 👁 📨, ⚫️ 💪 🏃 💗 👜 🎏 🧵 (🧵), ⚓️ 🔛 🚥 👆 ⚙️ `async def` ⚖️ 😐 `def`. 👉 ⚫️❔ 🤝 🌐 🎭 📈 FastAPI. + +✋️ 🐍 3️⃣.7️⃣ & 🔛 🚚 🌖 🏧 🎛 `threading.local`, 👈 💪 ⚙️ 🥉 🌐❔ `threading.local` 🔜 ⚙️, ✋️ 🔗 ⏮️ 🆕 🔁 ⚒. + +👥 🔜 ⚙️ 👈. ⚫️ 🤙 `contextvars`. + +👥 🔜 🔐 🔗 🍕 🏒 👈 ⚙️ `threading.local` & ❎ 👫 ⏮️ `contextvars`, ⏮️ 🔗 ℹ. + +👉 5️⃣📆 😑 🍖 🏗 (& ⚫️ 🤙), 👆 🚫 🤙 💪 🍕 🤔 ❔ ⚫️ 👷 ⚙️ ⚫️. + +👥 🔜 ✍ `PeeweeConnectionState`: + +```Python hl_lines="10-19" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +👉 🎓 😖 ⚪️➡️ 🎁 🔗 🎓 ⚙️ 🏒. + +⚫️ ✔️ 🌐 ⚛ ⚒ 🏒 ⚙️ `contextvars` ↩️ `threading.local`. + +`contextvars` 👷 🍖 🎏 🌘 `threading.local`. ✋️ 🎂 🏒 🔗 📟 🤔 👈 👉 🎓 👷 ⏮️ `threading.local`. + +, 👥 💪 ➕ 🎱 ⚒ ⚫️ 👷 🚥 ⚫️ ⚙️ `threading.local`. `__init__`, `__setattr__`, & `__getattr__` 🛠️ 🌐 ✔ 🎱 👉 ⚙️ 🏒 🍵 🤔 👈 ⚫️ 🔜 🔗 ⏮️ FastAPI. + +!!! tip + 👉 🔜 ⚒ 🏒 🎭 ☑ 🕐❔ ⚙️ ⏮️ FastAPI. 🚫 🎲 📂 ⚖️ 📪 🔗 👈 ➖ ⚙️, 🏗 ❌, ♒️. + + ✋️ ⚫️ 🚫 🤝 🏒 🔁 💎-🏋️. 👆 🔜 ⚙️ 😐 `def` 🔢 & 🚫 `async def`. + +### ⚙️ 🛃 `PeeweeConnectionState` 🎓 + +🔜, 📁 `._state` 🔗 🔢 🏒 💽 `db` 🎚 ⚙️ 🆕 `PeeweeConnectionState`: + +```Python hl_lines="24" +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +!!! tip + ⚒ 💭 👆 📁 `db._state` *⏮️* 🏗 `db`. + +!!! tip + 👆 🔜 🎏 🙆 🎏 🏒 💽, 🔌 `PostgresqlDatabase`, `MySQLDatabase`, ♒️. + +## ✍ 💽 🏷 + +➡️ 🔜 👀 📁 `sql_app/models.py`. + +### ✍ 🏒 🏷 👆 💽 + +🔜 ✍ 🏒 🏷 (🎓) `User` & `Item`. + +👉 🎏 👆 🔜 🚥 👆 ⏩ 🏒 🔰 & ℹ 🏷 ✔️ 🎏 💽 🇸🇲 🔰. + +!!! tip + 🏒 ⚙️ ⚖ "**🏷**" 🔗 👉 🎓 & 👐 👈 🔗 ⏮️ 💽. + + ✋️ Pydantic ⚙️ ⚖ "**🏷**" 🔗 🕳 🎏, 💽 🔬, 🛠️, & 🧾 🎓 & 👐. + +🗄 `db` ⚪️➡️ `database` (📁 `database.py` ⚪️➡️ 🔛) & ⚙️ ⚫️ 📥. + +```Python hl_lines="3 6-12 15-21" +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} +``` + +!!! tip + 🏒 ✍ 📚 🎱 🔢. + + ⚫️ 🔜 🔁 🚮 `id` 🔢 🔢 👑 🔑. + + ⚫️ 🔜 ⚒ 📛 🏓 ⚓️ 🔛 🎓 📛. + + `Item`, ⚫️ 🔜 ✍ 🔢 `owner_id` ⏮️ 🔢 🆔 `User`. ✋️ 👥 🚫 📣 ⚫️ 🙆. + +## ✍ Pydantic 🏷 + +🔜 ➡️ ✅ 📁 `sql_app/schemas.py`. + +!!! tip + ❎ 😨 🖖 🏒 *🏷* & Pydantic *🏷*, 👥 🔜 ✔️ 📁 `models.py` ⏮️ 🏒 🏷, & 📁 `schemas.py` ⏮️ Pydantic 🏷. + + 👫 Pydantic 🏷 🔬 🌅 ⚖️ 🌘 "🔗" (☑ 📊 💠). + + 👉 🔜 ℹ 👥 ❎ 😨 ⏪ ⚙️ 👯‍♂️. + +### ✍ Pydantic *🏷* / 🔗 + +✍ 🌐 🎏 Pydantic 🏷 🇸🇲 🔰: + +```Python hl_lines="16-18 21-22 25-30 34-35 38-39 42-48" +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +!!! tip + 📥 👥 🏗 🏷 ⏮️ `id`. + + 👥 🚫 🎯 ✔ `id` 🔢 🏒 🏷, ✋️ 🏒 🚮 1️⃣ 🔁. + + 👥 ❎ 🎱 `owner_id` 🔢 `Item`. + +### ✍ `PeeweeGetterDict` Pydantic *🏷* / 🔗 + +🕐❔ 👆 🔐 💛 🏒 🎚, 💖 `some_user.items`, 🏒 🚫 🚚 `list` `Item`. + +⚫️ 🚚 🎁 🛃 🎚 🎓 `ModelSelect`. + +⚫️ 💪 ✍ `list` 🚮 🏬 ⏮️ `list(some_user.items)`. + +✋️ 🎚 ⚫️ 🚫 `list`. & ⚫️ 🚫 ☑ 🐍 🚂. ↩️ 👉, Pydantic 🚫 💭 🔢 ❔ 🗜 ⚫️ `list` Pydantic *🏷* / 🔗. + +✋️ ⏮️ ⏬ Pydantic ✔ 🚚 🛃 🎓 👈 😖 ⚪️➡️ `pydantic.utils.GetterDict`, 🚚 🛠️ ⚙️ 🕐❔ ⚙️ `orm_mode = True` 🗃 💲 🐜 🏷 🔢. + +👥 🔜 ✍ 🛃 `PeeweeGetterDict` 🎓 & ⚙️ ⚫️ 🌐 🎏 Pydantic *🏷* / 🔗 👈 ⚙️ `orm_mode`: + +```Python hl_lines="3 8-13 31 49" +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +📥 👥 ✅ 🚥 🔢 👈 ➖ 🔐 (✅ `.items` `some_user.items`) 👐 `peewee.ModelSelect`. + +& 🚥 👈 💼, 📨 `list` ⏮️ ⚫️. + +& ⤴️ 👥 ⚙️ ⚫️ Pydantic *🏷* / 🔗 👈 ⚙️ `orm_mode = True`, ⏮️ 📳 🔢 `getter_dict = PeeweeGetterDict`. + +!!! tip + 👥 🕴 💪 ✍ 1️⃣ `PeeweeGetterDict` 🎓, & 👥 💪 ⚙️ ⚫️ 🌐 Pydantic *🏷* / 🔗. + +## 💩 🇨🇻 + +🔜 ➡️ 👀 📁 `sql_app/crud.py`. + +### ✍ 🌐 💩 🇨🇻 + +✍ 🌐 🎏 💩 🇨🇻 🇸🇲 🔰, 🌐 📟 📶 🎏: + +```Python hl_lines="1 4-5 8-9 12-13 16-20 23-24 27-30" +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} +``` + +📤 🔺 ⏮️ 📟 🇸🇲 🔰. + +👥 🚫 🚶‍♀️ `db` 🔢 🤭. ↩️ 👥 ⚙️ 🏷 🔗. 👉 ↩️ `db` 🎚 🌐 🎚, 👈 🔌 🌐 🔗 ⚛. 👈 ⚫️❔ 👥 ✔️ 🌐 `contextvars` ℹ 🔛. + +🆖, 🕐❔ 🛬 📚 🎚, 💖 `get_users`, 👥 🔗 🤙 `list`, 💖: + +```Python +list(models.User.select()) +``` + +👉 🎏 🤔 👈 👥 ✔️ ✍ 🛃 `PeeweeGetterDict`. ✋️ 🛬 🕳 👈 ⏪ `list` ↩️ `peewee.ModelSelect` `response_model` *➡ 🛠️* ⏮️ `List[models.User]` (👈 👥 🔜 👀 ⏪) 🔜 👷 ☑. + +## 👑 **FastAPI** 📱 + +& 🔜 📁 `sql_app/main.py` ➡️ 🛠️ & ⚙️ 🌐 🎏 🍕 👥 ✍ ⏭. + +### ✍ 💽 🏓 + +📶 🙃 🌌 ✍ 💽 🏓: + +```Python hl_lines="9-11" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### ✍ 🔗 + +✍ 🔗 👈 🔜 🔗 💽 ▶️️ ▶️ 📨 & 🔌 ⚫️ 🔚: + +```Python hl_lines="23-29" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +📥 👥 ✔️ 🛁 `yield` ↩️ 👥 🤙 🚫 ⚙️ 💽 🎚 🔗. + +⚫️ 🔗 💽 & ♻ 🔗 💽 🔗 🔢 👈 🔬 🔠 📨 (⚙️ `contextvars` 🎱 ⚪️➡️ 🔛). + +↩️ 💽 🔗 ⚠ 👤/🅾 🚧, 👉 🔗 ✍ ⏮️ 😐 `def` 🔢. + +& ⤴️, 🔠 *➡ 🛠️ 🔢* 👈 💪 🔐 💽 👥 🚮 ⚫️ 🔗. + +✋️ 👥 🚫 ⚙️ 💲 👐 👉 🔗 (⚫️ 🤙 🚫 🤝 🙆 💲, ⚫️ ✔️ 🛁 `yield`). , 👥 🚫 🚮 ⚫️ *➡ 🛠️ 🔢* ✋️ *➡ 🛠️ 👨‍🎨* `dependencies` 🔢: + +```Python hl_lines="32 40 47 59 65 72" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### 🔑 🔢 🎧-🔗 + +🌐 `contextvars` 🍕 👷, 👥 💪 ⚒ 💭 👥 ✔️ 🔬 💲 `ContextVar` 🔠 📨 👈 ⚙️ 💽, & 👈 💲 🔜 ⚙️ 💽 🇵🇸 (🔗, 💵, ♒️) 🎂 📨. + +👈, 👥 💪 ✍ ➕1️⃣ `async` 🔗 `reset_db_state()` 👈 ⚙️ 🎧-🔗 `get_db()`. ⚫️ 🔜 ⚒ 💲 🔑 🔢 (⏮️ 🔢 `dict`) 👈 🔜 ⚙️ 💽 🇵🇸 🎂 📨. & ⤴️ 🔗 `get_db()` 🔜 🏪 ⚫️ 💽 🇵🇸 (🔗, 💵, ♒️). + +```Python hl_lines="18-20" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +**⏭ 📨**, 👥 🔜 ⏲ 👈 🔑 🔢 🔄 `async` 🔗 `reset_db_state()` & ⤴️ ✍ 🆕 🔗 `get_db()` 🔗, 👈 🆕 📨 🔜 ✔️ 🚮 👍 💽 🇵🇸 (🔗, 💵, ♒️). + +!!! tip + FastAPI 🔁 🛠️, 1️⃣ 📨 💪 ▶️ ➖ 🛠️, & ⏭ 🏁, ➕1️⃣ 📨 💪 📨 & ▶️ 🏭 👍, & ⚫️ 🌐 💪 🛠️ 🎏 🧵. + + ✋️ 🔑 🔢 🤔 👫 🔁 ⚒,, 🏒 💽 🇵🇸 ⚒ `async` 🔗 `reset_db_state()` 🔜 🚧 🚮 👍 💽 🎂 🎂 📨. + + & 🎏 🕰, 🎏 🛠️ 📨 🔜 ✔️ 🚮 👍 💽 🇵🇸 👈 🔜 🔬 🎂 📨. + +#### 🏒 🗳 + +🚥 👆 ⚙️ 🏒 🗳, ☑ 💽 `db.obj`. + +, 👆 🔜 ⏲ ⚫️ ⏮️: + +```Python hl_lines="3-4" +async def reset_db_state(): + database.db.obj._state._state.set(db_state_default.copy()) + database.db.obj._state.reset() +``` + +### ✍ 👆 **FastAPI** *➡ 🛠️* + +🔜, 😒, 📥 🐩 **FastAPI** *➡ 🛠️* 📟. + +```Python hl_lines="32-37 40-43 46-53 56-62 65-68 71-79" +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +### 🔃 `def` 🆚 `async def` + +🎏 ⏮️ 🇸🇲, 👥 🚫 🔨 🕳 💖: + +```Python +user = await models.User.select().first() +``` + +...✋️ ↩️ 👥 ⚙️: + +```Python +user = models.User.select().first() +``` + +, 🔄, 👥 🔜 📣 *➡ 🛠️ 🔢* & 🔗 🍵 `async def`, ⏮️ 😐 `def`,: + +```Python hl_lines="2" +# Something goes here +def read_users(skip: int = 0, limit: int = 100): + # Something goes here +``` + +## 🔬 🏒 ⏮️ 🔁 + +👉 🖼 🔌 ➕ *➡ 🛠️* 👈 🔬 📏 🏭 📨 ⏮️ `time.sleep(sleep_time)`. + +⚫️ 🔜 ✔️ 💽 🔗 📂 ▶️ & 🔜 ⌛ 🥈 ⏭ 🙇 🔙. & 🔠 🆕 📨 🔜 ⌛ 🕐 🥈 🌘. + +👉 🔜 💪 ➡️ 👆 💯 👈 👆 📱 ⏮️ 🏒 & FastAPI 🎭 ☑ ⏮️ 🌐 💩 🔃 🧵. + +🚥 👆 💚 ✅ ❔ 🏒 🔜 💔 👆 📱 🚥 ⚙️ 🍵 🛠️, 🚶 `sql_app/database.py` 📁 & 🏤 ⏸: + +```Python +# db._state = PeeweeConnectionState() +``` + +& 📁 `sql_app/main.py` 📁, 🏤 💪 `async` 🔗 `reset_db_state()` & ❎ ⚫️ ⏮️ `pass`: + +```Python +async def reset_db_state(): +# database.db._state._state.set(db_state_default.copy()) +# database.db._state.reset() + pass +``` + +⤴️ 🏃 👆 📱 ⏮️ Uvicorn: + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000/docs & ✍ 👩‍❤‍👨 👩‍💻. + +⤴️ 📂 1️⃣0️⃣ 📑 http://127.0.0.1:8000/docs#/default/read_🐌_👩‍💻_slowusers_ = 🎏 🕰. + +🚶 *➡ 🛠️* "🤚 `/slowusers/`" 🌐 📑. ⚙️ "🔄 ⚫️ 👅" 🔼 & 🛠️ 📨 🔠 📑, 1️⃣ ▶️️ ⏮️ 🎏. + +📑 🔜 ⌛ 🍖 & ⤴️ 👫 🔜 🎦 `Internal Server Error`. + +### ⚫️❔ 🔨 + +🥇 📑 🔜 ⚒ 👆 📱 ✍ 🔗 💽 & ⌛ 🥈 ⏭ 🙇 🔙 & 📪 💽 🔗. + +⤴️, 📨 ⏭ 📑, 👆 📱 🔜 ⌛ 🕐 🥈 🌘, & 🔛. + +👉 ⛓ 👈 ⚫️ 🔜 🔚 🆙 🏁 🏁 📑' 📨 ⏪ 🌘 ⏮️ 🕐. + +⤴️ 1️⃣ 🏁 📨 👈 ⌛ 🌘 🥈 🔜 🔄 📂 💽 🔗, ✋️ 1️⃣ 📚 ⏮️ 📨 🎏 📑 🔜 🎲 🍵 🎏 🧵 🥇 🕐, ⚫️ 🔜 ✔️ 🎏 💽 🔗 👈 ⏪ 📂, & 🏒 🔜 🚮 ❌ & 👆 🔜 👀 ⚫️ 📶, & 📨 🔜 ✔️ `Internal Server Error`. + +👉 🔜 🎲 🔨 🌅 🌘 1️⃣ 📚 📑. + +🚥 👆 ✔️ 💗 👩‍💻 💬 👆 📱 ⚫️❔ 🎏 🕰, 👉 ⚫️❔ 💪 🔨. + +& 👆 📱 ▶️ 🍵 🌅 & 🌖 👩‍💻 🎏 🕰, ⌛ 🕰 👁 📨 💪 📏 & 📏 ⏲ ❌. + +### 🔧 🏒 ⏮️ FastAPI + +🔜 🚶 🔙 📁 `sql_app/database.py`, & ✍ ⏸: + +```Python +db._state = PeeweeConnectionState() +``` + +& 📁 `sql_app/main.py` 📁, ✍ 💪 `async` 🔗 `reset_db_state()`: + +```Python +async def reset_db_state(): + database.db._state._state.set(db_state_default.copy()) + database.db._state.reset() +``` + +❎ 👆 🏃‍♂ 📱 & ▶️ ⚫️ 🔄. + +🔁 🎏 🛠️ ⏮️ 1️⃣0️⃣ 📑. 👉 🕰 🌐 👫 🔜 ⌛ & 👆 🔜 🤚 🌐 🏁 🍵 ❌. + +...👆 🔧 ⚫️ ❗ + +## 📄 🌐 📁 + + 💭 👆 🔜 ✔️ 📁 📛 `my_super_project` (⚖️ 👐 👆 💚) 👈 🔌 🎧-📁 🤙 `sql_app`. + +`sql_app` 🔜 ✔️ 📄 📁: + +* `sql_app/__init__.py`: 🛁 📁. + +* `sql_app/database.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/database.py!} +``` + +* `sql_app/models.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/models.py!} +``` + +* `sql_app/schemas.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/schemas.py!} +``` + +* `sql_app/crud.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/crud.py!} +``` + +* `sql_app/main.py`: + +```Python +{!../../../docs_src/sql_databases_peewee/sql_app/main.py!} +``` + +## 📡 ℹ + +!!! warning + 👉 📶 📡 ℹ 👈 👆 🎲 🚫 💪. + +### ⚠ + +🏒 ⚙️ `threading.local` 🔢 🏪 ⚫️ 💽 "🇵🇸" 💽 (🔗, 💵, ♒️). + +`threading.local` ✍ 💲 🌟 ⏮️ 🧵, ✋️ 🔁 🛠️ 🔜 🏃 🌐 📟 (✅ 🔠 📨) 🎏 🧵, & 🎲 🚫 ✔. + +🔛 🔝 👈, 🔁 🛠️ 💪 🏃 🔁 📟 🧵 (⚙️ `asyncio.run_in_executor`), ✋️ 🔗 🎏 📨. + +👉 ⛓ 👈, ⏮️ 🏒 ⏮️ 🛠️, 💗 📋 💪 ⚙️ 🎏 `threading.local` 🔢 & 🔚 🆙 🤝 🎏 🔗 & 💽 (👈 👫 🚫🔜 🚫), & 🎏 🕰, 🚥 👫 🛠️ 🔁 👤/🅾-🚧 📟 🧵 (⏮️ 😐 `def` 🔢 FastAPI, *➡ 🛠️* & 🔗), 👈 📟 🏆 🚫 ✔️ 🔐 💽 🇵🇸 🔢, ⏪ ⚫️ 🍕 🎏 📨 & ⚫️ 🔜 💪 🤚 🔐 🎏 💽 🇵🇸. + +### 🔑 🔢 + +🐍 3️⃣.7️⃣ ✔️ `contextvars` 👈 💪 ✍ 🇧🇿 🔢 📶 🎏 `threading.local`, ✋️ 🔗 👫 🔁 ⚒. + +📤 📚 👜 ✔️ 🤯. + +`ContextVar` ✔️ ✍ 🔝 🕹, 💖: + +```Python +some_var = ContextVar("some_var", default="default value") +``` + +⚒ 💲 ⚙️ ⏮️ "🔑" (✅ ⏮️ 📨) ⚙️: + +```Python +some_var.set("new value") +``` + +🤚 💲 🙆 🔘 🔑 (✅ 🙆 🍕 🚚 ⏮️ 📨) ⚙️: + +```Python +some_var.get() +``` + +### ⚒ 🔑 🔢 `async` 🔗 `reset_db_state()` + +🚥 🍕 🔁 📟 ⚒ 💲 ⏮️ `some_var.set("updated in function")` (✅ 💖 `async` 🔗), 🎂 📟 ⚫️ & 📟 👈 🚶 ⏮️ (✅ 📟 🔘 `async` 🔢 🤙 ⏮️ `await`) 🔜 👀 👈 🆕 💲. + +, 👆 💼, 🚥 👥 ⚒ 🏒 🇵🇸 🔢 (⏮️ 🔢 `dict`) `async` 🔗, 🌐 🎂 🔗 📟 👆 📱 🔜 👀 👉 💲 & 🔜 💪 ♻ ⚫️ 🎂 📨. + +& 🔑 🔢 🔜 ⚒ 🔄 ⏭ 📨, 🚥 👫 🛠️. + +### ⚒ 💽 🇵🇸 🔗 `get_db()` + +`get_db()` 😐 `def` 🔢, **FastAPI** 🔜 ⚒ ⚫️ 🏃 🧵, ⏮️ *📁* "🔑", 🧑‍🤝‍🧑 🎏 💲 🔑 🔢 ( `dict` ⏮️ ⏲ 💽 🇵🇸). ⤴️ ⚫️ 💪 🚮 💽 🇵🇸 👈 `dict`, 💖 🔗, ♒️. + +✋️ 🚥 💲 🔑 🔢 (🔢 `dict`) ⚒ 👈 😐 `def` 🔢, ⚫️ 🔜 ✍ 🆕 💲 👈 🔜 🚧 🕴 👈 🧵 🧵, & 🎂 📟 (💖 *➡ 🛠️ 🔢*) 🚫🔜 ✔️ 🔐 ⚫️. `get_db()` 👥 💪 🕴 ⚒ 💲 `dict`, ✋️ 🚫 🎂 `dict` ⚫️. + +, 👥 💪 ✔️ `async` 🔗 `reset_db_state()` ⚒ `dict` 🔑 🔢. 👈 🌌, 🌐 📟 ✔️ 🔐 🎏 `dict` 💽 🇵🇸 👁 📨. + +### 🔗 & 🔌 🔗 `get_db()` + +⤴️ ⏭ ❔ 🔜, ⚫️❔ 🚫 🔗 & 🔌 💽 `async` 🔗 ⚫️, ↩️ `get_db()`❓ + +`async` 🔗 ✔️ `async` 🔑 🔢 🛡 🎂 📨, ✋️ 🏗 & 📪 💽 🔗 ⚠ 🚧, ⚫️ 💪 📉 🎭 🚥 ⚫️ 📤. + +👥 💪 😐 `def` 🔗 `get_db()`. diff --git a/docs/em/docs/advanced/sub-applications.md b/docs/em/docs/advanced/sub-applications.md new file mode 100644 index 000000000..e0391453b --- /dev/null +++ b/docs/em/docs/advanced/sub-applications.md @@ -0,0 +1,73 @@ +# 🎧 🈸 - 🗻 + +🚥 👆 💪 ✔️ 2️⃣ 🔬 FastAPI 🈸, ⏮️ 👫 👍 🔬 🗄 & 👫 👍 🩺 ⚜, 👆 💪 ✔️ 👑 📱 & "🗻" 1️⃣ (⚖️ 🌅) 🎧-🈸(Ⓜ). + +## 🗜 **FastAPI** 🈸 + +"🗜" ⛓ ❎ 🍕 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🔽 👈 ➡, ⏮️ _➡ 🛠️_ 📣 👈 🎧-🈸. + +### 🔝-🎚 🈸 + +🥇, ✍ 👑, 🔝-🎚, **FastAPI** 🈸, & 🚮 *➡ 🛠️*: + +```Python hl_lines="3 6-8" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### 🎧-🈸 + +⤴️, ✍ 👆 🎧-🈸, & 🚮 *➡ 🛠️*. + +👉 🎧-🈸 ➕1️⃣ 🐩 FastAPI 🈸, ✋️ 👉 1️⃣ 👈 🔜 "🗻": + +```Python hl_lines="11 14-16" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### 🗻 🎧-🈸 + +👆 🔝-🎚 🈸, `app`, 🗻 🎧-🈸, `subapi`. + +👉 💼, ⚫️ 🔜 📌 ➡ `/subapi`: + +```Python hl_lines="11 19" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### ✅ 🏧 🛠️ 🩺 + +🔜, 🏃 `uvicorn` ⏮️ 👑 📱, 🚥 👆 📁 `main.py`, ⚫️ 🔜: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& 📂 🩺 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🛠️ 🩺 👑 📱, 🔌 🕴 🚮 👍 _➡ 🛠️_: + + + +& ⤴️, 📂 🩺 🎧-🈸, http://127.0.0.1:8000/subapi/docs. + +👆 🔜 👀 🏧 🛠️ 🩺 🎧-🈸, ✅ 🕴 🚮 👍 _➡ 🛠️_, 🌐 🔽 ☑ 🎧-➡ 🔡 `/subapi`: + + + +🚥 👆 🔄 🔗 ⏮️ 🙆 2️⃣ 👩‍💻 🔢, 👫 🔜 👷 ☑, ↩️ 🖥 🔜 💪 💬 🔠 🎯 📱 ⚖️ 🎧-📱. + +### 📡 ℹ: `root_path` + +🕐❔ 👆 🗻 🎧-🈸 🔬 🔛, FastAPI 🔜 ✊ 💅 🔗 🗻 ➡ 🎧-🈸 ⚙️ 🛠️ ⚪️➡️ 🔫 🔧 🤙 `root_path`. + +👈 🌌, 🎧-🈸 🔜 💭 ⚙️ 👈 ➡ 🔡 🩺 🎚. + +& 🎧-🈸 💪 ✔️ 🚮 👍 📌 🎧-🈸 & 🌐 🔜 👷 ☑, ↩️ FastAPI 🍵 🌐 👉 `root_path`Ⓜ 🔁. + +👆 🔜 💡 🌅 🔃 `root_path` & ❔ ⚙️ ⚫️ 🎯 📄 🔃 [⛅ 🗳](./behind-a-proxy.md){.internal-link target=_blank}. diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md new file mode 100644 index 000000000..1fb57725a --- /dev/null +++ b/docs/em/docs/advanced/templates.md @@ -0,0 +1,77 @@ +# 📄 + +👆 💪 ⚙️ 🙆 📄 🚒 👆 💚 ⏮️ **FastAPI**. + +⚠ ⚒ Jinja2️⃣, 🎏 1️⃣ ⚙️ 🏺 & 🎏 🧰. + +📤 🚙 🔗 ⚫️ 💪 👈 👆 💪 ⚙️ 🔗 👆 **FastAPI** 🈸 (🚚 💃). + +## ❎ 🔗 + +❎ `jinja2`: + +
+ +```console +$ pip install jinja2 + +---> 100% +``` + +
+ +## ⚙️ `Jinja2Templates` + +* 🗄 `Jinja2Templates`. +* ✍ `templates` 🎚 👈 👆 💪 🏤-⚙️ ⏪. +* 📣 `Request` 🔢 *➡ 🛠️* 👈 🔜 📨 📄. +* ⚙️ `templates` 👆 ✍ ✍ & 📨 `TemplateResponse`, 🚶‍♀️ `request` 1️⃣ 🔑-💲 👫 Jinja2️⃣ "🔑". + +```Python hl_lines="4 11 15-16" +{!../../../docs_src/templates/tutorial001.py!} +``` + +!!! note + 👀 👈 👆 ✔️ 🚶‍♀️ `request` 🍕 🔑-💲 👫 🔑 Jinja2️⃣. , 👆 ✔️ 📣 ⚫️ 👆 *➡ 🛠️*. + +!!! tip + 📣 `response_class=HTMLResponse` 🩺 🎚 🔜 💪 💭 👈 📨 🔜 🕸. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.templating import Jinja2Templates`. + + **FastAPI** 🚚 🎏 `starlette.templating` `fastapi.templating` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request` & `StaticFiles`. + +## ✍ 📄 + +⤴️ 👆 💪 ✍ 📄 `templates/item.html` ⏮️: + +```jinja hl_lines="7" +{!../../../docs_src/templates/templates/item.html!} +``` + +⚫️ 🔜 🎦 `id` ✊ ⚪️➡️ "🔑" `dict` 👆 🚶‍♀️: + +```Python +{"request": request, "id": id} +``` + +## 📄 & 🎻 📁 + +& 👆 💪 ⚙️ `url_for()` 🔘 📄, & ⚙️ ⚫️, 🖼, ⏮️ `StaticFiles` 👆 📌. + +```jinja hl_lines="4" +{!../../../docs_src/templates/templates/item.html!} +``` + +👉 🖼, ⚫️ 🔜 🔗 🎚 📁 `static/styles.css` ⏮️: + +```CSS hl_lines="4" +{!../../../docs_src/templates/static/styles.css!} +``` + +& ↩️ 👆 ⚙️ `StaticFiles`, 👈 🎚 📁 🔜 🍦 🔁 👆 **FastAPI** 🈸 📛 `/static/styles.css`. + +## 🌅 ℹ + +🌅 ℹ, 🔌 ❔ 💯 📄, ✅ 💃 🩺 🔛 📄. diff --git a/docs/em/docs/advanced/testing-database.md b/docs/em/docs/advanced/testing-database.md new file mode 100644 index 000000000..93acd710e --- /dev/null +++ b/docs/em/docs/advanced/testing-database.md @@ -0,0 +1,95 @@ +# 🔬 💽 + +👆 💪 ⚙️ 🎏 🔗 🔐 ⚪️➡️ [🔬 🔗 ⏮️ 🔐](testing-dependencies.md){.internal-link target=_blank} 📉 💽 🔬. + +👆 💪 💚 ⚒ 🆙 🎏 💽 🔬, 💾 💽 ⏮️ 💯, 🏤-🥧 ⚫️ ⏮️ 🔬 💽, ♒️. + +👑 💭 ⚫️❔ 🎏 👆 👀 👈 ⏮️ 📃. + +## 🚮 💯 🗄 📱 + +➡️ ℹ 🖼 ⚪️➡️ [🗄 (🔗) 💽](../tutorial/sql-databases.md){.internal-link target=_blank} ⚙️ 🔬 💽. + +🌐 📱 📟 🎏, 👆 💪 🚶 🔙 👈 📃 ✅ ❔ ⚫️. + +🕴 🔀 📥 🆕 🔬 📁. + +👆 😐 🔗 `get_db()` 🔜 📨 💽 🎉. + +💯, 👆 💪 ⚙️ 🔗 🔐 📨 👆 *🛃* 💽 🎉 ↩️ 1️⃣ 👈 🔜 ⚙️ 🛎. + +👉 🖼 👥 🔜 ✍ 🍕 💽 🕴 💯. + +## 📁 📊 + +👥 ✍ 🆕 📁 `sql_app/tests/test_sql_app.py`. + +🆕 📁 📊 👀 💖: + +``` hl_lines="9-11" +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + ├── schemas.py + └── tests + ├── __init__.py + └── test_sql_app.py +``` + +## ✍ 🆕 💽 🎉 + +🥇, 👥 ✍ 🆕 💽 🎉 ⏮️ 🆕 💽. + +💯 👥 🔜 ⚙️ 📁 `test.db` ↩️ `sql_app.db`. + +✋️ 🎂 🎉 📟 🌅 ⚖️ 🌘 🎏, 👥 📁 ⚫️. + +```Python hl_lines="8-13" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip + 👆 💪 📉 ❎ 👈 📟 🚮 ⚫️ 🔢 & ⚙️ ⚫️ ⚪️➡️ 👯‍♂️ `database.py` & `tests/test_sql_app.py`. + + 🦁 & 🎯 🔛 🎯 🔬 📟, 👥 🖨 ⚫️. + +## ✍ 💽 + +↩️ 🔜 👥 🔜 ⚙️ 🆕 💽 🆕 📁, 👥 💪 ⚒ 💭 👥 ✍ 💽 ⏮️: + +```Python +Base.metadata.create_all(bind=engine) +``` + +👈 🛎 🤙 `main.py`, ✋️ ⏸ `main.py` ⚙️ 💽 📁 `sql_app.db`, & 👥 💪 ⚒ 💭 👥 ✍ `test.db` 💯. + +👥 🚮 👈 ⏸ 📥, ⏮️ 🆕 📁. + +```Python hl_lines="16" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +## 🔗 🔐 + +🔜 👥 ✍ 🔗 🔐 & 🚮 ⚫️ 🔐 👆 📱. + +```Python hl_lines="19-24 27" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip + 📟 `override_get_db()` 🌖 ⚫️❔ 🎏 `get_db()`, ✋️ `override_get_db()` 👥 ⚙️ `TestingSessionLocal` 🔬 💽 ↩️. + +## 💯 📱 + +⤴️ 👥 💪 💯 📱 🛎. + +```Python hl_lines="32-47" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +& 🌐 🛠️ 👥 ⚒ 💽 ⏮️ 💯 🔜 `test.db` 💽 ↩️ 👑 `sql_app.db`. diff --git a/docs/em/docs/advanced/testing-dependencies.md b/docs/em/docs/advanced/testing-dependencies.md new file mode 100644 index 000000000..104a6325e --- /dev/null +++ b/docs/em/docs/advanced/testing-dependencies.md @@ -0,0 +1,49 @@ +# 🔬 🔗 ⏮️ 🔐 + +## 🔑 🔗 ⏮️ 🔬 + +📤 😐 🌐❔ 👆 💪 💚 🔐 🔗 ⏮️ 🔬. + +👆 🚫 💚 ⏮️ 🔗 🏃 (🚫 🙆 🎧-🔗 ⚫️ 💪 ✔️). + +↩️, 👆 💚 🚚 🎏 🔗 👈 🔜 ⚙️ 🕴 ⏮️ 💯 (🎲 🕴 🎯 💯), & 🔜 🚚 💲 👈 💪 ⚙️ 🌐❔ 💲 ⏮️ 🔗 ⚙️. + +### ⚙️ 💼: 🔢 🐕‍🦺 + +🖼 💪 👈 👆 ✔️ 🔢 🤝 🐕‍🦺 👈 👆 💪 🤙. + +👆 📨 ⚫️ 🤝 & ⚫️ 📨 🔓 👩‍💻. + +👉 🐕‍🦺 5️⃣📆 🔌 👆 📍 📨, & 🤙 ⚫️ 💪 ✊ ➕ 🕰 🌘 🚥 👆 ✔️ 🔧 🎁 👩‍💻 💯. + +👆 🎲 💚 💯 🔢 🐕‍🦺 🕐, ✋️ 🚫 🎯 🤙 ⚫️ 🔠 💯 👈 🏃. + +👉 💼, 👆 💪 🔐 🔗 👈 🤙 👈 🐕‍🦺, & ⚙️ 🛃 🔗 👈 📨 🎁 👩‍💻, 🕴 👆 💯. + +### ⚙️ `app.dependency_overrides` 🔢 + +👫 💼, 👆 **FastAPI** 🈸 ✔️ 🔢 `app.dependency_overrides`, ⚫️ 🙅 `dict`. + +🔐 🔗 🔬, 👆 🚮 🔑 ⏮️ 🔗 (🔢), & 💲, 👆 🔗 🔐 (➕1️⃣ 🔢). + +& ⤴️ **FastAPI** 🔜 🤙 👈 🔐 ↩️ ⏮️ 🔗. + +```Python hl_lines="28-29 32" +{!../../../docs_src/dependency_testing/tutorial001.py!} +``` + +!!! tip + 👆 💪 ⚒ 🔗 🔐 🔗 ⚙️ 🙆 👆 **FastAPI** 🈸. + + ⏮️ 🔗 💪 ⚙️ *➡ 🛠️ 🔢*, *➡ 🛠️ 👨‍🎨* (🕐❔ 👆 🚫 ⚙️ 📨 💲), `.include_router()` 🤙, ♒️. + + FastAPI 🔜 💪 🔐 ⚫️. + +⤴️ 👆 💪 ⏲ 👆 🔐 (❎ 👫) ⚒ `app.dependency_overrides` 🛁 `dict`: + +```Python +app.dependency_overrides = {} +``` + +!!! tip + 🚥 👆 💚 🔐 🔗 🕴 ⏮️ 💯, 👆 💪 ⚒ 🔐 ▶️ 💯 (🔘 💯 🔢) & ⏲ ⚫️ 🔚 (🔚 💯 🔢). diff --git a/docs/em/docs/advanced/testing-events.md b/docs/em/docs/advanced/testing-events.md new file mode 100644 index 000000000..d64436eb9 --- /dev/null +++ b/docs/em/docs/advanced/testing-events.md @@ -0,0 +1,7 @@ +# 🔬 🎉: 🕴 - 🤫 + +🕐❔ 👆 💪 👆 🎉 🐕‍🦺 (`startup` & `shutdown`) 🏃 👆 💯, 👆 💪 ⚙️ `TestClient` ⏮️ `with` 📄: + +```Python hl_lines="9-12 20-24" +{!../../../docs_src/app_testing/tutorial003.py!} +``` diff --git a/docs/em/docs/advanced/testing-websockets.md b/docs/em/docs/advanced/testing-websockets.md new file mode 100644 index 000000000..3b8e7e420 --- /dev/null +++ b/docs/em/docs/advanced/testing-websockets.md @@ -0,0 +1,12 @@ +# 🔬 *️⃣ + +👆 💪 ⚙️ 🎏 `TestClient` 💯*️⃣. + +👉, 👆 ⚙️ `TestClient` `with` 📄, 🔗*️⃣: + +```Python hl_lines="27-31" +{!../../../docs_src/app_testing/tutorial002.py!} +``` + +!!! note + 🌅 ℹ, ✅ 💃 🧾 🔬 *️⃣ . diff --git a/docs/em/docs/advanced/using-request-directly.md b/docs/em/docs/advanced/using-request-directly.md new file mode 100644 index 000000000..faeadb1aa --- /dev/null +++ b/docs/em/docs/advanced/using-request-directly.md @@ -0,0 +1,52 @@ +# ⚙️ 📨 🔗 + +🆙 🔜, 👆 ✔️ 📣 🍕 📨 👈 👆 💪 ⏮️ 👫 🆎. + +✊ 📊 ⚪️➡️: + +* ➡ 🔢. +* 🎚. +* 🍪. +* ♒️. + +& 🔨, **FastAPI** ⚖ 👈 💽, 🏭 ⚫️ & 🏭 🧾 👆 🛠️ 🔁. + +✋️ 📤 ⚠ 🌐❔ 👆 💪 💪 🔐 `Request` 🎚 🔗. + +## ℹ 🔃 `Request` 🎚 + +**FastAPI** 🤙 **💃** 🔘, ⏮️ 🧽 📚 🧰 🔛 🔝, 👆 💪 ⚙️ 💃 `Request` 🎚 🔗 🕐❔ 👆 💪. + +⚫️ 🔜 ⛓ 👈 🚥 👆 🤚 📊 ⚪️➡️ `Request` 🎚 🔗 (🖼, ✍ 💪) ⚫️ 🏆 🚫 ✔, 🗜 ⚖️ 📄 (⏮️ 🗄, 🏧 🛠️ 👩‍💻 🔢) FastAPI. + +👐 🙆 🎏 🔢 📣 🛎 (🖼, 💪 ⏮️ Pydantic 🏷) 🔜 ✔, 🗜, ✍, ♒️. + +✋️ 📤 🎯 💼 🌐❔ ⚫️ ⚠ 🤚 `Request` 🎚. + +## ⚙️ `Request` 🎚 🔗 + +➡️ 🌈 👆 💚 🤚 👩‍💻 📢 📢/🦠 🔘 👆 *➡ 🛠️ 🔢*. + +👈 👆 💪 🔐 📨 🔗. + +```Python hl_lines="1 7-8" +{!../../../docs_src/using_request_directly/tutorial001.py!} +``` + +📣 *➡ 🛠️ 🔢* 🔢 ⏮️ 🆎 ➖ `Request` **FastAPI** 🔜 💭 🚶‍♀️ `Request` 👈 🔢. + +!!! tip + 🗒 👈 👉 💼, 👥 📣 ➡ 🔢 ⤴️ 📨 🔢. + + , ➡ 🔢 🔜 ⚗, ✔, 🗜 ✔ 🆎 & ✍ ⏮️ 🗄. + + 🎏 🌌, 👆 💪 📣 🙆 🎏 🔢 🛎, & ➡, 🤚 `Request` 💁‍♂️. + +## `Request` 🧾 + +👆 💪 ✍ 🌅 ℹ 🔃 `Request` 🎚 🛂 💃 🧾 🕸. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request`. + + **FastAPI** 🚚 ⚫️ 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. diff --git a/docs/em/docs/advanced/websockets.md b/docs/em/docs/advanced/websockets.md new file mode 100644 index 000000000..6ba9b999d --- /dev/null +++ b/docs/em/docs/advanced/websockets.md @@ -0,0 +1,184 @@ +# *️⃣ + +👆 💪 ⚙️ *️⃣ ⏮️ **FastAPI**. + +## ❎ `WebSockets` + +🥇 👆 💪 ❎ `WebSockets`: + +
+ +```console +$ pip install websockets + +---> 100% +``` + +
+ +## *️⃣ 👩‍💻 + +### 🏭 + +👆 🏭 ⚙️, 👆 🎲 ✔️ 🕸 ✍ ⏮️ 🏛 🛠️ 💖 😥, Vue.js ⚖️ 📐. + +& 🔗 ⚙️ *️⃣ ⏮️ 👆 👩‍💻 👆 🔜 🎲 ⚙️ 👆 🕸 🚙. + +⚖️ 👆 💪 ✔️ 🇦🇸 📱 🈸 👈 🔗 ⏮️ 👆 *️⃣ 👩‍💻 🔗, 🇦🇸 📟. + +⚖️ 👆 5️⃣📆 ✔️ 🙆 🎏 🌌 🔗 ⏮️ *️⃣ 🔗. + +--- + +✋️ 👉 🖼, 👥 🔜 ⚙️ 📶 🙅 🕸 📄 ⏮️ 🕸, 🌐 🔘 📏 🎻. + +👉, ↗️, 🚫 ⚖ & 👆 🚫🔜 ⚙️ ⚫️ 🏭. + +🏭 👆 🔜 ✔️ 1️⃣ 🎛 🔛. + +✋️ ⚫️ 🙅 🌌 🎯 🔛 💽-🚄 *️⃣ & ✔️ 👷 🖼: + +```Python hl_lines="2 6-38 41-43" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +## ✍ `websocket` + +👆 **FastAPI** 🈸, ✍ `websocket`: + +```Python hl_lines="1 46-47" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.websockets import WebSocket`. + + **FastAPI** 🚚 🎏 `WebSocket` 🔗 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## ⌛ 📧 & 📨 📧 + +👆 *️⃣ 🛣 👆 💪 `await` 📧 & 📨 📧. + +```Python hl_lines="48-52" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +👆 💪 📨 & 📨 💱, ✍, & 🎻 💽. + +## 🔄 ⚫️ + +🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000. + +👆 🔜 👀 🙅 📃 💖: + + + +👆 💪 🆎 📧 🔢 📦, & 📨 👫: + + + +& 👆 **FastAPI** 🈸 ⏮️ *️⃣ 🔜 📨 🔙: + + + +👆 💪 📨 (& 📨) 📚 📧: + + + +& 🌐 👫 🔜 ⚙️ 🎏 *️⃣ 🔗. + +## ⚙️ `Depends` & 🎏 + +*️⃣ 🔗 👆 💪 🗄 ⚪️➡️ `fastapi` & ⚙️: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +👫 👷 🎏 🌌 🎏 FastAPI 🔗/*➡ 🛠️*: + +```Python hl_lines="66-77 76-91" +{!../../../docs_src/websockets/tutorial002.py!} +``` + +!!! info + 👉 *️⃣ ⚫️ 🚫 🤙 ⚒ 🔑 🤚 `HTTPException`, ↩️ 👥 🤚 `WebSocketException`. + + 👆 💪 ⚙️ 📪 📟 ⚪️➡️ ☑ 📟 🔬 🔧. + +### 🔄 *️⃣ ⏮️ 🔗 + +🚥 👆 📁 📛 `main.py`, 🏃 👆 🈸 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +📂 👆 🖥 http://127.0.0.1:8000. + +📤 👆 💪 ⚒: + +* "🏬 🆔", ⚙️ ➡. +* "🤝" ⚙️ 🔢 🔢. + +!!! tip + 👀 👈 🔢 `token` 🔜 🍵 🔗. + +⏮️ 👈 👆 💪 🔗 *️⃣ & ⤴️ 📨 & 📨 📧: + + + +## 🚚 🔀 & 💗 👩‍💻 + +🕐❔ *️⃣ 🔗 📪, `await websocket.receive_text()` 🔜 🤚 `WebSocketDisconnect` ⚠, ❔ 👆 💪 ⤴️ ✊ & 🍵 💖 👉 🖼. + +```Python hl_lines="81-83" +{!../../../docs_src/websockets/tutorial003.py!} +``` + +🔄 ⚫️ 👅: + +* 📂 📱 ⏮️ 📚 🖥 📑. +* ✍ 📧 ⚪️➡️ 👫. +* ⤴️ 🔐 1️⃣ 📑. + +👈 🔜 🤚 `WebSocketDisconnect` ⚠, & 🌐 🎏 👩‍💻 🔜 📨 📧 💖: + +``` +Client #1596980209979 left the chat +``` + +!!! tip + 📱 🔛 ⭐ & 🙅 🖼 🎦 ❔ 🍵 & 📻 📧 📚 *️⃣ 🔗. + + ✋️ ✔️ 🤯 👈, 🌐 🍵 💾, 👁 📇, ⚫️ 🔜 🕴 👷 ⏪ 🛠️ 🏃, & 🔜 🕴 👷 ⏮️ 👁 🛠️. + + 🚥 👆 💪 🕳 ⏩ 🛠️ ⏮️ FastAPI ✋️ 👈 🌖 🏋️, 🐕‍🦺 ✳, ✳ ⚖️ 🎏, ✅ 🗜/📻. + +## 🌅 ℹ + +💡 🌅 🔃 🎛, ✅ 💃 🧾: + +* `WebSocket` 🎓. +* 🎓-⚓️ *️⃣ 🚚. diff --git a/docs/em/docs/advanced/wsgi.md b/docs/em/docs/advanced/wsgi.md new file mode 100644 index 000000000..4d051807f --- /dev/null +++ b/docs/em/docs/advanced/wsgi.md @@ -0,0 +1,37 @@ +# ✅ 🇨🇻 - 🏺, ✳, 🎏 + +👆 💪 🗻 🇨🇻 🈸 👆 👀 ⏮️ [🎧 🈸 - 🗻](./sub-applications.md){.internal-link target=_blank}, [⛅ 🗳](./behind-a-proxy.md){.internal-link target=_blank}. + +👈, 👆 💪 ⚙️ `WSGIMiddleware` & ⚙️ ⚫️ 🎁 👆 🇨🇻 🈸, 🖼, 🏺, ✳, ♒️. + +## ⚙️ `WSGIMiddleware` + +👆 💪 🗄 `WSGIMiddleware`. + +⤴️ 🎁 🇨🇻 (✅ 🏺) 📱 ⏮️ 🛠️. + +& ⤴️ 🗻 👈 🔽 ➡. + +```Python hl_lines="2-3 22" +{!../../../docs_src/wsgi/tutorial001.py!} +``` + +## ✅ ⚫️ + +🔜, 🔠 📨 🔽 ➡ `/v1/` 🔜 🍵 🏺 🈸. + +& 🎂 🔜 🍵 **FastAPI**. + +🚥 👆 🏃 ⚫️ ⏮️ Uvicorn & 🚶 http://localhost:8000/v1/ 👆 🔜 👀 📨 ⚪️➡️ 🏺: + +```txt +Hello, World from Flask! +``` + +& 🚥 👆 🚶 http://localhost:8000/v2 👆 🔜 👀 📨 ⚪️➡️ FastAPI: + +```JSON +{ + "message": "Hello World" +} +``` diff --git a/docs/em/docs/alternatives.md b/docs/em/docs/alternatives.md new file mode 100644 index 000000000..6169aa52d --- /dev/null +++ b/docs/em/docs/alternatives.md @@ -0,0 +1,414 @@ +# 🎛, 🌈 & 🔺 + +⚫️❔ 😮 **FastAPI**, ❔ ⚫️ 🔬 🎏 🎛 & ⚫️❔ ⚫️ 🇭🇲 ⚪️➡️ 👫. + +## 🎶 + +**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. + +📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. + +👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. + +✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). + +## ⏮️ 🧰 + +### + +⚫️ 🌅 🌟 🐍 🛠️ & 🛎 🕴. ⚫️ ⚙️ 🏗 ⚙️ 💖 👱📔. + +⚫️ 📶 😆 🔗 ⏮️ 🔗 💽 (💖 ✳ ⚖️ ✳),, ✔️ ☁ 💽 (💖 🗄, ✳, 👸, ♒️) 👑 🏪 🚒 🚫 📶 ⏩. + +⚫️ ✍ 🏗 🕸 👩‍💻, 🚫 ✍ 🔗 ⚙️ 🏛 🕸 (💖 😥, Vue.js & 📐) ⚖️ 🎏 ⚙️ (💖 📳) 🔗 ⏮️ ⚫️. + +### ✳ 🎂 🛠️ + +✳ 🎂 🛠️ ✍ 🗜 🧰 🏗 🕸 🔗 ⚙️ ✳ 🔘, 📉 🚮 🛠️ 🛠️. + +⚫️ ⚙️ 📚 🏢 ✅ 🦎, 🟥 👒 & 🎟. + +⚫️ 🕐 🥇 🖼 **🏧 🛠️ 🧾**, & 👉 🎯 🕐 🥇 💭 👈 😮 "🔎" **FastAPI**. + +!!! note + ✳ 🎂 🛠️ ✍ ✡ 🇺🇸🏛. 🎏 👼 💃 & Uvicorn, 🔛 ❔ **FastAPI** ⚓️. + + +!!! check "😮 **FastAPI** " + ✔️ 🏧 🛠️ 🧾 🕸 👩‍💻 🔢. + +### 🏺 + +🏺 "🕸", ⚫️ 🚫 🔌 💽 🛠️ 🚫 📚 👜 👈 👟 🔢 ✳. + +👉 🦁 & 💪 ✔ 🔨 👜 💖 ⚙️ ☁ 💽 👑 💽 💾 ⚙️. + +⚫️ 📶 🙅, ⚫️ 📶 🏋️ 💡, 👐 🧾 🤚 🙁 📡 ☝. + +⚫️ 🛎 ⚙️ 🎏 🈸 👈 🚫 🎯 💪 💽, 👩‍💻 🧾, ⚖️ 🙆 📚 ⚒ 👈 👟 🏤-🏗 ✳. 👐 📚 👫 ⚒ 💪 🚮 ⏮️ 🔌-🔌. + +👉 ⚖ 🍕, & ➖ "🕸" 👈 💪 ↔ 📔 ⚫️❔ ⚫️❔ 💪 🔑 ⚒ 👈 👤 💚 🚧. + +👐 🦁 🏺, ⚫️ 😑 💖 👍 🏏 🏗 🔗. ⏭ 👜 🔎 "✳ 🎂 🛠️" 🏺. + +!!! check "😮 **FastAPI** " + ◾-🛠️. ⚒ ⚫️ ⏩ 🌀 & 🏏 🧰 & 🍕 💪. + + ✔️ 🙅 & ⏩ ⚙️ 🕹 ⚙️. + + +### 📨 + +**FastAPI** 🚫 🤙 🎛 **📨**. 👫 ↔ 📶 🎏. + +⚫️ 🔜 🤙 ⚠ ⚙️ 📨 *🔘* FastAPI 🈸. + +✋️, FastAPI 🤚 🌈 ⚪️➡️ 📨. + +**📨** 🗃 *🔗* ⏮️ 🔗 (👩‍💻), ⏪ **FastAPI** 🗃 *🏗* 🔗 (💽). + +👫, 🌖 ⚖️ 🌘, 🔄 🔚, 🔗 🔠 🎏. + +📨 ✔️ 📶 🙅 & 🏋️ 🔧, ⚫️ 📶 ⏩ ⚙️, ⏮️ 🤔 🔢. ✋️ 🎏 🕰, ⚫️ 📶 🏋️ & 🛃. + +👈 ⚫️❔, 💬 🛂 🕸: + +> 📨 1️⃣ 🏆 ⏬ 🐍 📦 🌐 🕰 + +🌌 👆 ⚙️ ⚫️ 📶 🙅. 🖼, `GET` 📨, 👆 🔜 ✍: + +```Python +response = requests.get("http://example.com/some/url") +``` + +FastAPI 😑 🛠️ *➡ 🛠️* 💪 👀 💖: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +👀 🔀 `requests.get(...)` & `@app.get(...)`. + +!!! check "😮 **FastAPI** " + * ✔️ 🙅 & 🏋️ 🛠️. + * ⚙️ 🇺🇸🔍 👩‍🔬 📛 (🛠️) 🔗, 🎯 & 🏋️ 🌌. + * ✔️ 🤔 🔢, ✋️ 🏋️ 🛃. + + +### 🦁 / 🗄 + +👑 ⚒ 👤 💚 ⚪️➡️ ✳ 🎂 🛠️ 🏧 🛠️ 🧾. + +⤴️ 👤 🔎 👈 📤 🐩 📄 🔗, ⚙️ 🎻 (⚖️ 📁, ↔ 🎻) 🤙 🦁. + +& 📤 🕸 👩‍💻 🔢 🦁 🛠️ ⏪ ✍. , 💆‍♂ 💪 🏗 🦁 🧾 🛠️ 🔜 ✔ ⚙️ 👉 🕸 👩‍💻 🔢 🔁. + +☝, 🦁 👐 💾 🏛, 📁 🗄. + +👈 ⚫️❔ 🕐❔ 💬 🔃 ⏬ 2️⃣.0️⃣ ⚫️ ⚠ 💬 "🦁", & ⏬ 3️⃣ ➕ "🗄". + +!!! check "😮 **FastAPI** " + 🛠️ & ⚙️ 📂 🐩 🛠️ 🔧, ↩️ 🛃 🔗. + + & 🛠️ 🐩-⚓️ 👩‍💻 🔢 🧰: + + * 🦁 🎚 + * 📄 + + 👫 2️⃣ 👐 ➖ 📶 🌟 & ⚖, ✋️ 🔨 ⏩ 🔎, 👆 💪 🔎 💯 🌖 🎛 👩‍💻 🔢 🗄 (👈 👆 💪 ⚙️ ⏮️ **FastAPI**). + +### 🏺 🎂 🛠️ + +📤 📚 🏺 🎂 🛠️, ✋️ ⏮️ 💰 🕰 & 👷 🔘 🔬 👫, 👤 🔎 👈 📚 😞 ⚖️ 🚫, ⏮️ 📚 🧍 ❔ 👈 ⚒ 👫 🙃. + +### 🍭 + +1️⃣ 👑 ⚒ 💪 🛠️ ⚙️ 📊 "🛠️" ❔ ✊ 📊 ⚪️➡️ 📟 (🐍) & 🏭 ⚫️ 🔘 🕳 👈 💪 📨 🔘 🕸. 🖼, 🏭 🎚 ⚗ 📊 ⚪️➡️ 💽 🔘 🎻 🎚. 🏭 `datetime` 🎚 🔘 🎻, ♒️. + +➕1️⃣ 🦏 ⚒ 💚 🔗 💽 🔬, ⚒ 💭 👈 💽 ☑, 🤝 🎯 🔢. 🖼, 👈 🏑 `int`, & 🚫 🎲 🎻. 👉 ✴️ ⚠ 📨 💽. + +🍵 💽 🔬 ⚙️, 👆 🔜 ✔️ 🌐 ✅ ✋, 📟. + +👫 ⚒ ⚫️❔ 🍭 🏗 🚚. ⚫️ 👑 🗃, & 👤 ✔️ ⚙️ ⚫️ 📚 ⏭. + +✋️ ⚫️ ✍ ⏭ 📤 🔀 🐍 🆎 🔑. , 🔬 🔠 🔗 👆 💪 ⚙️ 🎯 🇨🇻 & 🎓 🚚 🍭. + +!!! check "😮 **FastAPI** " + ⚙️ 📟 🔬 "🔗" 👈 🚚 💽 🆎 & 🔬, 🔁. + +### Webarg + +➕1️⃣ 🦏 ⚒ ✔ 🔗 📊 ⚪️➡️ 📨 📨. + +Webarg 🧰 👈 ⚒ 🚚 👈 🔛 🔝 📚 🛠️, 🔌 🏺. + +⚫️ ⚙️ 🍭 🔘 💽 🔬. & ⚫️ ✍ 🎏 👩‍💻. + +⚫️ 👑 🧰 & 👤 ✔️ ⚙️ ⚫️ 📚 💁‍♂️, ⏭ ✔️ **FastAPI**. + +!!! info + Webarg ✍ 🎏 🍭 👩‍💻. + +!!! check "😮 **FastAPI** " + ✔️ 🏧 🔬 📨 📨 💽. + +### APISpec + +🍭 & Webarg 🚚 🔬, ✍ & 🛠️ 🔌-🔌. + +✋️ 🧾 ❌. ⤴️ APISpec ✍. + +⚫️ 🔌-📚 🛠️ (& 📤 🔌-💃 💁‍♂️). + +🌌 ⚫️ 👷 👈 👆 ✍ 🔑 🔗 ⚙️ 📁 📁 🔘 #️⃣ 🔠 🔢 🚚 🛣. + +& ⚫️ 🏗 🗄 🔗. + +👈 ❔ ⚫️ 👷 🏺, 💃, 🆘, ♒️. + +✋️ ⤴️, 👥 ✔️ 🔄 ⚠ ✔️ ◾-❕, 🔘 🐍 🎻 (🦏 📁). + +👨‍🎨 💪 🚫 ℹ 🌅 ⏮️ 👈. & 🚥 👥 🔀 🔢 ⚖️ 🍭 🔗 & 💭 🔀 👈 📁#️⃣, 🏗 🔗 🔜 ❌. + +!!! info + APISpec ✍ 🎏 🍭 👩‍💻. + + +!!! check "😮 **FastAPI** " + 🐕‍🦺 📂 🐩 🛠️, 🗄. + +### 🏺-Apispec + +⚫️ 🏺 🔌 -, 👈 👔 👯‍♂️ Webarg, 🍭 & APISpec. + +⚫️ ⚙️ ℹ ⚪️➡️ Webarg & 🍭 🔁 🏗 🗄 🔗, ⚙️ APISpec. + +⚫️ 👑 🧰, 📶 🔽-📈. ⚫️ 🔜 🌌 🌖 🌟 🌘 📚 🏺 🔌-🔌 👅 📤. ⚫️ 💪 ↩️ 🚮 🧾 ➖ 💁‍♂️ 🩲 & 📝. + +👉 ❎ ✔️ ✍ 📁 (➕1️⃣ ❕) 🔘 🐍 ✍. + +👉 🌀 🏺, 🏺-Apispec ⏮️ 🍭 & Webarg 👇 💕 👩‍💻 📚 ⏭ 🏗 **FastAPI**. + +⚙️ ⚫️ ↘️ 🏗 📚 🏺 🌕-📚 🚂. 👫 👑 📚 👤 (& 📚 🔢 🏉) ✔️ ⚙️ 🆙 🔜: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +& 👫 🎏 🌕-📚 🚂 🧢 [**FastAPI** 🏗 🚂](project-generation.md){.internal-link target=_blank}. + +!!! info + 🏺-Apispec ✍ 🎏 🍭 👩‍💻. + +!!! check "😮 **FastAPI** " + 🏗 🗄 🔗 🔁, ⚪️➡️ 🎏 📟 👈 🔬 🛠️ & 🔬. + +### NestJS (& 📐) + +👉 ➖🚫 🚫 🐍, NestJS 🕸 (📕) ✳ 🛠️ 😮 📐. + +⚫️ 🏆 🕳 🙁 🎏 ⚫️❔ 💪 🔨 ⏮️ 🏺-Apispec. + +⚫️ ✔️ 🛠️ 🔗 💉 ⚙️, 😮 📐 2️⃣. ⚫️ 🚚 🏤-® "💉" (💖 🌐 🎏 🔗 💉 ⚙️ 👤 💭),, ⚫️ 🚮 🎭 & 📟 🔁. + +🔢 🔬 ⏮️ 📕 🆎 (🎏 🐍 🆎 🔑), 👨‍🎨 🐕‍🦺 👍. + +✋️ 📕 📊 🚫 🛡 ⏮️ 📹 🕸, ⚫️ 🚫🔜 ⚓️ 🔛 🆎 🔬 🔬, 🛠️ & 🧾 🎏 🕰. ↩️ 👉 & 🔧 🚫, 🤚 🔬, 🛠️ & 🏧 🔗 ⚡, ⚫️ 💪 🚮 👨‍🎨 📚 🥉. , ⚫️ ▶️️ 🔁. + +⚫️ 💪 🚫 🍵 🔁 🏷 📶 👍. , 🚥 🎻 💪 📨 🎻 🎚 👈 ✔️ 🔘 🏑 👈 🔄 🐦 🎻 🎚, ⚫️ 🚫🔜 ☑ 📄 & ✔. + +!!! check "😮 **FastAPI** " + ⚙️ 🐍 🆎 ✔️ 👑 👨‍🎨 🐕‍🦺. + + ✔️ 🏋️ 🔗 💉 ⚙️. 🔎 🌌 📉 📟 🔁. + +### 🤣 + +⚫️ 🕐 🥇 📶 ⏩ 🐍 🛠️ ⚓️ 🔛 `asyncio`. ⚫️ ⚒ 📶 🎏 🏺. + +!!! note "📡 ℹ" + ⚫️ ⚙️ `uvloop` ↩️ 🔢 🐍 `asyncio` ➰. 👈 ⚫️❔ ⚒ ⚫️ ⏩. + + ⚫️ 🎯 😮 Uvicorn & 💃, 👈 ⏳ ⏩ 🌘 🤣 📂 📇. + +!!! check "😮 **FastAPI** " + 🔎 🌌 ✔️ 😜 🎭. + + 👈 ⚫️❔ **FastAPI** ⚓️ 🔛 💃, ⚫️ ⏩ 🛠️ 💪 (💯 🥉-🥳 📇). + +### 🦅 + +🦅 ➕1️⃣ ↕ 🎭 🐍 🛠️, ⚫️ 🔧 ⭐, & 👷 🏛 🎏 🛠️ 💖 🤗. + +⚫️ 🏗 ✔️ 🔢 👈 📨 2️⃣ 🔢, 1️⃣ "📨" & 1️⃣ "📨". ⤴️ 👆 "✍" 🍕 ⚪️➡️ 📨, & "✍" 🍕 📨. ↩️ 👉 🔧, ⚫️ 🚫 💪 📣 📨 🔢 & 💪 ⏮️ 🐩 🐍 🆎 🔑 🔢 🔢. + +, 💽 🔬, 🛠️, & 🧾, ✔️ ⌛ 📟, 🚫 🔁. ⚖️ 👫 ✔️ 🛠️ 🛠️ 🔛 🔝 🦅, 💖 🤗. 👉 🎏 🔺 🔨 🎏 🛠️ 👈 😮 🦅 🔧, ✔️ 1️⃣ 📨 🎚 & 1️⃣ 📨 🎚 🔢. + +!!! check "😮 **FastAPI** " + 🔎 🌌 🤚 👑 🎭. + + ⤴️ ⏮️ 🤗 (🤗 ⚓️ 🔛 🦅) 😮 **FastAPI** 📣 `response` 🔢 🔢. + + 👐 FastAPI ⚫️ 📦, & ⚙️ ✴️ ⚒ 🎚, 🍪, & 🎛 👔 📟. + +### + +👤 🔎 ♨ 🥇 ▶️ 🏗 **FastAPI**. & ⚫️ ✔️ 🎏 💭: + +* ⚓️ 🔛 🐍 🆎 🔑. +* 🔬 & 🧾 ⚪️➡️ 👫 🆎. +* 🔗 💉 ⚙️. + +⚫️ 🚫 ⚙️ 💽 🔬, 🛠️ & 🧾 🥉-🥳 🗃 💖 Pydantic, ⚫️ ✔️ 🚮 👍. , 👫 💽 🆎 🔑 🔜 🚫 ♻ 💪. + +⚫️ 🚚 🐥 🍖 🌅 🔁 📳. & ⚫️ ⚓️ 🔛 🇨🇻 (↩️ 🔫), ⚫️ 🚫 🔧 ✊ 📈 ↕-🎭 🚚 🧰 💖 Uvicorn, 💃 & 🤣. + +🔗 💉 ⚙️ 🚚 🏤-® 🔗 & 🔗 ❎ 🧢 🔛 📣 🆎. , ⚫️ 🚫 💪 📣 🌅 🌘 1️⃣ "🦲" 👈 🚚 🎯 🆎. + +🛣 📣 👁 🥉, ⚙️ 🔢 📣 🎏 🥉 (↩️ ⚙️ 👨‍🎨 👈 💪 🥉 ▶️️ 🔛 🔝 🔢 👈 🍵 🔗). 👉 🔐 ❔ ✳ 🔨 ⚫️ 🌘 ❔ 🏺 (& 💃) 🔨 ⚫️. ⚫️ 🎏 📟 👜 👈 📶 😆 🔗. + +!!! check "😮 **FastAPI** " + 🔬 ➕ 🔬 💽 🆎 ⚙️ "🔢" 💲 🏷 🔢. 👉 📉 👨‍🎨 🐕‍🦺, & ⚫️ 🚫 💪 Pydantic ⏭. + + 👉 🤙 😮 🛠️ 🍕 Pydantic, 🐕‍🦺 🎏 🔬 📄 👗 (🌐 👉 🛠️ 🔜 ⏪ 💪 Pydantic). + +### 🤗 + +🤗 🕐 🥇 🛠️ 🛠️ 📄 🛠️ 🔢 🆎 ⚙️ 🐍 🆎 🔑. 👉 👑 💭 👈 😮 🎏 🧰 🎏. + +⚫️ ⚙️ 🛃 🆎 🚮 📄 ↩️ 🐩 🐍 🆎, ✋️ ⚫️ 🦏 🔁 ⏩. + +⚫️ 🕐 🥇 🛠️ 🏗 🛃 🔗 📣 🎂 🛠️ 🎻. + +⚫️ 🚫 ⚓️ 🔛 🐩 💖 🗄 & 🎻 🔗. ⚫️ 🚫🔜 🎯 🛠️ ⚫️ ⏮️ 🎏 🧰, 💖 🦁 🎚. ✋️ 🔄, ⚫️ 📶 💡 💭. + +⚫️ ✔️ 😌, ⭐ ⚒: ⚙️ 🎏 🛠️, ⚫️ 💪 ✍ 🔗 & 🇳🇨. + +⚫️ ⚓️ 🔛 ⏮️ 🐩 🔁 🐍 🕸 🛠️ (🇨🇻), ⚫️ 💪 🚫 🍵 *️⃣ & 🎏 👜, 👐 ⚫️ ✔️ ↕ 🎭 💁‍♂️. + +!!! info + 🤗 ✍ ✡ 🗄, 🎏 👼 `isort`, 👑 🧰 🔁 😇 🗄 🐍 📁. + +!!! check "💭 😮 **FastAPI**" + 🤗 😮 🍕 APIStar, & 1️⃣ 🧰 👤 🔎 🏆 👍, 🌟 APIStar. + + 🤗 ℹ 😍 **FastAPI** ⚙️ 🐍 🆎 🔑 📣 🔢, & 🏗 🔗 ⚖ 🛠️ 🔁. + + 🤗 😮 **FastAPI** 📣 `response` 🔢 🔢 ⚒ 🎚 & 🍪. + +### APIStar (<= 0️⃣.5️⃣) + +▶️️ ⏭ 🤔 🏗 **FastAPI** 👤 🔎 **APIStar** 💽. ⚫️ ✔️ 🌖 🌐 👤 👀 & ✔️ 👑 🔧. + +⚫️ 🕐 🥇 🛠️ 🛠️ ⚙️ 🐍 🆎 🔑 📣 🔢 & 📨 👈 👤 ⏱ 👀 (⏭ NestJS & ♨). 👤 🔎 ⚫️ 🌅 ⚖️ 🌘 🎏 🕰 🤗. ✋️ APIStar ⚙️ 🗄 🐩. + +⚫️ ✔️ 🏧 💽 🔬, 💽 🛠️ & 🗄 🔗 ⚡ ⚓️ 🔛 🎏 🆎 🔑 📚 🥉. + +💪 🔗 🔑 🚫 ⚙️ 🎏 🐍 🆎 🔑 💖 Pydantic, ⚫️ 🍖 🌅 🎏 🍭,, 👨‍🎨 🐕‍🦺 🚫🔜 👍, ✋️, APIStar 🏆 💪 🎛. + +⚫️ ✔️ 🏆 🎭 📇 🕰 (🕴 💥 💃). + +🥇, ⚫️ 🚫 ✔️ 🏧 🛠️ 🧾 🕸 🎚, ✋️ 👤 💭 👤 💪 🚮 🦁 🎚 ⚫️. + +⚫️ ✔️ 🔗 💉 ⚙️. ⚫️ ✔ 🏤-® 🦲, 🎏 🧰 🔬 🔛. ✋️, ⚫️ 👑 ⚒. + +👤 🙅 💪 ⚙️ ⚫️ 🌕 🏗, ⚫️ 🚫 ✔️ 💂‍♂ 🛠️,, 👤 🚫 🚫 ❎ 🌐 ⚒ 👤 ✔️ ⏮️ 🌕-📚 🚂 ⚓️ 🔛 🏺-Apispec. 👤 ✔️ 👇 📈 🏗 ✍ 🚲 📨 ❎ 👈 🛠️. + +✋️ ⤴️, 🏗 🎯 🔀. + +⚫️ 🙅‍♂ 📏 🛠️ 🕸 🛠️, 👼 💪 🎯 🔛 💃. + +🔜 APIStar ⚒ 🧰 ✔ 🗄 🔧, 🚫 🕸 🛠️. + +!!! info + APIStar ✍ ✡ 🇺🇸🏛. 🎏 👨 👈 ✍: + + * ✳ 🎂 🛠️ + * 💃 (❔ **FastAPI** ⚓️) + * Uvicorn (⚙️ 💃 & **FastAPI**) + +!!! check "😮 **FastAPI** " + 🔀. + + 💭 📣 💗 👜 (💽 🔬, 🛠️ & 🧾) ⏮️ 🎏 🐍 🆎, 👈 🎏 🕰 🚚 👑 👨‍🎨 🐕‍🦺, 🕳 👤 🤔 💎 💭. + + & ⏮️ 🔎 📏 🕰 🎏 🛠️ & 🔬 📚 🎏 🎛, APIStar 🏆 🎛 💪. + + ⤴️ APIStar ⛔️ 🔀 💽 & 💃 ✍, & 🆕 👻 🏛 ✅ ⚙️. 👈 🏁 🌈 🏗 **FastAPI**. + + 👤 🤔 **FastAPI** "🛐 👨‍💼" APIStar, ⏪ 📉 & 📈 ⚒, ⌨ ⚙️, & 🎏 🍕, ⚓️ 🔛 🏫 ⚪️➡️ 🌐 👉 ⏮️ 🧰. + +## ⚙️ **FastAPI** + +### Pydantic + +Pydantic 🗃 🔬 💽 🔬, 🛠️ & 🧾 (⚙️ 🎻 🔗) ⚓️ 🔛 🐍 🆎 🔑. + +👈 ⚒ ⚫️ 📶 🏋️. + +⚫️ ⭐ 🍭. 👐 ⚫️ ⏩ 🌘 🍭 📇. & ⚫️ ⚓️ 🔛 🎏 🐍 🆎 🔑, 👨‍🎨 🐕‍🦺 👑. + +!!! check "**FastAPI** ⚙️ ⚫️" + 🍵 🌐 💽 🔬, 💽 🛠️ & 🏧 🏷 🧾 (⚓️ 🔛 🎻 🔗). + + **FastAPI** ⤴️ ✊ 👈 🎻 🔗 💽 & 🚮 ⚫️ 🗄, ↖️ ⚪️➡️ 🌐 🎏 👜 ⚫️ 🔨. + +### 💃 + +💃 💿 🔫 🛠️/🧰, ❔ 💯 🏗 ↕-🎭 ✳ 🐕‍🦺. + +⚫️ 📶 🙅 & 🏋️. ⚫️ 🔧 💪 🏧, & ✔️ 🔧 🦲. + +⚫️ ✔️: + +* 🤙 🎆 🎭. +* *️⃣ 🐕‍🦺. +* -🛠️ 🖥 📋. +* 🕴 & 🤫 🎉. +* 💯 👩‍💻 🏗 🔛 🇸🇲. +* ⚜, 🗜, 🎻 📁, 🎏 📨. +* 🎉 & 🍪 🐕‍🦺. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. +* 👩‍❤‍👨 🏋️ 🔗. + +💃 ⏳ ⏩ 🐍 🛠️ 💯. 🕴 💥 Uvicorn, ❔ 🚫 🛠️, ✋️ 💽. + +💃 🚚 🌐 🔰 🕸 🕸 🛠️. + +✋️ ⚫️ 🚫 🚚 🏧 💽 🔬, 🛠️ ⚖️ 🧾. + +👈 1️⃣ 👑 👜 👈 **FastAPI** 🚮 🔛 🔝, 🌐 ⚓️ 🔛 🐍 🆎 🔑 (⚙️ Pydantic). 👈, ➕ 🔗 💉 ⚙️, 💂‍♂ 🚙, 🗄 🔗 ⚡, ♒️. + +!!! note "📡 ℹ" + 🔫 🆕 "🐩" ➖ 🛠️ ✳ 🐚 🏉 👨‍🎓. ⚫️ 🚫 "🐍 🐩" (🇩🇬), 👐 👫 🛠️ 🔨 👈. + + 👐, ⚫️ ⏪ ➖ ⚙️ "🐩" 📚 🧰. 👉 📉 📉 🛠️, 👆 💪 🎛 Uvicorn 🙆 🎏 🔫 💽 (💖 👸 ⚖️ Hypercorn), ⚖️ 👆 💪 🚮 🔫 🔗 🧰, 💖 `python-socketio`. + +!!! check "**FastAPI** ⚙️ ⚫️" + 🍵 🌐 🐚 🕸 🍕. ❎ ⚒ 🔛 🔝. + + 🎓 `FastAPI` ⚫️ 😖 🔗 ⚪️➡️ 🎓 `Starlette`. + + , 🕳 👈 👆 💪 ⏮️ 💃, 👆 💪 ⚫️ 🔗 ⏮️ **FastAPI**, ⚫️ 🌖 💃 🔛 💊. + +### Uvicorn + +Uvicorn 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. + +⚫️ 🚫 🕸 🛠️, ✋️ 💽. 🖼, ⚫️ 🚫 🚚 🧰 🕹 ➡. 👈 🕳 👈 🛠️ 💖 💃 (⚖️ **FastAPI**) 🔜 🚚 🔛 🔝. + +⚫️ 👍 💽 💃 & **FastAPI**. + +!!! check "**FastAPI** 👍 ⚫️" + 👑 🕸 💽 🏃 **FastAPI** 🈸. + + 👆 💪 🌀 ⚫️ ⏮️ 🐁, ✔️ 🔁 👁-🛠️ 💽. + + ✅ 🌅 ℹ [🛠️](deployment/index.md){.internal-link target=_blank} 📄. + +## 📇 & 🚅 + +🤔, 🔬, & 👀 🔺 🖖 Uvicorn, 💃 & FastAPI, ✅ 📄 🔃 [📇](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/em/docs/async.md b/docs/em/docs/async.md new file mode 100644 index 000000000..13b362b5d --- /dev/null +++ b/docs/em/docs/async.md @@ -0,0 +1,430 @@ +# 🛠️ & 🔁 / ⌛ + +ℹ 🔃 `async def` ❕ *➡ 🛠️ 🔢* & 🖥 🔃 🔁 📟, 🛠️, & 🔁. + +## 🏃 ❓ + +🆑;👩‍⚕️: + +🚥 👆 ⚙️ 🥉 🥳 🗃 👈 💬 👆 🤙 👫 ⏮️ `await`, 💖: + +```Python +results = await some_library() +``` + +⤴️, 📣 👆 *➡ 🛠️ 🔢* ⏮️ `async def` 💖: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note + 👆 💪 🕴 ⚙️ `await` 🔘 🔢 ✍ ⏮️ `async def`. + +--- + +🚥 👆 ⚙️ 🥉 🥳 🗃 👈 🔗 ⏮️ 🕳 (💽, 🛠️, 📁 ⚙️, ♒️.) & 🚫 ✔️ 🐕‍🦺 ⚙️ `await`, (👉 ⏳ 💼 🌅 💽 🗃), ⤴️ 📣 👆 *➡ 🛠️ 🔢* 🛎, ⏮️ `def`, 💖: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +🚥 👆 🈸 (😫) 🚫 ✔️ 🔗 ⏮️ 🕳 🙆 & ⌛ ⚫️ 📨, ⚙️ `async def`. + +--- + +🚥 👆 🚫 💭, ⚙️ 😐 `def`. + +--- + +**🗒**: 👆 💪 🌀 `def` & `async def` 👆 *➡ 🛠️ 🔢* 🌅 👆 💪 & 🔬 🔠 1️⃣ ⚙️ 🏆 🎛 👆. FastAPI 🔜 ▶️️ 👜 ⏮️ 👫. + +😆, 🙆 💼 🔛, FastAPI 🔜 👷 🔁 & 📶 ⏩. + +✋️ 📄 📶 🔛, ⚫️ 🔜 💪 🎭 🛠️. + +## 📡 ℹ + +🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. + +➡️ 👀 👈 🔤 🍕 📄 🔛: + +* **🔁 📟** +* **`async` & `await`** +* **🔁** + +## 🔁 📟 + +🔁 📟 ⛓ 👈 🇪🇸 👶 ✔️ 🌌 💬 💻 / 📋 👶 👈 ☝ 📟, ⚫️ 👶 🔜 ✔️ ⌛ *🕳 🙆* 🏁 👱 🙆. ➡️ 💬 👈 *🕳 🙆* 🤙 "🐌-📁" 👶. + +, ⏮️ 👈 🕰, 💻 💪 🚶 & 🎏 👷, ⏪ "🐌-📁" 👶 🏁. + +⤴️ 💻 / 📋 👶 🔜 👟 🔙 🔠 🕰 ⚫️ ✔️ 🤞 ↩️ ⚫️ ⌛ 🔄, ⚖️ 🕐❔ ⚫️ 👶 🏁 🌐 👷 ⚫️ ✔️ 👈 ☝. & ⚫️ 👶 🔜 👀 🚥 🙆 📋 ⚫️ ⌛ ✔️ ⏪ 🏁, 🤸 ⚫️❔ ⚫️ ✔️. + +⏭, ⚫️ 👶 ✊ 🥇 📋 🏁 (➡️ 💬, 👆 "🐌-📁" 👶) & 😣 ⚫️❔ ⚫️ ✔️ ⏮️ ⚫️. + +👈 "⌛ 🕳 🙆" 🛎 🔗 👤/🅾 🛠️ 👈 📶 "🐌" (🔬 🚅 🕹 & 💾 💾), 💖 ⌛: + +* 📊 ⚪️➡️ 👩‍💻 📨 🔘 🕸 +* 📊 📨 👆 📋 📨 👩‍💻 🔘 🕸 +* 🎚 📁 💾 ✍ ⚙️ & 🤝 👆 📋 +* 🎚 👆 📋 🤝 ⚙️ ✍ 💾 +* 🛰 🛠️ 🛠️ +* 💽 🛠️ 🏁 +* 💽 🔢 📨 🏁 +* ♒️. + +🛠️ 🕰 🍴 ✴️ ⌛ 👤/🅾 🛠️, 👫 🤙 👫 "👤/🅾 🔗" 🛠️. + +⚫️ 🤙 "🔁" ↩️ 💻 / 📋 🚫 ✔️ "🔁" ⏮️ 🐌 📋, ⌛ ☑ 🙍 👈 📋 🏁, ⏪ 🔨 🕳, 💪 ✊ 📋 🏁 & 😣 👷. + +↩️ 👈, 💆‍♂ "🔁" ⚙️, 🕐 🏁, 📋 💪 ⌛ ⏸ 🐥 👄 (⏲) 💻 / 📋 🏁 ⚫️❔ ⚫️ 🚶, & ⤴️ 👟 🔙 ✊ 🏁 & 😣 👷 ⏮️ 👫. + +"🔁" (👽 "🔁") 👫 🛎 ⚙️ ⚖ "🔁", ↩️ 💻 / 📋 ⏩ 🌐 📶 🔁 ⏭ 🔀 🎏 📋, 🚥 👈 🔁 🔌 ⌛. + +### 🛠️ & 🍔 + +👉 💭 **🔁** 📟 🔬 🔛 🕣 🤙 **"🛠️"**. ⚫️ 🎏 ⚪️➡️ **"🔁"**. + +**🛠️** & **🔁** 👯‍♂️ 🔗 "🎏 👜 😥 🌅 ⚖️ 🌘 🎏 🕰". + +✋️ ℹ 🖖 *🛠️* & *🔁* 🎏. + +👀 🔺, 🌈 📄 📖 🔃 🍔: + +### 🛠️ 🍔 + +👆 🚶 ⏮️ 👆 🥰 🤚 ⏩ 🥕, 👆 🧍 ⏸ ⏪ 🏧 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. 👶 + + + +⤴️ ⚫️ 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. 👶 👶 + + + +🏧 💬 🕳 🍳 👨‍🍳 👫 💭 👫 ✔️ 🏗 👆 🍔 (✋️ 👫 ⏳ 🏗 🕐 ⏮️ 👩‍💻). + + + +👆 💸. 👶 + +🏧 🤝 👆 🔢 👆 🔄. + + + +⏪ 👆 ⌛, 👆 🚶 ⏮️ 👆 🥰 & ⚒ 🏓, 👆 🧎 & 💬 ⏮️ 👆 🥰 📏 🕰 (👆 🍔 📶 🎀 & ✊ 🕰 🏗). + +👆 🏖 🏓 ⏮️ 👆 🥰, ⏪ 👆 ⌛ 🍔, 👆 💪 💸 👈 🕰 😮 ❔ 👌, 🐨 & 🙃 👆 🥰 👶 👶 👶. + + + +⏪ ⌛ & 💬 👆 🥰, ⚪️➡️ 🕰 🕰, 👆 ✅ 🔢 🖥 🔛 ⏲ 👀 🚥 ⚫️ 👆 🔄 ⏪. + +⤴️ ☝, ⚫️ 😒 👆 🔄. 👆 🚶 ⏲, 🤚 👆 🍔 & 👟 🔙 🏓. + + + +👆 & 👆 🥰 🍴 🍔 & ✔️ 👌 🕰. 👶 + + + +!!! info + 🌹 🖼 👯 🍏. 👶 + +--- + +🌈 👆 💻 / 📋 👶 👈 📖. + +⏪ 👆 ⏸, 👆 ⛽ 👶, ⌛ 👆 🔄, 🚫 🔨 🕳 📶 "😌". ✋️ ⏸ ⏩ ↩️ 🏧 🕴 ✊ ✔ (🚫 🏗 👫), 👈 👌. + +⤴️, 🕐❔ ⚫️ 👆 🔄, 👆 ☑ "😌" 👷, 👆 🛠️ 🍣, 💭 ⚫️❔ 👆 💚, 🤚 👆 🥰 ⚒, 💸, ✅ 👈 👆 🤝 ☑ 💵 ⚖️ 💳, ✅ 👈 👆 🈚 ☑, ✅ 👈 ✔ ✔️ ☑ 🏬, ♒️. + +✋️ ⤴️, ✋️ 👆 🚫 ✔️ 👆 🍔, 👆 👷 ⏮️ 🏧 "🔛 ⏸" ⏸, ↩️ 👆 ✔️ ⌛ 👶 👆 🍔 🔜. + +✋️ 👆 🚶 ↖️ ⚪️➡️ ⏲ & 🧎 🏓 ⏮️ 🔢 👆 🔄, 👆 💪 🎛 👶 👆 🙋 👆 🥰, & "👷" 👶 👶 🔛 👈. ⤴️ 👆 🔄 🔨 🕳 📶 "😌" 😏 ⏮️ 👆 🥰 👶. + +⤴️ 🏧 👶 💬 "👤 🏁 ⏮️ 🔨 🍔" 🚮 👆 🔢 🔛 ⏲ 🖥, ✋️ 👆 🚫 🦘 💖 😜 ⏪ 🕐❔ 🖥 🔢 🔀 👆 🔄 🔢. 👆 💭 🙅‍♂ 1️⃣ 🔜 📎 👆 🍔 ↩️ 👆 ✔️ 🔢 👆 🔄, & 👫 ✔️ 👫. + +👆 ⌛ 👆 🥰 🏁 📖 (🏁 ⏮️ 👷 👶 / 📋 ➖ 🛠️ 👶), 😀 🖐 & 💬 👈 👆 🔜 🍔 ⏸. + +⤴️ 👆 🚶 ⏲ 👶, ▶️ 📋 👈 🔜 🏁 👶, ⚒ 🍔, 💬 👏 & ✊ 👫 🏓. 👈 🏁 👈 🔁 / 📋 🔗 ⏮️ ⏲ ⏹. 👈 🔄, ✍ 🆕 📋, "🍴 🍔" 👶 👶, ✋️ ⏮️ 1️⃣ "🤚 🍔" 🏁 ⏹. + +### 🔗 🍔 + +🔜 ➡️ 🌈 👫 ➖🚫 🚫 "🛠️ 🍔", ✋️ "🔗 🍔". + +👆 🚶 ⏮️ 👆 🥰 🤚 🔗 ⏩ 🥕. + +👆 🧍 ⏸ ⏪ 📚 (➡️ 💬 8️⃣) 🏧 👈 🎏 🕰 🍳 ✊ ✔ ⚪️➡️ 👫👫 🚪 👆. + +👱 ⏭ 👆 ⌛ 👫 🍔 🔜 ⏭ 🍂 ⏲ ↩️ 🔠 8️⃣ 🏧 🚶 & 🏗 🍔 ▶️️ ↖️ ⏭ 💆‍♂ ⏭ ✔. + + + +⤴️ ⚫️ 😒 👆 🔄, 👆 🥉 👆 ✔ 2️⃣ 📶 🎀 🍔 👆 🥰 & 👆. + +👆 💸 👶. + + + +🏧 🚶 👨‍🍳. + +👆 ⌛, 🧍 🚪 ⏲ 👶, 👈 🙅‍♂ 1️⃣ 🙆 ✊ 👆 🍔 ⏭ 👆, 📤 🙅‍♂ 🔢 🔄. + + + +👆 & 👆 🥰 😩 🚫 ➡️ 🙆 🤚 🚪 👆 & ✊ 👆 🍔 🕐❔ 👫 🛬, 👆 🚫🔜 💸 🙋 👆 🥰. 👶 + +👉 "🔁" 👷, 👆 "🔁" ⏮️ 🏧/🍳 👶 👶. 👆 ✔️ ⌛ 👶 & 📤 ☑ 🙍 👈 🏧/🍳 👶 👶 🏁 🍔 & 🤝 👫 👆, ⚖️ ⏪, 👱 🙆 💪 ✊ 👫. + + + +⤴️ 👆 🏧/🍳 👶 👶 😒 👟 🔙 ⏮️ 👆 🍔, ⏮️ 📏 🕰 ⌛ 👶 📤 🚪 ⏲. + + + +👆 ✊ 👆 🍔 & 🚶 🏓 ⏮️ 👆 🥰. + +👆 🍴 👫, & 👆 🔨. ⏹ + + + +📤 🚫 🌅 💬 ⚖️ 😏 🌅 🕰 💸 ⌛ 👶 🚪 ⏲. 👶 + +!!! info + 🌹 🖼 👯 🍏. 👶 + +--- + +👉 😐 🔗 🍔, 👆 💻 / 📋 👶 ⏮️ 2️⃣ 🕹 (👆 & 👆 🥰), 👯‍♂️ ⌛ 👶 & 💡 👫 🙋 👶 "⌛ 🔛 ⏲" 👶 📏 🕰. + +⏩ 🥕 🏪 ✔️ 8️⃣ 🕹 (🏧/🍳). ⏪ 🛠️ 🍔 🏪 💪 ✔️ ✔️ 🕴 2️⃣ (1️⃣ 🏧 & 1️⃣ 🍳). + +✋️, 🏁 💡 🚫 🏆. 👶 + +--- + +👉 🔜 🔗 🌓 📖 🍔. 👶 + +🌅 "🎰 👨‍❤‍👨" 🖼 👉, 🌈 🏦. + +🆙 ⏳, 🏆 🏦 ✔️ 💗 🏧 👶 👶 👶 👶 👶 👶 👶 👶 & 🦏 ⏸ 👶 👶 👶 👶 👶 👶 👶 👶. + +🌐 🏧 🔨 🌐 👷 ⏮️ 1️⃣ 👩‍💻 ⏮️ 🎏 👶 👶 👶. + +& 👆 ✔️ ⌛ 👶 ⏸ 📏 🕰 ⚖️ 👆 💸 👆 🔄. + +👆 🎲 🚫🔜 💚 ✊ 👆 🥰 👶 ⏮️ 👆 👷 🏦 👶. + +### 🍔 🏁 + +👉 😐 "⏩ 🥕 🍔 ⏮️ 👆 🥰", 📤 📚 ⌛ 👶, ⚫️ ⚒ 📚 🌅 🔑 ✔️ 🛠️ ⚙️ ⏸ 👶 👶. + +👉 💼 🌅 🕸 🈸. + +📚, 📚 👩‍💻, ✋️ 👆 💽 ⌛ 👶 👫 🚫--👍 🔗 📨 👫 📨. + +& ⤴️ ⌛ 👶 🔄 📨 👟 🔙. + +👉 "⌛" 👶 ⚖ ⏲, ✋️, ⚖ ⚫️ 🌐, ⚫️ 📚 ⌛ 🔚. + +👈 ⚫️❔ ⚫️ ⚒ 📚 🔑 ⚙️ 🔁 ⏸ 👶 👶 📟 🕸 🔗. + +👉 😇 🔀 ⚫️❔ ⚒ ✳ 🌟 (✋️ ✳ 🚫 🔗) & 👈 💪 🚶 🛠️ 🇪🇸. + +& 👈 🎏 🎚 🎭 👆 🤚 ⏮️ **FastAPI**. + +& 👆 💪 ✔️ 🔁 & 🔀 🎏 🕰, 👆 🤚 ↕ 🎭 🌘 🌅 💯 ✳ 🛠️ & 🔛 🇷🇪 ⏮️ 🚶, ❔ ✍ 🇪🇸 🔐 🅱 (🌐 👏 💃). + +### 🛠️ 👍 🌘 🔁 ❓ + +😆 ❗ 👈 🚫 🛐 📖. + +🛠️ 🎏 🌘 🔁. & ⚫️ 👻 🔛 **🎯** 😐 👈 🔌 📚 ⌛. ↩️ 👈, ⚫️ 🛎 📚 👍 🌘 🔁 🕸 🈸 🛠️. ✋️ 🚫 🌐. + +, ⚖ 👈 👅, 🌈 📄 📏 📖: + +> 👆 ✔️ 🧹 🦏, 💩 🏠. + +*😆, 👈 🎂 📖*. + +--- + +📤 🙅‍♂ ⌛ 👶 🙆, 📚 👷 🔨, 🔛 💗 🥉 🏠. + +👆 💪 ✔️ 🔄 🍔 🖼, 🥇 🏠 🧖‍♂, ⤴️ 👨‍🍳, ✋️ 👆 🚫 ⌛ 👶 🕳, 🧹 & 🧹, 🔄 🚫🔜 📉 🕳. + +⚫️ 🔜 ✊ 🎏 💸 🕰 🏁 ⏮️ ⚖️ 🍵 🔄 (🛠️) & 👆 🔜 ✔️ ⌛ 🎏 💸 👷. + +✋️ 👉 💼, 🚥 👆 💪 ✊️ 8️⃣ 👰-🏧/🍳/🔜-🧹, & 🔠 1️⃣ 👫 (➕ 👆) 💪 ✊ 🏒 🏠 🧹 ⚫️, 👆 💪 🌐 👷 **🔗**, ⏮️ ➕ ℹ, & 🏁 🌅 🔜. + +👉 😐, 🔠 1️⃣ 🧹 (🔌 👆) 🔜 🕹, 🤸 👫 🍕 👨‍🏭. + +& 🏆 🛠️ 🕰 ✊ ☑ 👷 (↩️ ⌛), & 👷 💻 ⌛ 💽, 👫 🤙 👫 ⚠ "💽 🎁". + +--- + +⚠ 🖼 💽 🔗 🛠️ 👜 👈 🚚 🏗 🧪 🏭. + +🖼: + +* **🎧** ⚖️ **🖼 🏭**. +* **💻 👓**: 🖼 ✍ 💯 🔅, 🔠 🔅 ✔️ 3️⃣ 💲 / 🎨, 🏭 👈 🛎 🚚 💻 🕳 🔛 📚 🔅, 🌐 🎏 🕰. +* **🎰 🏫**: ⚫️ 🛎 🚚 📚 "✖" & "🖼" ✖. 💭 🦏 📋 ⏮️ 🔢 & ✖ 🌐 👫 👯‍♂️ 🎏 🕰. +* **⏬ 🏫**: 👉 🎧-🏑 🎰 🏫,, 🎏 ✔. ⚫️ 👈 📤 🚫 👁 📋 🔢 ✖, ✋️ 🦏 ⚒ 👫, & 📚 💼, 👆 ⚙️ 🎁 🕹 🏗 & / ⚖️ ⚙️ 👈 🏷. + +### 🛠️ ➕ 🔁: 🕸 ➕ 🎰 🏫 + +⏮️ **FastAPI** 👆 💪 ✊ 📈 🛠️ 👈 📶 ⚠ 🕸 🛠️ (🎏 👑 🧲 ✳). + +✋️ 👆 💪 🐄 💰 🔁 & 💾 (✔️ 💗 🛠️ 🏃‍♂ 🔗) **💽 🎁** ⚖ 💖 👈 🎰 🏫 ⚙️. + +👈, ➕ 🙅 👐 👈 🐍 👑 🇪🇸 **💽 🧪**, 🎰 🏫 & ✴️ ⏬ 🏫, ⚒ FastAPI 📶 👍 🏏 💽 🧪 / 🎰 🏫 🕸 🔗 & 🈸 (👪 📚 🎏). + +👀 ❔ 🏆 👉 🔁 🏭 👀 📄 🔃 [🛠️](deployment/index.md){.internal-link target=_blank}. + +## `async` & `await` + +🏛 ⏬ 🐍 ✔️ 📶 🏋️ 🌌 🔬 🔁 📟. 👉 ⚒ ⚫️ 👀 💖 😐 "🔁" 📟 & "⌛" 👆 ▶️️ 🙍. + +🕐❔ 📤 🛠️ 👈 🔜 🚚 ⌛ ⏭ 🤝 🏁 & ✔️ 🐕‍🦺 👉 🆕 🐍 ⚒, 👆 💪 📟 ⚫️ 💖: + +```Python +burgers = await get_burgers(2) +``` + +🔑 📥 `await`. ⚫️ 💬 🐍 👈 ⚫️ ✔️ ⌛ ⏸ `get_burgers(2)` 🏁 🔨 🚮 👜 👶 ⏭ ♻ 🏁 `burgers`. ⏮️ 👈, 🐍 🔜 💭 👈 ⚫️ 💪 🚶 & 🕳 🙆 👶 👶 👐 (💖 📨 ➕1️⃣ 📨). + +`await` 👷, ⚫️ ✔️ 🔘 🔢 👈 🐕‍🦺 👉 🔀. 👈, 👆 📣 ⚫️ ⏮️ `async def`: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...↩️ `def`: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +⏮️ `async def`, 🐍 💭 👈, 🔘 👈 🔢, ⚫️ ✔️ 🤔 `await` 🧬, & 👈 ⚫️ 💪 "⏸" ⏸ 🛠️ 👈 🔢 & 🚶 🕳 🙆 👶 ⏭ 👟 🔙. + +🕐❔ 👆 💚 🤙 `async def` 🔢, 👆 ✔️ "⌛" ⚫️. , 👉 🏆 🚫 👷: + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +, 🚥 👆 ⚙️ 🗃 👈 💬 👆 👈 👆 💪 🤙 ⚫️ ⏮️ `await`, 👆 💪 ✍ *➡ 🛠️ 🔢* 👈 ⚙️ ⚫️ ⏮️ `async def`, 💖: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 🌅 📡 ℹ + +👆 💪 ✔️ 👀 👈 `await` 💪 🕴 ⚙️ 🔘 🔢 🔬 ⏮️ `async def`. + +✋️ 🎏 🕰, 🔢 🔬 ⏮️ `async def` ✔️ "⌛". , 🔢 ⏮️ `async def` 💪 🕴 🤙 🔘 🔢 🔬 ⏮️ `async def` 💁‍♂️. + +, 🔃 🥚 & 🐔, ❔ 👆 🤙 🥇 `async` 🔢 ❓ + +🚥 👆 👷 ⏮️ **FastAPI** 👆 🚫 ✔️ 😟 🔃 👈, ↩️ 👈 "🥇" 🔢 🔜 👆 *➡ 🛠️ 🔢*, & FastAPI 🔜 💭 ❔ ▶️️ 👜. + +✋️ 🚥 👆 💚 ⚙️ `async` / `await` 🍵 FastAPI, 👆 💪 ⚫️ 👍. + +### ✍ 👆 👍 🔁 📟 + +💃 (& **FastAPI**) ⚓️ 🔛 AnyIO, ❔ ⚒ ⚫️ 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. + +🎯, 👆 💪 🔗 ⚙️ AnyIO 👆 🏧 🛠️ ⚙️ 💼 👈 🚚 🌅 🏧 ⚓ 👆 👍 📟. + +& 🚥 👆 🚫 ⚙️ FastAPI, 👆 💪 ✍ 👆 👍 🔁 🈸 ⏮️ AnyIO 🏆 🔗 & 🤚 🚮 💰 (✅ *📊 🛠️*). + +### 🎏 📨 🔁 📟 + +👉 👗 ⚙️ `async` & `await` 📶 🆕 🇪🇸. + +✋️ ⚫️ ⚒ 👷 ⏮️ 🔁 📟 📚 ⏩. + +👉 🎏 ❕ (⚖️ 🌖 🌓) 🔌 ⏳ 🏛 ⏬ 🕸 (🖥 & ✳). + +✋️ ⏭ 👈, 🚚 🔁 📟 🌖 🏗 & ⚠. + +⏮️ ⏬ 🐍, 👆 💪 ✔️ ⚙️ 🧵 ⚖️ 🐁. ✋️ 📟 🌌 🌖 🏗 🤔, ℹ, & 💭 🔃. + +⏮️ ⏬ ✳ / 🖥 🕸, 👆 🔜 ✔️ ⚙️ "⏲". ❔ ↘️ ⏲ 🔥😈. + +## 🔁 + +**🔁** 📶 🎀 ⚖ 👜 📨 `async def` 🔢. 🐍 💭 👈 ⚫️ 🕳 💖 🔢 👈 ⚫️ 💪 ▶️ & 👈 ⚫️ 🔜 🔚 ☝, ✋️ 👈 ⚫️ 5️⃣📆 ⏸ ⏸ 🔘 💁‍♂️, 🕐❔ 📤 `await` 🔘 ⚫️. + +✋️ 🌐 👉 🛠️ ⚙️ 🔁 📟 ⏮️ `async` & `await` 📚 🕰 🔬 ⚙️ "🔁". ⚫️ ⭐ 👑 🔑 ⚒ 🚶, "🔁". + +## 🏁 + +➡️ 👀 🎏 🔤 ⚪️➡️ 🔛: + +> 🏛 ⏬ 🐍 ✔️ 🐕‍🦺 **"🔁 📟"** ⚙️ 🕳 🤙 **"🔁"**, ⏮️ **`async` & `await`** ❕. + +👈 🔜 ⚒ 🌅 🔑 🔜. 👶 + +🌐 👈 ⚫️❔ 🏋️ FastAPI (🔘 💃) & ⚫️❔ ⚒ ⚫️ ✔️ ✅ 🎆 🎭. + +## 📶 📡 ℹ + +!!! warning + 👆 💪 🎲 🚶 👉. + + 👉 📶 📡 ℹ ❔ **FastAPI** 👷 🔘. + + 🚥 👆 ✔️ 📡 💡 (🈶-🏋, 🧵, 🍫, ♒️.) & 😟 🔃 ❔ FastAPI 🍵 `async def` 🆚 😐 `def`, 🚶 ⤴️. + +### ➡ 🛠️ 🔢 + +🕐❔ 👆 📣 *➡ 🛠️ 🔢* ⏮️ 😐 `def` ↩️ `async def`, ⚫️ 🏃 🔢 🧵 👈 ⤴️ ⌛, ↩️ ➖ 🤙 🔗 (⚫️ 🔜 🍫 💽). + +🚥 👆 👟 ⚪️➡️ ➕1️⃣ 🔁 🛠️ 👈 🔨 🚫 👷 🌌 🔬 🔛 & 👆 ⚙️ ⚖ 🙃 📊-🕴 *➡ 🛠️ 🔢* ⏮️ ✅ `def` 🤪 🎭 📈 (🔃 1️⃣0️⃣0️⃣ 💓), 🙏 🗒 👈 **FastAPI** ⭐ 🔜 🔄. 👫 💼, ⚫️ 👻 ⚙️ `async def` 🚥 👆 *➡ 🛠️ 🔢* ⚙️ 📟 👈 🎭 🚧 👤/🅾. + +, 👯‍♂️ ⚠, 🤞 👈 **FastAPI** 🔜 [⏩](/#performance){.internal-link target=_blank} 🌘 (⚖️ 🌘 ⭐) 👆 ⏮️ 🛠️. + +### 🔗 + +🎏 ✔ [🔗](/tutorial/dependencies/index.md){.internal-link target=_blank}. 🚥 🔗 🐩 `def` 🔢 ↩️ `async def`, ⚫️ 🏃 🔢 🧵. + +### 🎧-🔗 + +👆 💪 ✔️ 💗 🔗 & [🎧-🔗](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} 🚫 🔠 🎏 (🔢 🔢 🔑), 👫 💪 ✍ ⏮️ `async def` & ⏮️ 😐 `def`. ⚫️ 🔜 👷, & 🕐 ✍ ⏮️ 😐 `def` 🔜 🤙 🔛 🔢 🧵 (⚪️➡️ 🧵) ↩️ ➖ "⌛". + +### 🎏 🚙 🔢 + +🙆 🎏 🚙 🔢 👈 👆 🤙 🔗 💪 ✍ ⏮️ 😐 `def` ⚖️ `async def` & FastAPI 🏆 🚫 📉 🌌 👆 🤙 ⚫️. + +👉 🔅 🔢 👈 FastAPI 🤙 👆: *➡ 🛠️ 🔢* & 🔗. + +🚥 👆 🚙 🔢 😐 🔢 ⏮️ `def`, ⚫️ 🔜 🤙 🔗 (👆 ✍ ⚫️ 👆 📟), 🚫 🧵, 🚥 🔢 ✍ ⏮️ `async def` ⤴️ 👆 🔜 `await` 👈 🔢 🕐❔ 👆 🤙 ⚫️ 👆 📟. + +--- + +🔄, 👉 📶 📡 ℹ 👈 🔜 🎲 ⚠ 🚥 👆 👟 🔎 👫. + +⏪, 👆 🔜 👍 ⏮️ 📄 ⚪️➡️ 📄 🔛: 🏃 ❓. diff --git a/docs/em/docs/benchmarks.md b/docs/em/docs/benchmarks.md new file mode 100644 index 000000000..003c3f62d --- /dev/null +++ b/docs/em/docs/benchmarks.md @@ -0,0 +1,34 @@ +# 📇 + +🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) + +✋️ 🕐❔ ✅ 📇 & 🔺 👆 🔜 ✔️ 📄 🤯. + +## 📇 & 🚅 + +🕐❔ 👆 ✅ 📇, ⚫️ ⚠ 👀 📚 🧰 🎏 🆎 🔬 🌓. + +🎯, 👀 Uvicorn, 💃 & FastAPI 🔬 👯‍♂️ (👪 📚 🎏 🧰). + +🙅 ⚠ ❎ 🧰, 👍 🎭 ⚫️ 🔜 🤚. & 🏆 📇 🚫 💯 🌖 ⚒ 🚚 🧰. + +🔗 💖: + +* **Uvicorn**: 🔫 💽 + * **💃**: (⚙️ Uvicorn) 🕸 🕸 + * **FastAPI**: (⚙️ 💃) 🛠️ 🕸 ⏮️ 📚 🌖 ⚒ 🏗 🔗, ⏮️ 💽 🔬, ♒️. + +* **Uvicorn**: + * 🔜 ✔️ 🏆 🎭, ⚫️ 🚫 ✔️ 🌅 ➕ 📟 ↖️ ⚪️➡️ 💽 ⚫️. + * 👆 🚫🔜 ✍ 🈸 Uvicorn 🔗. 👈 🔜 ⛓ 👈 👆 📟 🔜 ✔️ 🔌 🌖 ⚖️ 🌘, 🌘, 🌐 📟 🚚 💃 (⚖️ **FastAPI**). & 🚥 👆 👈, 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 ✔️ ⚙️ 🛠️ & 📉 👆 📱 📟 & 🐛. + * 🚥 👆 ⚖ Uvicorn, 🔬 ⚫️ 🛡 👸, Hypercorn, ✳, ♒️. 🈸 💽. +* **💃**: + * 🔜 ✔️ ⏭ 🏆 🎭, ⏮️ Uvicorn. 👐, 💃 ⚙️ Uvicorn 🏃. , ⚫️ 🎲 💪 🕴 🤚 "🐌" 🌘 Uvicorn ✔️ 🛠️ 🌅 📟. + * ✋️ ⚫️ 🚚 👆 🧰 🏗 🙅 🕸 🈸, ⏮️ 🕹 ⚓️ 🔛 ➡, ♒️. + * 🚥 👆 ⚖ 💃, 🔬 ⚫️ 🛡 🤣, 🏺, ✳, ♒️. 🕸 🛠️ (⚖️ 🕸). +* **FastAPI**: + * 🎏 🌌 👈 💃 ⚙️ Uvicorn & 🚫🔜 ⏩ 🌘 ⚫️, **FastAPI** ⚙️ 💃, ⚫️ 🚫🔜 ⏩ 🌘 ⚫️. + * FastAPI 🚚 🌅 ⚒ 🔛 🔝 💃. ⚒ 👈 👆 🌖 🕧 💪 🕐❔ 🏗 🔗, 💖 💽 🔬 & 🛠️. & ⚙️ ⚫️, 👆 🤚 🏧 🧾 🆓 (🏧 🧾 🚫 🚮 🌥 🏃‍♂ 🈸, ⚫️ 🏗 🔛 🕴). + * 🚥 👆 🚫 ⚙️ FastAPI & ⚙️ 💃 🔗 (⚖️ ➕1️⃣ 🧰, 💖 🤣, 🏺, 🆘, ♒️) 👆 🔜 ✔️ 🛠️ 🌐 💽 🔬 & 🛠️ 👆. , 👆 🏁 🈸 🔜 ✔️ 🎏 🌥 🚥 ⚫️ 🏗 ⚙️ FastAPI. & 📚 💼, 👉 💽 🔬 & 🛠️ 🦏 💸 📟 ✍ 🈸. + * , ⚙️ FastAPI 👆 ♻ 🛠️ 🕰, 🐛, ⏸ 📟, & 👆 🔜 🎲 🤚 🎏 🎭 (⚖️ 👍) 👆 🔜 🚥 👆 🚫 ⚙️ ⚫️ (👆 🔜 ✔️ 🛠️ ⚫️ 🌐 👆 📟). + * 🚥 👆 ⚖ FastAPI, 🔬 ⚫️ 🛡 🕸 🈸 🛠️ (⚖️ ⚒ 🧰) 👈 🚚 💽 🔬, 🛠️ & 🧾, 💖 🏺-apispec, NestJS, ♨, ♒️. 🛠️ ⏮️ 🛠️ 🏧 💽 🔬, 🛠️ & 🧾. diff --git a/docs/em/docs/contributing.md b/docs/em/docs/contributing.md new file mode 100644 index 000000000..7749d27a1 --- /dev/null +++ b/docs/em/docs/contributing.md @@ -0,0 +1,465 @@ +# 🛠️ - 📉 + +🥇, 👆 💪 💚 👀 🔰 🌌 [ℹ FastAPI & 🤚 ℹ](help-fastapi.md){.internal-link target=_blank}. + +## 🛠️ + +🚥 👆 ⏪ 🖖 🗃 & 👆 💭 👈 👆 💪 ⏬ 🤿 📟, 📥 📄 ⚒ 🆙 👆 🌐. + +### 🕹 🌐 ⏮️ `venv` + +👆 💪 ✍ 🕹 🌐 📁 ⚙️ 🐍 `venv` 🕹: + +
+ +```console +$ python -m venv env +``` + +
+ +👈 🔜 ✍ 📁 `./env/` ⏮️ 🐍 💱 & ⤴️ 👆 🔜 💪 ❎ 📦 👈 ❎ 🌐. + +### 🔓 🌐 + +🔓 🆕 🌐 ⏮️: + +=== "💾, 🇸🇻" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "🚪 🎉" + + ⚖️ 🚥 👆 ⚙️ 🎉 🖥 (✅ 🐛 🎉): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +✅ ⚫️ 👷, ⚙️: + +=== "💾, 🇸🇻, 🚪 🎉" + +
+ + ```console + $ which pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +🚥 ⚫️ 🎦 `pip` 💱 `env/bin/pip` ⤴️ ⚫️ 👷. 👶 + +⚒ 💭 👆 ✔️ 📰 🐖 ⏬ 🔛 👆 🕹 🌐 ❎ ❌ 🔛 ⏭ 📶: + +
+ +```console +$ python -m pip install --upgrade pip + +---> 100% +``` + +
+ +!!! tip + 🔠 🕰 👆 ❎ 🆕 📦 ⏮️ `pip` 🔽 👈 🌐, 🔓 🌐 🔄. + + 👉 ⚒ 💭 👈 🚥 👆 ⚙️ 📶 📋 ❎ 👈 📦, 👆 ⚙️ 1️⃣ ⚪️➡️ 👆 🇧🇿 🌐 & 🚫 🙆 🎏 👈 💪 ❎ 🌐. + +### 🐖 + +⏮️ 🔓 🌐 🔬 🔛: + +
+ +```console +$ pip install -e ."[dev,doc,test]" + +---> 100% +``` + +
+ +⚫️ 🔜 ❎ 🌐 🔗 & 👆 🇧🇿 FastAPI 👆 🇧🇿 🌐. + +#### ⚙️ 👆 🇧🇿 FastAPI + +🚥 👆 ✍ 🐍 📁 👈 🗄 & ⚙️ FastAPI, & 🏃 ⚫️ ⏮️ 🐍 ⚪️➡️ 👆 🇧🇿 🌐, ⚫️ 🔜 ⚙️ 👆 🇧🇿 FastAPI ℹ 📟. + +& 🚥 👆 ℹ 👈 🇧🇿 FastAPI ℹ 📟, ⚫️ ❎ ⏮️ `-e`, 🕐❔ 👆 🏃 👈 🐍 📁 🔄, ⚫️ 🔜 ⚙️ 🍋 ⏬ FastAPI 👆 ✍. + +👈 🌌, 👆 🚫 ✔️ "❎" 👆 🇧🇿 ⏬ 💪 💯 🔠 🔀. + +### 📁 + +📤 ✍ 👈 👆 💪 🏃 👈 🔜 📁 & 🧹 🌐 👆 📟: + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +⚫️ 🔜 🚘-😇 🌐 👆 🗄. + +⚫️ 😇 👫 ☑, 👆 💪 ✔️ FastAPI ❎ 🌐 👆 🌐, ⏮️ 📋 📄 🔛 ⚙️ `-e`. + +## 🩺 + +🥇, ⚒ 💭 👆 ⚒ 🆙 👆 🌐 🔬 🔛, 👈 🔜 ❎ 🌐 📄. + +🧾 ⚙️ . + +& 📤 ➕ 🧰/✍ 🥉 🍵 ✍ `./scripts/docs.py`. + +!!! tip + 👆 🚫 💪 👀 📟 `./scripts/docs.py`, 👆 ⚙️ ⚫️ 📋 ⏸. + +🌐 🧾 ✍ 📁 📁 `./docs/en/`. + +📚 🔰 ✔️ 🍫 📟. + +🌅 💼, 👫 🍫 📟 ☑ 🏁 🈸 👈 💪 🏃. + +👐, 👈 🍫 📟 🚫 ✍ 🔘 ✍, 👫 🐍 📁 `./docs_src/` 📁. + +& 👈 🐍 📁 🔌/💉 🧾 🕐❔ 🏭 🕸. + +### 🩺 💯 + +🏆 💯 🤙 🏃 🛡 🖼 ℹ 📁 🧾. + +👉 ℹ ⚒ 💭 👈: + +* 🧾 🆙 📅. +* 🧾 🖼 💪 🏃. +* 🌅 ⚒ 📔 🧾, 🚚 💯 💰. + +⏮️ 🇧🇿 🛠️, 📤 ✍ 👈 🏗 🕸 & ✅ 🙆 🔀, 🖖-🔫: + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +⚫️ 🔜 🍦 🧾 🔛 `http://127.0.0.1:8008`. + +👈 🌌, 👆 💪 ✍ 🧾/ℹ 📁 & 👀 🔀 🖖. + +#### 🏎 ✳ (📦) + +👩‍🌾 📥 🎦 👆 ❔ ⚙️ ✍ `./scripts/docs.py` ⏮️ `python` 📋 🔗. + +✋️ 👆 💪 ⚙️ 🏎 ✳, & 👆 🔜 🤚 ✍ 👆 📶 📋 ⏮️ ❎ 🛠️. + +🚥 👆 ❎ 🏎 ✳, 👆 💪 ❎ 🛠️ ⏮️: + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### 📱 & 🩺 🎏 🕰 + +🚥 👆 🏃 🖼 ⏮️, ✅: + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Uvicorn 🔢 🔜 ⚙️ ⛴ `8000`, 🧾 🔛 ⛴ `8008` 🏆 🚫 ⚔. + +### ✍ + +ℹ ⏮️ ✍ 📶 🌅 👍 ❗ & ⚫️ 💪 🚫 🔨 🍵 ℹ ⚪️➡️ 👪. 👶 👶 + +📥 📶 ℹ ⏮️ ✍. + +#### 💁‍♂ & 📄 + +* ✅ ⏳ ♻ 🚲 📨 👆 🇪🇸 & 🚮 📄 ✔ 🔀 ⚖️ ✔ 👫. + +!!! tip + 👆 💪 🚮 🏤 ⏮️ 🔀 🔑 ♻ 🚲 📨. + + ✅ 🩺 🔃 ❎ 🚲 📨 📄 ✔ ⚫️ ⚖️ 📨 🔀. + +* ✅ 👀 🚥 📤 1️⃣ 🛠️ ✍ 👆 🇪🇸. + +* 🚮 👁 🚲 📨 📍 📃 💬. 👈 🔜 ⚒ ⚫️ 🌅 ⏩ 🎏 📄 ⚫️. + +🇪🇸 👤 🚫 💬, 👤 🔜 ⌛ 📚 🎏 📄 ✍ ⏭ 🔗. + +* 👆 💪 ✅ 🚥 📤 ✍ 👆 🇪🇸 & 🚮 📄 👫, 👈 🔜 ℹ 👤 💭 👈 ✍ ☑ & 👤 💪 🔗 ⚫️. + +* ⚙️ 🎏 🐍 🖼 & 🕴 💬 ✍ 🩺. 👆 🚫 ✔️ 🔀 🕳 👉 👷. + +* ⚙️ 🎏 🖼, 📁 📛, & 🔗. 👆 🚫 ✔️ 🔀 🕳 ⚫️ 👷. + +* ✅ 2️⃣-🔤 📟 🇪🇸 👆 💚 💬 👆 💪 ⚙️ 🏓 📇 💾 6️⃣3️⃣9️⃣-1️⃣ 📟. + +#### ♻ 🇪🇸 + +➡️ 💬 👆 💚 💬 📃 🇪🇸 👈 ⏪ ✔️ ✍ 📃, 💖 🇪🇸. + +💼 🇪🇸, 2️⃣-🔤 📟 `es`. , 📁 🇪🇸 ✍ 🔎 `docs/es/`. + +!!! tip + 👑 ("🛂") 🇪🇸 🇪🇸, 🔎 `docs/en/`. + +🔜 🏃 🖖 💽 🩺 🇪🇸: + +
+ +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +🔜 👆 💪 🚶 http://127.0.0.1:8008 & 👀 👆 🔀 🖖. + +🚥 👆 👀 FastAPI 🩺 🕸, 👆 🔜 👀 👈 🔠 🇪🇸 ✔️ 🌐 📃. ✋️ 📃 🚫 💬 & ✔️ 📨 🔃 ❌ ✍. + +✋️ 🕐❔ 👆 🏃 ⚫️ 🌐 💖 👉, 👆 🔜 🕴 👀 📃 👈 ⏪ 💬. + +🔜 ➡️ 💬 👈 👆 💚 🚮 ✍ 📄 [⚒](features.md){.internal-link target=_blank}. + +* 📁 📁: + +``` +docs/en/docs/features.md +``` + +* 📋 ⚫️ ⚫️❔ 🎏 🗺 ✋️ 🇪🇸 👆 💚 💬, ✅: + +``` +docs/es/docs/features.md +``` + +!!! tip + 👀 👈 🕴 🔀 ➡ & 📁 📛 🇪🇸 📟, ⚪️➡️ `en` `es`. + +* 🔜 📂 ⬜ 📁 📁 🇪🇸: + +``` +docs/en/mkdocs.yml +``` + +* 🔎 🥉 🌐❔ 👈 `docs/features.md` 🔎 📁 📁. 👱 💖: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* 📂 ⬜ 📁 📁 🇪🇸 👆 ✍, ✅: + +``` +docs/es/mkdocs.yml +``` + +* 🚮 ⚫️ 📤 ☑ 🎏 🗺 ⚫️ 🇪🇸, ✅: + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +⚒ 💭 👈 🚥 📤 🎏 ⛔, 🆕 ⛔ ⏮️ 👆 ✍ ⚫️❔ 🎏 ✔ 🇪🇸 ⏬. + +🚥 👆 🚶 👆 🖥 👆 🔜 👀 👈 🔜 🩺 🎦 👆 🆕 📄. 👶 + +🔜 👆 💪 💬 ⚫️ 🌐 & 👀 ❔ ⚫️ 👀 👆 🖊 📁. + +#### 🆕 🇪🇸 + +➡️ 💬 👈 👆 💚 🚮 ✍ 🇪🇸 👈 🚫 💬, 🚫 📃. + +➡️ 💬 👆 💚 🚮 ✍ 🇭🇹, & ⚫️ 🚫 📤 🩺. + +✅ 🔗 ⚪️➡️ 🔛, 📟 "🇭🇹" `ht`. + +⏭ 🔁 🏃 ✍ 🏗 🆕 ✍ 📁: + +
+ +```console +// Use the command new-lang, pass the language code as a CLI argument +$ python ./scripts/docs.py new-lang ht + +Successfully initialized: docs/ht +Updating ht +Updating en +``` + +
+ +🔜 👆 💪 ✅ 👆 📟 👨‍🎨 ⏳ ✍ 📁 `docs/ht/`. + +!!! tip + ✍ 🥇 🚲 📨 ⏮️ 👉, ⚒ 🆙 📳 🆕 🇪🇸, ⏭ ❎ ✍. + + 👈 🌌 🎏 💪 ℹ ⏮️ 🎏 📃 ⏪ 👆 👷 🔛 🥇 🕐. 👶 + +▶️ ✍ 👑 📃, `docs/ht/index.md`. + +⤴️ 👆 💪 😣 ⏮️ ⏮️ 👩‍🌾, "♻ 🇪🇸". + +##### 🆕 🇪🇸 🚫 🐕‍🦺 + +🚥 🕐❔ 🏃‍♂ 🖖 💽 ✍ 👆 🤚 ❌ 🔃 🇪🇸 🚫 ➖ 🐕‍🦺, 🕳 💖: + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +👈 ⛓ 👈 🎢 🚫 🐕‍🦺 👈 🇪🇸 (👉 💼, ⏮️ ❌ 2️⃣-🔤 📟 `xx`). + +✋️ 🚫 😟, 👆 💪 ⚒ 🎢 🇪🇸 🇪🇸 & ⤴️ 💬 🎚 🩺. + +🚥 👆 💪 👈, ✍ `mkdocs.yml` 👆 🆕 🇪🇸, ⚫️ 🔜 ✔️ 🕳 💖: + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +🔀 👈 🇪🇸 ⚪️➡️ `xx` (⚪️➡️ 👆 🇪🇸 📟) `en`. + +⤴️ 👆 💪 ▶️ 🖖 💽 🔄. + +#### 🎮 🏁 + +🕐❔ 👆 ⚙️ ✍ `./scripts/docs.py` ⏮️ `live` 📋 ⚫️ 🕴 🎦 📁 & ✍ 💪 ⏮️ 🇪🇸. + +✋️ 🕐 👆 🔨, 👆 💪 💯 ⚫️ 🌐 ⚫️ 🔜 👀 💳. + +👈, 🥇 🏗 🌐 🩺: + +
+ +```console +// Use the command "build-all", this will take a bit +$ python ./scripts/docs.py build-all + +Updating es +Updating en +Building docs for: en +Building docs for: es +Successfully built docs for: es +Copying en index.md to README.md +``` + +
+ +👈 🏗 🌐 🩺 `./docs_build/` 🔠 🇪🇸. 👉 🔌 ❎ 🙆 📁 ⏮️ ❌ ✍, ⏮️ 🗒 💬 👈 "👉 📁 🚫 ✔️ ✍". ✋️ 👆 🚫 ✔️ 🕳 ⏮️ 👈 📁. + +⤴️ ⚫️ 🏗 🌐 👈 🔬 ⬜ 🕸 🔠 🇪🇸, 🌀 👫, & 🏗 🏁 🔢 `./site/`. + +⤴️ 👆 💪 🍦 👈 ⏮️ 📋 `serve`: + +
+ +```console +// Use the command "serve" after running "build-all" +$ python ./scripts/docs.py serve + +Warning: this is a very simple server. For development, use mkdocs serve instead. +This is here only to preview a site with translations already built. +Make sure you run the build-all command first. +Serving at: http://127.0.0.1:8008 +``` + +
+ +## 💯 + +📤 ✍ 👈 👆 💪 🏃 🌐 💯 🌐 📟 & 🏗 💰 📄 🕸: + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +👉 📋 🏗 📁 `./htmlcov/`, 🚥 👆 📂 📁 `./htmlcov/index.html` 👆 🖥, 👆 💪 🔬 🖥 🇹🇼 📟 👈 📔 💯, & 👀 🚥 📤 🙆 🇹🇼 ❌. diff --git a/docs/em/docs/deployment/concepts.md b/docs/em/docs/deployment/concepts.md new file mode 100644 index 000000000..8ce775411 --- /dev/null +++ b/docs/em/docs/deployment/concepts.md @@ -0,0 +1,311 @@ +# 🛠️ 🔧 + +🕐❔ 🛠️ **FastAPI** 🈸, ⚖️ 🤙, 🙆 🆎 🕸 🛠️, 📤 📚 🔧 👈 👆 🎲 💅 🔃, & ⚙️ 👫 👆 💪 🔎 **🏆 ☑** 🌌 **🛠️ 👆 🈸**. + +⚠ 🔧: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +👥 🔜 👀 ❔ 👫 🔜 📉 **🛠️**. + +🔚, 🏆 🎯 💪 **🍦 👆 🛠️ 👩‍💻** 🌌 👈 **🔐**, **❎ 📉**, & ⚙️ **📊 ℹ** (🖼 🛰 💽/🕹 🎰) ♻ 💪. 👶 + +👤 🔜 💬 👆 🍖 🌖 🔃 👫 **🔧** 📥, & 👈 🔜 🤞 🤝 👆 **🤔** 👆 🔜 💪 💭 ❔ 🛠️ 👆 🛠️ 📶 🎏 🌐, 🎲 **🔮** 🕐 👈 🚫 🔀. + +🤔 👫 🔧, 👆 🔜 💪 **🔬 & 🔧** 🏆 🌌 🛠️ **👆 👍 🔗**. + +⏭ 📃, 👤 🔜 🤝 👆 🌅 **🧱 🍮** 🛠️ FastAPI 🈸. + +✋️ 🔜, ➡️ ✅ 👉 ⚠ **⚛ 💭**. 👫 🔧 ✔ 🙆 🎏 🆎 🕸 🛠️. 👶 + +## 💂‍♂ - 🇺🇸🔍 + +[⏮️ 📃 🔃 🇺🇸🔍](./https.md){.internal-link target=_blank} 👥 🇭🇲 🔃 ❔ 🇺🇸🔍 🚚 🔐 👆 🛠️. + +👥 👀 👈 🇺🇸🔍 🛎 🚚 🦲 **🔢** 👆 🈸 💽, **🤝 ❎ 🗳**. + +& 📤 ✔️ 🕳 🈚 **♻ 🇺🇸🔍 📄**, ⚫️ 💪 🎏 🦲 ⚖️ ⚫️ 💪 🕳 🎏. + +### 🖼 🧰 🇺🇸🔍 + +🧰 👆 💪 ⚙️ 🤝 ❎ 🗳: + +* Traefik + * 🔁 🍵 📄 🔕 👶 +* 📥 + * 🔁 🍵 📄 🔕 👶 +* 👌 + * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 +* ✳ + * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 +* Kubernete ⏮️ 🚧 🕹 💖 👌 + * ⏮️ 🔢 🦲 💖 🛂-👨‍💼 📄 🔕 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 (✍ 🔛 👶) + +➕1️⃣ 🎛 👈 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🌖 👷 ✅ ⚒ 🆙 🇺🇸🔍. ⚫️ 💪 ✔️ 🚫 ⚖️ 🈚 👆 🌅, ♒️. ✋️ 👈 💼, 👆 🚫🔜 ✔️ ⚒ 🆙 🤝 ❎ 🗳 👆. + +👤 🔜 🎦 👆 🧱 🖼 ⏭ 📃. + +--- + +⤴️ ⏭ 🔧 🤔 🌐 🔃 📋 🏃 👆 ☑ 🛠️ (✅ Uvicorn). + +## 📋 & 🛠️ + +👥 🔜 💬 📚 🔃 🏃 "**🛠️**", ⚫️ ⚠ ✔️ ☯ 🔃 ⚫️❔ ⚫️ ⛓, & ⚫️❔ 🔺 ⏮️ 🔤 "**📋**". + +### ⚫️❔ 📋 + +🔤 **📋** 🛎 ⚙️ 🔬 📚 👜: + +* **📟** 👈 👆 ✍, **🐍 📁**. +* **📁** 👈 💪 **🛠️** 🏃‍♂ ⚙️, 🖼: `python`, `python.exe` ⚖️ `uvicorn`. +* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏗 ⚙️, ⚙️ 💽, & ♻ 👜 🔛 💾. 👉 🤙 **🛠️**. + +### ⚫️❔ 🛠️ + +🔤 **🛠️** 🛎 ⚙️ 🌖 🎯 🌌, 🕴 🔗 👜 👈 🏃 🏃‍♂ ⚙️ (💖 🏁 ☝ 🔛): + +* 🎯 📋 ⏪ ⚫️ **🏃‍♂** 🔛 🏃‍♂ ⚙️. + * 👉 🚫 🔗 📁, 🚫 📟, ⚫️ 🔗 **🎯** 👜 👈 ➖ **🛠️** & 🔄 🏃‍♂ ⚙️. +* 🙆 📋, 🙆 📟, **💪 🕴 👜** 🕐❔ ⚫️ ➖ **🛠️**. , 🕐❔ 📤 **🛠️ 🏃**. +* 🛠️ 💪 **❎** (⚖️ "💥") 👆, ⚖️ 🏃‍♂ ⚙️. 👈 ☝, ⚫️ ⛔️ 🏃/➖ 🛠️, & ⚫️ 💪 **🙅‍♂ 📏 👜**. +* 🔠 🈸 👈 👆 ✔️ 🏃 🔛 👆 💻 ✔️ 🛠️ ⛅ ⚫️, 🔠 🏃‍♂ 📋, 🔠 🚪, ♒️. & 📤 🛎 📚 🛠️ 🏃 **🎏 🕰** ⏪ 💻 🔛. +* 📤 💪 **💗 🛠️** **🎏 📋** 🏃 🎏 🕰. + +🚥 👆 ✅ 👅 "📋 👨‍💼" ⚖️ "⚙️ 🖥" (⚖️ 🎏 🧰) 👆 🏃‍♂ ⚙️, 👆 🔜 💪 👀 📚 👈 🛠️ 🏃‍♂. + +& , 🖼, 👆 🔜 🎲 👀 👈 📤 💗 🛠️ 🏃 🎏 🖥 📋 (🦎, 💄, 📐, ♒️). 👫 🛎 🏃 1️⃣ 🛠️ 📍 📑, ➕ 🎏 ➕ 🛠️. + + + +--- + +🔜 👈 👥 💭 🔺 🖖 ⚖ **🛠️** & **📋**, ➡️ 😣 💬 🔃 🛠️. + +## 🏃‍♂ 🔛 🕴 + +🌅 💼, 🕐❔ 👆 ✍ 🕸 🛠️, 👆 💚 ⚫️ **🕧 🏃‍♂**, ➡, 👈 👆 👩‍💻 💪 🕧 🔐 ⚫️. 👉 ↗️, 🚥 👆 ✔️ 🎯 🤔 ⚫️❔ 👆 💚 ⚫️ 🏃 🕴 🎯 ⚠, ✋️ 🌅 🕰 👆 💚 ⚫️ 🕧 🏃‍♂ & **💪**. + +### 🛰 💽 + +🕐❔ 👆 ⚒ 🆙 🛰 💽 (☁ 💽, 🕹 🎰, ♒️.) 🙅 👜 👆 💪 🏃 Uvicorn (⚖️ 🎏) ❎, 🎏 🌌 👆 🕐❔ 🛠️ 🌐. + +& ⚫️ 🔜 👷 & 🔜 ⚠ **⏮️ 🛠️**. + +✋️ 🚥 👆 🔗 💽 💸, **🏃‍♂ 🛠️** 🔜 🎲 ☠️. + +& 🚥 💽 ⏏ (🖼 ⏮️ ℹ, ⚖️ 🛠️ ⚪️➡️ ☁ 🐕‍🦺) 👆 🎲 **🏆 🚫 👀 ⚫️**. & ↩️ 👈, 👆 🏆 🚫 💭 👈 👆 ✔️ ⏏ 🛠️ ❎. , 👆 🛠️ 🔜 🚧 ☠️. 👶 + +### 🏃 🔁 🔛 🕴 + +🏢, 👆 🔜 🎲 💚 💽 📋 (✅ Uvicorn) ▶️ 🔁 🔛 💽 🕴, & 🍵 💪 🙆 **🗿 🏥**, ✔️ 🛠️ 🕧 🏃 ⏮️ 👆 🛠️ (✅ Uvicorn 🏃‍♂ 👆 FastAPI 📱). + +### 🎏 📋 + +🏆 👉, 👆 🔜 🛎 ✔️ **🎏 📋** 👈 🔜 ⚒ 💭 👆 🈸 🏃 🔛 🕴. & 📚 💼, ⚫️ 🔜 ⚒ 💭 🎏 🦲 ⚖️ 🈸 🏃, 🖼, 💽. + +### 🖼 🧰 🏃 🕴 + +🖼 🧰 👈 💪 👉 👨‍🏭: + +* ☁ +* Kubernete +* ☁ ✍ +* ☁ 🐝 📳 +* ✳ +* 👨‍💻 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 +* 🎏... + +👤 🔜 🤝 👆 🌅 🧱 🖼 ⏭ 📃. + +## ⏏ + +🎏 ⚒ 💭 👆 🈸 🏃 🔛 🕴, 👆 🎲 💚 ⚒ 💭 ⚫️ **⏏** ⏮️ ❌. + +### 👥 ⚒ ❌ + +👥, 🗿, ⚒ **❌**, 🌐 🕰. 🖥 🌖 *🕧* ✔️ **🐛** 🕵‍♂ 🎏 🥉. 👶 + +& 👥 👩‍💻 🚧 📉 📟 👥 🔎 👈 🐛 & 👥 🛠️ 🆕 ⚒ (🎲 ❎ 🆕 🐛 💁‍♂️ 👶). + +### 🤪 ❌ 🔁 🍵 + +🕐❔ 🏗 🕸 🔗 ⏮️ FastAPI, 🚥 📤 ❌ 👆 📟, FastAPI 🔜 🛎 🔌 ⚫️ 👁 📨 👈 ⏲ ❌. 🛡 + +👩‍💻 🔜 🤚 **5️⃣0️⃣0️⃣ 🔗 💽 ❌** 👈 📨, ✋️ 🈸 🔜 😣 👷 ⏭ 📨 ↩️ 💥 🍕. + +### 🦏 ❌ - 💥 + +👐, 📤 5️⃣📆 💼 🌐❔ 👥 ✍ 📟 👈 **💥 🎂 🈸** ⚒ Uvicorn & 🐍 💥. 👶 + +& , 👆 🔜 🎲 🚫 💚 🈸 🚧 ☠️ ↩️ 📤 ❌ 1️⃣ 🥉, 👆 🎲 💚 ⚫️ **😣 🏃** 🌘 *➡ 🛠️* 👈 🚫 💔. + +### ⏏ ⏮️ 💥 + +✋️ 👈 💼 ⏮️ 🤙 👎 ❌ 👈 💥 🏃‍♂ **🛠️**, 👆 🔜 💚 🔢 🦲 👈 🈚 **🔁** 🛠️, 🌘 👩‍❤‍👨 🕰... + +!!! tip + ...👐 🚥 🎂 🈸 **💥 ⏪** ⚫️ 🎲 🚫 ⚒ 🔑 🚧 🔁 ⚫️ ♾. ✋️ 📚 💼, 👆 🔜 🎲 👀 ⚫️ ⏮️ 🛠️, ⚖️ 🌘 ▶️️ ⏮️ 🛠️. + + ➡️ 🎯 🔛 👑 💼, 🌐❔ ⚫️ 💪 💥 🍕 🎯 💼 **🔮**, & ⚫️ ⚒ 🔑 ⏏ ⚫️. + +👆 🔜 🎲 💚 ✔️ 👜 🈚 🔁 👆 🈸 **🔢 🦲**, ↩️ 👈 ☝, 🎏 🈸 ⏮️ Uvicorn & 🐍 ⏪ 💥, 📤 🕳 🎏 📟 🎏 📱 👈 💪 🕳 🔃 ⚫️. + +### 🖼 🧰 ⏏ 🔁 + +🏆 💼, 🎏 🧰 👈 ⚙️ **🏃 📋 🔛 🕴** ⚙️ 🍵 🏧 **⏏**. + +🖼, 👉 💪 🍵: + +* ☁ +* Kubernete +* ☁ ✍ +* ☁ 🐝 📳 +* ✳ +* 👨‍💻 +* 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 +* 🎏... + +## 🧬 - 🛠️ & 💾 + +⏮️ FastAPI 🈸, ⚙️ 💽 📋 💖 Uvicorn, 🏃‍♂ ⚫️ 🕐 **1️⃣ 🛠️** 💪 🍦 💗 👩‍💻 🔁. + +✋️ 📚 💼, 👆 🔜 💚 🏃 📚 👨‍🏭 🛠️ 🎏 🕰. + +### 💗 🛠️ - 👨‍🏭 + +🚥 👆 ✔️ 🌅 👩‍💻 🌘 ⚫️❔ 👁 🛠️ 💪 🍵 (🖼 🚥 🕹 🎰 🚫 💁‍♂️ 🦏) & 👆 ✔️ **💗 🐚** 💽 💽, ⤴️ 👆 💪 ✔️ **💗 🛠️** 🏃‍♂ ⏮️ 🎏 🈸 🎏 🕰, & 📎 🌐 📨 👪 👫. + +🕐❔ 👆 🏃 **💗 🛠️** 🎏 🛠️ 📋, 👫 🛎 🤙 **👨‍🏭**. + +### 👨‍🏭 🛠️ & ⛴ + +💭 ⚪️➡️ 🩺 [🔃 🇺🇸🔍](./https.md){.internal-link target=_blank} 👈 🕴 1️⃣ 🛠️ 💪 👂 🔛 1️⃣ 🌀 ⛴ & 📢 📢 💽 ❓ + +👉 ☑. + +, 💪 ✔️ **💗 🛠️** 🎏 🕰, 📤 ✔️ **👁 🛠️ 👂 🔛 ⛴** 👈 ⤴️ 📶 📻 🔠 👨‍🏭 🛠️ 🌌. + +### 💾 📍 🛠️ + +🔜, 🕐❔ 📋 📐 👜 💾, 🖼, 🎰 🏫 🏷 🔢, ⚖️ 🎚 ⭕ 📁 🔢, 🌐 👈 **🍴 👄 💾 (💾)** 💽. + +& 💗 🛠️ 🛎 **🚫 💰 🙆 💾**. 👉 ⛓ 👈 🔠 🏃 🛠️ ✔️ 🚮 👍 👜, 🔢, & 💾. & 🚥 👆 😩 ⭕ 💸 💾 👆 📟, **🔠 🛠️** 🔜 🍴 🌓 💸 💾. + +### 💽 💾 + +🖼, 🚥 👆 📟 📐 🎰 🏫 🏷 ⏮️ **1️⃣ 💾 📐**, 🕐❔ 👆 🏃 1️⃣ 🛠️ ⏮️ 👆 🛠️, ⚫️ 🔜 🍴 🌘 1️⃣ 💾 💾. & 🚥 👆 ▶️ **4️⃣ 🛠️** (4️⃣ 👨‍🏭), 🔠 🔜 🍴 1️⃣ 💾 💾. 🌐, 👆 🛠️ 🔜 🍴 **4️⃣ 💾 💾**. + +& 🚥 👆 🛰 💽 ⚖️ 🕹 🎰 🕴 ✔️ 3️⃣ 💾 💾, 🔄 📐 🌅 🌘 4️⃣ 💾 💾 🔜 🤕 ⚠. 👶 + +### 💗 🛠️ - 🖼 + +👉 🖼, 📤 **👨‍💼 🛠️** 👈 ▶️ & 🎛 2️⃣ **👨‍🏭 🛠️**. + +👉 👨‍💼 🛠️ 🔜 🎲 1️⃣ 👂 🔛 **⛴** 📢. & ⚫️ 🔜 📶 🌐 📻 👨‍🏭 🛠️. + +👈 👨‍🏭 🛠️ 🔜 🕐 🏃‍♂ 👆 🈸, 👫 🔜 🎭 👑 📊 📨 **📨** & 📨 **📨**, & 👫 🔜 📐 🕳 👆 🚮 🔢 💾. + + + +& ↗️, 🎏 🎰 🔜 🎲 ✔️ **🎏 🛠️** 🏃 👍, ↖️ ⚪️➡️ 👆 🈸. + +😌 ℹ 👈 🌐 **💽 ⚙️** 🔠 🛠️ 💪 **🪀** 📚 🤭 🕰, ✋️ **💾 (💾)** 🛎 🚧 🌖 ⚖️ 🌘 **⚖**. + +🚥 👆 ✔️ 🛠️ 👈 🔨 ⭐ 💸 📊 🔠 🕰 & 👆 ✔️ 📚 👩‍💻, ⤴️ **💽 🛠️** 🔜 🎲 *⚖* (↩️ 🕧 🔜 🆙 & 🔽 🔜). + +### 🖼 🧬 🧰 & 🎛 + +📤 💪 📚 🎯 🏆 👉, & 👤 🔜 💬 👆 🌅 🔃 🎯 🎛 ⏭ 📃, 🖼 🕐❔ 💬 🔃 ☁ & 📦. + +👑 ⚛ 🤔 👈 📤 ✔️ **👁** 🦲 🚚 **⛴** **📢 📢**. & ⤴️ ⚫️ ✔️ ✔️ 🌌 **📶** 📻 🔁 **🛠️/👨‍🏭**. + +📥 💪 🌀 & 🎛: + +* **🐁** 🛠️ **Uvicorn 👨‍🏭** + * 🐁 🔜 **🛠️ 👨‍💼** 👂 🔛 **📢** & **⛴**, 🧬 🔜 ✔️ **💗 Uvicorn 👨‍🏭 🛠️** +* **Uvicorn** 🛠️ **Uvicorn 👨‍🏭** + * 1️⃣ Uvicorn **🛠️ 👨‍💼** 🔜 👂 🔛 **📢** & **⛴**, & ⚫️ 🔜 ▶️ **💗 Uvicorn 👨‍🏭 🛠️** +* **Kubernete** & 🎏 📎 **📦 ⚙️** + * 🕳 **☁** 🧽 🔜 👂 🔛 **📢** & **⛴**. 🧬 🔜 ✔️ **💗 📦**, 🔠 ⏮️ **1️⃣ Uvicorn 🛠️** 🏃‍♂ +* **☁ 🐕‍🦺** 👈 🍵 👉 👆 + * ☁ 🐕‍🦺 🔜 🎲 **🍵 🧬 👆**. ⚫️ 🔜 🎲 ➡️ 👆 🔬 **🛠️ 🏃**, ⚖️ **📦 🖼** ⚙️, 🙆 💼, ⚫️ 🔜 🌅 🎲 **👁 Uvicorn 🛠️**, & ☁ 🐕‍🦺 🔜 🈚 🔁 ⚫️. + +!!! tip + 🚫 😟 🚥 👫 🏬 🔃 **📦**, ☁, ⚖️ Kubernete 🚫 ⚒ 📚 🔑. + + 👤 🔜 💬 👆 🌅 🔃 📦 🖼, ☁, Kubernete, ♒️. 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + +## ⏮️ 🔁 ⏭ ▶️ + +📤 📚 💼 🌐❔ 👆 💚 🎭 📶 **⏭ ▶️** 👆 🈸. + +🖼, 👆 💪 💚 🏃 **💽 🛠️**. + +✋️ 🌅 💼, 👆 🔜 💚 🎭 👉 🔁 🕴 **🕐**. + +, 👆 🔜 💚 ✔️ **👁 🛠️** 🎭 👈 **⏮️ 🔁**, ⏭ ▶️ 🈸. + +& 👆 🔜 ✔️ ⚒ 💭 👈 ⚫️ 👁 🛠️ 🏃 👈 ⏮️ 🔁 ** 🚥 ⏮️, 👆 ▶️ **💗 🛠️** (💗 👨‍🏭) 🈸 ⚫️. 🚥 👈 🔁 🏃 **💗 🛠️**, 👫 🔜 **❎** 👷 🏃‍♂ ⚫️ 🔛 **🔗**, & 🚥 📶 🕳 💎 💖 💽 🛠️, 👫 💪 🤕 ⚔ ⏮️ 🔠 🎏. + +↗️, 📤 💼 🌐❔ 📤 🙅‍♂ ⚠ 🏃 ⏮️ 🔁 💗 🕰, 👈 💼, ⚫️ 📚 ⏩ 🍵. + +!!! tip + , ✔️ 🤯 👈 ⚓️ 🔛 👆 🖥, 💼 👆 **5️⃣📆 🚫 💪 🙆 ⏮️ 🔁** ⏭ ▶️ 👆 🈸. + + 👈 💼, 👆 🚫🔜 ✔️ 😟 🔃 🙆 👉. 🤷 + +### 🖼 ⏮️ 🔁 🎛 + +👉 🔜 **🪀 🙇** 🔛 🌌 👆 **🛠️ 👆 ⚙️**, & ⚫️ 🔜 🎲 🔗 🌌 👆 ▶️ 📋, 🚚 ⏏, ♒️. + +📥 💪 💭: + +* "🕑 📦" Kubernete 👈 🏃 ⏭ 👆 📱 📦 +* 🎉 ✍ 👈 🏃 ⏮️ 🔁 & ⤴️ ▶️ 👆 🈸 + * 👆 🔜 💪 🌌 ▶️/⏏ *👈* 🎉 ✍, 🔍 ❌, ♒️. + +!!! tip + 👤 🔜 🤝 👆 🌅 🧱 🖼 🔨 👉 ⏮️ 📦 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + +## ℹ 🛠️ + +👆 💽(Ⓜ) () **ℹ**, 👆 💪 🍴 ⚖️ **⚙️**, ⏮️ 👆 📋, 📊 🕰 🔛 💽, & 💾 💾 💪. + +❔ 🌅 ⚙️ ℹ 👆 💚 😩/♻ ❓ ⚫️ 💪 ⏩ 💭 "🚫 🌅", ✋️ 🌌, 👆 🔜 🎲 💚 🍴 **🌅 💪 🍵 💥**. + +🚥 👆 💸 3️⃣ 💽 ✋️ 👆 ⚙️ 🕴 🐥 🍖 👫 💾 & 💽, 👆 🎲 **🗑 💸** 👶, & 🎲 **🗑 💽 🔦 🏋️** 👶, ♒️. + +👈 💼, ⚫️ 💪 👻 ✔️ 🕴 2️⃣ 💽 & ⚙️ ↕ 🌐 👫 ℹ (💽, 💾, 💾, 🕸 💿, ♒️). + +🔛 🎏 ✋, 🚥 👆 ✔️ 2️⃣ 💽 & 👆 ⚙️ **1️⃣0️⃣0️⃣ 💯 👫 💽 & 💾**, ☝ 1️⃣ 🛠️ 🔜 💭 🌅 💾, & 💽 🔜 ✔️ ⚙️ 💾 "💾" (❔ 💪 💯 🕰 🐌), ⚖️ **💥**. ⚖️ 1️⃣ 🛠️ 💪 💪 📊 & 🔜 ✔️ ⌛ ⏭ 💽 🆓 🔄. + +👉 💼, ⚫️ 🔜 👍 🤚 **1️⃣ ➕ 💽** & 🏃 🛠️ 🔛 ⚫️ 👈 👫 🌐 ✔️ **🥃 💾 & 💽 🕰**. + +📤 🤞 👈 🤔 👆 ✔️ **🌵** ⚙️ 👆 🛠️. 🎲 ⚫️ 🚶 🦠, ⚖️ 🎲 🎏 🐕‍🦺 ⚖️ 🤖 ▶️ ⚙️ ⚫️. & 👆 💪 💚 ✔️ ➕ ℹ 🔒 👈 💼. + +👆 💪 🚮 **❌ 🔢** 🎯, 🖼, 🕳 **🖖 5️⃣0️⃣ 💯 9️⃣0️⃣ 💯** ℹ 🛠️. ☝ 👈 📚 🎲 👑 👜 👆 🔜 💚 ⚖ & ⚙️ ⚒ 👆 🛠️. + +👆 💪 ⚙️ 🙅 🧰 💖 `htop` 👀 💽 & 💾 ⚙️ 👆 💽 ⚖️ 💸 ⚙️ 🔠 🛠️. ⚖️ 👆 💪 ⚙️ 🌖 🏗 ⚖ 🧰, ❔ 5️⃣📆 📎 🤭 💽, ♒️. + +## 🌃 + +👆 ✔️ 👂 📥 👑 🔧 👈 👆 🔜 🎲 💪 ✔️ 🤯 🕐❔ 🤔 ❔ 🛠️ 👆 🈸: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🤔 👉 💭 & ❔ ✔ 👫 🔜 🤝 👆 🤔 💪 ✊ 🙆 🚫 🕐❔ 🛠️ & 🛠️ 👆 🛠️. 👶 + +⏭ 📄, 👤 🔜 🤝 👆 🌅 🧱 🖼 💪 🎛 👆 💪 ⏩. 👶 diff --git a/docs/em/docs/deployment/deta.md b/docs/em/docs/deployment/deta.md new file mode 100644 index 000000000..89b6c4bdb --- /dev/null +++ b/docs/em/docs/deployment/deta.md @@ -0,0 +1,258 @@ +# 🛠️ FastAPI 🔛 🪔 + +👉 📄 👆 🔜 💡 ❔ 💪 🛠️ **FastAPI** 🈸 🔛 🪔 ⚙️ 🆓 📄. 👶 + +⚫️ 🔜 ✊ 👆 🔃 **1️⃣0️⃣ ⏲**. + +!!! info + 🪔 **FastAPI** 💰. 👶 + +## 🔰 **FastAPI** 📱 + +* ✍ 📁 👆 📱, 🖼, `./fastapideta/` & ⛔ 🔘 ⚫️. + +### FastAPI 📟 + +* ✍ `main.py` 📁 ⏮️: + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int): + return {"item_id": item_id} +``` + +### 📄 + +🔜, 🎏 📁 ✍ 📁 `requirements.txt` ⏮️: + +```text +fastapi +``` + +!!! tip + 👆 🚫 💪 ❎ Uvicorn 🛠️ 🔛 🪔, 👐 👆 🔜 🎲 💚 ❎ ⚫️ 🌐 💯 👆 📱. + +### 📁 📊 + +👆 🔜 🔜 ✔️ 1️⃣ 📁 `./fastapideta/` ⏮️ 2️⃣ 📁: + +``` +. +└── main.py +└── requirements.txt +``` + +## ✍ 🆓 🪔 🏧 + +🔜 ✍ 🆓 🏧 🔛 🪔, 👆 💪 📧 & 🔐. + +👆 🚫 💪 💳. + +## ❎ ✳ + +🕐 👆 ✔️ 👆 🏧, ❎ 🪔 : + +=== "💾, 🇸🇻" + +
+ + ```console + $ curl -fsSL https://get.deta.dev/cli.sh | sh + ``` + +
+ +=== "🚪 📋" + +
+ + ```console + $ iwr https://get.deta.dev/cli.ps1 -useb | iex + ``` + +
+ +⏮️ ❎ ⚫️, 📂 🆕 📶 👈 ❎ ✳ 🔍. + +🆕 📶, ✔ 👈 ⚫️ ☑ ❎ ⏮️: + +
+ +```console +$ deta --help + +Deta command line interface for managing deta micros. +Complete documentation available at https://docs.deta.sh + +Usage: + deta [flags] + deta [command] + +Available Commands: + auth Change auth settings for a deta micro + +... +``` + +
+ +!!! tip + 🚥 👆 ✔️ ⚠ ❎ ✳, ✅ 🛂 🪔 🩺. + +## 💳 ⏮️ ✳ + +🔜 💳 🪔 ⚪️➡️ ✳ ⏮️: + +
+ +```console +$ deta login + +Please, log in from the web page. Waiting.. +Logged in successfully. +``` + +
+ +👉 🔜 📂 🕸 🖥 & 🔓 🔁. + +## 🛠️ ⏮️ 🪔 + +⏭, 🛠️ 👆 🈸 ⏮️ 🪔 ✳: + +
+ +```console +$ deta new + +Successfully created a new micro + +// Notice the "endpoint" 🔍 + +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} + +Adding dependencies... + + +---> 100% + + +Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 +``` + +
+ +👆 🔜 👀 🎻 📧 🎏: + +```JSON hl_lines="4" +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} +``` + +!!! tip + 👆 🛠️ 🔜 ✔️ 🎏 `"endpoint"` 📛. + +## ✅ ⚫️ + +🔜 📂 👆 🖥 👆 `endpoint` 📛. 🖼 🔛 ⚫️ `https://qltnci.deta.dev`, ✋️ 👆 🔜 🎏. + +👆 🔜 👀 🎻 📨 ⚪️➡️ 👆 FastAPI 📱: + +```JSON +{ + "Hello": "World" +} +``` + +& 🔜 🚶 `/docs` 👆 🛠️, 🖼 🔛 ⚫️ 🔜 `https://qltnci.deta.dev/docs`. + +⚫️ 🔜 🎦 👆 🩺 💖: + + + +## 🛠️ 📢 🔐 + +🔢, 🪔 🔜 🍵 🤝 ⚙️ 🍪 👆 🏧. + +✋️ 🕐 👆 🔜, 👆 💪 ⚒ ⚫️ 📢 ⏮️: + +
+ +```console +$ deta auth disable + +Successfully disabled http auth +``` + +
+ +🔜 👆 💪 💰 👈 📛 ⏮️ 🙆 & 👫 🔜 💪 🔐 👆 🛠️. 👶 + +## 🇺🇸🔍 + +㊗ ❗ 👆 🛠️ 👆 FastAPI 📱 🪔 ❗ 👶 👶 + +, 👀 👈 🪔 ☑ 🍵 🇺🇸🔍 👆, 👆 🚫 ✔️ ✊ 💅 👈 & 💪 💭 👈 👆 👩‍💻 🔜 ✔️ 🔐 🗜 🔗. 👶 👶 + +## ✅ 🕶 + +⚪️➡️ 👆 🩺 🎚 (👫 🔜 📛 💖 `https://qltnci.deta.dev/docs`) 📨 📨 👆 *➡ 🛠️* `/items/{item_id}`. + +🖼 ⏮️ 🆔 `5`. + +🔜 🚶 https://web.deta.sh. + +👆 🔜 👀 📤 📄 ◀️ 🤙 "◾" ⏮️ 🔠 👆 📱. + +👆 🔜 👀 📑 ⏮️ "ℹ", & 📑 "🕶", 🚶 📑 "🕶". + +📤 👆 💪 ✔ ⏮️ 📨 📨 👆 📱. + +👆 💪 ✍ 👫 & 🏤-🤾 👫. + + + +## 💡 🌅 + +☝, 👆 🔜 🎲 💚 🏪 💽 👆 📱 🌌 👈 😣 🔘 🕰. 👈 👆 💪 ⚙️ 🪔 🧢, ⚫️ ✔️ 👍 **🆓 🎚**. + +👆 💪 ✍ 🌅 🪔 🩺. + +## 🛠️ 🔧 + +👟 🔙 🔧 👥 🔬 [🛠️ 🔧](./concepts.md){.internal-link target=_blank}, 📥 ❔ 🔠 👫 🔜 🍵 ⏮️ 🪔: + +* **🇺🇸🔍**: 🍵 🪔, 👫 🔜 🤝 👆 📁 & 🍵 🇺🇸🔍 🔁. +* **🏃‍♂ 🔛 🕴**: 🍵 🪔, 🍕 👫 🐕‍🦺. +* **⏏**: 🍵 🪔, 🍕 👫 🐕‍🦺. +* **🧬**: 🍵 🪔, 🍕 👫 🐕‍🦺. +* **💾**: 📉 🔁 🪔, 👆 💪 📧 👫 📈 ⚫️. +* **⏮️ 🔁 ⏭ ▶️**: 🚫 🔗 🐕‍🦺, 👆 💪 ⚒ ⚫️ 👷 ⏮️ 👫 💾 ⚙️ ⚖️ 🌖 ✍. + +!!! note + 🪔 🔧 ⚒ ⚫️ ⏩ (& 🆓) 🛠️ 🙅 🈸 🔜. + + ⚫️ 💪 📉 📚 ⚙️ 💼, ✋️ 🎏 🕰, ⚫️ 🚫 🐕‍🦺 🎏, 💖 ⚙️ 🔢 💽 (↖️ ⚪️➡️ 🪔 👍 ☁ 💽 ⚙️), 🛃 🕹 🎰, ♒️. + + 👆 💪 ✍ 🌅 ℹ 🪔 🩺 👀 🚥 ⚫️ ▶️️ ⚒ 👆. diff --git a/docs/em/docs/deployment/docker.md b/docs/em/docs/deployment/docker.md new file mode 100644 index 000000000..51ece5599 --- /dev/null +++ b/docs/em/docs/deployment/docker.md @@ -0,0 +1,698 @@ +# FastAPI 📦 - ☁ + +🕐❔ 🛠️ FastAPI 🈸 ⚠ 🎯 🏗 **💾 📦 🖼**. ⚫️ 🛎 🔨 ⚙️ **☁**. 👆 💪 ⤴️ 🛠️ 👈 📦 🖼 1️⃣ 👩‍❤‍👨 💪 🌌. + +⚙️ 💾 📦 ✔️ 📚 📈 ✅ **💂‍♂**, **🔬**, **🦁**, & 🎏. + +!!! tip + 🏃 & ⏪ 💭 👉 💩 ❓ 🦘 [`Dockerfile` 🔛 👶](#build-a-docker-image-for-fastapi). + +
+📁 🎮 👶 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +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"] +``` + +
+ +## ⚫️❔ 📦 + +📦 (✴️ 💾 📦) 📶 **💿** 🌌 📦 🈸 ✅ 🌐 👫 🔗 & 💪 📁 ⏪ 🚧 👫 ❎ ⚪️➡️ 🎏 📦 (🎏 🈸 ⚖️ 🦲) 🎏 ⚙️. + +💾 📦 🏃 ⚙️ 🎏 💾 💾 🦠 (🎰, 🕹 🎰, ☁ 💽, ♒️). 👉 ⛓ 👈 👫 📶 💿 (🔬 🌕 🕹 🎰 👍 🎂 🏃‍♂ ⚙️). + +👉 🌌, 📦 🍴 **🐥 ℹ**, 💸 ⭐ 🏃‍♂ 🛠️ 🔗 (🕹 🎰 🔜 🍴 🌅 🌅). + +📦 ✔️ 👫 👍 **❎** 🏃‍♂ 🛠️ (🛎 1️⃣ 🛠️), 📁 ⚙️, & 🕸, 🔬 🛠️, 💂‍♂, 🛠️, ♒️. + +## ⚫️❔ 📦 🖼 + +**📦** 🏃 ⚪️➡️ **📦 🖼**. + +📦 🖼 **🎻** ⏬ 🌐 📁, 🌐 🔢, & 🔢 📋/📋 👈 🔜 🎁 📦. **🎻** 📥 ⛓ 👈 📦 **🖼** 🚫 🏃, ⚫️ 🚫 ➖ 🛠️, ⚫️ 🕴 📦 📁 & 🗃. + +🔅 "**📦 🖼**" 👈 🏪 🎻 🎚,"**📦**" 🛎 🔗 🏃‍♂ 👐, 👜 👈 ➖ **🛠️**. + +🕐❔ **📦** ▶️ & 🏃‍♂ (▶️ ⚪️➡️ **📦 🖼**) ⚫️ 💪 ✍ ⚖️ 🔀 📁, 🌐 🔢, ♒️. 👈 🔀 🔜 🔀 🕴 👈 📦, ✋️ 🔜 🚫 😣 👽 📦 🖼 (🔜 🚫 🖊 💾). + +📦 🖼 ⭐ **📋** 📁 & 🎚, ✅ `python` & 📁 `main.py`. + +& **📦** ⚫️ (🔅 **📦 🖼**) ☑ 🏃 👐 🖼, ⭐ **🛠️**. 👐, 📦 🏃 🕴 🕐❔ ⚫️ ✔️ **🛠️ 🏃** (& 🛎 ⚫️ 🕴 👁 🛠️). 📦 ⛔️ 🕐❔ 📤 🙅‍♂ 🛠️ 🏃 ⚫️. + +## 📦 🖼 + +☁ ✔️ 1️⃣ 👑 🧰 ✍ & 🛠️ **📦 🖼** & **📦**. + +& 📤 📢 ☁ 🎡 ⏮️ 🏤-⚒ **🛂 📦 🖼** 📚 🧰, 🌐, 💽, & 🈸. + +🖼, 📤 🛂 🐍 🖼. + +& 📤 📚 🎏 🖼 🎏 👜 💖 💽, 🖼: + +* +* +* +* , ♒️. + +⚙️ 🏤-⚒ 📦 🖼 ⚫️ 📶 ⏩ **🌀** & ⚙️ 🎏 🧰. 🖼, 🔄 👅 🆕 💽. 🌅 💼, 👆 💪 ⚙️ **🛂 🖼**, & 🔗 👫 ⏮️ 🌐 🔢. + +👈 🌌, 📚 💼 👆 💪 💡 🔃 📦 & ☁ & 🏤-⚙️ 👈 💡 ⏮️ 📚 🎏 🧰 & 🦲. + +, 👆 🔜 🏃 **💗 📦** ⏮️ 🎏 👜, 💖 💽, 🐍 🈸, 🕸 💽 ⏮️ 😥 🕸 🈸, & 🔗 👫 👯‍♂️ 📨 👫 🔗 🕸. + +🌐 📦 🧾 ⚙️ (💖 ☁ ⚖️ Kubernete) ✔️ 👫 🕸 ⚒ 🛠️ 🔘 👫. + +## 📦 & 🛠️ + +**📦 🖼** 🛎 🔌 🚮 🗃 🔢 📋 ⚖️ 📋 👈 🔜 🏃 🕐❔ **📦** ▶️ & 🔢 🚶‍♀️ 👈 📋. 📶 🎏 ⚫️❔ 🔜 🚥 ⚫️ 📋 ⏸. + +🕐❔ **📦** ▶️, ⚫️ 🔜 🏃 👈 📋/📋 (👐 👆 💪 🔐 ⚫️ & ⚒ ⚫️ 🏃 🎏 📋/📋). + +📦 🏃 📏 **👑 🛠️** (📋 ⚖️ 📋) 🏃. + +📦 🛎 ✔️ **👁 🛠️**, ✋️ ⚫️ 💪 ▶️ ✳ ⚪️➡️ 👑 🛠️, & 👈 🌌 👆 🔜 ✔️ **💗 🛠️** 🎏 📦. + +✋️ ⚫️ 🚫 💪 ✔️ 🏃‍♂ 📦 🍵 **🌘 1️⃣ 🏃‍♂ 🛠️**. 🚥 👑 🛠️ ⛔️, 📦 ⛔️. + +## 🏗 ☁ 🖼 FastAPI + +🆗, ➡️ 🏗 🕳 🔜 ❗ 👶 + +👤 🔜 🎦 👆 ❔ 🏗 **☁ 🖼** FastAPI **⚪️➡️ 🖌**, ⚓️ 🔛 **🛂 🐍** 🖼. + +👉 ⚫️❔ 👆 🔜 💚 **🏆 💼**, 🖼: + +* ⚙️ **Kubernete** ⚖️ 🎏 🧰 +* 🕐❔ 🏃‍♂ 🔛 **🍓 👲** +* ⚙️ ☁ 🐕‍🦺 👈 🔜 🏃 📦 🖼 👆, ♒️. + +### 📦 📄 + +👆 🔜 🛎 ✔️ **📦 📄** 👆 🈸 📁. + +⚫️ 🔜 🪀 ✴️ 🔛 🧰 👆 ⚙️ **❎** 👈 📄. + +🌅 ⚠ 🌌 ⚫️ ✔️ 📁 `requirements.txt` ⏮️ 📦 📛 & 👫 ⏬, 1️⃣ 📍 ⏸. + +👆 🔜 ↗️ ⚙️ 🎏 💭 👆 ✍ [🔃 FastAPI ⏬](./versions.md){.internal-link target=_blank} ⚒ ↔ ⏬. + +🖼, 👆 `requirements.txt` 💪 👀 💖: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +& 👆 🔜 🛎 ❎ 👈 📦 🔗 ⏮️ `pip`, 🖼: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + 📤 🎏 📁 & 🧰 🔬 & ❎ 📦 🔗. + + 👤 🔜 🎦 👆 🖼 ⚙️ 🎶 ⏪ 📄 🔛. 👶 + +### ✍ **FastAPI** 📟 + +* ✍ `app` 📁 & ⛔ ⚫️. +* ✍ 🛁 📁 `__init__.py`. +* ✍ `main.py` 📁 ⏮️: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### 📁 + +🔜 🎏 🏗 📁 ✍ 📁 `Dockerfile` ⏮️: + +```{ .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️⃣. ▶️ ⚪️➡️ 🛂 🐍 🧢 🖼. + +2️⃣. ⚒ ⏮️ 👷 📁 `/code`. + + 👉 🌐❔ 👥 🔜 🚮 `requirements.txt` 📁 & `app` 📁. + +3️⃣. 📁 📁 ⏮️ 📄 `/code` 📁. + + 📁 **🕴** 📁 ⏮️ 📄 🥇, 🚫 🎂 📟. + + 👉 📁 **🚫 🔀 🛎**, ☁ 🔜 🔍 ⚫️ & ⚙️ **💾** 👉 🔁, 🛠️ 💾 ⏭ 🔁 💁‍♂️. + +4️⃣. ❎ 📦 🔗 📄 📁. + + `--no-cache-dir` 🎛 💬 `pip` 🚫 🖊 ⏬ 📦 🌐, 👈 🕴 🚥 `pip` 🔜 🏃 🔄 ❎ 🎏 📦, ✋️ 👈 🚫 💼 🕐❔ 👷 ⏮️ 📦. + + !!! note + `--no-cache-dir` 🕴 🔗 `pip`, ⚫️ ✔️ 🕳 ⏮️ ☁ ⚖️ 📦. + + `--upgrade` 🎛 💬 `pip` ♻ 📦 🚥 👫 ⏪ ❎. + + ↩️ ⏮️ 🔁 🖨 📁 💪 🔍 **☁ 💾**, 👉 🔁 🔜 **⚙️ ☁ 💾** 🕐❔ 💪. + + ⚙️ 💾 👉 🔁 🔜 **🖊** 👆 📚 **🕰** 🕐❔ 🏗 🖼 🔄 & 🔄 ⏮️ 🛠️, ↩️ **⏬ & ❎** 🌐 🔗 **🔠 🕰**. + +5️⃣. 📁 `./app` 📁 🔘 `/code` 📁. + + 👉 ✔️ 🌐 📟 ❔ ⚫️❔ **🔀 🌅 🛎** ☁ **💾** 🏆 🚫 ⚙️ 👉 ⚖️ 🙆 **📄 🔁** 💪. + + , ⚫️ ⚠ 🚮 👉 **🏘 🔚** `Dockerfile`, 🔬 📦 🖼 🏗 🕰. + +6️⃣. ⚒ **📋** 🏃 `uvicorn` 💽. + + `CMD` ✊ 📇 🎻, 🔠 👫 🎻 ⚫️❔ 👆 🔜 🆎 📋 ⏸ 👽 🚀. + + 👉 📋 🔜 🏃 ⚪️➡️ **⏮️ 👷 📁**, 🎏 `/code` 📁 👆 ⚒ 🔛 ⏮️ `WORKDIR /code`. + + ↩️ 📋 🔜 ▶️ `/code` & 🔘 ⚫️ 📁 `./app` ⏮️ 👆 📟, **Uvicorn** 🔜 💪 👀 & **🗄** `app` ⚪️➡️ `app.main`. + +!!! tip + 📄 ⚫️❔ 🔠 ⏸ 🔨 🖊 🔠 🔢 💭 📟. 👶 + +👆 🔜 🔜 ✔️ 📁 📊 💖: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### ⛅ 🤝 ❎ 🗳 + +🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers`, 👉 🔜 💬 Uvicorn 💙 🎚 📨 👈 🗳 💬 ⚫️ 👈 🈸 🏃 ⛅ 🇺🇸🔍, ♒️. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### ☁ 💾 + +📤 ⚠ 🎱 👉 `Dockerfile`, 👥 🥇 📁 **📁 ⏮️ 🔗 😞**, 🚫 🎂 📟. ➡️ 👤 💬 👆 ⚫️❔ 👈. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +☁ & 🎏 🧰 **🏗** 👉 📦 🖼 **🔁**, 🚮 **1️⃣ 🧽 🔛 🔝 🎏**, ▶️ ⚪️➡️ 🔝 `Dockerfile` & ❎ 🙆 📁 ✍ 🔠 👩‍🌾 `Dockerfile`. + +☁ & 🎏 🧰 ⚙️ **🔗 💾** 🕐❔ 🏗 🖼, 🚥 📁 🚫 🔀 ↩️ 🏁 🕰 🏗 📦 🖼, ⤴️ ⚫️ 🔜 **🏤-⚙️ 🎏 🧽** ✍ 🏁 🕰, ↩️ 🖨 📁 🔄 & 🏗 🆕 🧽 ⚪️➡️ 🖌. + +❎ 📁 📁 🚫 🎯 📉 👜 💁‍♂️ 🌅, ✋️ ↩️ ⚫️ ⚙️ 💾 👈 🔁, ⚫️ 💪 **⚙️ 💾 ⏭ 🔁**. 🖼, ⚫️ 💪 ⚙️ 💾 👩‍🌾 👈 ❎ 🔗 ⏮️: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +📁 ⏮️ 📦 📄 **🏆 🚫 🔀 🛎**. , 🖨 🕴 👈 📁, ☁ 🔜 💪 **⚙️ 💾** 👈 🔁. + +& ⤴️, ☁ 🔜 💪 **⚙️ 💾 ⏭ 🔁** 👈 ⏬ & ❎ 👈 🔗. & 📥 🌐❔ 👥 **🖊 📚 🕰**. 👶 ...& ❎ 😩 ⌛. 👶 👶 + +⏬ & ❎ 📦 🔗 **💪 ✊ ⏲**, ✋️ ⚙️ **💾** 🔜 **✊ 🥈** 🌅. + +& 👆 🔜 🏗 📦 🖼 🔄 & 🔄 ⏮️ 🛠️ ✅ 👈 👆 📟 🔀 👷, 📤 📚 📈 🕰 👉 🔜 🖊. + +⤴️, 🏘 🔚 `Dockerfile`, 👥 📁 🌐 📟. 👉 ⚫️❔ **🔀 🏆 🛎**, 👥 🚮 ⚫️ 🏘 🔚, ↩️ 🌖 🕧, 🕳 ⏮️ 👉 🔁 🔜 🚫 💪 ⚙️ 💾. + +```Dockerfile +COPY ./app /code/app +``` + +### 🏗 ☁ 🖼 + +🔜 👈 🌐 📁 🥉, ➡️ 🏗 📦 🖼. + +* 🚶 🏗 📁 (🌐❔ 👆 `Dockerfile` , ⚗ 👆 `app` 📁). +* 🏗 👆 FastAPI 🖼: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +!!! tip + 👀 `.` 🔚, ⚫️ 🌓 `./`, ⚫️ 💬 ☁ 📁 ⚙️ 🏗 📦 🖼. + + 👉 💼, ⚫️ 🎏 ⏮️ 📁 (`.`). + +### ▶️ ☁ 📦 + +* 🏃 📦 ⚓️ 🔛 👆 🖼: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## ✅ ⚫️ + +👆 🔜 💪 ✅ ⚫️ 👆 ☁ 📦 📛, 🖼: http://192.168.99.100/items/5?q=somequery ⚖️ http://127.0.0.1/items/5?q=somequery (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🕳 💖: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## 🎓 🛠️ 🩺 + +🔜 👆 💪 🚶 http://192.168.99.100/docs ⚖️ http://127.0.0.1/docs (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## 🎛 🛠️ 🩺 + +& 👆 💪 🚶 http://192.168.99.100/redoc ⚖️ http://127.0.0.1/redoc (⚖️ 🌓, ⚙️ 👆 ☁ 🦠). + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 🏗 ☁ 🖼 ⏮️ 👁-📁 FastAPI + +🚥 👆 FastAPI 👁 📁, 🖼, `main.py` 🍵 `./app` 📁, 👆 📁 📊 💪 👀 💖 👉: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +⤴️ 👆 🔜 ✔️ 🔀 🔗 ➡ 📁 📁 🔘 `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️⃣. 📁 `main.py` 📁 `/code` 📁 🔗 (🍵 🙆 `./app` 📁). + +2️⃣. 🏃 Uvicorn & 💬 ⚫️ 🗄 `app` 🎚 ⚪️➡️ `main` (↩️ 🏭 ⚪️➡️ `app.main`). + +⤴️ 🔆 Uvicorn 📋 ⚙️ 🆕 🕹 `main` ↩️ `app.main` 🗄 FastAPI 🎚 `app`. + +## 🛠️ 🔧 + +➡️ 💬 🔄 🔃 🎏 [🛠️ 🔧](./concepts.md){.internal-link target=_blank} ⚖ 📦. + +📦 ✴️ 🧰 📉 🛠️ **🏗 & 🛠️** 🈸, ✋️ 👫 🚫 🛠️ 🎯 🎯 🍵 👉 **🛠️ 🔧**, & 📤 📚 💪 🎛. + +**👍 📰** 👈 ⏮️ 🔠 🎏 🎛 📤 🌌 📔 🌐 🛠️ 🔧. 👶 + +➡️ 📄 👉 **🛠️ 🔧** ⚖ 📦: + +* 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +## 🇺🇸🔍 + +🚥 👥 🎯 🔛 **📦 🖼** FastAPI 🈸 (& ⏪ 🏃‍♂ **📦**), 🇺🇸🔍 🛎 🔜 🍵 **🗜** ➕1️⃣ 🧰. + +⚫️ 💪 ➕1️⃣ 📦, 🖼 ⏮️ Traefik, 🚚 **🇺🇸🔍** & **🏧** 🛠️ **📄**. + +!!! tip + Traefik ✔️ 🛠️ ⏮️ ☁, Kubernete, & 🎏, ⚫️ 📶 ⏩ ⚒ 🆙 & 🔗 🇺🇸🔍 👆 📦 ⏮️ ⚫️. + +👐, 🇺🇸🔍 💪 🍵 ☁ 🐕‍🦺 1️⃣ 👫 🐕‍🦺 (⏪ 🏃 🈸 📦). + +## 🏃‍♂ 🔛 🕴 & ⏏ + +📤 🛎 ➕1️⃣ 🧰 🈚 **▶️ & 🏃‍♂** 👆 📦. + +⚫️ 💪 **☁** 🔗, **☁ ✍**, **Kubernete**, **☁ 🐕‍🦺**, ♒️. + +🌅 (⚖️ 🌐) 💼, 📤 🙅 🎛 🛠️ 🏃 📦 🔛 🕴 & 🛠️ ⏏ 🔛 ❌. 🖼, ☁, ⚫️ 📋 ⏸ 🎛 `--restart`. + +🍵 ⚙️ 📦, ⚒ 🈸 🏃 🔛 🕴 & ⏮️ ⏏ 💪 ⚠ & ⚠. ✋️ 🕐❔ **👷 ⏮️ 📦** 🌅 💼 👈 🛠️ 🔌 🔢. 👶 + +## 🧬 - 🔢 🛠️ + +🚥 👆 ✔️ 🌑 🎰 ⏮️ **☁**, ☁ 🐝 📳, 🖖, ⚖️ ➕1️⃣ 🎏 🏗 ⚙️ 🛠️ 📎 📦 🔛 💗 🎰, ⤴️ 👆 🔜 🎲 💚 **🍵 🧬** **🌑 🎚** ↩️ ⚙️ **🛠️ 👨‍💼** (💖 🐁 ⏮️ 👨‍🏭) 🔠 📦. + +1️⃣ 📚 📎 📦 🧾 ⚙️ 💖 Kubernete 🛎 ✔️ 🛠️ 🌌 🚚 **🧬 📦** ⏪ 🔗 **📐 ⚖** 📨 📨. 🌐 **🌑 🎚**. + +📚 💼, 👆 🔜 🎲 💚 🏗 **☁ 🖼 ⚪️➡️ 🖌** [🔬 🔛](#dockerfile), ❎ 👆 🔗, & 🏃‍♂ **👁 Uvicorn 🛠️** ↩️ 🏃‍♂ 🕳 💖 🐁 ⏮️ Uvicorn 👨‍🏭. + +### 📐 ⚙ + +🕐❔ ⚙️ 📦, 👆 🔜 🛎 ✔️ 🦲 **👂 🔛 👑 ⛴**. ⚫️ 💪 🎲 ➕1️⃣ 📦 👈 **🤝 ❎ 🗳** 🍵 **🇺🇸🔍** ⚖️ 🎏 🧰. + +👉 🦲 🔜 ✊ **📐** 📨 & 📎 👈 👪 👨‍🏭 (🤞) **⚖** 🌌, ⚫️ 🛎 🤙 **📐 ⚙**. + +!!! tip + 🎏 **🤝 ❎ 🗳** 🦲 ⚙️ 🇺🇸🔍 🔜 🎲 **📐 ⚙**. + +& 🕐❔ 👷 ⏮️ 📦, 🎏 ⚙️ 👆 ⚙️ ▶️ & 🛠️ 👫 🔜 ⏪ ✔️ 🔗 🧰 📶 **🕸 📻** (✅ 🇺🇸🔍 📨) ⚪️➡️ 👈 **📐 ⚙** (👈 💪 **🤝 ❎ 🗳**) 📦(Ⓜ) ⏮️ 👆 📱. + +### 1️⃣ 📐 ⚙ - 💗 👨‍🏭 📦 + +🕐❔ 👷 ⏮️ **Kubernete** ⚖️ 🎏 📎 📦 🧾 ⚙️, ⚙️ 👫 🔗 🕸 🛠️ 🔜 ✔ 👁 **📐 ⚙** 👈 👂 🔛 👑 **⛴** 📶 📻 (📨) 🎲 **💗 📦** 🏃 👆 📱. + +🔠 👫 📦 🏃‍♂ 👆 📱 🔜 🛎 ✔️ **1️⃣ 🛠️** (✅ Uvicorn 🛠️ 🏃 👆 FastAPI 🈸). 👫 🔜 🌐 **🌓 📦**, 🏃‍♂ 🎏 👜, ✋️ 🔠 ⏮️ 🚮 👍 🛠️, 💾, ♒️. 👈 🌌 👆 🔜 ✊ 📈 **🛠️** **🎏 🐚** 💽, ⚖️ **🎏 🎰**. + +& 📎 📦 ⚙️ ⏮️ **📐 ⚙** 🔜 **📎 📨** 🔠 1️⃣ 📦 ⏮️ 👆 📱 **🔄**. , 🔠 📨 💪 🍵 1️⃣ 💗 **🔁 📦** 🏃 👆 📱. + +& 🛎 👉 **📐 ⚙** 🔜 💪 🍵 📨 👈 🚶 *🎏* 📱 👆 🌑 (✅ 🎏 🆔, ⚖️ 🔽 🎏 📛 ➡ 🔡), & 🔜 📶 👈 📻 ▶️️ 📦 *👈 🎏* 🈸 🏃‍♂ 👆 🌑. + +### 1️⃣ 🛠️ 📍 📦 + +👉 🆎 😐, 👆 🎲 🔜 💚 ✔️ **👁 (Uvicorn) 🛠️ 📍 📦**, 👆 🔜 ⏪ 🚚 🧬 🌑 🎚. + +, 👉 💼, 👆 **🔜 🚫** 💚 ✔️ 🛠️ 👨‍💼 💖 🐁 ⏮️ Uvicorn 👨‍🏭, ⚖️ Uvicorn ⚙️ 🚮 👍 Uvicorn 👨‍🏭. 👆 🔜 💚 ✔️ **👁 Uvicorn 🛠️** 📍 📦 (✋️ 🎲 💗 📦). + +✔️ ➕1️⃣ 🛠️ 👨‍💼 🔘 📦 (🔜 ⏮️ 🐁 ⚖️ Uvicorn 🛠️ Uvicorn 👨‍🏭) 🔜 🕴 🚮 **🙃 🔀** 👈 👆 🌅 🎲 ⏪ ✊ 💅 ⏮️ 👆 🌑 ⚙️. + +### 📦 ⏮️ 💗 🛠️ & 🎁 💼 + +↗️, 📤 **🎁 💼** 🌐❔ 👆 💪 💚 ✔️ **📦** ⏮️ **🐁 🛠️ 👨‍💼** ▶️ 📚 **Uvicorn 👨‍🏭 🛠️** 🔘. + +📚 💼, 👆 💪 ⚙️ **🛂 ☁ 🖼** 👈 🔌 **🐁** 🛠️ 👨‍💼 🏃‍♂ 💗 **Uvicorn 👨‍🏭 🛠️**, & 🔢 ⚒ 🔆 🔢 👨‍🏭 ⚓️ 🔛 ⏮️ 💽 🐚 🔁. 👤 🔜 💬 👆 🌅 🔃 ⚫️ 🔛 [🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn](#official-docker-image-with-gunicorn-uvicorn). + +📥 🖼 🕐❔ 👈 💪 ⚒ 🔑: + +#### 🙅 📱 + +👆 💪 💚 🛠️ 👨‍💼 📦 🚥 👆 🈸 **🙅 🥃** 👈 👆 🚫 💪 (🐥 🚫) 👌-🎶 🔢 🛠️ 💁‍♂️ 🌅, & 👆 💪 ⚙️ 🏧 🔢 (⏮️ 🛂 ☁ 🖼), & 👆 🏃‍♂ ⚫️ 🔛 **👁 💽**, 🚫 🌑. + +#### ☁ ✍ + +👆 💪 🛠️ **👁 💽** (🚫 🌑) ⏮️ **☁ ✍**, 👆 🚫🔜 ✔️ ⏩ 🌌 🛠️ 🧬 📦 (⏮️ ☁ ✍) ⏪ 🛡 🔗 🕸 & **📐 ⚖**. + +⤴️ 👆 💪 💚 ✔️ **👁 📦** ⏮️ **🛠️ 👨‍💼** ▶️ **📚 👨‍🏭 🛠️** 🔘. + +#### 🤴 & 🎏 🤔 + +👆 💪 ✔️ **🎏 🤔** 👈 🔜 ⚒ ⚫️ ⏩ ✔️ **👁 📦** ⏮️ **💗 🛠️** ↩️ ✔️ **💗 📦** ⏮️ **👁 🛠️** 🔠 👫. + +🖼 (🪀 🔛 👆 🖥) 👆 💪 ✔️ 🧰 💖 🤴 🏭 🎏 📦 👈 🔜 ✔️ 🔐 **🔠 📨** 👈 👟. + +👉 💼, 🚥 👆 ✔️ **💗 📦**, 🔢, 🕐❔ 🤴 👟 **✍ ⚖**, ⚫️ 🔜 🤚 🕐 **👁 📦 🔠 🕰** (📦 👈 🍵 👈 🎯 📨), ↩️ 🤚 **📈 ⚖** 🌐 🔁 📦. + +⤴️, 👈 💼, ⚫️ 💪 🙅 ✔️ **1️⃣ 📦** ⏮️ **💗 🛠️**, & 🇧🇿 🧰 (✅ 🤴 🏭) 🔛 🎏 📦 📈 🤴 ⚖ 🌐 🔗 🛠️ & 🎦 👈 ⚖ 🔛 👈 👁 📦. + +--- + +👑 ☝, **👌** 👉 **🚫 ✍ 🗿** 👈 👆 ✔️ 😄 ⏩. 👆 💪 ⚙️ 👫 💭 **🔬 👆 👍 ⚙️ 💼** & 💭 ⚫️❔ 👍 🎯 👆 ⚙️, ✅ 👅 ❔ 🛠️ 🔧: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +## 💾 + +🚥 👆 🏃 **👁 🛠️ 📍 📦** 👆 🔜 ✔️ 🌅 ⚖️ 🌘 👍-🔬, ⚖, & 📉 💸 💾 🍴 🔠 👈 📦 (🌅 🌘 1️⃣ 🚥 👫 🔁). + +& ⤴️ 👆 💪 ⚒ 👈 🎏 💾 📉 & 📄 👆 📳 👆 📦 🧾 ⚙️ (🖼 **Kubernete**). 👈 🌌 ⚫️ 🔜 💪 **🔁 📦** **💪 🎰** ✊ 🔘 🏧 💸 💾 💪 👫, & 💸 💪 🎰 🌑. + +🚥 👆 🈸 **🙅**, 👉 🔜 🎲 **🚫 ⚠**, & 👆 💪 🚫 💪 ✔ 🏋️ 💾 📉. ✋️ 🚥 👆 **⚙️ 📚 💾** (🖼 ⏮️ **🎰 🏫** 🏷), 👆 🔜 ✅ ❔ 🌅 💾 👆 😩 & 🔆 **🔢 📦** 👈 🏃 **🔠 🎰** (& 🎲 🚮 🌖 🎰 👆 🌑). + +🚥 👆 🏃 **💗 🛠️ 📍 📦** (🖼 ⏮️ 🛂 ☁ 🖼) 👆 🔜 ✔️ ⚒ 💭 👈 🔢 🛠️ ▶️ 🚫 **🍴 🌖 💾** 🌘 ⚫️❔ 💪. + +## ⏮️ 🔁 ⏭ ▶️ & 📦 + +🚥 👆 ⚙️ 📦 (✅ ☁, Kubernete), ⤴️ 📤 2️⃣ 👑 🎯 👆 💪 ⚙️. + +### 💗 📦 + +🚥 👆 ✔️ **💗 📦**, 🎲 🔠 1️⃣ 🏃 **👁 🛠️** (🖼, **Kubernete** 🌑), ⤴️ 👆 🔜 🎲 💚 ✔️ **🎏 📦** 🔨 👷 **⏮️ 📶** 👁 📦, 🏃 👁 🛠️, **⏭** 🏃 🔁 👨‍🏭 📦. + +!!! info + 🚥 👆 ⚙️ Kubernete, 👉 🔜 🎲 🕑 📦. + +🚥 👆 ⚙️ 💼 📤 🙅‍♂ ⚠ 🏃‍♂ 👈 ⏮️ 📶 **💗 🕰 🔗** (🖼 🚥 👆 🚫 🏃 💽 🛠️, ✋️ ✅ 🚥 💽 🔜), ⤴️ 👆 💪 🚮 👫 🔠 📦 ▶️️ ⏭ ▶️ 👑 🛠️. + +### 👁 📦 + +🚥 👆 ✔️ 🙅 🖥, ⏮️ **👁 📦** 👈 ⤴️ ▶️ 💗 **👨‍🏭 🛠️** (⚖️ 1️⃣ 🛠️), ⤴️ 👆 💪 🏃 👈 ⏮️ 🔁 🎏 📦, ▶️️ ⏭ ▶️ 🛠️ ⏮️ 📱. 🛂 ☁ 🖼 🐕‍🦺 👉 🔘. + +## 🛂 ☁ 🖼 ⏮️ 🐁 - Uvicorn + +📤 🛂 ☁ 🖼 👈 🔌 🐁 🏃‍♂ ⏮️ Uvicorn 👨‍🏭, ℹ ⏮️ 📃: [💽 👨‍🏭 - 🐁 ⏮️ Uvicorn](./server-workers.md){.internal-link target=_blank}. + +👉 🖼 🔜 ⚠ ✴️ ⚠ 🔬 🔛: [📦 ⏮️ 💗 🛠️ & 🎁 💼](#containers-with-multiple-processes-and-special-cases). + +* tiangolo/uvicorn-🐁-fastapi. + +!!! warning + 📤 ↕ 🤞 👈 👆 **🚫** 💪 👉 🧢 🖼 ⚖️ 🙆 🎏 🎏 1️⃣, & 🔜 👻 📆 🏗 🖼 ⚪️➡️ 🖌 [🔬 🔛: 🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). + +👉 🖼 ✔️ **🚘-📳** 🛠️ 🔌 ⚒ **🔢 👨‍🏭 🛠️** ⚓️ 🔛 💽 🐚 💪. + +⚫️ ✔️ **🤔 🔢**, ✋️ 👆 💪 🔀 & ℹ 🌐 📳 ⏮️ **🌐 🔢** ⚖️ 📳 📁. + +⚫️ 🐕‍🦺 🏃 **⏮️ 🔁 ⏭ ▶️** ⏮️ ✍. + +!!! tip + 👀 🌐 📳 & 🎛, 🚶 ☁ 🖼 📃: Tiangolo/uvicorn-🐁-fastapi. + +### 🔢 🛠️ 🔛 🛂 ☁ 🖼 + +**🔢 🛠️** 🔛 👉 🖼 **📊 🔁** ⚪️➡️ 💽 **🐚** 💪. + +👉 ⛓ 👈 ⚫️ 🔜 🔄 **🗜** 🌅 **🎭** ⚪️➡️ 💽 💪. + +👆 💪 🔆 ⚫️ ⏮️ 📳 ⚙️ **🌐 🔢**, ♒️. + +✋️ ⚫️ ⛓ 👈 🔢 🛠️ 🪀 🔛 💽 📦 🏃, **💸 💾 🍴** 🔜 🪀 🔛 👈. + +, 🚥 👆 🈸 🍴 📚 💾 (🖼 ⏮️ 🎰 🏫 🏷), & 👆 💽 ✔️ 📚 💽 🐚 **✋️ 🐥 💾**, ⤴️ 👆 📦 💪 🔚 🆙 🔄 ⚙️ 🌅 💾 🌘 ⚫️❔ 💪, & 🤕 🎭 📚 (⚖️ 💥). 👶 + +### ✍ `Dockerfile` + +📥 ❔ 👆 🔜 ✍ `Dockerfile` ⚓️ 🔛 👉 🖼: + +```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 +``` + +### 🦏 🈸 + +🚥 👆 ⏩ 📄 🔃 🏗 [🦏 🈸 ⏮️ 💗 📁](../tutorial/bigger-applications.md){.internal-link target=_blank}, 👆 `Dockerfile` 💪 ↩️ 👀 💖: + +```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 +``` + +### 🕐❔ ⚙️ + +👆 🔜 🎲 **🚫** ⚙️ 👉 🛂 🧢 🖼 (⚖️ 🙆 🎏 🎏 1️⃣) 🚥 👆 ⚙️ **Kubernete** (⚖️ 🎏) & 👆 ⏪ ⚒ **🧬** 🌑 🎚, ⏮️ 💗 **📦**. 📚 💼, 👆 👍 📆 **🏗 🖼 ⚪️➡️ 🖌** 🔬 🔛: [🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). + +👉 🖼 🔜 ⚠ ✴️ 🎁 💼 🔬 🔛 [📦 ⏮️ 💗 🛠️ & 🎁 💼](#containers-with-multiple-processes-and-special-cases). 🖼, 🚥 👆 🈸 **🙅 🥃** 👈 ⚒ 🔢 🔢 🛠️ ⚓️ 🔛 💽 👷 👍, 👆 🚫 💚 😥 ⏮️ ❎ 🛠️ 🧬 🌑 🎚, & 👆 🚫 🏃 🌅 🌘 1️⃣ 📦 ⏮️ 👆 📱. ⚖️ 🚥 👆 🛠️ ⏮️ **☁ ✍**, 🏃 🔛 👁 💽, ♒️. + +## 🛠️ 📦 🖼 + +⏮️ ✔️ 📦 (☁) 🖼 📤 📚 🌌 🛠️ ⚫️. + +🖼: + +* ⏮️ **☁ ✍** 👁 💽 +* ⏮️ **Kubernete** 🌑 +* ⏮️ ☁ 🐝 📳 🌑 +* ⏮️ ➕1️⃣ 🧰 💖 🖖 +* ⏮️ ☁ 🐕‍🦺 👈 ✊ 👆 📦 🖼 & 🛠️ ⚫️ + +## ☁ 🖼 ⏮️ 🎶 + +🚥 👆 ⚙️ 🎶 🛠️ 👆 🏗 🔗, 👆 💪 ⚙️ ☁ 👁-▶️ 🏗: + +```{ .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️⃣. 👉 🥇 ▶️, ⚫️ 🌟 `requirements-stage`. + +2️⃣. ⚒ `/tmp` ⏮️ 👷 📁. + + 📥 🌐❔ 👥 🔜 🏗 📁 `requirements.txt` + +3️⃣. ❎ 🎶 👉 ☁ ▶️. + +4️⃣. 📁 `pyproject.toml` & `poetry.lock` 📁 `/tmp` 📁. + + ↩️ ⚫️ ⚙️ `./poetry.lock*` (▶️ ⏮️ `*`), ⚫️ 🏆 🚫 💥 🚥 👈 📁 🚫 💪. + +5️⃣. 🏗 `requirements.txt` 📁. + +6️⃣. 👉 🏁 ▶️, 🕳 📥 🔜 🛡 🏁 📦 🖼. + +7️⃣. ⚒ ⏮️ 👷 📁 `/code`. + +8️⃣. 📁 `requirements.txt` 📁 `/code` 📁. + + 👉 📁 🕴 🖖 ⏮️ ☁ ▶️, 👈 ⚫️❔ 👥 ⚙️ `--from-requirements-stage` 📁 ⚫️. + +9️⃣. ❎ 📦 🔗 🏗 `requirements.txt` 📁. + +1️⃣0️⃣. 📁 `app` 📁 `/code` 📁. + +1️⃣1️⃣. 🏃 `uvicorn` 📋, 💬 ⚫️ ⚙️ `app` 🎚 🗄 ⚪️➡️ `app.main`. + +!!! tip + 🖊 💭 🔢 👀 ⚫️❔ 🔠 ⏸ 🔨. + +**☁ ▶️** 🍕 `Dockerfile` 👈 👷 **🍕 📦 🖼** 👈 🕴 ⚙️ 🏗 📁 ⚙️ ⏪. + +🥇 ▶️ 🔜 🕴 ⚙️ **❎ 🎶** & **🏗 `requirements.txt`** ⏮️ 👆 🏗 🔗 ⚪️➡️ 🎶 `pyproject.toml` 📁. + +👉 `requirements.txt` 📁 🔜 ⚙️ ⏮️ `pip` ⏪ **⏭ ▶️**. + +🏁 📦 🖼 **🕴 🏁 ▶️** 🛡. ⏮️ ▶️(Ⓜ) 🔜 ❎. + +🕐❔ ⚙️ 🎶, ⚫️ 🔜 ⚒ 🔑 ⚙️ **☁ 👁-▶️ 🏗** ↩️ 👆 🚫 🤙 💪 ✔️ 🎶 & 🚮 🔗 ❎ 🏁 📦 🖼, 👆 **🕴 💪** ✔️ 🏗 `requirements.txt` 📁 ❎ 👆 🏗 🔗. + +⤴️ ⏭ (& 🏁) ▶️ 👆 🔜 🏗 🖼 🌅 ⚖️ 🌘 🎏 🌌 🔬 ⏭. + +### ⛅ 🤝 ❎ 🗳 - 🎶 + +🔄, 🚥 👆 🏃‍♂ 👆 📦 ⛅ 🤝 ❎ 🗳 (📐 ⚙) 💖 👌 ⚖️ Traefik, 🚮 🎛 `--proxy-headers` 📋: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## 🌃 + +⚙️ 📦 ⚙️ (✅ ⏮️ **☁** & **Kubernete**) ⚫️ ▶️️ 📶 🎯 🍵 🌐 **🛠️ 🔧**: + +* 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🌅 💼, 👆 🎲 🏆 🚫 💚 ⚙️ 🙆 🧢 🖼, & ↩️ **🏗 📦 🖼 ⚪️➡️ 🖌** 1️⃣ ⚓️ 🔛 🛂 🐍 ☁ 🖼. + +✊ 💅 **✔** 👩‍🌾 `Dockerfile` & **☁ 💾** 👆 💪 **📉 🏗 🕰**, 📉 👆 📈 (& ❎ 😩). 👶 + +🎯 🎁 💼, 👆 💪 💚 ⚙️ 🛂 ☁ 🖼 FastAPI. 👶 diff --git a/docs/em/docs/deployment/https.md b/docs/em/docs/deployment/https.md new file mode 100644 index 000000000..3feb3a2c2 --- /dev/null +++ b/docs/em/docs/deployment/https.md @@ -0,0 +1,190 @@ +# 🔃 🇺🇸🔍 + +⚫️ ⏩ 🤔 👈 🇺🇸🔍 🕳 👈 "🛠️" ⚖️ 🚫. + +✋️ ⚫️ 🌌 🌖 🏗 🌘 👈. + +!!! tip + 🚥 👆 🏃 ⚖️ 🚫 💅, 😣 ⏮️ ⏭ 📄 🔁 🔁 👩‍🌾 ⚒ 🌐 🆙 ⏮️ 🎏 ⚒. + +**💡 🔰 🇺🇸🔍**, ⚪️➡️ 🏬 🤔, ✅ https://howhttps.works/. + +🔜, ⚪️➡️ **👩‍💻 🤔**, 📥 📚 👜 ✔️ 🤯 ⏪ 💭 🔃 🇺🇸🔍: + +* 🇺🇸🔍, **💽** 💪 **✔️ "📄"** 🏗 **🥉 🥳**. + * 📚 📄 🤙 **🏆** ⚪️➡️ 🥉 🥳, 🚫 "🏗". +* 📄 ✔️ **1️⃣2️⃣🗓️**. + * 👫 **🕛**. + * & ⤴️ 👫 💪 **♻**, **🏆 🔄** ⚪️➡️ 🥉 🥳. +* 🔐 🔗 🔨 **🕸 🎚**. + * 👈 1️⃣ 🧽 **🔛 🇺🇸🔍**. + * , **📄 & 🔐** 🍵 🔨 **⏭ 🇺🇸🔍**. +* **🕸 🚫 💭 🔃 "🆔"**. 🕴 🔃 📢 📢. + * ℹ 🔃 **🎯 🆔** 📨 🚶 **🇺🇸🔍 💽**. +* **🇺🇸🔍 📄** "✔" **🎯 🆔**, ✋️ 🛠️ & 🔐 🔨 🕸 🎚, **⏭ 💭** ❔ 🆔 ➖ 🙅 ⏮️. +* **🔢**, 👈 🔜 ⛓ 👈 👆 💪 🕴 ✔️ **1️⃣ 🇺🇸🔍 📄 📍 📢 📢**. + * 🙅‍♂ 🤔 ❔ 🦏 👆 💽 ⚖️ ❔ 🤪 🔠 🈸 👆 ✔️ 🔛 ⚫️ 💪. + * 📤 **⚗** 👉, 👐. +* 📤 **↔** **🤝** 🛠️ (1️⃣ 🚚 🔐 🕸 🎚, ⏭ 🇺🇸🔍) 🤙 **👲**. + * 👉 👲 ↔ ✔ 1️⃣ 👁 💽 (⏮️ **👁 📢 📢**) ✔️ **📚 🇺🇸🔍 📄** & 🍦 **💗 🇺🇸🔍 🆔/🈸**. + * 👉 👷, **👁** 🦲 (📋) 🏃 🔛 💽, 👂 🔛 **📢 📢 📢**, 🔜 ✔️ **🌐 🇺🇸🔍 📄** 💽. +* **⏮️** 🏆 🔐 🔗, 📻 🛠️ **🇺🇸🔍**. + * 🎚 **🗜**, ✋️ 👫 ➖ 📨 ⏮️ **🇺🇸🔍 🛠️**. + +⚫️ ⚠ 💡 ✔️ **1️⃣ 📋/🇺🇸🔍 💽** 🏃 🔛 💽 (🎰, 🦠, ♒️.) & **🛠️ 🌐 🇺🇸🔍 🍕**: 📨 **🗜 🇺🇸🔍 📨**, 📨 **🗜 🇺🇸🔍 📨** ☑ 🇺🇸🔍 🈸 🏃 🎏 💽 ( **FastAPI** 🈸, 👉 💼), ✊ **🇺🇸🔍 📨** ⚪️➡️ 🈸, **🗜 ⚫️** ⚙️ ☑ **🇺🇸🔍 📄** & 📨 ⚫️ 🔙 👩‍💻 ⚙️ **🇺🇸🔍**. 👉 💽 🛎 🤙 **🤝 ❎ 🗳**. + +🎛 👆 💪 ⚙️ 🤝 ❎ 🗳: + +* Traefik (👈 💪 🍵 📄 🔕) +* 📥 (👈 💪 🍵 📄 🔕) +* 👌 +* ✳ + +## ➡️ 🗜 + +⏭ ➡️ 🗜, 👫 **🇺🇸🔍 📄** 💲 💙 🥉 🥳. + +🛠️ 📎 1️⃣ 👫 📄 ⚙️ ⚠, 🚚 📠 & 📄 😥. + +✋️ ⤴️ **➡️ 🗜** ✍. + +⚫️ 🏗 ⚪️➡️ 💾 🏛. ⚫️ 🚚 **🇺🇸🔍 📄 🆓**, 🏧 🌌. 👫 📄 ⚙️ 🌐 🐩 🔐 💂‍♂, & 📏-🖖 (🔃 3️⃣ 🗓️), **💂‍♂ 🤙 👍** ↩️ 👫 📉 🔆. + +🆔 🔐 ✔ & 📄 🏗 🔁. 👉 ✔ 🏧 🔕 👫 📄. + +💭 🏧 🛠️ & 🔕 👫 📄 👈 👆 💪 ✔️ **🔐 🇺🇸🔍, 🆓, ♾**. + +## 🇺🇸🔍 👩‍💻 + +📥 🖼 ❔ 🇺🇸🔍 🛠️ 💪 👀 💖, 🔁 🔁, 💸 🙋 ✴️ 💭 ⚠ 👩‍💻. + +### 🆔 📛 + +⚫️ 🔜 🎲 🌐 ▶️ 👆 **🏗** **🆔 📛**. ⤴️, 👆 🔜 🔗 ⚫️ 🏓 💽 (🎲 👆 🎏 ☁ 🐕‍🦺). + +👆 🔜 🎲 🤚 ☁ 💽 (🕹 🎰) ⚖️ 🕳 🎏, & ⚫️ 🔜 ✔️ 🔧 **📢 📢 📢**. + +🏓 💽(Ⓜ) 👆 🔜 🔗 ⏺ ("`A record`") ☝ **👆 🆔** 📢 **📢 📢 👆 💽**. + +👆 🔜 🎲 👉 🕐, 🥇 🕰, 🕐❔ ⚒ 🌐 🆙. + +!!! tip + 👉 🆔 📛 🍕 🌌 ⏭ 🇺🇸🔍, ✋️ 🌐 🪀 🔛 🆔 & 📢 📢, ⚫️ 💸 💬 ⚫️ 📥. + +### 🏓 + +🔜 ➡️ 🎯 🔛 🌐 ☑ 🇺🇸🔍 🍕. + +🥇, 🖥 🔜 ✅ ⏮️ **🏓 💽** ⚫️❔ **📢 🆔**, 👉 💼, `someapp.example.com`. + +🏓 💽 🔜 💬 🖥 ⚙️ 🎯 **📢 📢**. 👈 🔜 📢 📢 📢 ⚙️ 👆 💽, 👈 👆 🔗 🏓 💽. + + + +### 🤝 🤝 ▶️ + +🖥 🔜 ⤴️ 🔗 ⏮️ 👈 📢 📢 🔛 **⛴ 4️⃣4️⃣3️⃣** (🇺🇸🔍 ⛴). + +🥇 🍕 📻 🛠️ 🔗 🖖 👩‍💻 & 💽 & 💭 🔐 🔑 👫 🔜 ⚙️, ♒️. + + + +👉 🔗 🖖 👩‍💻 & 💽 🛠️ 🤝 🔗 🤙 **🤝 🤝**. + +### 🤝 ⏮️ 👲 ↔ + +**🕴 1️⃣ 🛠️** 💽 💪 👂 🔛 🎯 **⛴** 🎯 **📢 📢**. 📤 💪 🎏 🛠️ 👂 🔛 🎏 ⛴ 🎏 📢 📢, ✋️ 🕴 1️⃣ 🔠 🌀 📢 📢 & ⛴. + +🤝 (🇺🇸🔍) ⚙️ 🎯 ⛴ `443` 🔢. 👈 ⛴ 👥 🔜 💪. + +🕴 1️⃣ 🛠️ 💪 👂 🔛 👉 ⛴, 🛠️ 👈 🔜 ⚫️ 🔜 **🤝 ❎ 🗳**. + +🤝 ❎ 🗳 🔜 ✔️ 🔐 1️⃣ ⚖️ 🌅 **🤝 📄** (🇺🇸🔍 📄). + +⚙️ **👲 ↔** 🔬 🔛, 🤝 ❎ 🗳 🔜 ✅ ❔ 🤝 (🇺🇸🔍) 📄 💪 ⚫️ 🔜 ⚙️ 👉 🔗, ⚙️ 1️⃣ 👈 🏏 🆔 📈 👩‍💻. + +👉 💼, ⚫️ 🔜 ⚙️ 📄 `someapp.example.com`. + + + +👩‍💻 ⏪ **💙** 👨‍💼 👈 🏗 👈 🤝 📄 (👉 💼 ➡️ 🗜, ✋️ 👥 🔜 👀 🔃 👈 ⏪), ⚫️ 💪 **✔** 👈 📄 ☑. + +⤴️, ⚙️ 📄, 👩‍💻 & 🤝 ❎ 🗳 **💭 ❔ 🗜** 🎂 **🕸 📻**. 👉 🏁 **🤝 🤝** 🍕. + +⏮️ 👉, 👩‍💻 & 💽 ✔️ **🗜 🕸 🔗**, 👉 ⚫️❔ 🤝 🚚. & ⤴️ 👫 💪 ⚙️ 👈 🔗 ▶️ ☑ **🇺🇸🔍 📻**. + +& 👈 ⚫️❔ **🇺🇸🔍** , ⚫️ ✅ **🇺🇸🔍** 🔘 **🔐 🤝 🔗** ↩️ 😁 (💽) 🕸 🔗. + +!!! tip + 👀 👈 🔐 📻 🔨 **🕸 🎚**, 🚫 🇺🇸🔍 🎚. + +### 🇺🇸🔍 📨 + +🔜 👈 👩‍💻 & 💽 (🎯 🖥 & 🤝 ❎ 🗳) ✔️ **🗜 🕸 🔗**, 👫 💪 ▶️ **🇺🇸🔍 📻**. + +, 👩‍💻 📨 **🇺🇸🔍 📨**. 👉 🇺🇸🔍 📨 🔘 🗜 🤝 🔗. + + + +### 🗜 📨 + +🤝 ❎ 🗳 🔜 ⚙️ 🔐 ✔ **🗜 📨**, & 🔜 📶 **✅ (🗜) 🇺🇸🔍 📨** 🛠️ 🏃 🈸 (🖼 🛠️ ⏮️ Uvicorn 🏃‍♂ FastAPI 🈸). + + + +### 🇺🇸🔍 📨 + +🈸 🔜 🛠️ 📨 & 📨 **✅ (💽) 🇺🇸🔍 📨** 🤝 ❎ 🗳. + + + +### 🇺🇸🔍 📨 + +🤝 ❎ 🗳 🔜 ⤴️ **🗜 📨** ⚙️ ⚛ ✔ ⏭ (👈 ▶️ ⏮️ 📄 `someapp.example.com`), & 📨 ⚫️ 🔙 🖥. + +⏭, 🖥 🔜 ✔ 👈 📨 ☑ & 🗜 ⏮️ ▶️️ 🔐 🔑, ♒️. ⚫️ 🔜 ⤴️ **🗜 📨** & 🛠️ ⚫️. + + + +👩‍💻 (🖥) 🔜 💭 👈 📨 👟 ⚪️➡️ ☑ 💽 ↩️ ⚫️ ⚙️ ⚛ 👫 ✔ ⚙️ **🇺🇸🔍 📄** ⏭. + +### 💗 🈸 + +🎏 💽 (⚖️ 💽), 📤 💪 **💗 🈸**, 🖼, 🎏 🛠️ 📋 ⚖️ 💽. + +🕴 1️⃣ 🛠️ 💪 🚚 🎯 📢 & ⛴ (🤝 ❎ 🗳 👆 🖼) ✋️ 🎏 🈸/🛠️ 💪 🏃 🔛 💽(Ⓜ) 💁‍♂️, 📏 👫 🚫 🔄 ⚙️ 🎏 **🌀 📢 📢 & ⛴**. + + + +👈 🌌, 🤝 ❎ 🗳 💪 🍵 🇺🇸🔍 & 📄 **💗 🆔**, 💗 🈸, & ⤴️ 📶 📨 ▶️️ 🈸 🔠 💼. + +### 📄 🔕 + +☝ 🔮, 🔠 📄 🔜 **🕛** (🔃 3️⃣ 🗓️ ⏮️ 🏗 ⚫️). + +& ⤴️, 📤 🔜 ➕1️⃣ 📋 (💼 ⚫️ ➕1️⃣ 📋, 💼 ⚫️ 💪 🎏 🤝 ❎ 🗳) 👈 🔜 💬 ➡️ 🗜, & ♻ 📄(Ⓜ). + + + +**🤝 📄** **🔗 ⏮️ 🆔 📛**, 🚫 ⏮️ 📢 📢. + +, ♻ 📄, 🔕 📋 💪 **🎦** 🛃 (➡️ 🗜) 👈 ⚫️ 👐 **"👍" & 🎛 👈 🆔**. + +👈, & 🏗 🎏 🈸 💪, 📤 📚 🌌 ⚫️ 💪 ⚫️. 🌟 🌌: + +* **🔀 🏓 ⏺**. + * 👉, 🔕 📋 💪 🐕‍🦺 🔗 🏓 🐕‍🦺,, ⚓️ 🔛 🏓 🐕‍🦺 👆 ⚙️, 👉 5️⃣📆 ⚖️ 💪 🚫 🎛. +* **🏃 💽** (🌘 ⏮️ 📄 🛠️ 🛠️) 🔛 📢 📢 📢 🔗 ⏮️ 🆔. + * 👥 💬 🔛, 🕴 1️⃣ 🛠️ 💪 👂 🔛 🎯 📢 & ⛴. + * 👉 1️⃣ 🤔 ⚫️❔ ⚫️ 📶 ⚠ 🕐❔ 🎏 🤝 ❎ 🗳 ✊ 💅 📄 🔕 🛠️. + * ⏪, 👆 💪 ✔️ ⛔️ 🤝 ❎ 🗳 😖, ▶️ 🔕 📋 📎 📄, ⤴️ 🔗 👫 ⏮️ 🤝 ❎ 🗳, & ⤴️ ⏏ 🤝 ❎ 🗳. 👉 🚫 💯, 👆 📱(Ⓜ) 🔜 🚫 💪 ⏮️ 🕰 👈 🤝 ❎ 🗳 📆. + +🌐 👉 🔕 🛠️, ⏪ 🍦 📱, 1️⃣ 👑 🤔 ⚫️❔ 👆 🔜 💚 ✔️ **🎏 ⚙️ 🍵 🇺🇸🔍** ⏮️ 🤝 ❎ 🗳 ↩️ ⚙️ 🤝 📄 ⏮️ 🈸 💽 🔗 (✅ Uvicorn). + +## 🌃 + +✔️ **🇺🇸🔍** 📶 ⚠, & **🎯** 🏆 💼. 🌅 🎯 👆 👩‍💻 ✔️ 🚮 🤭 🇺🇸🔍 🔃 **🤔 👉 🔧** & ❔ 👫 👷. + +✋️ 🕐 👆 💭 🔰 ℹ **🇺🇸🔍 👩‍💻** 👆 💪 💪 🌀 & 🔗 🎏 🧰 ℹ 👆 🛠️ 🌐 🙅 🌌. + +⏭ 📃, 👤 🔜 🎦 👆 📚 🧱 🖼 ❔ ⚒ 🆙 **🇺🇸🔍** **FastAPI** 🈸. 👶 diff --git a/docs/em/docs/deployment/index.md b/docs/em/docs/deployment/index.md new file mode 100644 index 000000000..1010c589f --- /dev/null +++ b/docs/em/docs/deployment/index.md @@ -0,0 +1,21 @@ +# 🛠️ - 🎶 + +🛠️ **FastAPI** 🈸 📶 ⏩. + +## ⚫️❔ 🔨 🛠️ ⛓ + +**🛠️** 🈸 ⛓ 🎭 💪 📶 ⚒ ⚫️ **💪 👩‍💻**. + +**🕸 🛠️**, ⚫️ 🛎 🔌 🚮 ⚫️ **🛰 🎰**, ⏮️ **💽 📋** 👈 🚚 👍 🎭, ⚖, ♒️, 👈 👆 **👩‍💻** 💪 **🔐** 🈸 ♻ & 🍵 🔁 ⚖️ ⚠. + +👉 🔅 **🛠️** ▶️, 🌐❔ 👆 🕧 🔀 📟, 💔 ⚫️ & ♻ ⚫️, ⛔️ & 🔁 🛠️ 💽, ♒️. + +## 🛠️ 🎛 + +📤 📚 🌌 ⚫️ ⚓️ 🔛 👆 🎯 ⚙️ 💼 & 🧰 👈 👆 ⚙️. + +👆 💪 **🛠️ 💽** 👆 ⚙️ 🌀 🧰, 👆 💪 ⚙️ **☁ 🐕‍🦺** 👈 🔨 🍕 👷 👆, ⚖️ 🎏 💪 🎛. + +👤 🔜 🎦 👆 👑 🔧 👆 🔜 🎲 ✔️ 🤯 🕐❔ 🛠️ **FastAPI** 🈸 (👐 🌅 ⚫️ ✔ 🙆 🎏 🆎 🕸 🈸). + +👆 🔜 👀 🌖 ℹ ✔️ 🤯 & ⚒ ⚫️ ⏭ 📄. 👶 diff --git a/docs/em/docs/deployment/manually.md b/docs/em/docs/deployment/manually.md new file mode 100644 index 000000000..f27b423e2 --- /dev/null +++ b/docs/em/docs/deployment/manually.md @@ -0,0 +1,145 @@ +# 🏃 💽 ❎ - Uvicorn + +👑 👜 👆 💪 🏃 **FastAPI** 🈸 🛰 💽 🎰 🔫 💽 📋 💖 **Uvicorn**. + +📤 3️⃣ 👑 🎛: + +* Uvicorn: ↕ 🎭 🔫 💽. +* Hypercorn: 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣ & 🎻 👪 🎏 ⚒. +* 👸: 🔫 💽 🏗 ✳ 📻. + +## 💽 🎰 & 💽 📋 + +📤 🤪 ℹ 🔃 📛 ✔️ 🤯. 👶 + +🔤 "**💽**" 🛎 ⚙️ 🔗 👯‍♂️ 🛰/☁ 💻 (⚛ ⚖️ 🕹 🎰) & 📋 👈 🏃‍♂ 🔛 👈 🎰 (✅ Uvicorn). + +✔️ 👈 🤯 🕐❔ 👆 ✍ "💽" 🏢, ⚫️ 💪 🔗 1️⃣ 📚 2️⃣ 👜. + +🕐❔ 🔗 🛰 🎰, ⚫️ ⚠ 🤙 ⚫️ **💽**, ✋️ **🎰**, **💾** (🕹 🎰), **🕸**. 👈 🌐 🔗 🆎 🛰 🎰, 🛎 🏃‍♂ 💾, 🌐❔ 👆 🏃 📋. + +## ❎ 💽 📋 + +👆 💪 ❎ 🔫 🔗 💽 ⏮️: + +=== "Uvicorn" + + * Uvicorn, 🌩-⏩ 🔫 💽, 🏗 🔛 uvloop & httptool. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip + ❎ `standard`, Uvicorn 🔜 ❎ & ⚙️ 👍 ➕ 🔗. + + 👈 ✅ `uvloop`, ↕-🎭 💧-♻ `asyncio`, 👈 🚚 🦏 🛠️ 🎭 📈. + +=== "Hypercorn" + + * Hypercorn, 🔫 💽 🔗 ⏮️ 🇺🇸🔍/2️⃣. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...⚖️ 🙆 🎏 🔫 💽. + +## 🏃 💽 📋 + +👆 💪 ⤴️ 🏃 👆 🈸 🎏 🌌 👆 ✔️ ⌛ 🔰, ✋️ 🍵 `--reload` 🎛, ✅: + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning + 💭 ❎ `--reload` 🎛 🚥 👆 ⚙️ ⚫️. + + `--reload` 🎛 🍴 🌅 🌅 ℹ, 🌅 ⚠, ♒️. + + ⚫️ ℹ 📚 ⏮️ **🛠️**, ✋️ 👆 **🚫🔜 🚫** ⚙️ ⚫️ **🏭**. + +## Hypercorn ⏮️ 🎻 + +💃 & **FastAPI** ⚓️ 🔛 AnyIO, ❔ ⚒ 👫 🔗 ⏮️ 👯‍♂️ 🐍 🐩 🗃 & 🎻. + +👐, Uvicorn ⏳ 🕴 🔗 ⏮️ ✳, & ⚫️ 🛎 ⚙️ `uvloop`, ↕-🎭 💧-♻ `asyncio`. + +✋️ 🚥 👆 💚 🔗 ⚙️ **🎻**, ⤴️ 👆 💪 ⚙️ **Hypercorn** ⚫️ 🐕‍🦺 ⚫️. 👶 + +### ❎ Hypercorn ⏮️ 🎻 + +🥇 👆 💪 ❎ Hypercorn ⏮️ 🎻 🐕‍🦺: + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### 🏃 ⏮️ 🎻 + +⤴️ 👆 💪 🚶‍♀️ 📋 ⏸ 🎛 `--worker-class` ⏮️ 💲 `trio`: + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +& 👈 🔜 ▶️ Hypercorn ⏮️ 👆 📱 ⚙️ 🎻 👩‍💻. + +🔜 👆 💪 ⚙️ 🎻 🔘 👆 📱. ⚖️ 👍, 👆 💪 ⚙️ AnyIO, 🚧 👆 📟 🔗 ⏮️ 👯‍♂️ 🎻 & ✳. 👶 + +## 🛠️ 🔧 + +👫 🖼 🏃 💽 📋 (📧.Ⓜ Uvicorn), ▶️ **👁 🛠️**, 👂 🔛 🌐 📢 (`0.0.0.0`) 🔛 🔁 ⛴ (✅ `80`). + +👉 🔰 💭. ✋️ 👆 🔜 🎲 💚 ✊ 💅 🌖 👜, 💖: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* 🧬 (🔢 🛠️ 🏃) +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +👤 🔜 💬 👆 🌅 🔃 🔠 👫 🔧, ❔ 💭 🔃 👫, & 🧱 🖼 ⏮️ 🎛 🍵 👫 ⏭ 📃. 👶 diff --git a/docs/em/docs/deployment/server-workers.md b/docs/em/docs/deployment/server-workers.md new file mode 100644 index 000000000..ca068d744 --- /dev/null +++ b/docs/em/docs/deployment/server-workers.md @@ -0,0 +1,178 @@ +# 💽 👨‍🏭 - 🐁 ⏮️ Uvicorn + +➡️ ✅ 🔙 👈 🛠️ 🔧 ⚪️➡️ ⏭: + +* 💂‍♂ - 🇺🇸🔍 +* 🏃‍♂ 🔛 🕴 +* ⏏ +* **🧬 (🔢 🛠️ 🏃)** +* 💾 +* ⏮️ 🔁 ⏭ ▶️ + +🆙 👉 ☝, ⏮️ 🌐 🔰 🩺, 👆 ✔️ 🎲 🏃‍♂ **💽 📋** 💖 Uvicorn, 🏃‍♂ **👁 🛠️**. + +🕐❔ 🛠️ 🈸 👆 🔜 🎲 💚 ✔️ **🧬 🛠️** ✊ 📈 **💗 🐚** & 💪 🍵 🌅 📨. + +👆 👀 ⏮️ 📃 🔃 [🛠️ 🔧](./concepts.md){.internal-link target=_blank}, 📤 💗 🎛 👆 💪 ⚙️. + +📥 👤 🔜 🎦 👆 ❔ ⚙️ **🐁** ⏮️ **Uvicorn 👨‍🏭 🛠️**. + +!!! info + 🚥 👆 ⚙️ 📦, 🖼 ⏮️ ☁ ⚖️ Kubernete, 👤 🔜 💬 👆 🌅 🔃 👈 ⏭ 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + + 🎯, 🕐❔ 🏃 🔛 **Kubernete** 👆 🔜 🎲 **🚫** 💚 ⚙️ 🐁 & ↩️ 🏃 **👁 Uvicorn 🛠️ 📍 📦**, ✋️ 👤 🔜 💬 👆 🔃 ⚫️ ⏪ 👈 📃. + +## 🐁 ⏮️ Uvicorn 👨‍🏭 + +**🐁** ✴️ 🈸 💽 ⚙️ **🇨🇻 🐩**. 👈 ⛓ 👈 🐁 💪 🍦 🈸 💖 🏺 & ✳. 🐁 ⚫️ 🚫 🔗 ⏮️ **FastAPI**, FastAPI ⚙️ 🆕 **🔫 🐩**. + +✋️ 🐁 🐕‍🦺 👷 **🛠️ 👨‍💼** & 🤝 👩‍💻 💬 ⚫️ ❔ 🎯 **👨‍🏭 🛠️ 🎓** ⚙️. ⤴️ 🐁 🔜 ▶️ 1️⃣ ⚖️ 🌖 **👨‍🏭 🛠️** ⚙️ 👈 🎓. + +& **Uvicorn** ✔️ **🐁-🔗 👨‍🏭 🎓**. + +⚙️ 👈 🌀, 🐁 🔜 🚫 **🛠️ 👨‍💼**, 👂 🔛 **⛴** & **📢**. & ⚫️ 🔜 **📶** 📻 👨‍🏭 🛠️ 🏃 **Uvicorn 🎓**. + +& ⤴️ 🐁-🔗 **Uvicorn 👨‍🏭** 🎓 🔜 🈚 🏭 📊 📨 🐁 🔫 🐩 FastAPI ⚙️ ⚫️. + +## ❎ 🐁 & Uvicorn + +
+ +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +
+ +👈 🔜 ❎ 👯‍♂️ Uvicorn ⏮️ `standard` ➕ 📦 (🤚 ↕ 🎭) & 🐁. + +## 🏃 🐁 ⏮️ Uvicorn 👨‍🏭 + +⤴️ 👆 💪 🏃 🐁 ⏮️: + +
+ +```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. +``` + +
+ +➡️ 👀 ⚫️❔ 🔠 👈 🎛 ⛓: + +* `main:app`: 👉 🎏 ❕ ⚙️ Uvicorn, `main` ⛓ 🐍 🕹 📛 "`main`",, 📁 `main.py`. & `app` 📛 🔢 👈 **FastAPI** 🈸. + * 👆 💪 🌈 👈 `main:app` 🌓 🐍 `import` 📄 💖: + + ```Python + from main import app + ``` + + * , ❤ `main:app` 🔜 🌓 🐍 `import` 🍕 `from main import app`. +* `--workers`: 🔢 👨‍🏭 🛠️ ⚙️, 🔠 🔜 🏃 Uvicorn 👨‍🏭, 👉 💼, 4️⃣ 👨‍🏭. +* `--worker-class`: 🐁-🔗 👨‍🏭 🎓 ⚙️ 👨‍🏭 🛠️. + * 📥 👥 🚶‍♀️ 🎓 👈 🐁 💪 🗄 & ⚙️ ⏮️: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: 👉 💬 🐁 📢 & ⛴ 👂, ⚙️ ❤ (`:`) 🎏 📢 & ⛴. + * 🚥 👆 🏃‍♂ Uvicorn 🔗, ↩️ `--bind 0.0.0.0:80` (🐁 🎛) 👆 🔜 ⚙️ `--host 0.0.0.0` & `--port 80`. + +🔢, 👆 💪 👀 👈 ⚫️ 🎦 **🕹** (🛠️ 🆔) 🔠 🛠️ (⚫️ 🔢). + +👆 💪 👀 👈: + +* 🐁 **🛠️ 👨‍💼** ▶️ ⏮️ 🕹 `19499` (👆 💼 ⚫️ 🔜 🎏 🔢). +* ⤴️ ⚫️ ▶️ `Listening at: http://0.0.0.0:80`. +* ⤴️ ⚫️ 🔍 👈 ⚫️ ✔️ ⚙️ 👨‍🏭 🎓 `uvicorn.workers.UvicornWorker`. +* & ⤴️ ⚫️ ▶️ **4️⃣ 👨‍🏭**, 🔠 ⏮️ 🚮 👍 🕹: `19511`, `19513`, `19514`, & `19515`. + +🐁 🔜 ✊ 💅 🛠️ **☠️ 🛠️** & **🔁** 🆕 🕐 🚥 💚 🚧 🔢 👨‍🏭. 👈 ℹ 🍕 ⏮️ **⏏** 🔧 ⚪️➡️ 📇 🔛. + +👐, 👆 🔜 🎲 💚 ✔️ 🕳 🏞 ⚒ 💭 **⏏ 🐁** 🚥 💪, & **🏃 ⚫️ 🔛 🕴**, ♒️. + +## Uvicorn ⏮️ 👨‍🏭 + +Uvicorn ✔️ 🎛 ▶️ & 🏃 📚 **👨‍🏭 🛠️**. + +👐, 🔜, Uvicorn 🛠️ 🚚 👨‍🏭 🛠️ 🌅 📉 🌘 🐁. , 🚥 👆 💚 ✔️ 🛠️ 👨‍💼 👉 🎚 (🐍 🎚), ⤴️ ⚫️ 💪 👍 🔄 ⏮️ 🐁 🛠️ 👨‍💼. + +🙆 💼, 👆 🔜 🏃 ⚫️ 💖 👉: + +
+ +```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. +``` + +
+ +🕴 🆕 🎛 📥 `--workers` 💬 Uvicorn ▶️ 4️⃣ 👨‍🏭 🛠️. + +👆 💪 👀 👈 ⚫️ 🎦 **🕹** 🔠 🛠️, `27365` 👪 🛠️ (👉 **🛠️ 👨‍💼**) & 1️⃣ 🔠 👨‍🏭 🛠️: `27368`, `27369`, `27370`, & `27367`. + +## 🛠️ 🔧 + +📥 👆 👀 ❔ ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ **Uvicorn 👨‍🏭 🛠️** **🔁** 🛠️ 🈸, ✊ 📈 **💗 🐚** 💽, & 💪 🍦 **🌅 📨**. + +⚪️➡️ 📇 🛠️ 🔧 ⚪️➡️ 🔛, ⚙️ 👨‍🏭 🔜 ✴️ ℹ ⏮️ **🧬** 🍕, & 🐥 🍖 ⏮️ **⏏**, ✋️ 👆 💪 ✊ 💅 🎏: + +* **💂‍♂ - 🇺🇸🔍** +* **🏃‍♂ 🔛 🕴** +* ***⏏*** +* 🧬 (🔢 🛠️ 🏃) +* **💾** +* **⏮️ 🔁 ⏭ ▶️** + +## 📦 & ☁ + +⏭ 📃 🔃 [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank} 👤 🔜 💬 🎛 👆 💪 ⚙️ 🍵 🎏 **🛠️ 🔧**. + +👤 🔜 🎦 👆 **🛂 ☁ 🖼** 👈 🔌 **🐁 ⏮️ Uvicorn 👨‍🏭** & 🔢 📳 👈 💪 ⚠ 🙅 💼. + +📤 👤 🔜 🎦 👆 ❔ **🏗 👆 👍 🖼 ⚪️➡️ 🖌** 🏃 👁 Uvicorn 🛠️ (🍵 🐁). ⚫️ 🙅 🛠️ & 🎲 ⚫️❔ 👆 🔜 💚 🕐❔ ⚙️ 📎 📦 🧾 ⚙️ 💖 **Kubernete**. + +## 🌃 + +👆 💪 ⚙️ **🐁** (⚖️ Uvicorn) 🛠️ 👨‍💼 ⏮️ Uvicorn 👨‍🏭 ✊ 📈 **👁-🐚 💽**, 🏃 **💗 🛠️ 🔗**. + +👆 💪 ⚙️ 👉 🧰 & 💭 🚥 👆 ⚒ 🆙 **👆 👍 🛠️ ⚙️** ⏪ ✊ 💅 🎏 🛠️ 🔧 👆. + +✅ 👅 ⏭ 📃 💡 🔃 **FastAPI** ⏮️ 📦 (✅ ☁ & Kubernete). 👆 🔜 👀 👈 👈 🧰 ✔️ 🙅 🌌 ❎ 🎏 **🛠️ 🔧** 👍. 👶 diff --git a/docs/em/docs/deployment/versions.md b/docs/em/docs/deployment/versions.md new file mode 100644 index 000000000..8bfdf9731 --- /dev/null +++ b/docs/em/docs/deployment/versions.md @@ -0,0 +1,87 @@ +# 🔃 FastAPI ⏬ + +**FastAPI** ⏪ ➖ ⚙️ 🏭 📚 🈸 & ⚙️. & 💯 💰 🚧 1️⃣0️⃣0️⃣ 💯. ✋️ 🚮 🛠️ 🚚 🔜. + +🆕 ⚒ 🚮 🛎, 🐛 🔧 🛎, & 📟 🔁 📉. + +👈 ⚫️❔ ⏮️ ⏬ `0.x.x`, 👉 🎨 👈 🔠 ⏬ 💪 ⚠ ✔️ 💔 🔀. 👉 ⏩ ⚛ 🛠️ 🏛. + +👆 💪 ✍ 🏭 🈸 ⏮️ **FastAPI** ▶️️ 🔜 (& 👆 ✔️ 🎲 🔨 ⚫️ 🕰), 👆 ✔️ ⚒ 💭 👈 👆 ⚙️ ⏬ 👈 👷 ☑ ⏮️ 🎂 👆 📟. + +## 📌 👆 `fastapi` ⏬ + +🥇 👜 👆 🔜 "📌" ⏬ **FastAPI** 👆 ⚙️ 🎯 📰 ⏬ 👈 👆 💭 👷 ☑ 👆 🈸. + +🖼, ➡️ 💬 👆 ⚙️ ⏬ `0.45.0` 👆 📱. + +🚥 👆 ⚙️ `requirements.txt` 📁 👆 💪 ✔ ⏬ ⏮️: + +```txt +fastapi==0.45.0 +``` + +👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⚫️❔ ⏬ `0.45.0`. + +⚖️ 👆 💪 📌 ⚫️ ⏮️: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +👈 🔜 ⛓ 👈 👆 🔜 ⚙️ ⏬ `0.45.0` ⚖️ 🔛, ✋️ 🌘 🌘 `0.46.0`, 🖼, ⏬ `0.45.2` 🔜 🚫. + +🚥 👆 ⚙️ 🙆 🎏 🧰 🛠️ 👆 👷‍♂, 💖 🎶, Pipenv, ⚖️ 🎏, 👫 🌐 ✔️ 🌌 👈 👆 💪 ⚙️ 🔬 🎯 ⏬ 👆 📦. + +## 💪 ⏬ + +👆 💪 👀 💪 ⏬ (✅ ✅ ⚫️❔ ⏮️ 📰) [🚀 🗒](../release-notes.md){.internal-link target=_blank}. + +## 🔃 ⏬ + +📄 ⚛ 🛠️ 🏛, 🙆 ⏬ 🔛 `1.0.0` 💪 ⚠ 🚮 💔 🔀. + +FastAPI ⏩ 🏛 👈 🙆 "🐛" ⏬ 🔀 🐛 🔧 & 🚫-💔 🔀. + +!!! tip + "🐛" 🏁 🔢, 🖼, `0.2.3`, 🐛 ⏬ `3`. + +, 👆 🔜 💪 📌 ⏬ 💖: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +💔 🔀 & 🆕 ⚒ 🚮 "🇺🇲" ⏬. + +!!! tip + "🇺🇲" 🔢 🖕, 🖼, `0.2.3`, 🇺🇲 ⏬ `2`. + +## ♻ FastAPI ⏬ + +👆 🔜 🚮 💯 👆 📱. + +⏮️ **FastAPI** ⚫️ 📶 ⏩ (👏 💃), ✅ 🩺: [🔬](../tutorial/testing.md){.internal-link target=_blank} + +⏮️ 👆 ✔️ 💯, ⤴️ 👆 💪 ♻ **FastAPI** ⏬ 🌖 ⏮️ 1️⃣, & ⚒ 💭 👈 🌐 👆 📟 👷 ☑ 🏃 👆 💯. + +🚥 🌐 👷, ⚖️ ⏮️ 👆 ⚒ 💪 🔀, & 🌐 👆 💯 🚶‍♀️, ⤴️ 👆 💪 📌 👆 `fastapi` 👈 🆕 ⏮️ ⏬. + +## 🔃 💃 + +👆 🚫🔜 🚫 📌 ⏬ `starlette`. + +🎏 ⏬ **FastAPI** 🔜 ⚙️ 🎯 🆕 ⏬ 💃. + +, 👆 💪 ➡️ **FastAPI** ⚙️ ☑ 💃 ⏬. + +## 🔃 Pydantic + +Pydantic 🔌 💯 **FastAPI** ⏮️ 🚮 👍 💯, 🆕 ⏬ Pydantic (🔛 `1.0.0`) 🕧 🔗 ⏮️ FastAPI. + +👆 💪 📌 Pydantic 🙆 ⏬ 🔛 `1.0.0` 👈 👷 👆 & 🔛 `2.0.0`. + +🖼: + +```txt +pydantic>=1.2.0,<2.0.0 +``` diff --git a/docs/em/docs/external-links.md b/docs/em/docs/external-links.md new file mode 100644 index 000000000..4440b1f12 --- /dev/null +++ b/docs/em/docs/external-links.md @@ -0,0 +1,91 @@ +# 🔢 🔗 & 📄 + +**FastAPI** ✔️ 👑 👪 🕧 💗. + +📤 📚 🏤, 📄, 🧰, & 🏗, 🔗 **FastAPI**. + +📥 ❌ 📇 👫. + +!!! tip + 🚥 👆 ✔️ 📄, 🏗, 🧰, ⚖️ 🕳 🔗 **FastAPI** 👈 🚫 📇 📥, ✍ 🚲 📨 ❎ ⚫️. + +## 📄 + +### 🇪🇸 + +{% if external_links %} +{% for article in external_links.articles.english %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +### 🇯🇵 + +{% if external_links %} +{% for article in external_links.articles.japanese %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +### 🇻🇳 + +{% if external_links %} +{% for article in external_links.articles.vietnamese %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +### 🇷🇺 + +{% if external_links %} +{% for article in external_links.articles.russian %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +### 🇩🇪 + +{% if external_links %} +{% for article in external_links.articles.german %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +### 🇹🇼 + +{% if external_links %} +{% for article in external_links.articles.taiwanese %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +## 📻 + +{% if external_links %} +{% for article in external_links.podcasts.english %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +## 💬 + +{% if external_links %} +{% for article in external_links.talks.english %} + +* {{ article.title }} {{ article.author }}. +{% endfor %} +{% endif %} + +## 🏗 + +⏪ 📂 🏗 ⏮️ ❔ `fastapi`: + +
+
diff --git a/docs/em/docs/fastapi-people.md b/docs/em/docs/fastapi-people.md new file mode 100644 index 000000000..dc94d80da --- /dev/null +++ b/docs/em/docs/fastapi-people.md @@ -0,0 +1,178 @@ +# FastAPI 👫👫 + +FastAPI ✔️ 🎆 👪 👈 🙋 👫👫 ⚪️➡️ 🌐 🖥. + +## 👼 - 🐛 + +🙋 ❗ 👶 + +👉 👤: + +{% if people %} +
+{% for user in people.maintainers %} + +
@{{ user.login }}
❔: {{ user.answers }}
🚲 📨: {{ user.prs }}
+{% endfor %} + +
+{% endif %} + +👤 👼 & 🐛 **FastAPI**. 👆 💪 ✍ 🌅 🔃 👈 [ℹ FastAPI - 🤚 ℹ - 🔗 ⏮️ 📕](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. + +...✋️ 📥 👤 💚 🎦 👆 👪. + +--- + +**FastAPI** 📨 📚 🐕‍🦺 ⚪️➡️ 👪. & 👤 💚 🎦 👫 💰. + +👫 👫👫 👈: + +* [ℹ 🎏 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank}. +* [✍ 🚲 📨](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* 📄 🚲 📨, [✴️ ⚠ ✍](contributing.md#translations){.internal-link target=_blank}. + +👏 👫. 👶 👶 + +## 🌅 🦁 👩‍💻 🏁 🗓️ + +👫 👩‍💻 👈 ✔️ [🤝 🎏 🏆 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} ⏮️ 🏁 🗓️. 👶 + +{% if people %} +
+{% for user in people.last_month_active %} + +
@{{ user.login }}
❔ 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 🕴 + +📥 **FastAPI 🕴**. 👶 + +👫 👩‍💻 👈 ✔️ [ℹ 🎏 🏆 ⏮️ ❔ 📂](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} 🔘 *🌐 🕰*. + +👫 ✔️ 🎦 🕴 🤝 📚 🎏. 👶 + +{% if people %} +
+{% for user in people.experts %} + +
@{{ user.login }}
❔ 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 🔝 👨‍🔬 + +📥 **🔝 👨‍🔬**. 👶 + +👉 👩‍💻 ✔️ [✍ 🏆 🚲 📨](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} 👈 ✔️ *🔗*. + +👫 ✔️ 📉 ℹ 📟, 🧾, ✍, ♒️. 👶 + +{% if people %} +
+{% for user in people.top_contributors %} + +
@{{ user.login }}
🚲 📨: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +📤 📚 🎏 👨‍🔬 (🌅 🌘 💯), 👆 💪 👀 👫 🌐 FastAPI 📂 👨‍🔬 📃. 👶 + +## 🔝 👨‍🔬 + +👫 👩‍💻 **🔝 👨‍🔬**. 👶 👶 + +### 📄 ✍ + +👤 🕴 💬 👩‍❤‍👨 🇪🇸 (& 🚫 📶 👍 👶). , 👨‍🔬 🕐 👈 ✔️ [**🏋️ ✔ ✍**](contributing.md#translations){.internal-link target=_blank} 🧾. 🍵 👫, 📤 🚫🔜 🧾 📚 🎏 🇪🇸. + +--- + +**🔝 👨‍🔬** 👶 👶 ✔️ 📄 🏆 🚲 📨 ⚪️➡️ 🎏, 🚚 🔆 📟, 🧾, & ✴️, **✍**. + +{% if people %} +
+{% for user in people.top_reviewers %} + +
@{{ user.login }}
📄: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## 💰 + +👫 **💰**. 👶 + +👫 🔗 👇 👷 ⏮️ **FastAPI** (& 🎏), ✴️ 🔘 📂 💰. + +{% if sponsors %} + +{% if sponsors.gold %} + +### 🌟 💰 + +{% for sponsor in sponsors.gold -%} + +{% endfor %} +{% endif %} + +{% if sponsors.silver %} + +### 🥇1st 💰 + +{% for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + +{% if sponsors.bronze %} + +### 🥈2nd 💰 + +{% for sponsor in sponsors.bronze -%} + +{% endfor %} +{% endif %} + +{% endif %} + +### 🎯 💰 + +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} + +
+ +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} + + + +{% endif %} +{% endfor %} + +
+ +{% endfor %} +{% endif %} + +## 🔃 📊 - 📡 ℹ + +👑 🎯 👉 📃 🎦 🎯 👪 ℹ 🎏. + +✴️ ✅ 🎯 👈 🛎 🌘 ⭐, & 📚 💼 🌅 😩, 💖 🤝 🎏 ⏮️ ❔ & ⚖ 🚲 📨 ⏮️ ✍. + +💽 ⚖ 🔠 🗓️, 👆 💪 ✍ ℹ 📟 📥. + +📥 👤 🎦 💰 ⚪️➡️ 💰. + +👤 🏦 ▶️️ ℹ 📊, 📄, ⚡, ♒️ (💼 🤷). diff --git a/docs/em/docs/features.md b/docs/em/docs/features.md new file mode 100644 index 000000000..19193da07 --- /dev/null +++ b/docs/em/docs/features.md @@ -0,0 +1,200 @@ +# ⚒ + +## FastAPI ⚒ + +**FastAPI** 🤝 👆 📄: + +### ⚓️ 🔛 📂 🐩 + +* 🗄 🛠️ 🏗, ✅ 📄 🛠️, 🔢, 💪 📨, 💂‍♂, ♒️. +* 🏧 📊 🏷 🧾 ⏮️ 🎻 🔗 (🗄 ⚫️ 🧢 🔛 🎻 🔗). +* 🔧 🤭 👫 🐩, ⏮️ 😔 🔬. ↩️ 👎 🧽 🔛 🔝. +* 👉 ✔ ⚙️ 🏧 **👩‍💻 📟 ⚡** 📚 🇪🇸. + +### 🏧 🩺 + +🎓 🛠️ 🧾 & 🔬 🕸 👩‍💻 🔢. 🛠️ ⚓️ 🔛 🗄, 📤 💗 🎛, 2️⃣ 🔌 🔢. + +* 🦁 🎚, ⏮️ 🎓 🔬, 🤙 & 💯 👆 🛠️ 🔗 ⚪️➡️ 🖥. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* 🎛 🛠️ 🧾 ⏮️ 📄. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 🏛 🐍 + +⚫️ 🌐 ⚓️ 🔛 🐩 **🐍 3️⃣.6️⃣ 🆎** 📄 (👏 Pydantic). 🙅‍♂ 🆕 ❕ 💡. 🐩 🏛 🐍. + +🚥 👆 💪 2️⃣ ⏲ ↗️ ❔ ⚙️ 🐍 🆎 (🚥 👆 🚫 ⚙️ FastAPI), ✅ 📏 🔰: [🐍 🆎](python-types.md){.internal-link target=_blank}. + +👆 ✍ 🐩 🐍 ⏮️ 🆎: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +👈 💪 ⤴️ ⚙️ 💖: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` ⛓: + + 🚶‍♀️ 🔑 & 💲 `second_user_data` #️⃣ 🔗 🔑-💲 ❌, 🌓: `User(id=4, name="Mary", joined="2018-11-30")` + +### 👨‍🎨 🐕‍🦺 + +🌐 🛠️ 🏗 ⏩ & 🏋️ ⚙️, 🌐 🚫 💯 🔛 💗 👨‍🎨 ⏭ ▶️ 🛠️, 🚚 🏆 🛠️ 💡. + +🏁 🐍 👩‍💻 🔬 ⚫️ 🆑 👈 🌅 ⚙️ ⚒ "✍". + +🎂 **FastAPI** 🛠️ ⚓️ 😌 👈. ✍ 👷 🌐. + +👆 🔜 🛎 💪 👟 🔙 🩺. + +📥 ❔ 👆 👨‍🎨 💪 ℹ 👆: + +* 🎙 🎙 📟: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* 🗒: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +👆 🔜 🤚 🛠️ 📟 👆 5️⃣📆 🤔 💪 ⏭. 🖼, `price` 🔑 🔘 🎻 💪 (👈 💪 ✔️ 🐦) 👈 👟 ⚪️➡️ 📨. + +🙅‍♂ 🌖 ⌨ ❌ 🔑 📛, 👟 🔙 & ➡ 🖖 🩺, ⚖️ 📜 🆙 & 🔽 🔎 🚥 👆 😒 ⚙️ `username` ⚖️ `user_name`. + +### 📏 + +⚫️ ✔️ 🤔 **🔢** 🌐, ⏮️ 📦 📳 🌐. 🌐 🔢 💪 👌-🎧 ⚫️❔ 👆 💪 & 🔬 🛠️ 👆 💪. + +✋️ 🔢, ⚫️ 🌐 **"👷"**. + +### 🔬 + +* 🔬 🌅 (⚖️ 🌐 ❓) 🐍 **💽 🆎**, 🔌: + * 🎻 🎚 (`dict`). + * 🎻 🎻 (`list`) ⚖ 🏬 🆎. + * 🎻 (`str`) 🏑, 🔬 🕙 & 👟 📐. + * 🔢 (`int`, `float`) ⏮️ 🕙 & 👟 💲, ♒️. + +* 🔬 🌅 😍 🆎, 💖: + * 📛. + * 📧. + * 🆔. + * ...& 🎏. + +🌐 🔬 🍵 👍-🏛 & 🏋️ **Pydantic**. + +### 💂‍♂ & 🤝 + +💂‍♂ & 🤝 🛠️. 🍵 🙆 ⚠ ⏮️ 💽 ⚖️ 📊 🏷. + +🌐 💂‍♂ ⚖ 🔬 🗄, 🔌: + +* 🇺🇸🔍 🔰. +* **Oauth2️⃣** (⏮️ **🥙 🤝**). ✅ 🔰 🔛 [Oauth2️⃣ ⏮️ 🥙](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* 🛠️ 🔑: + * 🎚. + * 🔢 🔢. + * 🍪, ♒️. + +➕ 🌐 💂‍♂ ⚒ ⚪️➡️ 💃 (🔌 **🎉 🍪**). + +🌐 🏗 ♻ 🧰 & 🦲 👈 ⏩ 🛠️ ⏮️ 👆 ⚙️, 📊 🏪, 🔗 & ☁ 💽, ♒️. + +### 🔗 💉 + +FastAPI 🔌 📶 ⏩ ⚙️, ✋️ 📶 🏋️ 🔗 💉 ⚙️. + +* 🔗 💪 ✔️ 🔗, 🏗 🔗 ⚖️ **"📊" 🔗**. +* 🌐 **🔁 🍵** 🛠️. +* 🌐 🔗 💪 🚚 💽 ⚪️➡️ 📨 & **↔ ➡ 🛠️** ⚛ & 🏧 🧾. +* **🏧 🔬** *➡ 🛠️* 🔢 🔬 🔗. +* 🐕‍🦺 🏗 👩‍💻 🤝 ⚙️, **💽 🔗**, ♒️. +* **🙅‍♂ ⚠** ⏮️ 💽, 🕸, ♒️. ✋️ ⏩ 🛠️ ⏮️ 🌐 👫. + +### ♾ "🔌-🔌" + +⚖️ 🎏 🌌, 🙅‍♂ 💪 👫, 🗄 & ⚙️ 📟 👆 💪. + +🙆 🛠️ 🏗 🙅 ⚙️ (⏮️ 🔗) 👈 👆 💪 ✍ "🔌-" 👆 🈸 2️⃣ ⏸ 📟 ⚙️ 🎏 📊 & ❕ ⚙️ 👆 *➡ 🛠️*. + +### 💯 + +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ 📟 🧢. +* ⚙️ 🏭 🈸. + +## 💃 ⚒ + +**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) 💃. , 🙆 🌖 💃 📟 👆 ✔️, 🔜 👷. + +`FastAPI` 🤙 🎧-🎓 `Starlette`. , 🚥 👆 ⏪ 💭 ⚖️ ⚙️ 💃, 🌅 🛠️ 🔜 👷 🎏 🌌. + +⏮️ **FastAPI** 👆 🤚 🌐 **💃**'Ⓜ ⚒ (FastAPI 💃 🔛 💊): + +* 🤙 🎆 🎭. ⚫️ 1️⃣ ⏩ 🐍 🛠️ 💪, 🔛 🇷🇪 ⏮️ **✳** & **🚶**. +* ** *️⃣ ** 🐕‍🦺. +* -🛠️ 🖥 📋. +* 🕴 & 🤫 🎉. +* 💯 👩‍💻 🏗 🔛 🇸🇲. +* **⚜**, 🗜, 🎻 📁, 🎏 📨. +* **🎉 & 🍪** 🐕‍🦺. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. +* 1️⃣0️⃣0️⃣ 💯 🆎 ✍ ✍. + +## Pydantic ⚒ + +**FastAPI** 🍕 🔗 ⏮️ (& ⚓️ 🔛) Pydantic. , 🙆 🌖 Pydantic 📟 👆 ✔️, 🔜 👷. + +✅ 🔢 🗃 ⚓️ 🔛 Pydantic, 🐜Ⓜ, 🏭Ⓜ 💽. + +👉 ⛓ 👈 📚 💼 👆 💪 🚶‍♀️ 🎏 🎚 👆 🤚 ⚪️➡️ 📨 **🔗 💽**, 🌐 ✔ 🔁. + +🎏 ✔ 🎏 🌌 🤭, 📚 💼 👆 💪 🚶‍♀️ 🎚 👆 🤚 ⚪️➡️ 💽 **🔗 👩‍💻**. + +⏮️ **FastAPI** 👆 🤚 🌐 **Pydantic**'Ⓜ ⚒ (FastAPI ⚓️ 🔛 Pydantic 🌐 💽 🚚): + +* **🙅‍♂ 🔠**: + * 🙅‍♂ 🆕 🔗 🔑 ◾-🇪🇸 💡. + * 🚥 👆 💭 🐍 🆎 👆 💭 ❔ ⚙️ Pydantic. +* 🤾 🎆 ⏮️ 👆 **💾/🧶/🧠**: + * ↩️ Pydantic 📊 📊 👐 🎓 👆 🔬; 🚘-🛠️, 🧽, ✍ & 👆 🤔 🔜 🌐 👷 ☑ ⏮️ 👆 ✔ 💽. +* **⏩**: + * 📇 Pydantic ⏩ 🌘 🌐 🎏 💯 🗃. +* ✔ **🏗 📊**: + * ⚙️ 🔗 Pydantic 🏷, 🐍 `typing`'Ⓜ `List` & `Dict`, ♒️. + * & 💳 ✔ 🏗 💽 🔗 🎯 & 💪 🔬, ✅ & 📄 🎻 🔗. + * 👆 💪 ✔️ 🙇 **🐦 🎻** 🎚 & ✔️ 👫 🌐 ✔ & ✍. +* **🏧**: + * Pydantic ✔ 🛃 📊 🆎 🔬 ⚖️ 👆 💪 ↔ 🔬 ⏮️ 👩‍🔬 🔛 🏷 🎀 ⏮️ 💳 👨‍🎨. +* 1️⃣0️⃣0️⃣ 💯 💯 💰. diff --git a/docs/em/docs/help-fastapi.md b/docs/em/docs/help-fastapi.md new file mode 100644 index 000000000..d7b66185d --- /dev/null +++ b/docs/em/docs/help-fastapi.md @@ -0,0 +1,265 @@ +# ℹ FastAPI - 🤚 ℹ + +👆 💖 **FastAPI**❓ + +🔜 👆 💖 ℹ FastAPI, 🎏 👩‍💻, & 📕 ❓ + +⚖️ 🔜 👆 💖 🤚 ℹ ⏮️ **FastAPI**❓ + +📤 📶 🙅 🌌 ℹ (📚 🔌 1️⃣ ⚖️ 2️⃣ 🖊). + +& 📤 📚 🌌 🤚 ℹ 💁‍♂️. + +## 👱📔 📰 + +👆 💪 👱📔 (🐌) [**FastAPI & 👨‍👧‍👦** 📰](/newsletter/){.internal-link target=_blank} 🚧 ℹ 🔃: + +* 📰 🔃 FastAPI & 👨‍👧‍👦 👶 +* 🦮 👶 +* ⚒ 👶 +* 💔 🔀 👶 +* 💁‍♂ & 🎱 👶 + +## ⏩ FastAPI 🔛 👱📔 + +⏩ 🐶 Fastapi 🔛 **👱📔** 🤚 📰 📰 🔃 **FastAPI**. 👶 + +## ✴ **FastAPI** 📂 + +👆 💪 "✴" FastAPI 📂 (🖊 ✴ 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 👶 + +❎ ✴, 🎏 👩‍💻 🔜 💪 🔎 ⚫️ 🌅 💪 & 👀 👈 ⚫️ ✔️ ⏪ ⚠ 🎏. + +## ⌚ 📂 🗃 🚀 + +👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 + +📤 👆 💪 🖊 "🚀 🕴". + +🔨 ⚫️, 👆 🔜 📨 📨 (👆 📧) 🕐❔ 📤 🆕 🚀 (🆕 ⏬) **FastAPI** ⏮️ 🐛 🔧 & 🆕 ⚒. + +## 🔗 ⏮️ 📕 + +👆 💪 🔗 ⏮️ 👤 (🇹🇦 🇩🇬 / `tiangolo`), 📕. + +👆 💪: + +* ⏩ 👤 🔛 **📂**. + * 👀 🎏 📂 ℹ 🏗 👤 ✔️ ✍ 👈 💪 ℹ 👆. + * ⏩ 👤 👀 🕐❔ 👤 ✍ 🆕 📂 ℹ 🏗. +* ⏩ 👤 🔛 **👱📔** ⚖️ . + * 💬 👤 ❔ 👆 ⚙️ FastAPI (👤 💌 👂 👈). + * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰. + * 👆 💪 ⏩ 🐶 Fastapi 🔛 👱📔 (🎏 🏧). +* 🔗 ⏮️ 👤 🔛 **👱📔**. + * 👂 🕐❔ 👤 ⚒ 🎉 ⚖️ 🚀 🆕 🧰 (👐 👤 ⚙️ 👱📔 🌖 🛎 🤷 ♂). +* ✍ ⚫️❔ 👤 ✍ (⚖️ ⏩ 👤) 🔛 **🇸🇲.** ⚖️ **🔉**. + * ✍ 🎏 💭, 📄, & ✍ 🔃 🧰 👤 ✔️ ✍. + * ⏩ 👤 ✍ 🕐❔ 👤 ✍ 🕳 🆕. + +## 👱📔 🔃 **FastAPI** + +👱📔 🔃 **FastAPI** & ➡️ 👤 & 🎏 💭 ⚫️❔ 👆 💖 ⚫️. 👶 + +👤 💌 👂 🔃 ❔ **FastAPI** 💆‍♂ ⚙️, ⚫️❔ 👆 ✔️ 💖 ⚫️, ❔ 🏗/🏢 👆 ⚙️ ⚫️, ♒️. + +## 🗳 FastAPI + +* 🗳 **FastAPI** 📐. +* 🗳 **FastAPI** 📱. +* 💬 👆 ⚙️ **FastAPI** 🔛 ℹ. + +## ℹ 🎏 ⏮️ ❔ 📂 + +👆 💪 🔄 & ℹ 🎏 ⏮️ 👫 ❔: + +* 📂 💬 +* 📂 ❔ + +📚 💼 👆 5️⃣📆 ⏪ 💭 ❔ 📚 ❔. 👶 + +🚥 👆 🤝 📚 👫👫 ⏮️ 👫 ❔, 👆 🔜 ▶️️ 🛂 [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}. 👶 + +💭, 🏆 ⚠ ☝: 🔄 😇. 👫👫 👟 ⏮️ 👫 😩 & 📚 💼 🚫 💭 🏆 🌌, ✋️ 🔄 🏆 👆 💪 😇. 👶 + +💭 **FastAPI** 👪 😇 & 👍. 🎏 🕰, 🚫 🚫 🎭 ⚖️ 😛 🎭 ⤵ 🎏. 👥 ✔️ ✊ 💅 🔠 🎏. + +--- + +📥 ❔ ℹ 🎏 ⏮️ ❔ (💬 ⚖️ ❔): + +### 🤔 ❔ + +* ✅ 🚥 👆 💪 🤔 ⚫️❔ **🎯** & ⚙️ 💼 👨‍💼 💬. + +* ⤴️ ✅ 🚥 ❔ (⭕ 👪 ❔) **🆑**. + +* 📚 💼 ❔ 💭 🔃 👽 ⚗ ⚪️➡️ 👩‍💻, ✋️ 📤 💪 **👍** 1️⃣. 🚥 👆 💪 🤔 ⚠ & ⚙️ 💼 👍, 👆 💪 💪 🤔 👍 **🎛 ⚗**. + +* 🚥 👆 💪 🚫 🤔 ❔, 💭 🌖 **ℹ**. + +### 🔬 ⚠ + +🌅 💼 & 🏆 ❔ 📤 🕳 🔗 👨‍💼 **⏮️ 📟**. + +📚 💼 👫 🔜 🕴 📁 🧬 📟, ✋️ 👈 🚫 🥃 **🔬 ⚠**. + +* 👆 💪 💭 👫 🚚 ⭐, 🔬, 🖼, 👈 👆 💪 **📁-📋** & 🏃 🌐 👀 🎏 ❌ ⚖️ 🎭 👫 👀, ⚖️ 🤔 👫 ⚙️ 💼 👍. + +* 🚥 👆 😟 💁‍♂️ 👍, 👆 💪 🔄 **✍ 🖼** 💖 👈 👆, 🧢 🔛 📛 ⚠. ✔️ 🤯 👈 👉 💪 ✊ 📚 🕰 & ⚫️ 💪 👻 💭 👫 ✍ ⚠ 🥇. + +### 🤔 ⚗ + +* ⏮️ 💆‍♂ 💪 🤔 ❔, 👆 💪 🤝 👫 💪 **❔**. + +* 📚 💼, ⚫️ 👍 🤔 👫 **📈 ⚠ ⚖️ ⚙️ 💼**, ↩️ 📤 5️⃣📆 👍 🌌 ❎ ⚫️ 🌘 ⚫️❔ 👫 🔄. + +### 💭 🔐 + +🚥 👫 📨, 📤 ↕ 🤞 👆 🔜 ✔️ ❎ 👫 ⚠, ㊗, **👆 💂**❗ 🦸 + +* 🔜, 🚥 👈 ❎ 👫 ⚠, 👆 💪 💭 👫: + + * 📂 💬: ™ 🏤 **❔**. + * 📂 ❔: **🔐** ❔**. + +## ⌚ 📂 🗃 + +👆 💪 "⌚" FastAPI 📂 (🖊 "⌚" 🔼 🔝 ▶️️): https://github.com/tiangolo/fastapi. 👶 + +🚥 👆 🖊 "👀" ↩️ "🚀 🕴" 👆 🔜 📨 📨 🕐❔ 👱 ✍ 🆕 ❔ ⚖️ ❔. 👆 💪 ✔ 👈 👆 🕴 💚 🚨 🔃 🆕 ❔, ⚖️ 💬, ⚖️ 🎸, ♒️. + +⤴️ 👆 💪 🔄 & ℹ 👫 ❎ 👈 ❔. + +## 💭 ❔ + +👆 💪 ✍ 🆕 ❔ 📂 🗃, 🖼: + +* 💭 **❔** ⚖️ 💭 🔃 **⚠**. +* 🤔 🆕 **⚒**. + +**🗒**: 🚥 👆 ⚫️, ⤴️ 👤 🔜 💭 👆 ℹ 🎏. 👶 + +## 📄 🚲 📨 + +👆 💪 ℹ 👤 📄 🚲 📨 ⚪️➡️ 🎏. + +🔄, 🙏 🔄 👆 🏆 😇. 👶 + +--- + +📥 ⚫️❔ ✔️ 🤯 & ❔ 📄 🚲 📨: + +### 🤔 ⚠ + +* 🥇, ⚒ 💭 👆 **🤔 ⚠** 👈 🚲 📨 🔄 ❎. ⚫️ 💪 ✔️ 📏 💬 📂 💬 ⚖️ ❔. + +* 📤 👍 🤞 👈 🚲 📨 🚫 🤙 💪 ↩️ ⚠ 💪 ❎ **🎏 🌌**. ⤴️ 👆 💪 🤔 ⚖️ 💭 🔃 👈. + +### 🚫 😟 🔃 👗 + +* 🚫 😟 💁‍♂️ 🌅 🔃 👜 💖 💕 📧 👗, 👤 🔜 🥬 & 🔗 🛃 💕 ❎. + +* 🚫 😟 🔃 👗 🚫, 📤 ⏪ 🏧 🧰 ✅ 👈. + +& 🚥 📤 🙆 🎏 👗 ⚖️ ⚖ 💪, 👤 🔜 💭 🔗 👈, ⚖️ 👤 🔜 🚮 💕 🔛 🔝 ⏮️ 💪 🔀. + +### ✅ 📟 + +* ✅ & ✍ 📟, 👀 🚥 ⚫️ ⚒ 🔑, **🏃 ⚫️ 🌐** & 👀 🚥 ⚫️ 🤙 ❎ ⚠. + +* ⤴️ **🏤** 💬 👈 👆 👈, 👈 ❔ 👤 🔜 💭 👆 🤙 ✅ ⚫️. + +!!! info + 👐, 👤 💪 🚫 🎯 💙 🎸 👈 ✔️ 📚 ✔. + + 📚 🕰 ⚫️ ✔️ 🔨 👈 📤 🎸 ⏮️ 3️⃣, 5️⃣ ⚖️ 🌅 ✔, 🎲 ↩️ 📛 😌, ✋️ 🕐❔ 👤 ✅ 🎸, 👫 🤙 💔, ✔️ 🐛, ⚖️ 🚫 ❎ ⚠ 👫 🛄 ❎. 👶 + + , ⚫️ 🤙 ⚠ 👈 👆 🤙 ✍ & 🏃 📟, & ➡️ 👤 💭 🏤 👈 👆. 👶 + +* 🚥 🇵🇷 💪 📉 🌌, 👆 💪 💭 👈, ✋️ 📤 🙅‍♂ 💪 💁‍♂️ 😟, 📤 5️⃣📆 📚 🤔 ☝ 🎑 (& 👤 🔜 ✔️ 👇 👍 👍 👶), ⚫️ 👻 🚥 👆 💪 🎯 🔛 ⚛ 👜. + +### 💯 + +* ℹ 👤 ✅ 👈 🇵🇷 ✔️ **💯**. + +* ✅ 👈 💯 **❌** ⏭ 🇵🇷. 👶 + +* ⤴️ ✅ 👈 💯 **🚶‍♀️** ⏮️ 🇵🇷. 👶 + +* 📚 🎸 🚫 ✔️ 💯, 👆 💪 **🎗** 👫 🚮 💯, ⚖️ 👆 💪 **🤔** 💯 👆. 👈 1️⃣ 👜 👈 🍴 🌅 🕰 & 👆 💪 ℹ 📚 ⏮️ 👈. + +* ⤴️ 🏤 ⚫️❔ 👆 🔄, 👈 🌌 👤 🔜 💭 👈 👆 ✅ ⚫️. 👶 + +## ✍ 🚲 📨 + +👆 💪 [📉](contributing.md){.internal-link target=_blank} ℹ 📟 ⏮️ 🚲 📨, 🖼: + +* 🔧 🤭 👆 🔎 🔛 🧾. +* 💰 📄, 📹, ⚖️ 📻 👆 ✍ ⚖️ 🔎 🔃 FastAPI ✍ 👉 📁. + * ⚒ 💭 👆 🚮 👆 🔗 ▶️ 🔗 📄. +* ℹ [💬 🧾](contributing.md#translations){.internal-link target=_blank} 👆 🇪🇸. + * 👆 💪 ℹ 📄 ✍ ✍ 🎏. +* 🛠️ 🆕 🧾 📄. +* 🔧 ♻ ❔/🐛. + * ⚒ 💭 🚮 💯. +* 🚮 🆕 ⚒. + * ⚒ 💭 🚮 💯. + * ⚒ 💭 🚮 🧾 🚥 ⚫️ 🔗. + +## ℹ 🚧 FastAPI + +ℹ 👤 🚧 **FastAPI**❗ 👶 + +📤 📚 👷, & 🏆 ⚫️, **👆** 💪 ⚫️. + +👑 📋 👈 👆 💪 ▶️️ 🔜: + +* [ℹ 🎏 ⏮️ ❔ 📂](#help-others-with-questions-in-github){.internal-link target=_blank} (👀 📄 🔛). +* [📄 🚲 📨](#review-pull-requests){.internal-link target=_blank} (👀 📄 🔛). + +👈 2️⃣ 📋 ⚫️❔ **🍴 🕰 🏆**. 👈 👑 👷 🏆 FastAPI. + +🚥 👆 💪 ℹ 👤 ⏮️ 👈, **👆 🤝 👤 🚧 FastAPI** & ⚒ 💭 ⚫️ 🚧 **🛠️ ⏩ & 👻**. 👶 + +## 🛑 💬 + +🛑 👶 😧 💬 💽 👶 & 🤙 👅 ⏮️ 🎏 FastAPI 👪. + +!!! tip + ❔, 💭 👫 📂 💬, 📤 🌅 👍 🤞 👆 🔜 📨 ℹ [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}. + + ⚙️ 💬 🕴 🎏 🏢 💬. + +📤 ⏮️ 🥊 💬, ✋️ ⚫️ 🚫 ✔️ 📻 & 🏧 ⚒, 💬 🌖 ⚠, 😧 🔜 👍 ⚙️. + +### 🚫 ⚙️ 💬 ❔ + +✔️ 🤯 👈 💬 ✔ 🌅 "🆓 💬", ⚫️ ⏩ 💭 ❔ 👈 💁‍♂️ 🏢 & 🌅 ⚠ ❔,, 👆 💪 🚫 📨 ❔. + +📂, 📄 🔜 🦮 👆 ✍ ▶️️ ❔ 👈 👆 💪 🌖 💪 🤚 👍 ❔, ⚖️ ❎ ⚠ 👆 ⏭ 💬. & 📂 👤 💪 ⚒ 💭 👤 🕧 ❔ 🌐, 🚥 ⚫️ ✊ 🕰. 👤 💪 🚫 🤙 👈 ⏮️ 💬 ⚙️. 👶 + +💬 💬 ⚙️ 🚫 💪 📇 📂, ❔ & ❔ 5️⃣📆 🤚 💸 💬. & 🕴 🕐 📂 💯 ▶️️ [FastAPI 🕴](fastapi-people.md#experts){.internal-link target=_blank}, 👆 🔜 🌅 🎲 📨 🌅 🙋 📂. + +🔛 🎏 🚄, 📤 💯 👩‍💻 💬 ⚙️, 📤 ↕ 🤞 👆 🔜 🔎 👱 💬 📤, 🌖 🌐 🕰. 👶 + +## 💰 📕 + +👆 💪 💰 🐕‍🦺 📕 (👤) 🔘 📂 💰. + +📤 👆 💪 🛍 👤 ☕ 👶 👶 💬 👏. 👶 + +& 👆 💪 ▶️️ 🥇1st ⚖️ 🌟 💰 FastAPI. 👶 👶 + +## 💰 🧰 👈 🏋️ FastAPI + +👆 ✔️ 👀 🧾, FastAPI 🧍 🔛 ⌚ 🐘, 💃 & Pydantic. + +👆 💪 💰: + +* ✡ 🍏 (Pydantic) +* 🗜 (💃, Uvicorn) + +--- + +👏 ❗ 👶 diff --git a/docs/em/docs/history-design-future.md b/docs/em/docs/history-design-future.md new file mode 100644 index 000000000..7e39972de --- /dev/null +++ b/docs/em/docs/history-design-future.md @@ -0,0 +1,79 @@ +# 📖, 🔧 & 🔮 + +🕰 🏁, **FastAPI** 👩‍💻 💭: + +> ⚫️❔ 📖 👉 🏗 ❓ ⚫️ 😑 ✔️ 👟 ⚪️➡️ 🕳 👌 👩‍❤‍👨 🗓️ [...] + +📥 🐥 🍖 👈 📖. + +## 🎛 + +👤 ✔️ 🏗 🔗 ⏮️ 🏗 📄 📚 1️⃣2️⃣🗓️ (🎰 🏫, 📎 ⚙️, 🔁 👨‍🏭, ☁ 💽, ♒️), ↘️ 📚 🏉 👩‍💻. + +🍕 👈, 👤 💪 🔬, 💯 & ⚙️ 📚 🎛. + +📖 **FastAPI** 👑 🍕 📖 🚮 ⏪. + +🙆‍♀ 📄 [🎛](alternatives.md){.internal-link target=_blank}: + +
+ +**FastAPI** 🚫🔜 🔀 🚥 🚫 ⏮️ 👷 🎏. + +📤 ✔️ 📚 🧰 ✍ ⏭ 👈 ✔️ ℹ 😮 🚮 🏗. + +👤 ✔️ ❎ 🏗 🆕 🛠️ 📚 1️⃣2️⃣🗓️. 🥇 👤 🔄 ❎ 🌐 ⚒ 📔 **FastAPI** ⚙️ 📚 🎏 🛠️, 🔌-🔌, & 🧰. + +✋️ ☝, 📤 🙅‍♂ 🎏 🎛 🌘 🏗 🕳 👈 🚚 🌐 👫 ⚒, ✊ 🏆 💭 ⚪️➡️ ⏮️ 🧰, & 🌀 👫 🏆 🌌 💪, ⚙️ 🇪🇸 ⚒ 👈 ➖🚫 💪 ⏭ (🐍 3️⃣.6️⃣ ➕ 🆎 🔑). + +
+ +## 🔬 + +⚙️ 🌐 ⏮️ 🎛 👤 ✔️ 🤞 💡 ⚪️➡️ 🌐 👫, ✊ 💭, & 🌀 👫 🏆 🌌 👤 💪 🔎 👤 & 🏉 👩‍💻 👤 ✔️ 👷 ⏮️. + +🖼, ⚫️ 🆑 👈 🎲 ⚫️ 🔜 ⚓️ 🔛 🐩 🐍 🆎 🔑. + +, 🏆 🎯 ⚙️ ⏪ ♻ 🐩. + +, ⏭ ▶️ 📟 **FastAPI**, 👤 💸 📚 🗓️ 🎓 🔌 🗄, 🎻 🔗, Oauth2️⃣, ♒️. 🎯 👫 💛, 🔀, & 🔺. + +## 🔧 + +⤴️ 👤 💸 🕰 🔧 👩‍💻 "🛠️" 👤 💚 ✔️ 👩‍💻 (👩‍💻 ⚙️ FastAPI). + +👤 💯 📚 💭 🏆 🌟 🐍 👨‍🎨: 🗒, 🆚 📟, 🎠 🧢 👨‍🎨. + +🏁 🐍 👩‍💻 🔬, 👈 📔 🔃 8️⃣0️⃣ 💯 👩‍💻. + +⚫️ ⛓ 👈 **FastAPI** 🎯 💯 ⏮️ 👨‍🎨 ⚙️ 8️⃣0️⃣ 💯 🐍 👩‍💻. & 🏆 🎏 👨‍🎨 😑 👷 ➡, 🌐 🚮 💰 🔜 👷 🌖 🌐 👨‍🎨. + +👈 🌌 👤 💪 🔎 🏆 🌌 📉 📟 ❎ 🌅 💪, ✔️ 🛠️ 🌐, 🆎 & ❌ ✅, ♒️. + +🌐 🌌 👈 🚚 🏆 🛠️ 💡 🌐 👩‍💻. + +## 📄 + +⏮️ 🔬 📚 🎛, 👤 💭 👈 👤 🔜 ⚙️ **Pydantic** 🚮 📈. + +⤴️ 👤 📉 ⚫️, ⚒ ⚫️ 🍕 🛠️ ⏮️ 🎻 🔗, 🐕‍🦺 🎏 🌌 🔬 ⚛ 📄, & 📉 👨‍🎨 🐕‍🦺 (🆎 ✅, ✍) ⚓️ 🔛 💯 📚 👨‍🎨. + +⏮️ 🛠️, 👤 📉 **💃**, 🎏 🔑 📄. + +## 🛠️ + +🕰 👤 ▶️ 🏗 **FastAPI** ⚫️, 🏆 🍖 ⏪ 🥉, 🔧 🔬, 📄 & 🧰 🔜, & 💡 🔃 🐩 & 🔧 🆑 & 🍋. + +## 🔮 + +👉 ☝, ⚫️ ⏪ 🆑 👈 **FastAPI** ⏮️ 🚮 💭 ➖ ⚠ 📚 👫👫. + +⚫️ 💆‍♂ 👐 🤭 ⏮️ 🎛 ♣ 📚 ⚙️ 💼 👍. + +📚 👩‍💻 & 🏉 ⏪ 🪀 🔛 **FastAPI** 👫 🏗 (🔌 👤 & 👇 🏉). + +✋️, 📤 📚 📈 & ⚒ 👟. + +**FastAPI** ✔️ 👑 🔮 ⤴️. + +& [👆 ℹ](help-fastapi.md){.internal-link target=_blank} 📉 👍. diff --git a/docs/em/docs/index.md b/docs/em/docs/index.md new file mode 100644 index 000000000..ea8a9d41c --- /dev/null +++ b/docs/em/docs/index.md @@ -0,0 +1,469 @@ +

+ FastAPI +

+

+ FastAPI 🛠️, ↕ 🎭, ⏩ 💡, ⏩ 📟, 🔜 🏭 +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**🧾**: https://fastapi.tiangolo.com + +**ℹ 📟**: https://github.com/tiangolo/fastapi + +--- + +FastAPI 🏛, ⏩ (↕-🎭), 🕸 🛠️ 🏗 🛠️ ⏮️ 🐍 3️⃣.7️⃣ ➕ ⚓️ 🔛 🐩 🐍 🆎 🔑. + +🔑 ⚒: + +* **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). [1️⃣ ⏩ 🐍 🛠️ 💪](#performance). +* **⏩ 📟**: 📈 🚅 🛠️ ⚒ 🔃 2️⃣0️⃣0️⃣ 💯 3️⃣0️⃣0️⃣ 💯. * +* **👩‍❤‍👨 🐛**: 📉 🔃 4️⃣0️⃣ 💯 🗿 (👩‍💻) 📉 ❌. * +* **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. +* **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. +* **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. 👩‍❤‍👨 🐛. +* **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. +* **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 (⏪ 💭 🦁) & 🎻 🔗. + +* ⚖ ⚓️ 🔛 💯 🔛 🔗 🛠️ 🏉, 🏗 🏭 🈸. + +## 💰 + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +🎏 💰 + +## 🤔 + +"_[...] 👤 ⚙️ **FastAPI** 📚 👫 📆. [...] 👤 🤙 📆 ⚙️ ⚫️ 🌐 👇 🏉 **⚗ 🐕‍🦺 🤸‍♂**. 👫 💆‍♂ 🛠️ 🔘 🐚 **🖥** 🏬 & **📠** 🏬._" + +
🧿 🇵🇰 - 🤸‍♂ (🇦🇪)
+ +--- + +"_👥 🛠️ **FastAPI** 🗃 🤖 **🎂** 💽 👈 💪 🔢 🚚 **🔮**. [👨📛]_" + +
🇮🇹 🇸🇻, 👨📛 👨📛, & 🇱🇰 🕉 🕉 - 🙃 (🇦🇪)
+ +--- + +"_**📺** 🙏 📣 📂-ℹ 🚀 👆 **⚔ 🧾** 🎶 🛠️: **📨**❗ [🏗 ⏮️ **FastAPI**]_" + +
✡ 🍏, 👖 🇪🇸, 🌲 🍏 - 📺 (🇦🇪)
+ +--- + +"_👤 🤭 🌕 😄 🔃 **FastAPI**. ⚫️ 🎊 ❗_" + +
✡ 🇭🇰 - 🐍 🔢 📻 🦠 (🇦🇪)
+ +--- + +"_🤙, ⚫️❔ 👆 ✔️ 🏗 👀 💎 💠 & 🇵🇱. 📚 🌌, ⚫️ ⚫️❔ 👤 💚 **🤗** - ⚫️ 🤙 😍 👀 👱 🏗 👈._" + +
✡ 🗄 - 🤗 👼 (🇦🇪)
+ +--- + +"_🚥 👆 👀 💡 1️⃣ **🏛 🛠️** 🏗 🎂 🔗, ✅ 👅 **FastAPI** [...] ⚫️ ⏩, ⏩ ⚙️ & ⏩ 💡 [...]_" + +"_👥 ✔️ 🎛 🤭 **FastAPI** 👆 **🔗** [...] 👤 💭 👆 🔜 💖 ⚫️ [...]_" + +
🇱🇨 🇸🇲 - ✡ Honnibal - 💥 👲 🕴 - 🌈 👼 (🇦🇪) - (🇦🇪)
+ +--- + +"_🚥 🙆 👀 🏗 🏭 🐍 🛠️, 👤 🔜 🏆 👍 **FastAPI**. ⚫️ **💎 🏗**, **🙅 ⚙️** & **🏆 🛠️**, ⚫️ ✔️ ▶️️ **🔑 🦲** 👆 🛠️ 🥇 🛠️ 🎛 & 🚘 📚 🏧 & 🐕‍🦺 ✅ 👆 🕹 🔫 👨‍💻._" + +
🇹🇦 🍰 - 📻 (🇦🇪)
+ +--- + +## **🏎**, FastAPI 🇳🇨 + + + +🚥 👆 🏗 📱 ⚙️ 📶 ↩️ 🕸 🛠️, ✅ 👅 **🏎**. + +**🏎** FastAPI 🐥 👪. & ⚫️ 🎯 **FastAPI 🇳🇨**. 👶 👶 👶 + +## 📄 + +🐍 3️⃣.7️⃣ ➕ + +FastAPI 🧍 🔛 ⌚ 🐘: + +* 💃 🕸 🍕. +* Pydantic 📊 🍕. + +## 👷‍♂ + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +👆 🔜 💪 🔫 💽, 🏭 ✅ Uvicorn ⚖️ Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## 🖼 + +### ✍ ⚫️ + +* ✍ 📁 `main.py` ⏮️: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+⚖️ ⚙️ async def... + +🚥 👆 📟 ⚙️ `async` / `await`, ⚙️ `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**🗒**: + +🚥 👆 🚫 💭, ✅ _"🏃 ❓" _ 📄 🔃 `async` & `await` 🩺. + +
+ +### 🏃 ⚫️ + +🏃 💽 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+🔃 📋 uvicorn main:app --reload... + +📋 `uvicorn main:app` 🔗: + +* `main`: 📁 `main.py` (🐍 "🕹"). +* `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. +* `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 👉 🛠️. + +
+ +### ✅ ⚫️ + +📂 👆 🖥 http://127.0.0.1:8000/items/5?q=somequery. + +👆 🔜 👀 🎻 📨: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +👆 ⏪ ✍ 🛠️ 👈: + +* 📨 🇺🇸🔍 📨 _➡_ `/` & `/items/{item_id}`. +* 👯‍♂️ _➡_ ✊ `GET` 🛠️ (💭 🇺🇸🔍 _👩‍🔬_). +* _➡_ `/items/{item_id}` ✔️ _➡ 🔢_ `item_id` 👈 🔜 `int`. +* _➡_ `/items/{item_id}` ✔️ 📦 `str` _🔢 = `q`. + +### 🎓 🛠️ 🩺 + +🔜 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 🎛 🛠️ 🩺 + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 🖼 ♻ + +🔜 🔀 📁 `main.py` 📨 💪 ⚪️➡️ `PUT` 📨. + +📣 💪 ⚙️ 🐩 🐍 🆎, 👏 Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +💽 🔜 🔃 🔁 (↩️ 👆 🚮 `--reload` `uvicorn` 📋 🔛). + +### 🎓 🛠️ 🩺 ♻ + +🔜 🚶 http://127.0.0.1:8000/docs. + +* 🎓 🛠️ 🧾 🔜 🔁 ℹ, 🔌 🆕 💪: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* 🖊 🔛 🔼 "🔄 ⚫️ 👅", ⚫️ ✔ 👆 🥧 🔢 & 🔗 🔗 ⏮️ 🛠️: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* ⤴️ 🖊 🔛 "🛠️" 🔼, 👩‍💻 🔢 🔜 🔗 ⏮️ 👆 🛠️, 📨 🔢, 🤚 🏁 & 🎦 👫 🔛 🖥: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### 🎛 🛠️ 🩺 ♻ + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +* 🎛 🧾 🔜 🎨 🆕 🔢 🔢 & 💪: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 🌃 + +📄, 👆 📣 **🕐** 🆎 🔢, 💪, ♒️. 🔢 🔢. + +👆 👈 ⏮️ 🐩 🏛 🐍 🆎. + +👆 🚫 ✔️ 💡 🆕 ❕, 👩‍🔬 ⚖️ 🎓 🎯 🗃, ♒️. + +🐩 **🐍 3️⃣.7️⃣ ➕**. + +🖼, `int`: + +```Python +item_id: int +``` + +⚖️ 🌖 🏗 `Item` 🏷: + +```Python +item: Item +``` + +...& ⏮️ 👈 👁 📄 👆 🤚: + +* 👨‍🎨 🐕‍🦺, 🔌: + * 🛠️. + * 🆎 ✅. +* 🔬 💽: + * 🏧 & 🆑 ❌ 🕐❔ 📊 ❌. + * 🔬 🙇 🐦 🎻 🎚. +* 🛠️ 🔢 💽: 👟 ⚪️➡️ 🕸 🐍 💽 & 🆎. 👂 ⚪️➡️: + * 🎻. + * ➡ 🔢. + * 🔢 🔢. + * 🍪. + * 🎚. + * 📨. + * 📁. +* 🛠️ 🔢 📊: 🗜 ⚪️➡️ 🐍 💽 & 🆎 🕸 💽 (🎻): + * 🗜 🐍 🆎 (`str`, `int`, `float`, `bool`, `list`, ♒️). + * `datetime` 🎚. + * `UUID` 🎚. + * 💽 🏷. + * ...& 📚 🌖. +* 🏧 🎓 🛠️ 🧾, 🔌 2️⃣ 🎛 👩‍💻 🔢: + * 🦁 🎚. + * 📄. + +--- + +👟 🔙 ⏮️ 📟 🖼, **FastAPI** 🔜: + +* ✔ 👈 📤 `item_id` ➡ `GET` & `PUT` 📨. +* ✔ 👈 `item_id` 🆎 `int` `GET` & `PUT` 📨. + * 🚥 ⚫️ 🚫, 👩‍💻 🔜 👀 ⚠, 🆑 ❌. +* ✅ 🚥 📤 📦 🔢 🔢 📛 `q` ( `http://127.0.0.1:8000/items/foo?q=somequery`) `GET` 📨. + * `q` 🔢 📣 ⏮️ `= None`, ⚫️ 📦. + * 🍵 `None` ⚫️ 🔜 🚚 (💪 💼 ⏮️ `PUT`). +* `PUT` 📨 `/items/{item_id}`, ✍ 💪 🎻: + * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `name` 👈 🔜 `str`. + * ✅ 👈 ⚫️ ✔️ ✔ 🔢 `price` 👈 ✔️ `float`. + * ✅ 👈 ⚫️ ✔️ 📦 🔢 `is_offer`, 👈 🔜 `bool`, 🚥 🎁. + * 🌐 👉 🔜 👷 🙇 🐦 🎻 🎚. +* 🗜 ⚪️➡️ & 🎻 🔁. +* 📄 🌐 ⏮️ 🗄, 👈 💪 ⚙️: + * 🎓 🧾 ⚙️. + * 🏧 👩‍💻 📟 ⚡ ⚙️, 📚 🇪🇸. +* 🚚 2️⃣ 🎓 🧾 🕸 🔢 🔗. + +--- + +👥 🖌 🧽, ✋️ 👆 ⏪ 🤚 💭 ❔ ⚫️ 🌐 👷. + +🔄 🔀 ⏸ ⏮️: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...⚪️➡️: + +```Python + ... "item_name": item.name ... +``` + +...: + +```Python + ... "item_price": item.price ... +``` + +...& 👀 ❔ 👆 👨‍🎨 🔜 🚘-🏁 🔢 & 💭 👫 🆎: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +🌅 🏁 🖼 🔌 🌅 ⚒, 👀 🔰 - 👩‍💻 🦮. + +**🚘 🚨**: 🔰 - 👩‍💻 🦮 🔌: + +* 📄 **🔢** ⚪️➡️ 🎏 🎏 🥉: **🎚**, **🍪**, **📨 🏑** & **📁**. +* ❔ ⚒ **🔬 ⚛** `maximum_length` ⚖️ `regex`. +* 📶 🏋️ & ⏩ ⚙️ **🔗 💉** ⚙️. +* 💂‍♂ & 🤝, ✅ 🐕‍🦺 **Oauth2️⃣** ⏮️ **🥙 🤝** & **🇺🇸🔍 🔰** 🔐. +* 🌅 🏧 (✋️ 😨 ⏩) ⚒ 📣 **🙇 🐦 🎻 🏷** (👏 Pydantic). +* **🕹** 🛠️ ⏮️ 🍓 & 🎏 🗃. +* 📚 ➕ ⚒ (👏 💃): + * ** *️⃣ ** + * 📶 ⏩ 💯 ⚓️ 🔛 🇸🇲 & `pytest` + * **⚜** + * **🍪 🎉** + * ...& 🌖. + +## 🎭 + +🔬 🇸🇲 📇 🎦 **FastAPI** 🈸 🏃‍♂ 🔽 Uvicorn 1️⃣ ⏩ 🐍 🛠️ 💪, 🕴 🔛 💃 & Uvicorn 👫 (⚙️ 🔘 FastAPI). (*) + +🤔 🌖 🔃 ⚫️, 👀 📄 📇. + +## 📦 🔗 + +⚙️ Pydantic: + +* ujson - ⏩ 🎻 "🎻". +* email_validator - 📧 🔬. + +⚙️ 💃: + +* httpx - ✔ 🚥 👆 💚 ⚙️ `TestClient`. +* jinja2 - ✔ 🚥 👆 💚 ⚙️ 🔢 📄 📳. +* python-multipart - ✔ 🚥 👆 💚 🐕‍🦺 📨 "✍", ⏮️ `request.form()`. +* itsdangerous - ✔ `SessionMiddleware` 🐕‍🦺. +* pyyaml - ✔ 💃 `SchemaGenerator` 🐕‍🦺 (👆 🎲 🚫 💪 ⚫️ ⏮️ FastAPI). +* ujson - ✔ 🚥 👆 💚 ⚙️ `UJSONResponse`. + +⚙️ FastAPI / 💃: + +* uvicorn - 💽 👈 📐 & 🍦 👆 🈸. +* orjson - ✔ 🚥 👆 💚 ⚙️ `ORJSONResponse`. + +👆 💪 ❎ 🌐 👫 ⏮️ `pip install "fastapi[all]"`. + +## 🛂 + +👉 🏗 ® 🔽 ⚖ 🇩🇪 🛂. diff --git a/docs/em/docs/project-generation.md b/docs/em/docs/project-generation.md new file mode 100644 index 000000000..5fd667ad1 --- /dev/null +++ b/docs/em/docs/project-generation.md @@ -0,0 +1,84 @@ +# 🏗 ⚡ - 📄 + +👆 💪 ⚙️ 🏗 🚂 🤚 ▶️, ⚫️ 🔌 📚 ▶️ ⚒ 🆙, 💂‍♂, 💽 & 🛠️ 🔗 ⏪ ⌛ 👆. + +🏗 🚂 🔜 🕧 ✔️ 📶 🙃 🖥 👈 👆 🔜 ℹ & 🛠️ 👆 👍 💪, ✋️ ⚫️ 💪 👍 ▶️ ☝ 👆 🏗. + +## 🌕 📚 FastAPI ✳ + +📂: https://github.com/tiangolo/full-stack-fastapi-postgresql + +### 🌕 📚 FastAPI ✳ - ⚒ + +* 🌕 **☁** 🛠️ (☁ 🧢). +* ☁ 🐝 📳 🛠️. +* **☁ ✍** 🛠️ & 🛠️ 🇧🇿 🛠️. +* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. +* 🐍 **FastAPI** 👩‍💻: + * **⏩**: 📶 ↕ 🎭, 🔛 🇷🇪 ⏮️ **✳** & **🚶** (👏 💃 & Pydantic). + * **🏋️**: 👑 👨‍🎨 🐕‍🦺. 🛠️ 🌐. 🌘 🕰 🛠️. + * **⏩**: 🔧 ⏩ ⚙️ & 💡. 🌘 🕰 👂 🩺. + * **📏**: 📉 📟 ❎. 💗 ⚒ ⚪️➡️ 🔠 🔢 📄. + * **🏋️**: 🤚 🏭-🔜 📟. ⏮️ 🏧 🎓 🧾. + * **🐩-⚓️**: ⚓️ 🔛 (& 🍕 🔗 ⏮️) 📂 🐩 🔗: 🗄 & 🎻 🔗. + * **📚 🎏 ⚒** 🔌 🏧 🔬, 🛠️, 🎓 🧾, 🤝 ⏮️ Oauth2️⃣ 🥙 🤝, ♒️. +* **🔐 🔐** 🔁 🔢. +* **🥙 🤝** 🤝. +* **🇸🇲** 🏷 (🔬 🏺 ↔, 👫 💪 ⚙️ ⏮️ 🥒 👨‍🏭 🔗). +* 🔰 ▶️ 🏷 👩‍💻 (🔀 & ❎ 👆 💪). +* **⚗** 🛠️. +* **⚜** (✖️ 🇨🇳 ℹ 🤝). +* **🥒** 👨‍🏭 👈 💪 🗄 & ⚙️ 🏷 & 📟 ⚪️➡️ 🎂 👩‍💻 🍕. +* 🎂 👩‍💻 💯 ⚓️ 🔛 **✳**, 🛠️ ⏮️ ☁, 👆 💪 💯 🌕 🛠️ 🔗, 🔬 🔛 💽. ⚫️ 🏃 ☁, ⚫️ 💪 🏗 🆕 💽 🏪 ⚪️➡️ 🖌 🔠 🕰 (👆 💪 ⚙️ ✳, ✳, ✳, ⚖️ ⚫️❔ 👆 💚, & 💯 👈 🛠️ 👷). +* ⏩ 🐍 🛠️ ⏮️ **📂 💾** 🛰 ⚖️-☁ 🛠️ ⏮️ ↔ 💖 ⚛ ⚗ ⚖️ 🎙 🎙 📟 📂. +* **🎦** 🕸: + * 🏗 ⏮️ 🎦 ✳. + * **🥙 🤝** 🚚. + * 💳 🎑. + * ⏮️ 💳, 👑 🕹 🎑. + * 👑 🕹 ⏮️ 👩‍💻 🏗 & 📕. + * 👤 👩‍💻 📕. + * **🇷🇪**. + * **🎦-📻**. + * **Vuetify** 🌹 🧽 🔧 🦲. + * **📕**. + * ☁ 💽 ⚓️ 🔛 **👌** (📶 🤾 🎆 ⏮️ 🎦-📻). + * ☁ 👁-▶️ 🏗, 👆 🚫 💪 🖊 ⚖️ 💕 ✍ 📟. + * 🕸 💯 🏃 🏗 🕰 (💪 🔕 💁‍♂️). + * ⚒ 🔧 💪, ⚫️ 👷 👅 📦, ✋️ 👆 💪 🏤-🏗 ⏮️ 🎦 ✳ ⚖️ ✍ ⚫️ 👆 💪, & 🏤-⚙️ ⚫️❔ 👆 💚. +* ** *️⃣ ** ✳ 💽, 👆 💪 🔀 ⚫️ ⚙️ 📁 & ✳ 💪. +* **🥀** 🥒 👨‍🏭 ⚖. +* 📐 ⚖ 🖖 🕸 & 👩‍💻 ⏮️ **Traefik**, 👆 💪 ✔️ 👯‍♂️ 🔽 🎏 🆔, 👽 ➡, ✋️ 🍦 🎏 📦. +* Traefik 🛠️, ✅ ➡️ 🗜 **🇺🇸🔍** 📄 🏧 ⚡. +* ✳ **🆑** (🔁 🛠️), 🔌 🕸 & 👩‍💻 🔬. + +## 🌕 📚 FastAPI 🗄 + +📂: https://github.com/tiangolo/full-stack-fastapi-couchbase + +👶 👶 **⚠** 👶 👶 + +🚥 👆 ▶️ 🆕 🏗 ⚪️➡️ 🖌, ✅ 🎛 📥. + +🖼, 🏗 🚂 🌕 📚 FastAPI ✳ 💪 👍 🎛, ⚫️ 🎯 🚧 & ⚙️. & ⚫️ 🔌 🌐 🆕 ⚒ & 📈. + +👆 🆓 ⚙️ 🗄-⚓️ 🚂 🚥 👆 💚, ⚫️ 🔜 🎲 👷 👌, & 🚥 👆 ⏪ ✔️ 🏗 🏗 ⏮️ ⚫️ 👈 👌 👍 (& 👆 🎲 ⏪ ℹ ⚫️ ♣ 👆 💪). + +👆 💪 ✍ 🌅 🔃 ⚫️ 🩺 🏦. + +## 🌕 📚 FastAPI ✳ + +...💪 👟 ⏪, ⚓️ 🔛 👇 🕰 🚚 & 🎏 ⚖. 👶 👶 + +## 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI + +📂: https://github.com/microsoft/cookiecutter-spacy-fastapi + +### 🎰 🏫 🏷 ⏮️ 🌈 & FastAPI - ⚒ + +* **🌈** 🕜 🏷 🛠️. +* **☁ 🧠 🔎** 📨 📁 🏗. +* **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. +* **☁ 👩‍💻** Kubernete (🦲) 🆑/💿 🛠️ 🏗. +* **🤸‍♂** 💪 ⚒ 1️⃣ 🌈 🏗 🇪🇸 ⏮️ 🏗 🖥. +* **💪 🏧** 🎏 🏷 🛠️ (Pytorch, 🇸🇲), 🚫 🌈. diff --git a/docs/em/docs/python-types.md b/docs/em/docs/python-types.md new file mode 100644 index 000000000..e079d9039 --- /dev/null +++ b/docs/em/docs/python-types.md @@ -0,0 +1,490 @@ +# 🐍 🆎 🎶 + +🐍 ✔️ 🐕‍🦺 📦 "🆎 🔑". + +👫 **"🆎 🔑"** 🎁 ❕ 👈 ✔ 📣 🆎 🔢. + +📣 🆎 👆 🔢, 👨‍🎨 & 🧰 💪 🤝 👆 👍 🐕‍🦺. + +👉 **⏩ 🔰 / ↗️** 🔃 🐍 🆎 🔑. ⚫️ 📔 🕴 💯 💪 ⚙️ 👫 ⏮️ **FastAPI**... ❔ 🤙 📶 🐥. + +**FastAPI** 🌐 ⚓️ 🔛 👫 🆎 🔑, 👫 🤝 ⚫️ 📚 📈 & 💰. + +✋️ 🚥 👆 🙅 ⚙️ **FastAPI**, 👆 🔜 💰 ⚪️➡️ 🏫 🍖 🔃 👫. + +!!! note + 🚥 👆 🐍 🕴, & 👆 ⏪ 💭 🌐 🔃 🆎 🔑, 🚶 ⏭ 📃. + +## 🎯 + +➡️ ▶️ ⏮️ 🙅 🖼: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +🤙 👉 📋 🔢: + +``` +John Doe +``` + +🔢 🔨 📄: + +* ✊ `first_name` & `last_name`. +* 🗜 🥇 🔤 🔠 1️⃣ ↖ 💼 ⏮️ `title()`. +* 🔢 👫 ⏮️ 🚀 🖕. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### ✍ ⚫️ + +⚫️ 📶 🙅 📋. + +✋️ 🔜 🌈 👈 👆 ✍ ⚫️ ⚪️➡️ 🖌. + +☝ 👆 🔜 ✔️ ▶️ 🔑 🔢, 👆 ✔️ 🔢 🔜... + +✋️ ⤴️ 👆 ✔️ 🤙 "👈 👩‍🔬 👈 🗜 🥇 🔤 ↖ 💼". + +⚫️ `upper`❓ ⚫️ `uppercase`❓ `first_uppercase`❓ `capitalize`❓ + +⤴️, 👆 🔄 ⏮️ 🗝 👩‍💻 👨‍👧‍👦, 👨‍🎨 ✍. + +👆 🆎 🥇 🔢 🔢, `first_name`, ⤴️ ❣ (`.`) & ⤴️ 🎯 `Ctrl+Space` ⏲ 🛠️. + +✋️, 😞, 👆 🤚 🕳 ⚠: + + + +### 🚮 🆎 + +➡️ 🔀 👁 ⏸ ⚪️➡️ ⏮️ ⏬. + +👥 🔜 🔀 ⚫️❔ 👉 🧬, 🔢 🔢, ⚪️➡️: + +```Python + first_name, last_name +``` + +: + +```Python + first_name: str, last_name: str +``` + +👈 ⚫️. + +👈 "🆎 🔑": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +👈 🚫 🎏 📣 🔢 💲 💖 🔜 ⏮️: + +```Python + first_name="john", last_name="doe" +``` + +⚫️ 🎏 👜. + +👥 ⚙️ ❤ (`:`), 🚫 🌓 (`=`). + +& ❎ 🆎 🔑 🛎 🚫 🔀 ⚫️❔ 🔨 ⚪️➡️ ⚫️❔ 🔜 🔨 🍵 👫. + +✋️ 🔜, 🌈 👆 🔄 🖕 🏗 👈 🔢, ✋️ ⏮️ 🆎 🔑. + +🎏 ☝, 👆 🔄 ⏲ 📋 ⏮️ `Ctrl+Space` & 👆 👀: + + + +⏮️ 👈, 👆 💪 📜, 👀 🎛, ⏭ 👆 🔎 1️⃣ 👈 "💍 🔔": + + + +## 🌅 🎯 + +✅ 👉 🔢, ⚫️ ⏪ ✔️ 🆎 🔑: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +↩️ 👨‍🎨 💭 🆎 🔢, 👆 🚫 🕴 🤚 🛠️, 👆 🤚 ❌ ✅: + + + +🔜 👆 💭 👈 👆 ✔️ 🔧 ⚫️, 🗜 `age` 🎻 ⏮️ `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 📣 🆎 + +👆 👀 👑 🥉 📣 🆎 🔑. 🔢 🔢. + +👉 👑 🥉 👆 🔜 ⚙️ 👫 ⏮️ **FastAPI**. + +### 🙅 🆎 + +👆 💪 📣 🌐 🐩 🐍 🆎, 🚫 🕴 `str`. + +👆 💪 ⚙️, 🖼: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 💊 🆎 ⏮️ 🆎 🔢 + +📤 📊 📊 👈 💪 🔌 🎏 💲, 💖 `dict`, `list`, `set` & `tuple`. & 🔗 💲 💪 ✔️ 👫 👍 🆎 💁‍♂️. + +👉 🆎 👈 ✔️ 🔗 🆎 🤙 "**💊**" 🆎. & ⚫️ 💪 📣 👫, ⏮️ 👫 🔗 🆎. + +📣 👈 🆎 & 🔗 🆎, 👆 💪 ⚙️ 🐩 🐍 🕹 `typing`. ⚫️ 🔀 🎯 🐕‍🦺 👫 🆎 🔑. + +#### 🆕 ⏬ 🐍 + +❕ ⚙️ `typing` **🔗** ⏮️ 🌐 ⏬, ⚪️➡️ 🐍 3️⃣.6️⃣ ⏪ 🕐, ✅ 🐍 3️⃣.9️⃣, 🐍 3️⃣.1️⃣0️⃣, ♒️. + +🐍 🏧, **🆕 ⏬** 👟 ⏮️ 📉 🐕‍🦺 👉 🆎 ✍ & 📚 💼 👆 🏆 🚫 💪 🗄 & ⚙️ `typing` 🕹 📣 🆎 ✍. + +🚥 👆 💪 ⚒ 🌖 ⏮️ ⏬ 🐍 👆 🏗, 👆 🔜 💪 ✊ 📈 👈 ➕ 🦁. 👀 🖼 🔛. + +#### 📇 + +🖼, ➡️ 🔬 🔢 `list` `str`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ⚪️➡️ `typing`, 🗄 `List` (⏮️ 🔠 `L`): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + 📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. + + 🆎, 🚮 `List` 👈 👆 🗄 ⚪️➡️ `typing`. + + 📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + 📣 🔢, ⏮️ 🎏 ❤ (`:`) ❕. + + 🆎, 🚮 `list`. + + 📇 🆎 👈 🔌 🔗 🆎, 👆 🚮 👫 ⬜ 🗜: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info + 👈 🔗 🆎 ⬜ 🗜 🤙 "🆎 🔢". + + 👉 💼, `str` 🆎 🔢 🚶‍♀️ `List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛). + +👈 ⛓: "🔢 `items` `list`, & 🔠 🏬 👉 📇 `str`". + +!!! tip + 🚥 👆 ⚙️ 🐍 3️⃣.9️⃣ ⚖️ 🔛, 👆 🚫 ✔️ 🗄 `List` ⚪️➡️ `typing`, 👆 💪 ⚙️ 🎏 🥔 `list` 🆎 ↩️. + +🔨 👈, 👆 👨‍🎨 💪 🚚 🐕‍🦺 ⏪ 🏭 🏬 ⚪️➡️ 📇: + + + +🍵 🆎, 👈 🌖 💪 🏆. + +👀 👈 🔢 `item` 1️⃣ 🔣 📇 `items`. + +& , 👨‍🎨 💭 ⚫️ `str`, & 🚚 🐕‍🦺 👈. + +#### 🔢 & ⚒ + +👆 🔜 🎏 📣 `tuple`Ⓜ & `set`Ⓜ: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +👉 ⛓: + +* 🔢 `items_t` `tuple` ⏮️ 3️⃣ 🏬, `int`, ➕1️⃣ `int`, & `str`. +* 🔢 `items_s` `set`, & 🔠 🚮 🏬 🆎 `bytes`. + +#### #️⃣ + +🔬 `dict`, 👆 🚶‍♀️ 2️⃣ 🆎 🔢, 🎏 ❕. + +🥇 🆎 🔢 🔑 `dict`. + +🥈 🆎 🔢 💲 `dict`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +👉 ⛓: + +* 🔢 `prices` `dict`: + * 🔑 👉 `dict` 🆎 `str` (➡️ 💬, 📛 🔠 🏬). + * 💲 👉 `dict` 🆎 `float` (➡️ 💬, 🔖 🔠 🏬). + +#### 🇪🇺 + +👆 💪 📣 👈 🔢 💪 🙆 **📚 🆎**, 🖼, `int` ⚖️ `str`. + +🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 ⚙️ `Union` 🆎 ⚪️➡️ `typing` & 🚮 🔘 ⬜ 🗜 💪 🆎 🚫. + +🐍 3️⃣.1️⃣0️⃣ 📤 **🎛 ❕** 🌐❔ 👆 💪 🚮 💪 🆎 👽 ⏸ ⏸ (`|`). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +👯‍♂️ 💼 👉 ⛓ 👈 `item` 💪 `int` ⚖️ `str`. + +#### 🎲 `None` + +👆 💪 📣 👈 💲 💪 ✔️ 🆎, 💖 `str`, ✋️ 👈 ⚫️ 💪 `None`. + +🐍 3️⃣.6️⃣ & 🔛 (✅ 🐍 3️⃣.1️⃣0️⃣) 👆 💪 📣 ⚫️ 🏭 & ⚙️ `Optional` ⚪️➡️ `typing` 🕹. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +⚙️ `Optional[str]` ↩️ `str` 🔜 ➡️ 👨‍🎨 ℹ 👆 🔍 ❌ 🌐❔ 👆 💪 🤔 👈 💲 🕧 `str`, 🕐❔ ⚫️ 💪 🤙 `None` 💁‍♂️. + +`Optional[Something]` 🤙 ⌨ `Union[Something, None]`, 👫 🌓. + +👉 ⛓ 👈 🐍 3️⃣.1️⃣0️⃣, 👆 💪 ⚙️ `Something | None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "🐍 3️⃣.6️⃣ & 🔛 - 🎛" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +#### ⚙️ `Union` ⚖️ `Optional` + +🚥 👆 ⚙️ 🐍 ⏬ 🔛 3️⃣.1️⃣0️⃣, 📥 💁‍♂ ⚪️➡️ 👇 📶 **🤔** ☝ 🎑: + +* 👶 ❎ ⚙️ `Optional[SomeType]` +* ↩️ 👶 **⚙️ `Union[SomeType, None]`** 👶. + +👯‍♂️ 🌓 & 🔘 👫 🎏, ✋️ 👤 🔜 👍 `Union` ↩️ `Optional` ↩️ 🔤 "**📦**" 🔜 😑 🔑 👈 💲 📦, & ⚫️ 🤙 ⛓ "⚫️ 💪 `None`", 🚥 ⚫️ 🚫 📦 & ✔. + +👤 💭 `Union[SomeType, None]` 🌖 🔑 🔃 ⚫️❔ ⚫️ ⛓. + +⚫️ 🔃 🔤 & 📛. ✋️ 👈 🔤 💪 📉 ❔ 👆 & 👆 🤽‍♂ 💭 🔃 📟. + +🖼, ➡️ ✊ 👉 🔢: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +🔢 `name` 🔬 `Optional[str]`, ✋️ ⚫️ **🚫 📦**, 👆 🚫🔜 🤙 🔢 🍵 🔢: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +`name` 🔢 **✔** (🚫 *📦*) ↩️ ⚫️ 🚫 ✔️ 🔢 💲. , `name` 🚫 `None` 💲: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +👍 📰, 🕐 👆 🔛 🐍 3️⃣.1️⃣0️⃣ 👆 🏆 🚫 ✔️ 😟 🔃 👈, 👆 🔜 💪 🎯 ⚙️ `|` 🔬 🇪🇺 🆎: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +& ⤴️ 👆 🏆 🚫 ✔️ 😟 🔃 📛 💖 `Optional` & `Union`. 👶 + +#### 💊 🆎 + +👉 🆎 👈 ✊ 🆎 🔢 ⬜ 🗜 🤙 **💊 🆎** ⚖️ **💊**, 🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...& 🎏. + +=== "🐍 3️⃣.9️⃣ & 🔛" + + 👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): + + * `list` + * `tuple` + * `set` + * `dict` + + & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: + + * `Union` + * `Optional` + * ...& 🎏. + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + 👆 💪 ⚙️ 🎏 💽 🆎 💊 (⏮️ ⬜ 🗜 & 🆎 🔘): + + * `list` + * `tuple` + * `set` + * `dict` + + & 🎏 ⏮️ 🐍 3️⃣.6️⃣, ⚪️➡️ `typing` 🕹: + + * `Union` + * `Optional` (🎏 ⏮️ 🐍 3️⃣.6️⃣) + * ...& 🎏. + + 🐍 3️⃣.1️⃣0️⃣, 🎛 ⚙️ 💊 `Union` & `Optional`, 👆 💪 ⚙️ ⏸ ⏸ (`|`) 📣 🇪🇺 🆎. + +### 🎓 🆎 + +👆 💪 📣 🎓 🆎 🔢. + +➡️ 💬 👆 ✔️ 🎓 `Person`, ⏮️ 📛: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +⤴️ 👆 💪 📣 🔢 🆎 `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +& ⤴️, 🔄, 👆 🤚 🌐 👨‍🎨 🐕‍🦺: + + + +## Pydantic 🏷 + +Pydantic 🐍 🗃 🎭 📊 🔬. + +👆 📣 "💠" 💽 🎓 ⏮️ 🔢. + +& 🔠 🔢 ✔️ 🆎. + +⤴️ 👆 ✍ 👐 👈 🎓 ⏮️ 💲 & ⚫️ 🔜 ✔ 💲, 🗜 👫 ☑ 🆎 (🚥 👈 💼) & 🤝 👆 🎚 ⏮️ 🌐 💽. + +& 👆 🤚 🌐 👨‍🎨 🐕‍🦺 ⏮️ 👈 📉 🎚. + +🖼 ⚪️➡️ 🛂 Pydantic 🩺: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +!!! info + 💡 🌖 🔃 Pydantic, ✅ 🚮 🩺. + +**FastAPI** 🌐 ⚓️ 🔛 Pydantic. + +👆 🔜 👀 📚 🌅 🌐 👉 💡 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. + +!!! tip + Pydantic ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. + +## 🆎 🔑 **FastAPI** + +**FastAPI** ✊ 📈 👫 🆎 🔑 📚 👜. + +⏮️ **FastAPI** 👆 📣 🔢 ⏮️ 🆎 🔑 & 👆 🤚: + +* **👨‍🎨 🐕‍🦺**. +* **🆎 ✅**. + +...and **FastAPI** uses the same declarations : + +* **🔬 📄**: ⚪️➡️ 📨 ➡ 🔢, 🔢 🔢, 🎚, 💪, 🔗, ♒️. +* **🗜 💽**: ⚪️➡️ 📨 🚚 🆎. +* **✔ 💽**: 👟 ⚪️➡️ 🔠 📨: + * 🏭 **🏧 ❌** 📨 👩‍💻 🕐❔ 📊 ❌. +* **📄** 🛠️ ⚙️ 🗄: + * ❔ ⤴️ ⚙️ 🏧 🎓 🧾 👩‍💻 🔢. + +👉 5️⃣📆 🌐 🔊 📝. 🚫 😟. 👆 🔜 👀 🌐 👉 🎯 [🔰 - 👩‍💻 🦮](tutorial/index.md){.internal-link target=_blank}. + +⚠ 👜 👈 ⚙️ 🐩 🐍 🆎, 👁 🥉 (↩️ ❎ 🌖 🎓, 👨‍🎨, ♒️), **FastAPI** 🔜 📚 👷 👆. + +!!! info + 🚥 👆 ⏪ 🚶 🔘 🌐 🔰 & 👟 🔙 👀 🌅 🔃 🆎, 👍 ℹ "🎮 🎼" ⚪️➡️ `mypy`. diff --git a/docs/em/docs/tutorial/background-tasks.md b/docs/em/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..e28ead415 --- /dev/null +++ b/docs/em/docs/tutorial/background-tasks.md @@ -0,0 +1,102 @@ +# 🖥 📋 + +👆 💪 🔬 🖥 📋 🏃 *⏮️* 🛬 📨. + +👉 ⚠ 🛠️ 👈 💪 🔨 ⏮️ 📨, ✋️ 👈 👩‍💻 🚫 🤙 ✔️ ⌛ 🛠️ 🏁 ⏭ 📨 📨. + +👉 🔌, 🖼: + +* 📧 📨 📨 ⏮️ 🎭 🎯: + * 🔗 📧 💽 & 📨 📧 😑 "🐌" (📚 🥈), 👆 💪 📨 📨 ▶️️ ↖️ & 📨 📧 📨 🖥. +* 🏭 💽: + * 🖼, ➡️ 💬 👆 📨 📁 👈 🔜 🚶 🔘 🐌 🛠️, 👆 💪 📨 📨 "🚫" (🇺🇸🔍 2️⃣0️⃣2️⃣) & 🛠️ ⚫️ 🖥. + +## ⚙️ `BackgroundTasks` + +🥇, 🗄 `BackgroundTasks` & 🔬 🔢 👆 *➡ 🛠️ 🔢* ⏮️ 🆎 📄 `BackgroundTasks`: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** 🔜 ✍ 🎚 🆎 `BackgroundTasks` 👆 & 🚶‍♀️ ⚫️ 👈 🔢. + +## ✍ 📋 🔢 + +✍ 🔢 🏃 🖥 📋. + +⚫️ 🐩 🔢 👈 💪 📨 🔢. + +⚫️ 💪 `async def` ⚖️ 😐 `def` 🔢, **FastAPI** 🔜 💭 ❔ 🍵 ⚫️ ☑. + +👉 💼, 📋 🔢 🔜 ✍ 📁 (⚖ 📨 📧). + +& ✍ 🛠️ 🚫 ⚙️ `async` & `await`, 👥 🔬 🔢 ⏮️ 😐 `def`: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## 🚮 🖥 📋 + +🔘 👆 *➡ 🛠️ 🔢*, 🚶‍♀️ 👆 📋 🔢 *🖥 📋* 🎚 ⏮️ 👩‍🔬 `.add_task()`: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` 📨 ❌: + +* 📋 🔢 🏃 🖥 (`write_notification`). +* 🙆 🔁 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 ✔ (`email`). +* 🙆 🇨🇻 ❌ 👈 🔜 🚶‍♀️ 📋 🔢 (`message="some notification"`). + +## 🔗 💉 + +⚙️ `BackgroundTasks` 👷 ⏮️ 🔗 💉 ⚙️, 👆 💪 📣 🔢 🆎 `BackgroundTasks` 💗 🎚: *➡ 🛠️ 🔢*, 🔗 (☑), 🎧-🔗, ♒️. + +**FastAPI** 💭 ⚫️❔ 🔠 💼 & ❔ 🏤-⚙️ 🎏 🎚, 👈 🌐 🖥 📋 🔗 👯‍♂️ & 🏃 🖥 ⏮️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +👉 🖼, 📧 🔜 ✍ `log.txt` 📁 *⏮️* 📨 📨. + +🚥 📤 🔢 📨, ⚫️ 🔜 ✍ 🕹 🖥 📋. + +& ⤴️ ➕1️⃣ 🖥 📋 🏗 *➡ 🛠️ 🔢* 🔜 ✍ 📧 ⚙️ `email` ➡ 🔢. + +## 📡 ℹ + +🎓 `BackgroundTasks` 👟 🔗 ⚪️➡️ `starlette.background`. + +⚫️ 🗄/🔌 🔗 🔘 FastAPI 👈 👆 💪 🗄 ⚫️ ⚪️➡️ `fastapi` & ❎ 😫 🗄 🎛 `BackgroundTask` (🍵 `s` 🔚) ⚪️➡️ `starlette.background`. + +🕴 ⚙️ `BackgroundTasks` (& 🚫 `BackgroundTask`), ⚫️ ⤴️ 💪 ⚙️ ⚫️ *➡ 🛠️ 🔢* 🔢 & ✔️ **FastAPI** 🍵 🎂 👆, 💖 🕐❔ ⚙️ `Request` 🎚 🔗. + +⚫️ 💪 ⚙️ `BackgroundTask` 😞 FastAPI, ✋️ 👆 ✔️ ✍ 🎚 👆 📟 & 📨 💃 `Response` 🔌 ⚫️. + +👆 💪 👀 🌖 ℹ 💃 🛂 🩺 🖥 📋. + +## ⚠ + +🚥 👆 💪 🎭 🏋️ 🖥 📊 & 👆 🚫 🎯 💪 ⚫️ 🏃 🎏 🛠️ (🖼, 👆 🚫 💪 💰 💾, 🔢, ♒️), 👆 💪 💰 ⚪️➡️ ⚙️ 🎏 🦏 🧰 💖 🥒. + +👫 😑 🚚 🌖 🏗 📳, 📧/👨‍🏭 📤 👨‍💼, 💖 ✳ ⚖️ ✳, ✋️ 👫 ✔ 👆 🏃 🖥 📋 💗 🛠️, & ✴️, 💗 💽. + +👀 🖼, ✅ [🏗 🚂](../project-generation.md){.internal-link target=_blank}, 👫 🌐 🔌 🥒 ⏪ 📶. + +✋️ 🚥 👆 💪 🔐 🔢 & 🎚 ⚪️➡️ 🎏 **FastAPI** 📱, ⚖️ 👆 💪 🎭 🤪 🖥 📋 (💖 📨 📧 📨), 👆 💪 🎯 ⚙️ `BackgroundTasks`. + +## 🌃 + +🗄 & ⚙️ `BackgroundTasks` ⏮️ 🔢 *➡ 🛠️ 🔢* & 🔗 🚮 🖥 📋. diff --git a/docs/em/docs/tutorial/bigger-applications.md b/docs/em/docs/tutorial/bigger-applications.md new file mode 100644 index 000000000..7b4694387 --- /dev/null +++ b/docs/em/docs/tutorial/bigger-applications.md @@ -0,0 +1,488 @@ +# 🦏 🈸 - 💗 📁 + +🚥 👆 🏗 🈸 ⚖️ 🕸 🛠️, ⚫️ 🛎 💼 👈 👆 💪 🚮 🌐 🔛 👁 📁. + +**FastAPI** 🚚 🏪 🧰 📊 👆 🈸 ⏪ 🚧 🌐 💪. + +!!! info + 🚥 👆 👟 ⚪️➡️ 🏺, 👉 🔜 🌓 🏺 📗. + +## 🖼 📁 📊 + +➡️ 💬 👆 ✔️ 📁 📊 💖 👉: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   ├── dependencies.py +│   └── routers +│   │ ├── __init__.py +│   │ ├── items.py +│   │ └── users.py +│   └── internal +│   ├── __init__.py +│   └── admin.py +``` + +!!! tip + 📤 📚 `__init__.py` 📁: 1️⃣ 🔠 📁 ⚖️ 📁. + + 👉 ⚫️❔ ✔ 🏭 📟 ⚪️➡️ 1️⃣ 📁 🔘 ➕1️⃣. + + 🖼, `app/main.py` 👆 💪 ✔️ ⏸ 💖: + + ``` + from app.routers import items + ``` + +* `app` 📁 🔌 🌐. & ⚫️ ✔️ 🛁 📁 `app/__init__.py`, ⚫️ "🐍 📦" (🗃 "🐍 🕹"): `app`. +* ⚫️ 🔌 `app/main.py` 📁. ⚫️ 🔘 🐍 📦 (📁 ⏮️ 📁 `__init__.py`), ⚫️ "🕹" 👈 📦: `app.main`. +* 📤 `app/dependencies.py` 📁, 💖 `app/main.py`, ⚫️ "🕹": `app.dependencies`. +* 📤 📁 `app/routers/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ "🐍 📦": `app.routers`. +* 📁 `app/routers/items.py` 🔘 📦, `app/routers/`,, ⚫️ 🔁: `app.routers.items`. +* 🎏 ⏮️ `app/routers/users.py`, ⚫️ ➕1️⃣ 🔁: `app.routers.users`. +* 📤 📁 `app/internal/` ⏮️ ➕1️⃣ 📁 `__init__.py`, ⚫️ ➕1️⃣ "🐍 📦": `app.internal`. +* & 📁 `app/internal/admin.py` ➕1️⃣ 🔁: `app.internal.admin`. + + + +🎏 📁 📊 ⏮️ 🏤: + +``` +. +├── app # "app" is a Python package +│   ├── __init__.py # this file makes "app" a "Python package" +│   ├── main.py # "main" module, e.g. import app.main +│   ├── dependencies.py # "dependencies" module, e.g. import app.dependencies +│   └── routers # "routers" is a "Python subpackage" +│   │ ├── __init__.py # makes "routers" a "Python subpackage" +│   │ ├── items.py # "items" submodule, e.g. import app.routers.items +│   │ └── users.py # "users" submodule, e.g. import app.routers.users +│   └── internal # "internal" is a "Python subpackage" +│   ├── __init__.py # makes "internal" a "Python subpackage" +│   └── admin.py # "admin" submodule, e.g. import app.internal.admin +``` + +## `APIRouter` + +➡️ 💬 📁 💡 🚚 👩‍💻 🔁 `/app/routers/users.py`. + +👆 💚 ✔️ *➡ 🛠️* 🔗 👆 👩‍💻 👽 ⚪️➡️ 🎂 📟, 🚧 ⚫️ 🏗. + +✋️ ⚫️ 🍕 🎏 **FastAPI** 🈸/🕸 🛠️ (⚫️ 🍕 🎏 "🐍 📦"). + +👆 💪 ✍ *➡ 🛠️* 👈 🕹 ⚙️ `APIRouter`. + +### 🗄 `APIRouter` + +👆 🗄 ⚫️ & ✍ "👐" 🎏 🌌 👆 🔜 ⏮️ 🎓 `FastAPI`: + +```Python hl_lines="1 3" +{!../../../docs_src/bigger_applications/app/routers/users.py!} +``` + +### *➡ 🛠️* ⏮️ `APIRouter` + +& ⤴️ 👆 ⚙️ ⚫️ 📣 👆 *➡ 🛠️*. + +⚙️ ⚫️ 🎏 🌌 👆 🔜 ⚙️ `FastAPI` 🎓: + +```Python hl_lines="6 11 16" +{!../../../docs_src/bigger_applications/app/routers/users.py!} +``` + +👆 💪 💭 `APIRouter` "🐩 `FastAPI`" 🎓. + +🌐 🎏 🎛 🐕‍🦺. + +🌐 🎏 `parameters`, `responses`, `dependencies`, `tags`, ♒️. + +!!! tip + 👉 🖼, 🔢 🤙 `router`, ✋️ 👆 💪 📛 ⚫️ 👐 👆 💚. + +👥 🔜 🔌 👉 `APIRouter` 👑 `FastAPI` 📱, ✋️ 🥇, ➡️ ✅ 🔗 & ➕1️⃣ `APIRouter`. + +## 🔗 + +👥 👀 👈 👥 🔜 💪 🔗 ⚙️ 📚 🥉 🈸. + +👥 🚮 👫 👫 👍 `dependencies` 🕹 (`app/dependencies.py`). + +👥 🔜 🔜 ⚙️ 🙅 🔗 ✍ 🛃 `X-Token` 🎚: + +```Python hl_lines="1 4-6" +{!../../../docs_src/bigger_applications/app/dependencies.py!} +``` + +!!! tip + 👥 ⚙️ 💭 🎚 📉 👉 🖼. + + ✋️ 🎰 💼 👆 🔜 🤚 👍 🏁 ⚙️ 🛠️ [💂‍♂ 🚙](./security/index.md){.internal-link target=_blank}. + +## ➕1️⃣ 🕹 ⏮️ `APIRouter` + +➡️ 💬 👆 ✔️ 🔗 💡 🚚 "🏬" ⚪️➡️ 👆 🈸 🕹 `app/routers/items.py`. + +👆 ✔️ *➡ 🛠️* : + +* `/items/` +* `/items/{item_id}` + +⚫️ 🌐 🎏 📊 ⏮️ `app/routers/users.py`. + +✋️ 👥 💚 🙃 & 📉 📟 🍖. + +👥 💭 🌐 *➡ 🛠️* 👉 🕹 ✔️ 🎏: + +* ➡ `prefix`: `/items`. +* `tags`: (1️⃣ 🔖: `items`). +* ➕ `responses`. +* `dependencies`: 👫 🌐 💪 👈 `X-Token` 🔗 👥 ✍. + +, ↩️ ❎ 🌐 👈 🔠 *➡ 🛠️*, 👥 💪 🚮 ⚫️ `APIRouter`. + +```Python hl_lines="5-10 16 21" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +➡ 🔠 *➡ 🛠️* ✔️ ▶️ ⏮️ `/`, 💖: + +```Python hl_lines="1" +@router.get("/{item_id}") +async def read_item(item_id: str): + ... +``` + +...🔡 🔜 🚫 🔌 🏁 `/`. + +, 🔡 👉 💼 `/items`. + +👥 💪 🚮 📇 `tags` & ➕ `responses` 👈 🔜 ✔ 🌐 *➡ 🛠️* 🔌 👉 📻. + +& 👥 💪 🚮 📇 `dependencies` 👈 🔜 🚮 🌐 *➡ 🛠️* 📻 & 🔜 🛠️/❎ 🔠 📨 ⚒ 👫. + +!!! tip + 🗒 👈, 🌅 💖 [🔗 *➡ 🛠️ 👨‍🎨*](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 🙅‍♂ 💲 🔜 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. + +🔚 🏁 👈 🏬 ➡ 🔜: + +* `/items/` +* `/items/{item_id}` + +...👥 🎯. + +* 👫 🔜 ™ ⏮️ 📇 🔖 👈 🔌 👁 🎻 `"items"`. + * 👫 "🔖" ✴️ ⚠ 🏧 🎓 🧾 ⚙️ (⚙️ 🗄). +* 🌐 👫 🔜 🔌 🔁 `responses`. +* 🌐 👫 *➡ 🛠️* 🔜 ✔️ 📇 `dependencies` 🔬/🛠️ ⏭ 👫. + * 🚥 👆 📣 🔗 🎯 *➡ 🛠️*, **👫 🔜 🛠️ 💁‍♂️**. + * 📻 🔗 🛠️ 🥇, ⤴️ [`dependencies` 👨‍🎨](dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, & ⤴️ 😐 🔢 🔗. + * 👆 💪 🚮 [`Security` 🔗 ⏮️ `scopes`](../advanced/security/oauth2-scopes.md){.internal-link target=_blank}. + +!!! tip + ✔️ `dependencies` `APIRouter` 💪 ⚙️, 🖼, 🚚 🤝 🎂 👪 *➡ 🛠️*. 🚥 🔗 🚫 🚮 📦 🔠 1️⃣ 👫. + +!!! check + `prefix`, `tags`, `responses`, & `dependencies` 🔢 (📚 🎏 💼) ⚒ ⚪️➡️ **FastAPI** ℹ 👆 ❎ 📟 ❎. + +### 🗄 🔗 + +👉 📟 👨‍❤‍👨 🕹 `app.routers.items`, 📁 `app/routers/items.py`. + +& 👥 💪 🤚 🔗 🔢 ⚪️➡️ 🕹 `app.dependencies`, 📁 `app/dependencies.py`. + +👥 ⚙️ ⚖ 🗄 ⏮️ `..` 🔗: + +```Python hl_lines="3" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +#### ❔ ⚖ 🗄 👷 + +!!! tip + 🚥 👆 💭 👌 ❔ 🗄 👷, 😣 ⏭ 📄 🔛. + +👁 ❣ `.`, 💖: + +```Python +from .dependencies import get_token_header +``` + +🔜 ⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🔎 🕹 `dependencies` (👽 📁 `app/routers/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +✋️ 👈 📁 🚫 🔀, 👆 🔗 📁 `app/dependencies.py`. + +💭 ❔ 👆 📱/📁 📊 👀 💖: + + + +--- + +2️⃣ ❣ `..`, 💖: + +```Python +from ..dependencies import get_token_header +``` + +⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🚶 👪 📦 (📁 `app/`)... +* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +👈 👷 ☑ ❗ 👶 + +--- + +🎏 🌌, 🚥 👥 ✔️ ⚙️ 3️⃣ ❣ `...`, 💖: + +```Python +from ...dependencies import get_token_header +``` + +that 🔜 ⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/routers/items.py`) 🖖 (📁 `app/routers/`)... +* 🚶 👪 📦 (📁 `app/`)... +* ⤴️ 🚶 👪 👈 📦 (📤 🙅‍♂ 👪 📦, `app` 🔝 🎚 👶)... +* & 📤, 🔎 🕹 `dependencies` (📁 `app/dependencies.py`)... +* & ⚪️➡️ ⚫️, 🗄 🔢 `get_token_header`. + +👈 🔜 🔗 📦 🔛 `app/`, ⏮️ 🚮 👍 📁 `__init__.py`, ♒️. ✋️ 👥 🚫 ✔️ 👈. , 👈 🔜 🚮 ❌ 👆 🖼. 👶 + +✋️ 🔜 👆 💭 ❔ ⚫️ 👷, 👆 💪 ⚙️ ⚖ 🗄 👆 👍 📱 🙅‍♂ 🤔 ❔ 🏗 👫. 👶 + +### 🚮 🛃 `tags`, `responses`, & `dependencies` + +👥 🚫 ❎ 🔡 `/items` 🚫 `tags=["items"]` 🔠 *➡ 🛠️* ↩️ 👥 🚮 👫 `APIRouter`. + +✋️ 👥 💪 🚮 _🌅_ `tags` 👈 🔜 ✔ 🎯 *➡ 🛠️*, & ➕ `responses` 🎯 👈 *➡ 🛠️*: + +```Python hl_lines="30-31" +{!../../../docs_src/bigger_applications/app/routers/items.py!} +``` + +!!! tip + 👉 🏁 ➡ 🛠️ 🔜 ✔️ 🌀 🔖: `["items", "custom"]`. + + & ⚫️ 🔜 ✔️ 👯‍♂️ 📨 🧾, 1️⃣ `404` & 1️⃣ `403`. + +## 👑 `FastAPI` + +🔜, ➡️ 👀 🕹 `app/main.py`. + +📥 🌐❔ 👆 🗄 & ⚙️ 🎓 `FastAPI`. + +👉 🔜 👑 📁 👆 🈸 👈 👔 🌐 👯‍♂️. + +& 🏆 👆 ⚛ 🔜 🔜 🖖 🚮 👍 🎯 🕹, 👑 📁 🔜 🙅. + +### 🗄 `FastAPI` + +👆 🗄 & ✍ `FastAPI` 🎓 🛎. + +& 👥 💪 📣 [🌐 🔗](dependencies/global-dependencies.md){.internal-link target=_blank} 👈 🔜 🌀 ⏮️ 🔗 🔠 `APIRouter`: + +```Python hl_lines="1 3 7" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +### 🗄 `APIRouter` + +🔜 👥 🗄 🎏 🔁 👈 ✔️ `APIRouter`Ⓜ: + +```Python hl_lines="5" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +📁 `app/routers/users.py` & `app/routers/items.py` 🔁 👈 🍕 🎏 🐍 📦 `app`, 👥 💪 ⚙️ 👁 ❣ `.` 🗄 👫 ⚙️ "⚖ 🗄". + +### ❔ 🏭 👷 + +📄: + +```Python +from .routers import items, users +``` + +⛓: + +* ▶️ 🎏 📦 👈 👉 🕹 (📁 `app/main.py`) 🖖 (📁 `app/`)... +* 👀 📦 `routers` (📁 `app/routers/`)... +* & ⚪️➡️ ⚫️, 🗄 🔁 `items` (📁 `app/routers/items.py`) & `users` (📁 `app/routers/users.py`)... + +🕹 `items` 🔜 ✔️ 🔢 `router` (`items.router`). 👉 🎏 1️⃣ 👥 ✍ 📁 `app/routers/items.py`, ⚫️ `APIRouter` 🎚. + +& ⤴️ 👥 🎏 🕹 `users`. + +👥 💪 🗄 👫 💖: + +```Python +from app.routers import items, users +``` + +!!! info + 🥇 ⏬ "⚖ 🗄": + + ```Python + from .routers import items, users + ``` + + 🥈 ⏬ "🎆 🗄": + + ```Python + from app.routers import items, users + ``` + + 💡 🌅 🔃 🐍 📦 & 🕹, ✍ 🛂 🐍 🧾 🔃 🕹. + +### ❎ 📛 💥 + +👥 🏭 🔁 `items` 🔗, ↩️ 🏭 🚮 🔢 `router`. + +👉 ↩️ 👥 ✔️ ➕1️⃣ 🔢 📛 `router` 🔁 `users`. + +🚥 👥 ✔️ 🗄 1️⃣ ⏮️ 🎏, 💖: + +```Python +from .routers.items import router +from .routers.users import router +``` + +`router` ⚪️➡️ `users` 🔜 📁 1️⃣ ⚪️➡️ `items` & 👥 🚫🔜 💪 ⚙️ 👫 🎏 🕰. + +, 💪 ⚙️ 👯‍♂️ 👫 🎏 📁, 👥 🗄 🔁 🔗: + +```Python hl_lines="4" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +### 🔌 `APIRouter`Ⓜ `users` & `items` + +🔜, ➡️ 🔌 `router`Ⓜ ⚪️➡️ 🔁 `users` & `items`: + +```Python hl_lines="10-11" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +!!! info + `users.router` 🔌 `APIRouter` 🔘 📁 `app/routers/users.py`. + + & `items.router` 🔌 `APIRouter` 🔘 📁 `app/routers/items.py`. + +⏮️ `app.include_router()` 👥 💪 🚮 🔠 `APIRouter` 👑 `FastAPI` 🈸. + +⚫️ 🔜 🔌 🌐 🛣 ⚪️➡️ 👈 📻 🍕 ⚫️. + +!!! note "📡 ℹ" + ⚫️ 🔜 🤙 🔘 ✍ *➡ 🛠️* 🔠 *➡ 🛠️* 👈 📣 `APIRouter`. + + , ⛅ 🎑, ⚫️ 🔜 🤙 👷 🚥 🌐 🎏 👁 📱. + +!!! check + 👆 🚫 ✔️ 😟 🔃 🎭 🕐❔ ✅ 📻. + + 👉 🔜 ✊ ⏲ & 🔜 🕴 🔨 🕴. + + ⚫️ 🏆 🚫 📉 🎭. 👶 + +### 🔌 `APIRouter` ⏮️ 🛃 `prefix`, `tags`, `responses`, & `dependencies` + +🔜, ➡️ 🌈 👆 🏢 🤝 👆 `app/internal/admin.py` 📁. + +⚫️ 🔌 `APIRouter` ⏮️ 📡 *➡ 🛠️* 👈 👆 🏢 💰 🖖 📚 🏗. + +👉 🖼 ⚫️ 🔜 💎 🙅. ✋️ ➡️ 💬 👈 ↩️ ⚫️ 💰 ⏮️ 🎏 🏗 🏢, 👥 🚫🔜 🔀 ⚫️ & 🚮 `prefix`, `dependencies`, `tags`, ♒️. 🔗 `APIRouter`: + +```Python hl_lines="3" +{!../../../docs_src/bigger_applications/app/internal/admin.py!} +``` + +✋️ 👥 💚 ⚒ 🛃 `prefix` 🕐❔ ✅ `APIRouter` 👈 🌐 🚮 *➡ 🛠️* ▶️ ⏮️ `/admin`, 👥 💚 🔐 ⚫️ ⏮️ `dependencies` 👥 ⏪ ✔️ 👉 🏗, & 👥 💚 🔌 `tags` & `responses`. + +👥 💪 📣 🌐 👈 🍵 ✔️ 🔀 ⏮️ `APIRouter` 🚶‍♀️ 👈 🔢 `app.include_router()`: + +```Python hl_lines="14-17" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +👈 🌌, ⏮️ `APIRouter` 🔜 🚧 ⚗, 👥 💪 💰 👈 🎏 `app/internal/admin.py` 📁 ⏮️ 🎏 🏗 🏢. + +🏁 👈 👆 📱, 🔠 *➡ 🛠️* ⚪️➡️ `admin` 🕹 🔜 ✔️: + +* 🔡 `/admin`. +* 🔖 `admin`. +* 🔗 `get_token_header`. +* 📨 `418`. 👶 + +✋️ 👈 🔜 🕴 📉 👈 `APIRouter` 👆 📱, 🚫 🙆 🎏 📟 👈 ⚙️ ⚫️. + +, 🖼, 🎏 🏗 💪 ⚙️ 🎏 `APIRouter` ⏮️ 🎏 🤝 👩‍🔬. + +### 🔌 *➡ 🛠️* + +👥 💪 🚮 *➡ 🛠️* 🔗 `FastAPI` 📱. + +📥 👥 ⚫️... 🎦 👈 👥 💪 🤷: + +```Python hl_lines="21-23" +{!../../../docs_src/bigger_applications/app/main.py!} +``` + +& ⚫️ 🔜 👷 ☑, 👯‍♂️ ⏮️ 🌐 🎏 *➡ 🛠️* 🚮 ⏮️ `app.include_router()`. + +!!! info "📶 📡 ℹ" + **🗒**: 👉 📶 📡 ℹ 👈 👆 🎲 💪 **🚶**. + + --- + + `APIRouter`Ⓜ 🚫 "🗻", 👫 🚫 👽 ⚪️➡️ 🎂 🈸. + + 👉 ↩️ 👥 💚 🔌 👫 *➡ 🛠️* 🗄 🔗 & 👩‍💻 🔢. + + 👥 🚫🔜 ❎ 👫 & "🗻" 👫 ➡ 🎂, *➡ 🛠️* "🖖" (🏤-✍), 🚫 🔌 🔗. + +## ✅ 🏧 🛠️ 🩺 + +🔜, 🏃 `uvicorn`, ⚙️ 🕹 `app.main` & 🔢 `app`: + +
+ +```console +$ uvicorn app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& 📂 🩺 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🛠️ 🩺, ✅ ➡ ⚪️➡️ 🌐 🔁, ⚙️ ☑ ➡ (& 🔡) & ☑ 🔖: + + + +## 🔌 🎏 📻 💗 🕰 ⏮️ 🎏 `prefix` + +👆 💪 ⚙️ `.include_router()` 💗 🕰 ⏮️ *🎏* 📻 ⚙️ 🎏 🔡. + +👉 💪 ⚠, 🖼, 🎦 🎏 🛠️ 🔽 🎏 🔡, ✅ `/api/v1` & `/api/latest`. + +👉 🏧 ⚙️ 👈 👆 5️⃣📆 🚫 🤙 💪, ✋️ ⚫️ 📤 💼 👆. + +## 🔌 `APIRouter` ➕1️⃣ + +🎏 🌌 👆 💪 🔌 `APIRouter` `FastAPI` 🈸, 👆 💪 🔌 `APIRouter` ➕1️⃣ `APIRouter` ⚙️: + +```Python +router.include_router(other_router) +``` + +⚒ 💭 👆 ⚫️ ⏭ 🔌 `router` `FastAPI` 📱, 👈 *➡ 🛠️* ⚪️➡️ `other_router` 🔌. diff --git a/docs/em/docs/tutorial/body-fields.md b/docs/em/docs/tutorial/body-fields.md new file mode 100644 index 000000000..9f2c914f4 --- /dev/null +++ b/docs/em/docs/tutorial/body-fields.md @@ -0,0 +1,68 @@ +# 💪 - 🏑 + +🎏 🌌 👆 💪 📣 🌖 🔬 & 🗃 *➡ 🛠️ 🔢* 🔢 ⏮️ `Query`, `Path` & `Body`, 👆 💪 📣 🔬 & 🗃 🔘 Pydantic 🏷 ⚙️ Pydantic `Field`. + +## 🗄 `Field` + +🥇, 👆 ✔️ 🗄 ⚫️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +!!! warning + 👀 👈 `Field` 🗄 🔗 ⚪️➡️ `pydantic`, 🚫 ⚪️➡️ `fastapi` 🌐 🎂 (`Query`, `Path`, `Body`, ♒️). + +## 📣 🏷 🔢 + +👆 💪 ⤴️ ⚙️ `Field` ⏮️ 🏷 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +`Field` 👷 🎏 🌌 `Query`, `Path` & `Body`, ⚫️ ✔️ 🌐 🎏 🔢, ♒️. + +!!! note "📡 ℹ" + 🤙, `Query`, `Path` & 🎏 👆 🔜 👀 ⏭ ✍ 🎚 🏿 ⚠ `Param` 🎓, ❔ ⚫️ 🏿 Pydantic `FieldInfo` 🎓. + + & Pydantic `Field` 📨 👐 `FieldInfo` 👍. + + `Body` 📨 🎚 🏿 `FieldInfo` 🔗. & 📤 🎏 👆 🔜 👀 ⏪ 👈 🏿 `Body` 🎓. + + 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! tip + 👀 ❔ 🔠 🏷 🔢 ⏮️ 🆎, 🔢 💲 & `Field` ✔️ 🎏 📊 *➡ 🛠️ 🔢* 🔢, ⏮️ `Field` ↩️ `Path`, `Query` & `Body`. + +## 🚮 ➕ ℹ + +👆 💪 📣 ➕ ℹ `Field`, `Query`, `Body`, ♒️. & ⚫️ 🔜 🔌 🏗 🎻 🔗. + +👆 🔜 💡 🌅 🔃 ❎ ➕ ℹ ⏪ 🩺, 🕐❔ 🏫 📣 🖼. + +!!! warning + ➕ 🔑 🚶‍♀️ `Field` 🔜 🎁 📉 🗄 🔗 👆 🈸. + 👫 🔑 5️⃣📆 🚫 🎯 🍕 🗄 🔧, 🗄 🧰, 🖼 [🗄 💳](https://validator.swagger.io/), 5️⃣📆 🚫 👷 ⏮️ 👆 🏗 🔗. + +## 🌃 + +👆 💪 ⚙️ Pydantic `Field` 📣 ➕ 🔬 & 🗃 🏷 🔢. + +👆 💪 ⚙️ ➕ 🇨🇻 ❌ 🚶‍♀️ 🌖 🎻 🔗 🗃. diff --git a/docs/em/docs/tutorial/body-multiple-params.md b/docs/em/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..9ada7dee1 --- /dev/null +++ b/docs/em/docs/tutorial/body-multiple-params.md @@ -0,0 +1,213 @@ +# 💪 - 💗 🔢 + +🔜 👈 👥 ✔️ 👀 ❔ ⚙️ `Path` & `Query`, ➡️ 👀 🌅 🏧 ⚙️ 📨 💪 📄. + +## 🌀 `Path`, `Query` & 💪 🔢 + +🥇, ↗️, 👆 💪 🌀 `Path`, `Query` & 📨 💪 🔢 📄 ➡ & **FastAPI** 🔜 💭 ⚫️❔. + +& 👆 💪 📣 💪 🔢 📦, ⚒ 🔢 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +!!! note + 👀 👈, 👉 💼, `item` 👈 🔜 ✊ ⚪️➡️ 💪 📦. ⚫️ ✔️ `None` 🔢 💲. + +## 💗 💪 🔢 + +⏮️ 🖼, *➡ 🛠️* 🔜 ⌛ 🎻 💪 ⏮️ 🔢 `Item`, 💖: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +✋️ 👆 💪 📣 💗 💪 🔢, ✅ `item` & `user`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +👉 💼, **FastAPI** 🔜 👀 👈 📤 🌅 🌘 1️⃣ 💪 🔢 🔢 (2️⃣ 🔢 👈 Pydantic 🏷). + +, ⚫️ 🔜 ⤴️ ⚙️ 🔢 📛 🔑 (🏑 📛) 💪, & ⌛ 💪 💖: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note + 👀 👈 ✋️ `item` 📣 🎏 🌌 ⏭, ⚫️ 🔜 ⌛ 🔘 💪 ⏮️ 🔑 `item`. + + +**FastAPI** 🔜 🏧 🛠️ ⚪️➡️ 📨, 👈 🔢 `item` 📨 ⚫️ 🎯 🎚 & 🎏 `user`. + +⚫️ 🔜 🎭 🔬 ⚗ 💽, & 🔜 📄 ⚫️ 💖 👈 🗄 🔗 & 🏧 🩺. + +## ⭐ 💲 💪 + +🎏 🌌 📤 `Query` & `Path` 🔬 ➕ 💽 🔢 & ➡ 🔢, **FastAPI** 🚚 🌓 `Body`. + +🖼, ↔ ⏮️ 🏷, 👆 💪 💭 👈 👆 💚 ✔️ ➕1️⃣ 🔑 `importance` 🎏 💪, 🥈 `item` & `user`. + +🚥 👆 📣 ⚫️, ↩️ ⚫️ ⭐ 💲, **FastAPI** 🔜 🤔 👈 ⚫️ 🔢 🔢. + +✋️ 👆 💪 💡 **FastAPI** 😥 ⚫️ ➕1️⃣ 💪 🔑 ⚙️ `Body`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +👉 💼, **FastAPI** 🔜 ⌛ 💪 💖: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +🔄, ⚫️ 🔜 🗜 📊 🆎, ✔, 📄, ♒️. + +## 💗 💪 = & 🔢 + +↗️, 👆 💪 📣 🌖 🔢 🔢 🕐❔ 👆 💪, 🌖 🙆 💪 🔢. + +, 🔢, ⭐ 💲 🔬 🔢 🔢, 👆 🚫 ✔️ 🎯 🚮 `Query`, 👆 💪: + +```Python +q: Union[str, None] = None +``` + +⚖️ 🐍 3️⃣.1️⃣0️⃣ & 🔛: + +```Python +q: str | None = None +``` + +🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="26" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +!!! info + `Body` ✔️ 🌐 🎏 ➕ 🔬 & 🗃 🔢 `Query`,`Path` & 🎏 👆 🔜 👀 ⏪. + +## ⏯ 👁 💪 🔢 + +➡️ 💬 👆 🕴 ✔️ 👁 `item` 💪 🔢 ⚪️➡️ Pydantic 🏷 `Item`. + +🔢, **FastAPI** 🔜 ⤴️ ⌛ 🚮 💪 🔗. + +✋️ 🚥 👆 💚 ⚫️ ⌛ 🎻 ⏮️ 🔑 `item` & 🔘 ⚫️ 🏷 🎚, ⚫️ 🔨 🕐❔ 👆 📣 ➕ 💪 🔢, 👆 💪 ⚙️ 🎁 `Body` 🔢 `embed`: + +```Python +item: Item = Body(embed=True) +``` + +: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +👉 💼 **FastAPI** 🔜 ⌛ 💪 💖: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +↩️: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## 🌃 + +👆 💪 🚮 💗 💪 🔢 👆 *➡ 🛠️ 🔢*, ✋️ 📨 💪 🕴 ✔️ 👁 💪. + +✋️ **FastAPI** 🔜 🍵 ⚫️, 🤝 👆 ☑ 📊 👆 🔢, & ✔ & 📄 ☑ 🔗 *➡ 🛠️*. + +👆 💪 📣 ⭐ 💲 📨 🍕 💪. + +& 👆 💪 💡 **FastAPI** ⏯ 💪 🔑 🕐❔ 📤 🕴 👁 🔢 📣. diff --git a/docs/em/docs/tutorial/body-nested-models.md b/docs/em/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..f4bd50f5c --- /dev/null +++ b/docs/em/docs/tutorial/body-nested-models.md @@ -0,0 +1,382 @@ +# 💪 - 🔁 🏷 + +⏮️ **FastAPI**, 👆 💪 🔬, ✔, 📄, & ⚙️ 🎲 🙇 🐦 🏷 (👏 Pydantic). + +## 📇 🏑 + +👆 💪 🔬 🔢 🏾. 🖼, 🐍 `list`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +👉 🔜 ⚒ `tags` 📇, 👐 ⚫️ 🚫 📣 🆎 🔣 📇. + +## 📇 🏑 ⏮️ 🆎 🔢 + +✋️ 🐍 ✔️ 🎯 🌌 📣 📇 ⏮️ 🔗 🆎, ⚖️ "🆎 🔢": + +### 🗄 ⌨ `List` + +🐍 3️⃣.9️⃣ & 🔛 👆 💪 ⚙️ 🐩 `list` 📣 👫 🆎 ✍ 👥 🔜 👀 🔛. 👶 + +✋️ 🐍 ⏬ ⏭ 3️⃣.9️⃣ (3️⃣.6️⃣ & 🔛), 👆 🥇 💪 🗄 `List` ⚪️➡️ 🐩 🐍 `typing` 🕹: + +```Python hl_lines="1" +{!> ../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### 📣 `list` ⏮️ 🆎 🔢 + +📣 🆎 👈 ✔️ 🆎 🔢 (🔗 🆎), 💖 `list`, `dict`, `tuple`: + +* 🚥 👆 🐍 ⏬ 🔅 🌘 3️⃣.9️⃣, 🗄 👫 🌓 ⏬ ⚪️➡️ `typing` 🕹 +* 🚶‍♀️ 🔗 🆎(Ⓜ) "🆎 🔢" ⚙️ ⬜ 🗜: `[` & `]` + +🐍 3️⃣.9️⃣ ⚫️ 🔜: + +```Python +my_list: list[str] +``` + +⏬ 🐍 ⏭ 3️⃣.9️⃣, ⚫️ 🔜: + +```Python +from typing import List + +my_list: List[str] +``` + +👈 🌐 🐩 🐍 ❕ 🆎 📄. + +⚙️ 👈 🎏 🐩 ❕ 🏷 🔢 ⏮️ 🔗 🆎. + +, 👆 🖼, 👥 💪 ⚒ `tags` 🎯 "📇 🎻": + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +## ⚒ 🆎 + +✋️ ⤴️ 👥 💭 🔃 ⚫️, & 🤔 👈 🔖 🚫🔜 🚫 🔁, 👫 🔜 🎲 😍 🎻. + +& 🐍 ✔️ 🎁 💽 🆎 ⚒ 😍 🏬, `set`. + +⤴️ 👥 💪 📣 `tags` ⚒ 🎻: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +⏮️ 👉, 🚥 👆 📨 📨 ⏮️ ❎ 📊, ⚫️ 🔜 🗜 ⚒ 😍 🏬. + +& 🕐❔ 👆 🔢 👈 📊, 🚥 ℹ ✔️ ❎, ⚫️ 🔜 🔢 ⚒ 😍 🏬. + +& ⚫️ 🔜 ✍ / 📄 ➡️ 💁‍♂️. + +## 🐦 🏷 + +🔠 🔢 Pydantic 🏷 ✔️ 🆎. + +✋️ 👈 🆎 💪 ⚫️ ➕1️⃣ Pydantic 🏷. + +, 👆 💪 📣 🙇 🐦 🎻 "🎚" ⏮️ 🎯 🔢 📛, 🆎 & 🔬. + +🌐 👈, 🎲 🐦. + +### 🔬 📊 + +🖼, 👥 💪 🔬 `Image` 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +### ⚙️ 📊 🆎 + +& ⤴️ 👥 💪 ⚙️ ⚫️ 🆎 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +👉 🔜 ⛓ 👈 **FastAPI** 🔜 ⌛ 💪 🎏: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +🔄, 🤸 👈 📄, ⏮️ **FastAPI** 👆 🤚: + +* 👨‍🎨 🐕‍🦺 (🛠️, ♒️), 🐦 🏷 +* 💽 🛠️ +* 💽 🔬 +* 🏧 🧾 + +## 🎁 🆎 & 🔬 + +↖️ ⚪️➡️ 😐 ⭐ 🆎 💖 `str`, `int`, `float`, ♒️. 👆 💪 ⚙️ 🌅 🏗 ⭐ 🆎 👈 😖 ⚪️➡️ `str`. + +👀 🌐 🎛 👆 ✔️, 🛒 🩺 Pydantic 😍 🆎. 👆 🔜 👀 🖼 ⏭ 📃. + +🖼, `Image` 🏷 👥 ✔️ `url` 🏑, 👥 💪 📣 ⚫️ ↩️ `str`, Pydantic `HttpUrl`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +🎻 🔜 ✅ ☑ 📛, & 📄 🎻 🔗 / 🗄 ✅. + +## 🔢 ⏮️ 📇 📊 + +👆 💪 ⚙️ Pydantic 🏷 🏾 `list`, `set`, ♒️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +👉 🔜 ⌛ (🗜, ✔, 📄, ♒️) 🎻 💪 💖: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info + 👀 ❔ `images` 🔑 🔜 ✔️ 📇 🖼 🎚. + +## 🙇 🐦 🏷 + +👆 💪 🔬 🎲 🙇 🐦 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +!!! info + 👀 ❔ `Offer` ✔️ 📇 `Item`Ⓜ, ❔ 🔄 ✔️ 📦 📇 `Image`Ⓜ + +## 💪 😁 📇 + +🚥 🔝 🎚 💲 🎻 💪 👆 ⌛ 🎻 `array` (🐍 `list`), 👆 💪 📣 🆎 🔢 🔢, 🎏 Pydantic 🏷: + +```Python +images: List[Image] +``` + +⚖️ 🐍 3️⃣.9️⃣ & 🔛: + +```Python +images: list[Image] +``` + +: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +## 👨‍🎨 🐕‍🦺 🌐 + +& 👆 🤚 👨‍🎨 🐕‍🦺 🌐. + +🏬 🔘 📇: + + + +👆 🚫 🚫 🤚 👉 😇 👨‍🎨 🐕‍🦺 🚥 👆 👷 🔗 ⏮️ `dict` ↩️ Pydantic 🏷. + +✋️ 👆 🚫 ✔️ 😟 🔃 👫 👯‍♂️, 📨 #️⃣ 🗜 🔁 & 👆 🔢 🗜 🔁 🎻 💁‍♂️. + +## 💪 ❌ `dict`Ⓜ + +👆 💪 📣 💪 `dict` ⏮️ 🔑 🆎 & 💲 🎏 🆎. + +🍵 ✔️ 💭 ⏪ ⚫️❔ ☑ 🏑/🔢 📛 (🔜 💼 ⏮️ Pydantic 🏷). + +👉 🔜 ⚠ 🚥 👆 💚 📨 🔑 👈 👆 🚫 ⏪ 💭. + +--- + +🎏 ⚠ 💼 🕐❔ 👆 💚 ✔️ 🔑 🎏 🆎, ✅ `int`. + +👈 ⚫️❔ 👥 🔜 👀 📥. + +👉 💼, 👆 🔜 🚫 🙆 `dict` 📏 ⚫️ ✔️ `int` 🔑 ⏮️ `float` 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +!!! tip + ✔️ 🤯 👈 🎻 🕴 🐕‍🦺 `str` 🔑. + + ✋️ Pydantic ✔️ 🏧 💽 🛠️. + + 👉 ⛓ 👈, ✋️ 👆 🛠️ 👩‍💻 💪 🕴 📨 🎻 🔑, 📏 👈 🎻 🔌 😁 🔢, Pydantic 🔜 🗜 👫 & ✔ 👫. + + & `dict` 👆 📨 `weights` 🔜 🤙 ✔️ `int` 🔑 & `float` 💲. + +## 🌃 + +⏮️ **FastAPI** 👆 ✔️ 🔆 💪 🚚 Pydantic 🏷, ⏪ 🚧 👆 📟 🙅, 📏 & 😍. + +✋️ ⏮️ 🌐 💰: + +* 👨‍🎨 🐕‍🦺 (🛠️ 🌐 ❗) +* 💽 🛠️ (.Ⓜ.. ✍ / 🛠️) +* 💽 🔬 +* 🔗 🧾 +* 🏧 🩺 diff --git a/docs/em/docs/tutorial/body-updates.md b/docs/em/docs/tutorial/body-updates.md new file mode 100644 index 000000000..98058ab52 --- /dev/null +++ b/docs/em/docs/tutorial/body-updates.md @@ -0,0 +1,155 @@ +# 💪 - ℹ + +## ℹ ❎ ⏮️ `PUT` + +ℹ 🏬 👆 💪 ⚙️ 🇺🇸🔍 `PUT` 🛠️. + +👆 💪 ⚙️ `jsonable_encoder` 🗜 🔢 💽 📊 👈 💪 🏪 🎻 (✅ ⏮️ ☁ 💽). 🖼, 🏭 `datetime` `str`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ``` + +`PUT` ⚙️ 📨 💽 👈 🔜 ❎ ♻ 💽. + +### ⚠ 🔃 ❎ + +👈 ⛓ 👈 🚥 👆 💚 ℹ 🏬 `bar` ⚙️ `PUT` ⏮️ 💪 ⚗: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +↩️ ⚫️ 🚫 🔌 ⏪ 🏪 🔢 `"tax": 20.2`, 🔢 🏷 🔜 ✊ 🔢 💲 `"tax": 10.5`. + +& 📊 🔜 🖊 ⏮️ 👈 "🆕" `tax` `10.5`. + +## 🍕 ℹ ⏮️ `PATCH` + +👆 💪 ⚙️ 🇺🇸🔍 `PATCH` 🛠️ *🍕* ℹ 💽. + +👉 ⛓ 👈 👆 💪 📨 🕴 💽 👈 👆 💚 ℹ, 🍂 🎂 🐣. + +!!! Note + `PATCH` 🌘 🛎 ⚙️ & 💭 🌘 `PUT`. + + & 📚 🏉 ⚙️ 🕴 `PUT`, 🍕 ℹ. + + 👆 **🆓** ⚙️ 👫 👐 👆 💚, **FastAPI** 🚫 🚫 🙆 🚫. + + ✋️ 👉 🦮 🎦 👆, 🌖 ⚖️ 🌘, ❔ 👫 🎯 ⚙️. + +### ⚙️ Pydantic `exclude_unset` 🔢 + +🚥 👆 💚 📨 🍕 ℹ, ⚫️ 📶 ⚠ ⚙️ 🔢 `exclude_unset` Pydantic 🏷 `.dict()`. + +💖 `item.dict(exclude_unset=True)`. + +👈 🔜 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ 🕐❔ 🏗 `item` 🏷, 🚫 🔢 💲. + +⤴️ 👆 💪 ⚙️ 👉 🏗 `dict` ⏮️ 🕴 💽 👈 ⚒ (📨 📨), 🚫 🔢 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +### ⚙️ Pydantic `update` 🔢 + +🔜, 👆 💪 ✍ 📁 ♻ 🏷 ⚙️ `.copy()`, & 🚶‍♀️ `update` 🔢 ⏮️ `dict` ⚗ 💽 ℹ. + +💖 `stored_item_model.copy(update=update_data)`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +### 🍕 ℹ 🌃 + +📄, ✔ 🍕 ℹ 👆 🔜: + +* (⚗) ⚙️ `PATCH` ↩️ `PUT`. +* 🗃 🏪 💽. +* 🚮 👈 💽 Pydantic 🏷. +* 🏗 `dict` 🍵 🔢 💲 ⚪️➡️ 🔢 🏷 (⚙️ `exclude_unset`). + * 👉 🌌 👆 💪 ℹ 🕴 💲 🤙 ⚒ 👩‍💻, ↩️ 🔐 💲 ⏪ 🏪 ⏮️ 🔢 💲 👆 🏷. +* ✍ 📁 🏪 🏷, 🛠️ ⚫️ 🔢 ⏮️ 📨 🍕 ℹ (⚙️ `update` 🔢). +* 🗜 📁 🏷 🕳 👈 💪 🏪 👆 💽 (🖼, ⚙️ `jsonable_encoder`). + * 👉 ⭐ ⚙️ 🏷 `.dict()` 👩‍🔬 🔄, ✋️ ⚫️ ⚒ 💭 (& 🗜) 💲 💽 🆎 👈 💪 🗜 🎻, 🖼, `datetime` `str`. +* 🖊 💽 👆 💽. +* 📨 ℹ 🏷. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +!!! tip + 👆 💪 🤙 ⚙️ 👉 🎏 ⚒ ⏮️ 🇺🇸🔍 `PUT` 🛠️. + + ✋️ 🖼 📥 ⚙️ `PATCH` ↩️ ⚫️ ✍ 👫 ⚙️ 💼. + +!!! note + 👀 👈 🔢 🏷 ✔. + + , 🚥 👆 💚 📨 🍕 ℹ 👈 💪 🚫 🌐 🔢, 👆 💪 ✔️ 🏷 ⏮️ 🌐 🔢 ™ 📦 (⏮️ 🔢 💲 ⚖️ `None`). + + 🔬 ⚪️➡️ 🏷 ⏮️ 🌐 📦 💲 **ℹ** & 🏷 ⏮️ ✔ 💲 **🏗**, 👆 💪 ⚙️ 💭 🔬 [➕ 🏷](extra-models.md){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/body.md b/docs/em/docs/tutorial/body.md new file mode 100644 index 000000000..ca2f113bf --- /dev/null +++ b/docs/em/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# 📨 💪 + +🕐❔ 👆 💪 📨 📊 ⚪️➡️ 👩‍💻 (➡️ 💬, 🖥) 👆 🛠️, 👆 📨 ⚫️ **📨 💪**. + +**📨** 💪 📊 📨 👩‍💻 👆 🛠️. **📨** 💪 💽 👆 🛠️ 📨 👩‍💻. + +👆 🛠️ 🌖 🕧 ✔️ 📨 **📨** 💪. ✋️ 👩‍💻 🚫 🎯 💪 📨 **📨** 💪 🌐 🕰. + +📣 **📨** 💪, 👆 ⚙️ Pydantic 🏷 ⏮️ 🌐 👫 🏋️ & 💰. + +!!! info + 📨 💽, 👆 🔜 ⚙️ 1️⃣: `POST` (🌅 ⚠), `PUT`, `DELETE` ⚖️ `PATCH`. + + 📨 💪 ⏮️ `GET` 📨 ✔️ ⚠ 🎭 🔧, 👐, ⚫️ 🐕‍🦺 FastAPI, 🕴 📶 🏗/😕 ⚙️ 💼. + + ⚫️ 🚫, 🎓 🩺 ⏮️ 🦁 🎚 🏆 🚫 🎦 🧾 💪 🕐❔ ⚙️ `GET`, & 🗳 🖕 💪 🚫 🐕‍🦺 ⚫️. + +## 🗄 Pydantic `BaseModel` + +🥇, 👆 💪 🗄 `BaseModel` ⚪️➡️ `pydantic`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +## ✍ 👆 💽 🏷 + +⤴️ 👆 📣 👆 💽 🏷 🎓 👈 😖 ⚪️➡️ `BaseModel`. + +⚙️ 🐩 🐍 🆎 🌐 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +🎏 🕐❔ 📣 🔢 🔢, 🕐❔ 🏷 🔢 ✔️ 🔢 💲, ⚫️ 🚫 ✔. ⏪, ⚫️ ✔. ⚙️ `None` ⚒ ⚫️ 📦. + +🖼, 👉 🏷 🔛 📣 🎻 "`object`" (⚖️ 🐍 `dict`) 💖: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +... `description` & `tax` 📦 (⏮️ 🔢 💲 `None`), 👉 🎻 "`object`" 🔜 ☑: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## 📣 ⚫️ 🔢 + +🚮 ⚫️ 👆 *➡ 🛠️*, 📣 ⚫️ 🎏 🌌 👆 📣 ➡ & 🔢 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +...& 📣 🚮 🆎 🏷 👆 ✍, `Item`. + +## 🏁 + +⏮️ 👈 🐍 🆎 📄, **FastAPI** 🔜: + +* ✍ 💪 📨 🎻. +* 🗜 🔗 🆎 (🚥 💪). +* ✔ 💽. + * 🚥 💽 ❌, ⚫️ 🔜 📨 👌 & 🆑 ❌, ☠️ ⚫️❔ 🌐❔ & ⚫️❔ ❌ 📊. +* 🤝 👆 📨 📊 🔢 `item`. + * 👆 📣 ⚫️ 🔢 🆎 `Item`, 👆 🔜 ✔️ 🌐 👨‍🎨 🐕‍🦺 (🛠️, ♒️) 🌐 🔢 & 👫 🆎. +* 🏗 🎻 🔗 🔑 👆 🏷, 👆 💪 ⚙️ 👫 🙆 🙆 👆 💖 🚥 ⚫️ ⚒ 🔑 👆 🏗. +* 👈 🔗 🔜 🍕 🏗 🗄 🔗, & ⚙️ 🏧 🧾 . + +## 🏧 🩺 + +🎻 🔗 👆 🏷 🔜 🍕 👆 🗄 🏗 🔗, & 🔜 🎦 🎓 🛠️ 🩺: + + + +& 🔜 ⚙️ 🛠️ 🩺 🔘 🔠 *➡ 🛠️* 👈 💪 👫: + + + +## 👨‍🎨 🐕‍🦺 + +👆 👨‍🎨, 🔘 👆 🔢 👆 🔜 🤚 🆎 🔑 & 🛠️ 🌐 (👉 🚫🔜 🔨 🚥 👆 📨 `dict` ↩️ Pydantic 🏷): + + + +👆 🤚 ❌ ✅ ❌ 🆎 🛠️: + + + +👉 🚫 🤞, 🎂 🛠️ 🏗 🤭 👈 🔧. + +& ⚫️ 🙇 💯 🔧 🌓, ⏭ 🙆 🛠️, 🚚 ⚫️ 🔜 👷 ⏮️ 🌐 👨‍🎨. + +📤 🔀 Pydantic ⚫️ 🐕‍🦺 👉. + +⏮️ 🖼 ✊ ⏮️ 🎙 🎙 📟. + +✋️ 👆 🔜 🤚 🎏 👨‍🎨 🐕‍🦺 ⏮️ 🗒 & 🌅 🎏 🐍 👨‍🎨: + + + +!!! tip + 🚥 👆 ⚙️ 🗒 👆 👨‍🎨, 👆 💪 ⚙️ Pydantic 🗒 📁. + + ⚫️ 📉 👨‍🎨 🐕‍🦺 Pydantic 🏷, ⏮️: + + * 🚘-🛠️ + * 🆎 ✅ + * 🛠️ + * 🔎 + * 🔬 + +## ⚙️ 🏷 + +🔘 🔢, 👆 💪 🔐 🌐 🔢 🏷 🎚 🔗: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +## 📨 💪 ➕ ➡ 🔢 + +👆 💪 📣 ➡ 🔢 & 📨 💪 🎏 🕰. + +**FastAPI** 🔜 🤔 👈 🔢 🔢 👈 🏏 ➡ 🔢 🔜 **✊ ⚪️➡️ ➡**, & 👈 🔢 🔢 👈 📣 Pydantic 🏷 🔜 **✊ ⚪️➡️ 📨 💪**. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +## 📨 💪 ➕ ➡ ➕ 🔢 🔢 + +👆 💪 📣 **💪**, **➡** & **🔢** 🔢, 🌐 🎏 🕰. + +**FastAPI** 🔜 🤔 🔠 👫 & ✊ 📊 ⚪️➡️ ☑ 🥉. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +🔢 🔢 🔜 🤔 ⏩: + +* 🚥 🔢 📣 **➡**, ⚫️ 🔜 ⚙️ ➡ 🔢. +* 🚥 🔢 **⭐ 🆎** (💖 `int`, `float`, `str`, `bool`, ♒️) ⚫️ 🔜 🔬 **🔢** 🔢. +* 🚥 🔢 📣 🆎 **Pydantic 🏷**, ⚫️ 🔜 🔬 📨 **💪**. + +!!! note + FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. + + `Union` `Union[str, None]` 🚫 ⚙️ FastAPI, ✋️ 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. + +## 🍵 Pydantic + +🚥 👆 🚫 💚 ⚙️ Pydantic 🏷, 👆 💪 ⚙️ **💪** 🔢. 👀 🩺 [💪 - 💗 🔢: ⭐ 💲 💪](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/cookie-params.md b/docs/em/docs/tutorial/cookie-params.md new file mode 100644 index 000000000..47f4a62f5 --- /dev/null +++ b/docs/em/docs/tutorial/cookie-params.md @@ -0,0 +1,49 @@ +# 🍪 🔢 + +👆 💪 🔬 🍪 🔢 🎏 🌌 👆 🔬 `Query` & `Path` 🔢. + +## 🗄 `Cookie` + +🥇 🗄 `Cookie`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +## 📣 `Cookie` 🔢 + +⤴️ 📣 🍪 🔢 ⚙️ 🎏 📊 ⏮️ `Path` & `Query`. + +🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +!!! note "📡 ℹ" + `Cookie` "👭" 🎓 `Path` & `Query`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Cookie` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! info + 📣 🍪, 👆 💪 ⚙️ `Cookie`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. + +## 🌃 + +📣 🍪 ⏮️ `Cookie`, ⚙️ 🎏 ⚠ ⚓ `Query` & `Path`. diff --git a/docs/em/docs/tutorial/cors.md b/docs/em/docs/tutorial/cors.md new file mode 100644 index 000000000..8c5e33ed7 --- /dev/null +++ b/docs/em/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# ⚜ (✖️-🇨🇳 ℹ 🤝) + +⚜ ⚖️ "✖️-🇨🇳 ℹ 🤝" 🔗 ⚠ 🕐❔ 🕸 🏃‍♂ 🖥 ✔️ 🕸 📟 👈 🔗 ⏮️ 👩‍💻, & 👩‍💻 🎏 "🇨🇳" 🌘 🕸. + +## 🇨🇳 + +🇨🇳 🌀 🛠️ (`http`, `https`), 🆔 (`myapp.com`, `localhost`, `localhost.tiangolo.com`), & ⛴ (`80`, `443`, `8080`). + +, 🌐 👫 🎏 🇨🇳: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +🚥 👫 🌐 `localhost`, 👫 ⚙️ 🎏 🛠️ ⚖️ ⛴,, 👫 🎏 "🇨🇳". + +## 🔁 + +, ➡️ 💬 👆 ✔️ 🕸 🏃 👆 🖥 `http://localhost:8080`, & 🚮 🕸 🔄 🔗 ⏮️ 👩‍💻 🏃 `http://localhost` (↩️ 👥 🚫 ✔ ⛴, 🖥 🔜 🤔 🔢 ⛴ `80`). + +⤴️, 🖥 🔜 📨 🇺🇸🔍 `OPTIONS` 📨 👩‍💻, & 🚥 👩‍💻 📨 ☑ 🎚 ✔ 📻 ⚪️➡️ 👉 🎏 🇨🇳 (`http://localhost:8080`) ⤴️ 🖥 🔜 ➡️ 🕸 🕸 📨 🚮 📨 👩‍💻. + +🏆 👉, 👩‍💻 🔜 ✔️ 📇 "✔ 🇨🇳". + +👉 💼, ⚫️ 🔜 ✔️ 🔌 `http://localhost:8080` 🕸 👷 ☑. + +## 🃏 + +⚫️ 💪 📣 📇 `"*"` ("🃏") 💬 👈 🌐 ✔. + +✋️ 👈 🔜 🕴 ✔ 🎯 🆎 📻, 🚫 🌐 👈 🔌 🎓: 🍪, ✔ 🎚 💖 📚 ⚙️ ⏮️ 📨 🤝, ♒️. + +, 🌐 👷 ☑, ⚫️ 👻 ✔ 🎯 ✔ 🇨🇳. + +## ⚙️ `CORSMiddleware` + +👆 💪 🔗 ⚫️ 👆 **FastAPI** 🈸 ⚙️ `CORSMiddleware`. + +* 🗄 `CORSMiddleware`. +* ✍ 📇 ✔ 🇨🇳 (🎻). +* 🚮 ⚫️ "🛠️" 👆 **FastAPI** 🈸. + +👆 💪 ✔ 🚥 👆 👩‍💻 ✔: + +* 🎓 (✔ 🎚, 🍪, ♒️). +* 🎯 🇺🇸🔍 👩‍🔬 (`POST`, `PUT`) ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. +* 🎯 🇺🇸🔍 🎚 ⚖️ 🌐 👫 ⏮️ 🃏 `"*"`. + +```Python hl_lines="2 6-11 13-19" +{!../../../docs_src/cors/tutorial001.py!} +``` + +🔢 🔢 ⚙️ `CORSMiddleware` 🛠️ 🚫 🔢, 👆 🔜 💪 🎯 🛠️ 🎯 🇨🇳, 👩‍🔬, ⚖️ 🎚, ✔ 🖥 ✔ ⚙️ 👫 ✖️-🆔 🔑. + +📄 ❌ 🐕‍🦺: + +* `allow_origins` - 📇 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. 🤶 Ⓜ. `['https://example.org', 'https://www.example.org']`. 👆 💪 ⚙️ `['*']` ✔ 🙆 🇨🇳. +* `allow_origin_regex` - 🎻 🎻 🏏 🛡 🇨🇳 👈 🔜 ✔ ⚒ ✖️-🇨🇳 📨. ✅ `'https://.*\.example\.org'`. +* `allow_methods` - 📇 🇺🇸🔍 👩‍🔬 👈 🔜 ✔ ✖️-🇨🇳 📨. 🔢 `['GET']`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🐩 👩‍🔬. +* `allow_headers` - 📇 🇺🇸🔍 📨 🎚 👈 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `[]`. 👆 💪 ⚙️ `['*']` ✔ 🌐 🎚. `Accept`, `Accept-Language`, `Content-Language` & `Content-Type` 🎚 🕧 ✔ 🙅 ⚜ 📨. +* `allow_credentials` - 🎦 👈 🍪 🔜 🐕‍🦺 ✖️-🇨🇳 📨. 🔢 `False`. , `allow_origins` 🚫🔜 ⚒ `['*']` 🎓 ✔, 🇨🇳 🔜 ✔. +* `expose_headers` - 🎦 🙆 📨 🎚 👈 🔜 ⚒ ♿ 🖥. 🔢 `[]`. +* `max_age` - ⚒ 🔆 🕰 🥈 🖥 💾 ⚜ 📨. 🔢 `600`. + +🛠️ 📨 2️⃣ 🎯 🆎 🇺🇸🔍 📨... + +### ⚜ 🛫 📨 + +👉 🙆 `OPTIONS` 📨 ⏮️ `Origin` & `Access-Control-Request-Method` 🎚. + +👉 💼 🛠️ 🔜 🆘 📨 📨 & 📨 ⏮️ ☑ ⚜ 🎚, & 👯‍♂️ `200` ⚖️ `400` 📨 🎓 🎯. + +### 🙅 📨 + +🙆 📨 ⏮️ `Origin` 🎚. 👉 💼 🛠️ 🔜 🚶‍♀️ 📨 🔘 😐, ✋️ 🔜 🔌 ☑ ⚜ 🎚 🔛 📨. + +## 🌅 ℹ + +🌖 ℹ 🔃 , ✅ 🦎 ⚜ 🧾. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.middleware.cors import CORSMiddleware`. + + **FastAPI** 🚚 📚 🛠️ `fastapi.middleware` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 🛠️ 👟 🔗 ⚪️➡️ 💃. diff --git a/docs/em/docs/tutorial/debugging.md b/docs/em/docs/tutorial/debugging.md new file mode 100644 index 000000000..c7c11b5ce --- /dev/null +++ b/docs/em/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# 🛠️ + +👆 💪 🔗 🕹 👆 👨‍🎨, 🖼 ⏮️ 🎙 🎙 📟 ⚖️ 🗒. + +## 🤙 `uvicorn` + +👆 FastAPI 🈸, 🗄 & 🏃 `uvicorn` 🔗: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### 🔃 `__name__ == "__main__"` + +👑 🎯 `__name__ == "__main__"` ✔️ 📟 👈 🛠️ 🕐❔ 👆 📁 🤙 ⏮️: + +
+ +```console +$ python myapp.py +``` + +
+ +✋️ 🚫 🤙 🕐❔ ➕1️⃣ 📁 🗄 ⚫️, 💖: + +```Python +from myapp import app +``` + +#### 🌅 ℹ + +➡️ 💬 👆 📁 🌟 `myapp.py`. + +🚥 👆 🏃 ⚫️ ⏮️: + +
+ +```console +$ python myapp.py +``` + +
+ +⤴️ 🔗 🔢 `__name__` 👆 📁, ✍ 🔁 🐍, 🔜 ✔️ 💲 🎻 `"__main__"`. + +, 📄: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +🔜 🏃. + +--- + +👉 🏆 🚫 🔨 🚥 👆 🗄 👈 🕹 (📁). + +, 🚥 👆 ✔️ ➕1️⃣ 📁 `importer.py` ⏮️: + +```Python +from myapp import app + +# Some more code +``` + +👈 💼, 🏧 🔢 🔘 `myapp.py` 🔜 🚫 ✔️ 🔢 `__name__` ⏮️ 💲 `"__main__"`. + +, ⏸: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +🔜 🚫 🛠️. + +!!! info + 🌅 ℹ, ✅ 🛂 🐍 🩺. + +## 🏃 👆 📟 ⏮️ 👆 🕹 + +↩️ 👆 🏃 Uvicorn 💽 🔗 ⚪️➡️ 👆 📟, 👆 💪 🤙 👆 🐍 📋 (👆 FastAPI 🈸) 🔗 ⚪️➡️ 🕹. + +--- + +🖼, 🎙 🎙 📟, 👆 💪: + +* 🚶 "ℹ" 🎛. +* "🚮 📳...". +* 🖊 "🐍" +* 🏃 🕹 ⏮️ 🎛 "`Python: Current File (Integrated Terminal)`". + +⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. + +📥 ❔ ⚫️ 💪 👀: + + + +--- + +🚥 👆 ⚙️ 🗒, 👆 💪: + +* 📂 "🏃" 🍣. +* 🖊 🎛 "ℹ...". +* ⤴️ 🔑 🍣 🎦 🆙. +* 🖊 📁 ℹ (👉 💼, `main.py`). + +⚫️ 🔜 ⤴️ ▶️ 💽 ⏮️ 👆 **FastAPI** 📟, ⛔️ 👆 0️⃣, ♒️. + +📥 ❔ ⚫️ 💪 👀: + + diff --git a/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..e2d2686d3 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,247 @@ +# 🎓 🔗 + +⏭ 🤿 ⏬ 🔘 **🔗 💉** ⚙️, ➡️ ♻ ⏮️ 🖼. + +## `dict` ⚪️➡️ ⏮️ 🖼 + +⏮️ 🖼, 👥 🛬 `dict` ⚪️➡️ 👆 🔗 ("☑"): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +✋️ ⤴️ 👥 🤚 `dict` 🔢 `commons` *➡ 🛠️ 🔢*. + +& 👥 💭 👈 👨‍🎨 💪 🚫 🚚 📚 🐕‍🦺 (💖 🛠️) `dict`Ⓜ, ↩️ 👫 💪 🚫 💭 👫 🔑 & 💲 🆎. + +👥 💪 👍... + +## ⚫️❔ ⚒ 🔗 + +🆙 🔜 👆 ✔️ 👀 🔗 📣 🔢. + +✋️ 👈 🚫 🕴 🌌 📣 🔗 (👐 ⚫️ 🔜 🎲 🌖 ⚠). + +🔑 ⚖ 👈 🔗 🔜 "🇧🇲". + +"**🇧🇲**" 🐍 🕳 👈 🐍 💪 "🤙" 💖 🔢. + +, 🚥 👆 ✔️ 🎚 `something` (👈 💪 _🚫_ 🔢) & 👆 💪 "🤙" ⚫️ (🛠️ ⚫️) 💖: + +```Python +something() +``` + +⚖️ + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +⤴️ ⚫️ "🇧🇲". + +## 🎓 🔗 + +👆 5️⃣📆 👀 👈 ✍ 👐 🐍 🎓, 👆 ⚙️ 👈 🎏 ❕. + +🖼: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +👉 💼, `fluffy` 👐 🎓 `Cat`. + +& ✍ `fluffy`, 👆 "🤙" `Cat`. + +, 🐍 🎓 **🇧🇲**. + +⤴️, **FastAPI**, 👆 💪 ⚙️ 🐍 🎓 🔗. + +⚫️❔ FastAPI 🤙 ✅ 👈 ⚫️ "🇧🇲" (🔢, 🎓 ⚖️ 🕳 🙆) & 🔢 🔬. + +🚥 👆 🚶‍♀️ "🇧🇲" 🔗 **FastAPI**, ⚫️ 🔜 🔬 🔢 👈 "🇧🇲", & 🛠️ 👫 🎏 🌌 🔢 *➡ 🛠️ 🔢*. ✅ 🎧-🔗. + +👈 ✔ 🇧🇲 ⏮️ 🙅‍♂ 🔢 🌐. 🎏 ⚫️ 🔜 *➡ 🛠️ 🔢* ⏮️ 🙅‍♂ 🔢. + +⤴️, 👥 💪 🔀 🔗 "☑" `common_parameters` ⚪️➡️ 🔛 🎓 `CommonQueryParams`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +💸 🙋 `__init__` 👩‍🔬 ⚙️ ✍ 👐 🎓: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +...⚫️ ✔️ 🎏 🔢 👆 ⏮️ `common_parameters`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +📚 🔢 ⚫️❔ **FastAPI** 🔜 ⚙️ "❎" 🔗. + +👯‍♂️ 💼, ⚫️ 🔜 ✔️: + +* 📦 `q` 🔢 🔢 👈 `str`. +* `skip` 🔢 🔢 👈 `int`, ⏮️ 🔢 `0`. +* `limit` 🔢 🔢 👈 `int`, ⏮️ 🔢 `100`. + +👯‍♂️ 💼 💽 🔜 🗜, ✔, 📄 🔛 🗄 🔗, ♒️. + +## ⚙️ ⚫️ + +🔜 👆 💪 📣 👆 🔗 ⚙️ 👉 🎓. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +**FastAPI** 🤙 `CommonQueryParams` 🎓. 👉 ✍ "👐" 👈 🎓 & 👐 🔜 🚶‍♀️ 🔢 `commons` 👆 🔢. + +## 🆎 ✍ 🆚 `Depends` + +👀 ❔ 👥 ✍ `CommonQueryParams` 🕐 🔛 📟: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +🏁 `CommonQueryParams`,: + +```Python +... = Depends(CommonQueryParams) +``` + +...⚫️❔ **FastAPI** 🔜 🤙 ⚙️ 💭 ⚫️❔ 🔗. + +⚪️➡️ ⚫️ 👈 FastAPI 🔜 ⚗ 📣 🔢 & 👈 ⚫️❔ FastAPI 🔜 🤙 🤙. + +--- + +👉 💼, 🥇 `CommonQueryParams`,: + +```Python +commons: CommonQueryParams ... +``` + +...🚫 ✔️ 🙆 🎁 🔑 **FastAPI**. FastAPI 🏆 🚫 ⚙️ ⚫️ 💽 🛠️, 🔬, ♒️. (⚫️ ⚙️ `= Depends(CommonQueryParams)` 👈). + +👆 💪 🤙 ✍: + +```Python +commons = Depends(CommonQueryParams) +``` + +...: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +✋️ 📣 🆎 💡 👈 🌌 👆 👨‍🎨 🔜 💭 ⚫️❔ 🔜 🚶‍♀️ 🔢 `commons`, & ⤴️ ⚫️ 💪 ℹ 👆 ⏮️ 📟 🛠️, 🆎 ✅, ♒️: + + + +## ⌨ + +✋️ 👆 👀 👈 👥 ✔️ 📟 🔁 📥, ✍ `CommonQueryParams` 🕐: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +**FastAPI** 🚚 ⌨ 👫 💼, 🌐❔ 🔗 *🎯* 🎓 👈 **FastAPI** 🔜 "🤙" ✍ 👐 🎓 ⚫️. + +📚 🎯 💼, 👆 💪 📄: + +↩️ ✍: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...👆 ✍: + +```Python +commons: CommonQueryParams = Depends() +``` + +👆 📣 🔗 🆎 🔢, & 👆 ⚙️ `Depends()` 🚮 "🔢" 💲 (👈 ⏮️ `=`) 👈 🔢 🔢, 🍵 🙆 🔢 `Depends()`, ↩️ ✔️ ✍ 🌕 🎓 *🔄* 🔘 `Depends(CommonQueryParams)`. + +🎏 🖼 🔜 ⤴️ 👀 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +...& **FastAPI** 🔜 💭 ⚫️❔. + +!!! tip + 🚥 👈 😑 🌅 😨 🌘 👍, 🤷‍♂ ⚫️, 👆 🚫 *💪* ⚫️. + + ⚫️ ⌨. ↩️ **FastAPI** 💅 🔃 🤝 👆 📉 📟 🔁. diff --git a/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 000000000..4d54b91c7 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,71 @@ +# 🔗 ➡ 🛠️ 👨‍🎨 + +💼 👆 🚫 🤙 💪 📨 💲 🔗 🔘 👆 *➡ 🛠️ 🔢*. + +⚖️ 🔗 🚫 📨 💲. + +✋️ 👆 💪 ⚫️ 🛠️/❎. + +📚 💼, ↩️ 📣 *➡ 🛠️ 🔢* 🔢 ⏮️ `Depends`, 👆 💪 🚮 `list` `dependencies` *➡ 🛠️ 👨‍🎨*. + +## 🚮 `dependencies` *➡ 🛠️ 👨‍🎨* + +*➡ 🛠️ 👨‍🎨* 📨 📦 ❌ `dependencies`. + +⚫️ 🔜 `list` `Depends()`: + +```Python hl_lines="17" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +👉 🔗 🔜 🛠️/❎ 🎏 🌌 😐 🔗. ✋️ 👫 💲 (🚥 👫 📨 🙆) 🏆 🚫 🚶‍♀️ 👆 *➡ 🛠️ 🔢*. + +!!! tip + 👨‍🎨 ✅ ♻ 🔢 🔢, & 🎦 👫 ❌. + + ⚙️ 👉 `dependencies` *➡ 🛠️ 👨‍🎨* 👆 💪 ⚒ 💭 👫 🛠️ ⏪ ❎ 👨‍🎨/🏭 ❌. + + ⚫️ 💪 ℹ ❎ 😨 🆕 👩‍💻 👈 👀 ♻ 🔢 👆 📟 & 💪 💭 ⚫️ 🙃. + +!!! info + 👉 🖼 👥 ⚙️ 💭 🛃 🎚 `X-Key` & `X-Token`. + + ✋️ 🎰 💼, 🕐❔ 🛠️ 💂‍♂, 👆 🔜 🤚 🌖 💰 ⚪️➡️ ⚙️ 🛠️ [💂‍♂ 🚙 (⏭ 📃)](../security/index.md){.internal-link target=_blank}. + +## 🔗 ❌ & 📨 💲 + +👆 💪 ⚙️ 🎏 🔗 *🔢* 👆 ⚙️ 🛎. + +### 🔗 📄 + +👫 💪 📣 📨 📄 (💖 🎚) ⚖️ 🎏 🎧-🔗: + +```Python hl_lines="6 11" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 🤚 ⚠ + +👫 🔗 💪 `raise` ⚠, 🎏 😐 🔗: + +```Python hl_lines="8 13" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 📨 💲 + +& 👫 💪 📨 💲 ⚖️ 🚫, 💲 🏆 🚫 ⚙️. + +, 👆 💪 🏤-⚙️ 😐 🔗 (👈 📨 💲) 👆 ⏪ ⚙️ 👱 🙆, & ✋️ 💲 🏆 🚫 ⚙️, 🔗 🔜 🛠️: + +```Python hl_lines="9 14" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +## 🔗 👪 *➡ 🛠️* + +⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. + +## 🌐 🔗 + +⏭ 👥 🔜 👀 ❔ 🚮 🔗 🎂 `FastAPI` 🈸, 👈 👫 ✔ 🔠 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..9617667f4 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,219 @@ +# 🔗 ⏮️ 🌾 + +FastAPI 🐕‍🦺 🔗 👈 ➕ 🔁 ⏮️ 🏁. + +👉, ⚙️ `yield` ↩️ `return`, & ✍ ➕ 🔁 ⏮️. + +!!! tip + ⚒ 💭 ⚙️ `yield` 1️⃣ 👁 🕰. + +!!! note "📡 ℹ" + 🙆 🔢 👈 ☑ ⚙️ ⏮️: + + * `@contextlib.contextmanager` ⚖️ + * `@contextlib.asynccontextmanager` + + 🔜 ☑ ⚙️ **FastAPI** 🔗. + + 👐, FastAPI ⚙️ 📚 2️⃣ 👨‍🎨 🔘. + +## 💽 🔗 ⏮️ `yield` + +🖼, 👆 💪 ⚙️ 👉 ✍ 💽 🎉 & 🔐 ⚫️ ⏮️ 🏁. + +🕴 📟 ⏭ & 🔌 `yield` 📄 🛠️ ⏭ 📨 📨: + +```Python hl_lines="2-4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +🌾 💲 ⚫️❔ 💉 🔘 *➡ 🛠️* & 🎏 🔗: + +```Python hl_lines="4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +📟 📄 `yield` 📄 🛠️ ⏮️ 📨 ✔️ 🚚: + +```Python hl_lines="5-6" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +!!! tip + 👆 💪 ⚙️ `async` ⚖️ 😐 🔢. + + **FastAPI** 🔜 ▶️️ 👜 ⏮️ 🔠, 🎏 ⏮️ 😐 🔗. + +## 🔗 ⏮️ `yield` & `try` + +🚥 👆 ⚙️ `try` 🍫 🔗 ⏮️ `yield`, 👆 🔜 📨 🙆 ⚠ 👈 🚮 🕐❔ ⚙️ 🔗. + +🖼, 🚥 📟 ☝ 🖕, ➕1️⃣ 🔗 ⚖️ *➡ 🛠️*, ⚒ 💽 💵 "💾" ⚖️ ✍ 🙆 🎏 ❌, 👆 🔜 📨 ⚠ 👆 🔗. + +, 👆 💪 👀 👈 🎯 ⚠ 🔘 🔗 ⏮️ `except SomeException`. + +🎏 🌌, 👆 💪 ⚙️ `finally` ⚒ 💭 🚪 📶 🛠️, 🙅‍♂ 🤔 🚥 📤 ⚠ ⚖️ 🚫. + +```Python hl_lines="3 5" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +## 🎧-🔗 ⏮️ `yield` + +👆 💪 ✔️ 🎧-🔗 & "🌲" 🎧-🔗 🙆 📐 & 💠, & 🙆 ⚖️ 🌐 👫 💪 ⚙️ `yield`. + +**FastAPI** 🔜 ⚒ 💭 👈 "🚪 📟" 🔠 🔗 ⏮️ `yield` 🏃 ☑ ✔. + +🖼, `dependency_c` 💪 ✔️ 🔗 🔛 `dependency_b`, & `dependency_b` 🔛 `dependency_a`: + +```Python hl_lines="4 12 20" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +& 🌐 👫 💪 ⚙️ `yield`. + +👉 💼 `dependency_c`, 🛠️ 🚮 🚪 📟, 💪 💲 ⚪️➡️ `dependency_b` (📥 📛 `dep_b`) 💪. + +& , 🔄, `dependency_b` 💪 💲 ⚪️➡️ `dependency_a` (📥 📛 `dep_a`) 💪 🚮 🚪 📟. + +```Python hl_lines="16-17 24-25" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +🎏 🌌, 👆 💪 ✔️ 🔗 ⏮️ `yield` & `return` 🌀. + +& 👆 💪 ✔️ 👁 🔗 👈 🚚 📚 🎏 🔗 ⏮️ `yield`, ♒️. + +👆 💪 ✔️ 🙆 🌀 🔗 👈 👆 💚. + +**FastAPI** 🔜 ⚒ 💭 🌐 🏃 ☑ ✔. + +!!! note "📡 ℹ" + 👉 👷 👏 🐍 🔑 👨‍💼. + + **FastAPI** ⚙️ 👫 🔘 🏆 👉. + +## 🔗 ⏮️ `yield` & `HTTPException` + +👆 👀 👈 👆 💪 ⚙️ 🔗 ⏮️ `yield` & ✔️ `try` 🍫 👈 ✊ ⚠. + +⚫️ 5️⃣📆 😋 🤚 `HTTPException` ⚖️ 🎏 🚪 📟, ⏮️ `yield`. ✋️ **⚫️ 🏆 🚫 👷**. + +🚪 📟 🔗 ⏮️ `yield` 🛠️ *⏮️* 📨 📨, [⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} 🔜 ✔️ ⏪ 🏃. 📤 🕳 😽 ⚠ 🚮 👆 🔗 🚪 📟 (⏮️ `yield`). + +, 🚥 👆 🤚 `HTTPException` ⏮️ `yield`, 🔢 (⚖️ 🙆 🛃) ⚠ 🐕‍🦺 👈 ✊ `HTTPException`Ⓜ & 📨 🇺🇸🔍 4️⃣0️⃣0️⃣ 📨 🏆 🚫 📤 ✊ 👈 ⚠ 🚫🔜. + +👉 ⚫️❔ ✔ 🕳 ⚒ 🔗 (✅ 💽 🎉), 🖼, ⚙️ 🖥 📋. + +🖥 📋 🏃 *⏮️* 📨 ✔️ 📨. 📤 🙅‍♂ 🌌 🤚 `HTTPException` ↩️ 📤 🚫 🌌 🔀 📨 👈 *⏪ 📨*. + +✋️ 🚥 🖥 📋 ✍ 💽 ❌, 🌘 👆 💪 💾 ⚖️ 😬 🔐 🎉 🔗 ⏮️ `yield`, & 🎲 🕹 ❌ ⚖️ 📄 ⚫️ 🛰 🕵 ⚙️. + +🚥 👆 ✔️ 📟 👈 👆 💭 💪 🤚 ⚠, 🏆 😐/"🙃" 👜 & 🚮 `try` 🍫 👈 📄 📟. + +🚥 👆 ✔️ 🛃 ⚠ 👈 👆 🔜 💖 🍵 *⏭* 🛬 📨 & 🎲 ❎ 📨, 🎲 🙋‍♀ `HTTPException`, ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + +!!! tip + 👆 💪 🤚 ⚠ 🔌 `HTTPException` *⏭* `yield`. ✋️ 🚫 ⏮️. + +🔁 🛠️ 🌅 ⚖️ 🌘 💖 👉 📊. 🕰 💧 ⚪️➡️ 🔝 🔝. & 🔠 🏓 1️⃣ 🍕 🔗 ⚖️ 🛠️ 📟. + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +!!! info + 🕴 **1️⃣ 📨** 🔜 📨 👩‍💻. ⚫️ 💪 1️⃣ ❌ 📨 ⚖️ ⚫️ 🔜 📨 ⚪️➡️ *➡ 🛠️*. + + ⏮️ 1️⃣ 📚 📨 📨, 🙅‍♂ 🎏 📨 💪 📨. + +!!! tip + 👉 📊 🎦 `HTTPException`, ✋️ 👆 💪 🤚 🙆 🎏 ⚠ ❔ 👆 ✍ [🛃 ⚠ 🐕‍🦺](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + + 🚥 👆 🤚 🙆 ⚠, ⚫️ 🔜 🚶‍♀️ 🔗 ⏮️ 🌾, 🔌 `HTTPException`, & ⤴️ **🔄** ⚠ 🐕‍🦺. 🚥 📤 🙅‍♂ ⚠ 🐕‍🦺 👈 ⚠, ⚫️ 🔜 ⤴️ 🍵 🔢 🔗 `ServerErrorMiddleware`, 🛬 5️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟, ➡️ 👩‍💻 💭 👈 📤 ❌ 💽. + +## 🔑 👨‍💼 + +### ⚫️❔ "🔑 👨‍💼" + +"🔑 👨‍💼" 🙆 👈 🐍 🎚 👈 👆 💪 ⚙️ `with` 📄. + +🖼, 👆 💪 ⚙️ `with` ✍ 📁: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +🔘, `open("./somefile.txt")` ✍ 🎚 👈 🤙 "🔑 👨‍💼". + +🕐❔ `with` 🍫 🏁, ⚫️ ⚒ 💭 🔐 📁, 🚥 📤 ⚠. + +🕐❔ 👆 ✍ 🔗 ⏮️ `yield`, **FastAPI** 🔜 🔘 🗜 ⚫️ 🔑 👨‍💼, & 🌀 ⚫️ ⏮️ 🎏 🔗 🧰. + +### ⚙️ 🔑 👨‍💼 🔗 ⏮️ `yield` + +!!! warning + 👉, 🌅 ⚖️ 🌘, "🏧" 💭. + + 🚥 👆 ▶️ ⏮️ **FastAPI** 👆 💪 💚 🚶 ⚫️ 🔜. + +🐍, 👆 💪 ✍ 🔑 👨‍💼 🏗 🎓 ⏮️ 2️⃣ 👩‍🔬: `__enter__()` & `__exit__()`. + +👆 💪 ⚙️ 👫 🔘 **FastAPI** 🔗 ⏮️ `yield` ⚙️ +`with` ⚖️ `async with` 📄 🔘 🔗 🔢: + +```Python hl_lines="1-9 13" +{!../../../docs_src/dependencies/tutorial010.py!} +``` + +!!! tip + ➕1️⃣ 🌌 ✍ 🔑 👨‍💼 ⏮️: + + * `@contextlib.contextmanager` ⚖️ + * `@contextlib.asynccontextmanager` + + ⚙️ 👫 🎀 🔢 ⏮️ 👁 `yield`. + + 👈 ⚫️❔ **FastAPI** ⚙️ 🔘 🔗 ⏮️ `yield`. + + ✋️ 👆 🚫 ✔️ ⚙️ 👨‍🎨 FastAPI 🔗 (& 👆 🚫🔜 🚫). + + FastAPI 🔜 ⚫️ 👆 🔘. diff --git a/docs/em/docs/tutorial/dependencies/global-dependencies.md b/docs/em/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 000000000..81759d0e8 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,17 @@ +# 🌐 🔗 + +🆎 🈸 👆 💪 💚 🚮 🔗 🎂 🈸. + +🎏 🌌 👆 💪 [🚮 `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, 👆 💪 🚮 👫 `FastAPI` 🈸. + +👈 💼, 👫 🔜 ✔ 🌐 *➡ 🛠️* 🈸: + +```Python hl_lines="15" +{!../../../docs_src/dependencies/tutorial012.py!} +``` + +& 🌐 💭 📄 🔃 [❎ `dependencies` *➡ 🛠️ 👨‍🎨*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} ✔, ✋️ 👉 💼, 🌐 *➡ 🛠️* 📱. + +## 🔗 👪 *➡ 🛠️* + +⏪, 🕐❔ 👂 🔃 ❔ 📊 🦏 🈸 ([🦏 🈸 - 💗 📁](../../tutorial/bigger-applications.md){.internal-link target=_blank}), 🎲 ⏮️ 💗 📁, 👆 🔜 💡 ❔ 📣 👁 `dependencies` 🔢 👪 *➡ 🛠️*. diff --git a/docs/em/docs/tutorial/dependencies/index.md b/docs/em/docs/tutorial/dependencies/index.md new file mode 100644 index 000000000..f1c28c573 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/index.md @@ -0,0 +1,233 @@ +# 🔗 - 🥇 🔁 + +**FastAPI** ✔️ 📶 🏋️ ✋️ 🏋️ **🔗 💉** ⚙️. + +⚫️ 🏗 📶 🙅 ⚙️, & ⚒ ⚫️ 📶 ⏩ 🙆 👩‍💻 🛠️ 🎏 🦲 ⏮️ **FastAPI**. + +## ⚫️❔ "🔗 💉" + +**"🔗 💉"** ⛓, 📋, 👈 📤 🌌 👆 📟 (👉 💼, 👆 *➡ 🛠️ 🔢*) 📣 👜 👈 ⚫️ 🚚 👷 & ⚙️: "🔗". + +& ⤴️, 👈 ⚙️ (👉 💼 **FastAPI**) 🔜 ✊ 💅 🔨 ⚫️❔ 💪 🚚 👆 📟 ⏮️ 📚 💪 🔗 ("💉" 🔗). + +👉 📶 ⚠ 🕐❔ 👆 💪: + +* ✔️ 💰 ⚛ (🎏 📟 ⚛ 🔄 & 🔄). +* 💰 💽 🔗. +* 🛠️ 💂‍♂, 🤝, 🔑 📄, ♒️. +* & 📚 🎏 👜... + +🌐 👫, ⏪ 📉 📟 🔁. + +## 🥇 🔁 + +➡️ 👀 📶 🙅 🖼. ⚫️ 🔜 🙅 👈 ⚫️ 🚫 📶 ⚠, 🔜. + +✋️ 👉 🌌 👥 💪 🎯 🔛 ❔ **🔗 💉** ⚙️ 👷. + +### ✍ 🔗, ⚖️ "☑" + +➡️ 🥇 🎯 🔛 🔗. + +⚫️ 🔢 👈 💪 ✊ 🌐 🎏 🔢 👈 *➡ 🛠️ 🔢* 💪 ✊: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +👈 ⚫️. + +**2️⃣ ⏸**. + +& ⚫️ ✔️ 🎏 💠 & 📊 👈 🌐 👆 *➡ 🛠️ 🔢* ✔️. + +👆 💪 💭 ⚫️ *➡ 🛠️ 🔢* 🍵 "👨‍🎨" (🍵 `@app.get("/some-path")`). + +& ⚫️ 💪 📨 🕳 👆 💚. + +👉 💼, 👉 🔗 ⌛: + +* 📦 🔢 🔢 `q` 👈 `str`. +* 📦 🔢 🔢 `skip` 👈 `int`, & 🔢 `0`. +* 📦 🔢 🔢 `limit` 👈 `int`, & 🔢 `100`. + +& ⤴️ ⚫️ 📨 `dict` ⚗ 📚 💲. + +### 🗄 `Depends` + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +### 📣 🔗, "⚓️" + +🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️. ⏮️ 👆 *➡ 🛠️ 🔢* 🔢, ⚙️ `Depends` ⏮️ 🆕 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +👐 👆 ⚙️ `Depends` 🔢 👆 🔢 🎏 🌌 👆 ⚙️ `Body`, `Query`, ♒️, `Depends` 👷 👄 🎏. + +👆 🕴 🤝 `Depends` 👁 🔢. + +👉 🔢 🔜 🕳 💖 🔢. + +& 👈 🔢 ✊ 🔢 🎏 🌌 👈 *➡ 🛠️ 🔢* . + +!!! tip + 👆 🔜 👀 ⚫️❔ 🎏 "👜", ↖️ ⚪️➡️ 🔢, 💪 ⚙️ 🔗 ⏭ 📃. + +🕐❔ 🆕 📨 🛬, **FastAPI** 🔜 ✊ 💅: + +* 🤙 👆 🔗 ("☑") 🔢 ⏮️ ☑ 🔢. +* 🤚 🏁 ⚪️➡️ 👆 🔢. +* 🛠️ 👈 🏁 🔢 👆 *➡ 🛠️ 🔢*. + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +👉 🌌 👆 ✍ 🔗 📟 🕐 & **FastAPI** ✊ 💅 🤙 ⚫️ 👆 *➡ 🛠️*. + +!!! check + 👀 👈 👆 🚫 ✔️ ✍ 🎁 🎓 & 🚶‍♀️ ⚫️ 👱 **FastAPI** "®" ⚫️ ⚖️ 🕳 🎏. + + 👆 🚶‍♀️ ⚫️ `Depends` & **FastAPI** 💭 ❔ 🎂. + +## `async` ⚖️ 🚫 `async` + +🔗 🔜 🤙 **FastAPI** (🎏 👆 *➡ 🛠️ 🔢*), 🎏 🚫 ✔ ⏪ 🔬 👆 🔢. + +👆 💪 ⚙️ `async def` ⚖️ 😐 `def`. + +& 👆 💪 📣 🔗 ⏮️ `async def` 🔘 😐 `def` *➡ 🛠️ 🔢*, ⚖️ `def` 🔗 🔘 `async def` *➡ 🛠️ 🔢*, ♒️. + +⚫️ 🚫 🤔. **FastAPI** 🔜 💭 ⚫️❔. + +!!! note + 🚥 👆 🚫 💭, ✅ [🔁: *"🏃 ❓" *](../../async.md){.internal-link target=_blank} 📄 🔃 `async` & `await` 🩺. + +## 🛠️ ⏮️ 🗄 + +🌐 📨 📄, 🔬 & 📄 👆 🔗 (& 🎧-🔗) 🔜 🛠️ 🎏 🗄 🔗. + +, 🎓 🩺 🔜 ✔️ 🌐 ℹ ⚪️➡️ 👫 🔗 💁‍♂️: + + + +## 🙅 ⚙️ + +🚥 👆 👀 ⚫️, *➡ 🛠️ 🔢* 📣 ⚙️ 🕐❔ *➡* & *🛠️* 🏏, & ⤴️ **FastAPI** ✊ 💅 🤙 🔢 ⏮️ ☑ 🔢, ❎ 📊 ⚪️➡️ 📨. + +🤙, 🌐 (⚖️ 🏆) 🕸 🛠️ 👷 👉 🎏 🌌. + +👆 🙅 🤙 👈 🔢 🔗. 👫 🤙 👆 🛠️ (👉 💼, **FastAPI**). + +⏮️ 🔗 💉 ⚙️, 👆 💪 💬 **FastAPI** 👈 👆 *➡ 🛠️ 🔢* "🪀" 🔛 🕳 🙆 👈 🔜 🛠️ ⏭ 👆 *➡ 🛠️ 🔢*, & **FastAPI** 🔜 ✊ 💅 🛠️ ⚫️ & "💉" 🏁. + +🎏 ⚠ ⚖ 👉 🎏 💭 "🔗 💉": + +* ℹ +* 🐕‍🦺 +* 🐕‍🦺 +* 💉 +* 🦲 + +## **FastAPI** 🔌-🔌 + +🛠️ & "🔌-"Ⓜ 💪 🏗 ⚙️ **🔗 💉** ⚙️. ✋️ 👐, 📤 🤙 **🙅‍♂ 💪 ✍ "🔌-🔌"**, ⚙️ 🔗 ⚫️ 💪 📣 ♾ 🔢 🛠️ & 🔗 👈 ▶️️ 💪 👆 *➡ 🛠️ 🔢*. + +& 🔗 💪 ✍ 📶 🙅 & 🏋️ 🌌 👈 ✔ 👆 🗄 🐍 📦 👆 💪, & 🛠️ 👫 ⏮️ 👆 🛠️ 🔢 👩‍❤‍👨 ⏸ 📟, *🌖*. + +👆 🔜 👀 🖼 👉 ⏭ 📃, 🔃 🔗 & ☁ 💽, 💂‍♂, ♒️. + +## **FastAPI** 🔗 + +🦁 🔗 💉 ⚙️ ⚒ **FastAPI** 🔗 ⏮️: + +* 🌐 🔗 💽 +* ☁ 💽 +* 🔢 📦 +* 🔢 🔗 +* 🤝 & ✔ ⚙️ +* 🛠️ ⚙️ ⚖ ⚙️ +* 📨 💽 💉 ⚙️ +* ♒️. + +## 🙅 & 🏋️ + +👐 🔗 🔗 💉 ⚙️ 📶 🙅 🔬 & ⚙️, ⚫️ 📶 🏋️. + +👆 💪 🔬 🔗 👈 🔄 💪 🔬 🔗 👫. + +🔚, 🔗 🌲 🔗 🏗, & **🔗 💉** ⚙️ ✊ 💅 🔬 🌐 👉 🔗 👆 (& 👫 🎧-🔗) & 🚚 (💉) 🏁 🔠 🔁. + +🖼, ➡️ 💬 👆 ✔️ 4️⃣ 🛠️ 🔗 (*➡ 🛠️*): + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +⤴️ 👆 💪 🚮 🎏 ✔ 📄 🔠 👫 ⏮️ 🔗 & 🎧-🔗: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## 🛠️ ⏮️ **🗄** + +🌐 👫 🔗, ⏪ 📣 👫 📄, 🚮 🔢, 🔬, ♒️. 👆 *➡ 🛠️*. + +**FastAPI** 🔜 ✊ 💅 🚮 ⚫️ 🌐 🗄 🔗, 👈 ⚫️ 🎦 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/dependencies/sub-dependencies.md b/docs/em/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 000000000..454ff5129 --- /dev/null +++ b/docs/em/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,110 @@ +# 🎧-🔗 + +👆 💪 ✍ 🔗 👈 ✔️ **🎧-🔗**. + +👫 💪 **⏬** 👆 💪 👫. + +**FastAPI** 🔜 ✊ 💅 🔬 👫. + +## 🥇 🔗 "☑" + +👆 💪 ✍ 🥇 🔗 ("☑") 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +⚫️ 📣 📦 🔢 🔢 `q` `str`, & ⤴️ ⚫️ 📨 ⚫️. + +👉 🙅 (🚫 📶 ⚠), ✋️ 🔜 ℹ 👥 🎯 🔛 ❔ 🎧-🔗 👷. + +## 🥈 🔗, "☑" & "⚓️" + +⤴️ 👆 💪 ✍ ➕1️⃣ 🔗 🔢 ("☑") 👈 🎏 🕰 📣 🔗 🚮 👍 (⚫️ "⚓️" 💁‍♂️): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +➡️ 🎯 🔛 🔢 📣: + +* ✋️ 👉 🔢 🔗 ("☑") ⚫️, ⚫️ 📣 ➕1️⃣ 🔗 (⚫️ "🪀" 🔛 🕳 🙆). + * ⚫️ 🪀 🔛 `query_extractor`, & 🛠️ 💲 📨 ⚫️ 🔢 `q`. +* ⚫️ 📣 📦 `last_query` 🍪, `str`. + * 🚥 👩‍💻 🚫 🚚 🙆 🔢 `q`, 👥 ⚙️ 🏁 🔢 ⚙️, ❔ 👥 🖊 🍪 ⏭. + +## ⚙️ 🔗 + +⤴️ 👥 💪 ⚙️ 🔗 ⏮️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/dependencies/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial005_py310.py!} + ``` + +!!! info + 👀 👈 👥 🕴 📣 1️⃣ 🔗 *➡ 🛠️ 🔢*, `query_or_cookie_extractor`. + + ✋️ **FastAPI** 🔜 💭 👈 ⚫️ ✔️ ❎ `query_extractor` 🥇, 🚶‍♀️ 🏁 👈 `query_or_cookie_extractor` ⏪ 🤙 ⚫️. + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## ⚙️ 🎏 🔗 💗 🕰 + +🚥 1️⃣ 👆 🔗 📣 💗 🕰 🎏 *➡ 🛠️*, 🖼, 💗 🔗 ✔️ ⚠ 🎧-🔗, **FastAPI** 🔜 💭 🤙 👈 🎧-🔗 🕴 🕐 📍 📨. + +& ⚫️ 🔜 🖊 📨 💲 "💾" & 🚶‍♀️ ⚫️ 🌐 "⚓️" 👈 💪 ⚫️ 👈 🎯 📨, ↩️ 🤙 🔗 💗 🕰 🎏 📨. + +🏧 😐 🌐❔ 👆 💭 👆 💪 🔗 🤙 🔠 🔁 (🎲 💗 🕰) 🎏 📨 ↩️ ⚙️ "💾" 💲, 👆 💪 ⚒ 🔢 `use_cache=False` 🕐❔ ⚙️ `Depends`: + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +## 🌃 + +↖️ ⚪️➡️ 🌐 🎀 🔤 ⚙️ 📥, **🔗 💉** ⚙️ 🙅. + +🔢 👈 👀 🎏 *➡ 🛠️ 🔢*. + +✋️, ⚫️ 📶 🏋️, & ✔ 👆 📣 🎲 🙇 🐦 🔗 "📊" (🌲). + +!!! tip + 🌐 👉 💪 🚫 😑 ⚠ ⏮️ 👫 🙅 🖼. + + ✋️ 👆 🔜 👀 ❔ ⚠ ⚫️ 📃 🔃 **💂‍♂**. + + & 👆 🔜 👀 💸 📟 ⚫️ 🔜 🖊 👆. diff --git a/docs/em/docs/tutorial/encoder.md b/docs/em/docs/tutorial/encoder.md new file mode 100644 index 000000000..75ca3824d --- /dev/null +++ b/docs/em/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# 🎻 🔗 🔢 + +📤 💼 🌐❔ 👆 5️⃣📆 💪 🗜 💽 🆎 (💖 Pydantic 🏷) 🕳 🔗 ⏮️ 🎻 (💖 `dict`, `list`, ♒️). + +🖼, 🚥 👆 💪 🏪 ⚫️ 💽. + +👈, **FastAPI** 🚚 `jsonable_encoder()` 🔢. + +## ⚙️ `jsonable_encoder` + +➡️ 🌈 👈 👆 ✔️ 💽 `fake_db` 👈 🕴 📨 🎻 🔗 💽. + +🖼, ⚫️ 🚫 📨 `datetime` 🎚, 👈 🚫 🔗 ⏮️ 🎻. + +, `datetime` 🎚 🔜 ✔️ 🗜 `str` ⚗ 💽 💾 📁. + +🎏 🌌, 👉 💽 🚫🔜 📨 Pydantic 🏷 (🎚 ⏮️ 🔢), 🕴 `dict`. + +👆 💪 ⚙️ `jsonable_encoder` 👈. + +⚫️ 📨 🎚, 💖 Pydantic 🏷, & 📨 🎻 🔗 ⏬: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +👉 🖼, ⚫️ 🔜 🗜 Pydantic 🏷 `dict`, & `datetime` `str`. + +🏁 🤙 ⚫️ 🕳 👈 💪 🗜 ⏮️ 🐍 🐩 `json.dumps()`. + +⚫️ 🚫 📨 ⭕ `str` ⚗ 💽 🎻 📁 (🎻). ⚫️ 📨 🐍 🐩 💽 📊 (✅ `dict`) ⏮️ 💲 & 🎧-💲 👈 🌐 🔗 ⏮️ 🎻. + +!!! note + `jsonable_encoder` 🤙 ⚙️ **FastAPI** 🔘 🗜 💽. ✋️ ⚫️ ⚠ 📚 🎏 😐. diff --git a/docs/em/docs/tutorial/extra-data-types.md b/docs/em/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..dfdf6141b --- /dev/null +++ b/docs/em/docs/tutorial/extra-data-types.md @@ -0,0 +1,82 @@ +# ➕ 💽 🆎 + +🆙 🔜, 👆 ✔️ ⚙️ ⚠ 📊 🆎, 💖: + +* `int` +* `float` +* `str` +* `bool` + +✋️ 👆 💪 ⚙️ 🌅 🏗 📊 🆎. + +& 👆 🔜 ✔️ 🎏 ⚒ 👀 🆙 🔜: + +* 👑 👨‍🎨 🐕‍🦺. +* 💽 🛠️ ⚪️➡️ 📨 📨. +* 💽 🛠️ 📨 💽. +* 💽 🔬. +* 🏧 ✍ & 🧾. + +## 🎏 💽 🆎 + +📥 🌖 📊 🆎 👆 💪 ⚙️: + +* `UUID`: + * 🐩 "⭐ 😍 🆔", ⚠ 🆔 📚 💽 & ⚙️. + * 📨 & 📨 🔜 🎨 `str`. +* `datetime.datetime`: + * 🐍 `datetime.datetime`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * 🐍 `datetime.date`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `2008-09-15`. +* `datetime.time`: + * 🐍 `datetime.time`. + * 📨 & 📨 🔜 🎨 `str` 💾 8️⃣6️⃣0️⃣1️⃣ 📁, 💖: `14:23:55.003`. +* `datetime.timedelta`: + * 🐍 `datetime.timedelta`. + * 📨 & 📨 🔜 🎨 `float` 🌐 🥈. + * Pydantic ✔ 🎦 ⚫️ "💾 8️⃣6️⃣0️⃣1️⃣ 🕰 ➕ 🔢", 👀 🩺 🌅 ℹ. +* `frozenset`: + * 📨 & 📨, 😥 🎏 `set`: + * 📨, 📇 🔜 ✍, ❎ ❎ & 🏭 ⚫️ `set`. + * 📨, `set` 🔜 🗜 `list`. + * 🏗 🔗 🔜 ✔ 👈 `set` 💲 😍 (⚙️ 🎻 🔗 `uniqueItems`). +* `bytes`: + * 🐩 🐍 `bytes`. + * 📨 & 📨 🔜 😥 `str`. + * 🏗 🔗 🔜 ✔ 👈 ⚫️ `str` ⏮️ `binary` "📁". +* `Decimal`: + * 🐩 🐍 `Decimal`. + * 📨 & 📨, 🍵 🎏 `float`. +* 👆 💪 ✅ 🌐 ☑ Pydantic 📊 🆎 📥: Pydantic 📊 🆎. + +## 🖼 + +📥 🖼 *➡ 🛠️* ⏮️ 🔢 ⚙️ 🔛 🆎. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +🗒 👈 🔢 🔘 🔢 ✔️ 👫 🐠 💽 🆎, & 👆 💪, 🖼, 🎭 😐 📅 🎭, 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/em/docs/tutorial/extra-models.md b/docs/em/docs/tutorial/extra-models.md new file mode 100644 index 000000000..06c36285d --- /dev/null +++ b/docs/em/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# ➕ 🏷 + +▶️ ⏮️ ⏮️ 🖼, ⚫️ 🔜 ⚠ ✔️ 🌅 🌘 1️⃣ 🔗 🏷. + +👉 ✴️ 💼 👩‍💻 🏷, ↩️: + +* **🔢 🏷** 💪 💪 ✔️ 🔐. +* **🔢 🏷** 🔜 🚫 ✔️ 🔐. +* **💽 🏷** 🔜 🎲 💪 ✔️ #️⃣ 🔐. + +!!! danger + 🙅 🏪 👩‍💻 🔢 🔐. 🕧 🏪 "🔐 #️⃣" 👈 👆 💪 ⤴️ ✔. + + 🚥 👆 🚫 💭, 👆 🔜 💡 ⚫️❔ "🔐#️⃣" [💂‍♂ 📃](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## 💗 🏷 + +📥 🏢 💭 ❔ 🏷 💪 👀 💖 ⏮️ 👫 🔐 🏑 & 🥉 🌐❔ 👫 ⚙️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +### 🔃 `**user_in.dict()` + +#### Pydantic `.dict()` + +`user_in` Pydantic 🏷 🎓 `UserIn`. + +Pydantic 🏷 ✔️ `.dict()` 👩‍🔬 👈 📨 `dict` ⏮️ 🏷 💽. + +, 🚥 👥 ✍ Pydantic 🎚 `user_in` 💖: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +& ⤴️ 👥 🤙: + +```Python +user_dict = user_in.dict() +``` + +👥 🔜 ✔️ `dict` ⏮️ 💽 🔢 `user_dict` (⚫️ `dict` ↩️ Pydantic 🏷 🎚). + +& 🚥 👥 🤙: + +```Python +print(user_dict) +``` + +👥 🔜 🤚 🐍 `dict` ⏮️: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### 🎁 `dict` + +🚥 👥 ✊ `dict` 💖 `user_dict` & 🚶‍♀️ ⚫️ 🔢 (⚖️ 🎓) ⏮️ `**user_dict`, 🐍 🔜 "🎁" ⚫️. ⚫️ 🔜 🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌. + +, ▶️ ⏮️ `user_dict` ⚪️➡️ 🔛, ✍: + +```Python +UserInDB(**user_dict) +``` + +🔜 🏁 🕳 🌓: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +⚖️ 🌅 ⚫️❔, ⚙️ `user_dict` 🔗, ⏮️ ⚫️❔ 🎚 ⚫️ 💪 ✔️ 🔮: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Pydantic 🏷 ⚪️➡️ 🎚 ➕1️⃣ + +🖼 🔛 👥 🤚 `user_dict` ⚪️➡️ `user_in.dict()`, 👉 📟: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +🔜 🌓: + +```Python +UserInDB(**user_in.dict()) +``` + +...↩️ `user_in.dict()` `dict`, & ⤴️ 👥 ⚒ 🐍 "🎁" ⚫️ 🚶‍♀️ ⚫️ `UserInDB` 🔠 ⏮️ `**`. + +, 👥 🤚 Pydantic 🏷 ⚪️➡️ 💽 ➕1️⃣ Pydantic 🏷. + +#### 🎁 `dict` & ➕ 🇨🇻 + +& ⤴️ ❎ ➕ 🇨🇻 ❌ `hashed_password=hashed_password`, 💖: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...🔚 🆙 💆‍♂ 💖: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning + 🔗 🌖 🔢 🤖 💪 💧 💽, ✋️ 👫 ↗️ 🚫 🚚 🙆 🎰 💂‍♂. + +## 📉 ❎ + +📉 📟 ❎ 1️⃣ 🐚 💭 **FastAPI**. + +📟 ❎ 📈 🤞 🐛, 💂‍♂ ❔, 📟 🔁 ❔ (🕐❔ 👆 ℹ 1️⃣ 🥉 ✋️ 🚫 🎏), ♒️. + +& 👉 🏷 🌐 🤝 📚 💽 & ❎ 🔢 📛 & 🆎. + +👥 💪 👻. + +👥 💪 📣 `UserBase` 🏷 👈 🍦 🧢 👆 🎏 🏷. & ⤴️ 👥 💪 ⚒ 🏿 👈 🏷 👈 😖 🚮 🔢 (🆎 📄, 🔬, ♒️). + +🌐 💽 🛠️, 🔬, 🧾, ♒️. 🔜 👷 🛎. + +👈 🌌, 👥 💪 📣 🔺 🖖 🏷 (⏮️ 🔢 `password`, ⏮️ `hashed_password` & 🍵 🔐): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +## `Union` ⚖️ `anyOf` + +👆 💪 📣 📨 `Union` 2️⃣ 🆎, 👈 ⛓, 👈 📨 🔜 🙆 2️⃣. + +⚫️ 🔜 🔬 🗄 ⏮️ `anyOf`. + +👈, ⚙️ 🐩 🐍 🆎 🔑 `typing.Union`: + +!!! note + 🕐❔ ⚖ `Union`, 🔌 🏆 🎯 🆎 🥇, ⏩ 🌘 🎯 🆎. 🖼 🔛, 🌖 🎯 `PlaneItem` 👟 ⏭ `CarItem` `Union[PlaneItem, CarItem]`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` 🐍 3️⃣.1️⃣0️⃣ + +👉 🖼 👥 🚶‍♀️ `Union[PlaneItem, CarItem]` 💲 ❌ `response_model`. + +↩️ 👥 🚶‍♀️ ⚫️ **💲 ❌** ↩️ 🚮 ⚫️ **🆎 ✍**, 👥 ✔️ ⚙️ `Union` 🐍 3️⃣.1️⃣0️⃣. + +🚥 ⚫️ 🆎 ✍ 👥 💪 ✔️ ⚙️ ⏸ ⏸,: + +```Python +some_variable: PlaneItem | CarItem +``` + +✋️ 🚥 👥 🚮 👈 `response_model=PlaneItem | CarItem` 👥 🔜 🤚 ❌, ↩️ 🐍 🔜 🔄 🎭 **❌ 🛠️** 🖖 `PlaneItem` & `CarItem` ↩️ 🔬 👈 🆎 ✍. + +## 📇 🏷 + +🎏 🌌, 👆 💪 📣 📨 📇 🎚. + +👈, ⚙️ 🐩 🐍 `typing.List` (⚖️ `list` 🐍 3️⃣.9️⃣ & 🔛): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +## 📨 ⏮️ ❌ `dict` + +👆 💪 📣 📨 ⚙️ ✅ ❌ `dict`, 📣 🆎 🔑 & 💲, 🍵 ⚙️ Pydantic 🏷. + +👉 ⚠ 🚥 👆 🚫 💭 ☑ 🏑/🔢 📛 (👈 🔜 💪 Pydantic 🏷) ⏪. + +👉 💼, 👆 💪 ⚙️ `typing.Dict` (⚖️ `dict` 🐍 3️⃣.9️⃣ & 🔛): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +## 🌃 + +⚙️ 💗 Pydantic 🏷 & 😖 ➡ 🔠 💼. + +👆 🚫 💪 ✔️ 👁 💽 🏷 📍 👨‍💼 🚥 👈 👨‍💼 🔜 💪 ✔️ 🎏 "🇵🇸". 💼 ⏮️ 👩‍💻 "👨‍💼" ⏮️ 🇵🇸 ✅ `password`, `password_hash` & 🙅‍♂ 🔐. diff --git a/docs/em/docs/tutorial/first-steps.md b/docs/em/docs/tutorial/first-steps.md new file mode 100644 index 000000000..252e769f4 --- /dev/null +++ b/docs/em/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# 🥇 🔁 + +🙅 FastAPI 📁 💪 👀 💖 👉: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +📁 👈 📁 `main.py`. + +🏃 🖖 💽: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + 📋 `uvicorn main:app` 🔗: + + * `main`: 📁 `main.py` (🐍 "🕹"). + * `app`: 🎚 ✍ 🔘 `main.py` ⏮️ ⏸ `app = FastAPI()`. + * `--reload`: ⚒ 💽 ⏏ ⏮️ 📟 🔀. 🕴 ⚙️ 🛠️. + +🔢, 📤 ⏸ ⏮️ 🕳 💖: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +👈 ⏸ 🎦 📛 🌐❔ 👆 📱 ➖ 🍦, 👆 🇧🇿 🎰. + +### ✅ ⚫️ + +📂 👆 🖥 http://127.0.0.1:8000. + +👆 🔜 👀 🎻 📨: + +```JSON +{"message": "Hello World"} +``` + +### 🎓 🛠️ 🩺 + +🔜 🚶 http://127.0.0.1:8000/docs. + +👆 🔜 👀 🏧 🎓 🛠️ 🧾 (🚚 🦁 🎚): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### 🎛 🛠️ 🩺 + +& 🔜, 🚶 http://127.0.0.1:8000/redoc. + +👆 🔜 👀 🎛 🏧 🧾 (🚚 📄): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### 🗄 + +**FastAPI** 🏗 "🔗" ⏮️ 🌐 👆 🛠️ ⚙️ **🗄** 🐩 ⚖ 🔗. + +#### "🔗" + +"🔗" 🔑 ⚖️ 📛 🕳. 🚫 📟 👈 🛠️ ⚫️, ✋️ 📝 📛. + +#### 🛠️ "🔗" + +👉 💼, 🗄 🔧 👈 🤔 ❔ 🔬 🔗 👆 🛠️. + +👉 🔗 🔑 🔌 👆 🛠️ ➡, 💪 🔢 👫 ✊, ♒️. + +#### 💽 "🔗" + +⚖ "🔗" 💪 🔗 💠 💽, 💖 🎻 🎚. + +👈 💼, ⚫️ 🔜 ⛓ 🎻 🔢, & 📊 🆎 👫 ✔️, ♒️. + +#### 🗄 & 🎻 🔗 + +🗄 🔬 🛠️ 🔗 👆 🛠️. & 👈 🔗 🔌 🔑 (⚖️ "🔗") 📊 📨 & 📨 👆 🛠️ ⚙️ **🎻 🔗**, 🐩 🎻 📊 🔗. + +#### ✅ `openapi.json` + +🚥 👆 😟 🔃 ❔ 🍣 🗄 🔗 👀 💖, FastAPI 🔁 🏗 🎻 (🔗) ⏮️ 📛 🌐 👆 🛠️. + +👆 💪 👀 ⚫️ 🔗: http://127.0.0.1:8000/openapi.json. + +⚫️ 🔜 🎦 🎻 ▶️ ⏮️ 🕳 💖: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### ⚫️❔ 🗄 + +🗄 🔗 ⚫️❔ 🏋️ 2️⃣ 🎓 🧾 ⚙️ 🔌. + +& 📤 💯 🎛, 🌐 ⚓️ 🔛 🗄. 👆 💪 💪 🚮 🙆 📚 🎛 👆 🈸 🏗 ⏮️ **FastAPI**. + +👆 💪 ⚙️ ⚫️ 🏗 📟 🔁, 👩‍💻 👈 🔗 ⏮️ 👆 🛠️. 🖼, 🕸, 📱 ⚖️ ☁ 🈸. + +## 🌃, 🔁 🔁 + +### 🔁 1️⃣: 🗄 `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` 🐍 🎓 👈 🚚 🌐 🛠️ 👆 🛠️. + +!!! note "📡 ℹ" + `FastAPI` 🎓 👈 😖 🔗 ⚪️➡️ `Starlette`. + + 👆 💪 ⚙️ 🌐 💃 🛠️ ⏮️ `FastAPI` 💁‍♂️. + +### 🔁 2️⃣: ✍ `FastAPI` "👐" + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +📥 `app` 🔢 🔜 "👐" 🎓 `FastAPI`. + +👉 🔜 👑 ☝ 🔗 ✍ 🌐 👆 🛠️. + +👉 `app` 🎏 1️⃣ 🔗 `uvicorn` 📋: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +🚥 👆 ✍ 👆 📱 💖: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +& 🚮 ⚫️ 📁 `main.py`, ⤴️ 👆 🔜 🤙 `uvicorn` 💖: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### 🔁 3️⃣: ✍ *➡ 🛠️* + +#### ➡ + +"➡" 📥 🔗 🏁 🍕 📛 ▶️ ⚪️➡️ 🥇 `/`. + +, 📛 💖: + +``` +https://example.com/items/foo +``` + +...➡ 🔜: + +``` +/items/foo +``` + +!!! info + "➡" 🛎 🤙 "🔗" ⚖️ "🛣". + +⏪ 🏗 🛠️, "➡" 👑 🌌 🎏 "⚠" & "ℹ". + +#### 🛠️ + +"🛠️" 📥 🔗 1️⃣ 🇺🇸🔍 "👩‍🔬". + +1️⃣: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...& 🌅 😍 🕐: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +🇺🇸🔍 🛠️, 👆 💪 🔗 🔠 ➡ ⚙️ 1️⃣ (⚖️ 🌅) 👫 "👩‍🔬". + +--- + +🕐❔ 🏗 🔗, 👆 🛎 ⚙️ 👫 🎯 🇺🇸🔍 👩‍🔬 🎭 🎯 🎯. + +🛎 👆 ⚙️: + +* `POST`: ✍ 💽. +* `GET`: ✍ 💽. +* `PUT`: ℹ 💽. +* `DELETE`: ❎ 💽. + +, 🗄, 🔠 🇺🇸🔍 👩‍🔬 🤙 "🛠️". + +👥 🔜 🤙 👫 "**🛠️**" 💁‍♂️. + +#### 🔬 *➡ 🛠️ 👨‍🎨* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` 💬 **FastAPI** 👈 🔢 ▶️️ 🔛 🈚 🚚 📨 👈 🚶: + +* ➡ `/` +* ⚙️ get 🛠️ + +!!! info "`@decorator` ℹ" + 👈 `@something` ❕ 🐍 🤙 "👨‍🎨". + + 👆 🚮 ⚫️ 🔛 🔝 🔢. 💖 📶 📔 👒 (👤 💭 👈 🌐❔ ⚖ 👟 ⚪️➡️). + + "👨‍🎨" ✊ 🔢 🔛 & 🔨 🕳 ⏮️ ⚫️. + + 👆 💼, 👉 👨‍🎨 💬 **FastAPI** 👈 🔢 🔛 🔗 **➡** `/` ⏮️ **🛠️** `get`. + + ⚫️ "**➡ 🛠️ 👨‍🎨**". + +👆 💪 ⚙️ 🎏 🛠️: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +& 🌅 😍 🕐: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + 👆 🆓 ⚙️ 🔠 🛠️ (🇺🇸🔍 👩‍🔬) 👆 🎋. + + **FastAPI** 🚫 🛠️ 🙆 🎯 🔑. + + ℹ 📥 🎁 📄, 🚫 📄. + + 🖼, 🕐❔ ⚙️ 🕹 👆 🛎 🎭 🌐 🎯 ⚙️ 🕴 `POST` 🛠️. + +### 🔁 4️⃣: 🔬 **➡ 🛠️ 🔢** + +👉 👆 "**➡ 🛠️ 🔢**": + +* **➡**: `/`. +* **🛠️**: `get`. +* **🔢**: 🔢 🔛 "👨‍🎨" (🔛 `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +👉 🐍 🔢. + +⚫️ 🔜 🤙 **FastAPI** 🕐❔ ⚫️ 📨 📨 📛 "`/`" ⚙️ `GET` 🛠️. + +👉 💼, ⚫️ `async` 🔢. + +--- + +👆 💪 🔬 ⚫️ 😐 🔢 ↩️ `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + 🚥 👆 🚫 💭 🔺, ✅ [🔁: *"🏃 ❓"*](../async.md#in-a-hurry){.internal-link target=_blank}. + +### 🔁 5️⃣: 📨 🎚 + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +👆 💪 📨 `dict`, `list`, ⭐ 💲 `str`, `int`, ♒️. + +👆 💪 📨 Pydantic 🏷 (👆 🔜 👀 🌅 🔃 👈 ⏪). + +📤 📚 🎏 🎚 & 🏷 👈 🔜 🔁 🗜 🎻 (🔌 🐜, ♒️). 🔄 ⚙️ 👆 💕 🕐, ⚫️ 🏆 🎲 👈 👫 ⏪ 🐕‍🦺. + +## 🌃 + +* 🗄 `FastAPI`. +* ✍ `app` 👐. +* ✍ **➡ 🛠️ 👨‍🎨** (💖 `@app.get("/")`). +* ✍ **➡ 🛠️ 🔢** (💖 `def root(): ...` 🔛). +* 🏃 🛠️ 💽 (💖 `uvicorn main:app --reload`). diff --git a/docs/em/docs/tutorial/handling-errors.md b/docs/em/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..ef7bbfa65 --- /dev/null +++ b/docs/em/docs/tutorial/handling-errors.md @@ -0,0 +1,261 @@ +# 🚚 ❌ + +📤 📚 ⚠ 🌐❔ 👆 💪 🚨 ❌ 👩‍💻 👈 ⚙️ 👆 🛠️. + +👉 👩‍💻 💪 🖥 ⏮️ 🕸, 📟 ⚪️➡️ 👱 🙆, ☁ 📳, ♒️. + +👆 💪 💪 💬 👩‍💻 👈: + +* 👩‍💻 🚫 ✔️ 🥃 😌 👈 🛠️. +* 👩‍💻 🚫 ✔️ 🔐 👈 ℹ. +* 🏬 👩‍💻 🔄 🔐 🚫 🔀. +* ♒️. + +👫 💼, 👆 🔜 🛎 📨 **🇺🇸🔍 👔 📟** ↔ **4️⃣0️⃣0️⃣** (⚪️➡️ 4️⃣0️⃣0️⃣ 4️⃣9️⃣9️⃣). + +👉 🎏 2️⃣0️⃣0️⃣ 🇺🇸🔍 👔 📟 (⚪️➡️ 2️⃣0️⃣0️⃣ 2️⃣9️⃣9️⃣). 👈 "2️⃣0️⃣0️⃣" 👔 📟 ⛓ 👈 😫 📤 "🏆" 📨. + +👔 📟 4️⃣0️⃣0️⃣ ↔ ⛓ 👈 📤 ❌ ⚪️➡️ 👩‍💻. + +💭 🌐 👈 **"4️⃣0️⃣4️⃣ 🚫 🔎"** ❌ (& 🤣) ❓ + +## ⚙️ `HTTPException` + +📨 🇺🇸🔍 📨 ⏮️ ❌ 👩‍💻 👆 ⚙️ `HTTPException`. + +### 🗄 `HTTPException` + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### 🤚 `HTTPException` 👆 📟 + +`HTTPException` 😐 🐍 ⚠ ⏮️ 🌖 📊 🔗 🔗. + +↩️ ⚫️ 🐍 ⚠, 👆 🚫 `return` ⚫️, 👆 `raise` ⚫️. + +👉 ⛓ 👈 🚥 👆 🔘 🚙 🔢 👈 👆 🤙 🔘 👆 *➡ 🛠️ 🔢*, & 👆 🤚 `HTTPException` ⚪️➡️ 🔘 👈 🚙 🔢, ⚫️ 🏆 🚫 🏃 🎂 📟 *➡ 🛠️ 🔢*, ⚫️ 🔜 ❎ 👈 📨 ▶️️ ↖️ & 📨 🇺🇸🔍 ❌ ⚪️➡️ `HTTPException` 👩‍💻. + +💰 🙋‍♀ ⚠ 🤭 `return`😅 💲 🔜 🌖 ⭐ 📄 🔃 🔗 & 💂‍♂. + +👉 🖼, 🕐❔ 👩‍💻 📨 🏬 🆔 👈 🚫 🔀, 🤚 ⚠ ⏮️ 👔 📟 `404`: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### 📉 📨 + +🚥 👩‍💻 📨 `http://example.com/items/foo` ( `item_id` `"foo"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 2️⃣0️⃣0️⃣, & 🎻 📨: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +✋️ 🚥 👩‍💻 📨 `http://example.com/items/bar` (🚫-🚫 `item_id` `"bar"`), 👈 👩‍💻 🔜 📨 🇺🇸🔍 👔 📟 4️⃣0️⃣4️⃣ ("🚫 🔎" ❌), & 🎻 📨: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip + 🕐❔ 🙋‍♀ `HTTPException`, 👆 💪 🚶‍♀️ 🙆 💲 👈 💪 🗜 🎻 🔢 `detail`, 🚫 🕴 `str`. + + 👆 💪 🚶‍♀️ `dict`, `list`, ♒️. + + 👫 🍵 🔁 **FastAPI** & 🗜 🎻. + +## 🚮 🛃 🎚 + +📤 ⚠ 🌐❔ ⚫️ ⚠ 💪 🚮 🛃 🎚 🇺🇸🔍 ❌. 🖼, 🆎 💂‍♂. + +👆 🎲 🏆 🚫 💪 ⚙️ ⚫️ 🔗 👆 📟. + +✋️ 💼 👆 💪 ⚫️ 🏧 😐, 👆 💪 🚮 🛃 🎚: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## ❎ 🛃 ⚠ 🐕‍🦺 + +👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ 🎏 ⚠ 🚙 ⚪️➡️ 💃. + +➡️ 💬 👆 ✔️ 🛃 ⚠ `UnicornException` 👈 👆 (⚖️ 🗃 👆 ⚙️) 💪 `raise`. + +& 👆 💚 🍵 👉 ⚠ 🌐 ⏮️ FastAPI. + +👆 💪 🚮 🛃 ⚠ 🐕‍🦺 ⏮️ `@app.exception_handler()`: + +```Python hl_lines="5-7 13-18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +📥, 🚥 👆 📨 `/unicorns/yolo`, *➡ 🛠️* 🔜 `raise` `UnicornException`. + +✋️ ⚫️ 🔜 🍵 `unicorn_exception_handler`. + +, 👆 🔜 📨 🧹 ❌, ⏮️ 🇺🇸🔍 👔 📟 `418` & 🎻 🎚: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request` & `from starlette.responses import JSONResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. 🎏 ⏮️ `Request`. + +## 🔐 🔢 ⚠ 🐕‍🦺 + +**FastAPI** ✔️ 🔢 ⚠ 🐕‍🦺. + +👫 🐕‍🦺 🈚 🛬 🔢 🎻 📨 🕐❔ 👆 `raise` `HTTPException` & 🕐❔ 📨 ✔️ ❌ 💽. + +👆 💪 🔐 👫 ⚠ 🐕‍🦺 ⏮️ 👆 👍. + +### 🔐 📨 🔬 ⚠ + +🕐❔ 📨 🔌 ❌ 📊, **FastAPI** 🔘 🤚 `RequestValidationError`. + +& ⚫️ 🔌 🔢 ⚠ 🐕‍🦺 ⚫️. + +🔐 ⚫️, 🗄 `RequestValidationError` & ⚙️ ⚫️ ⏮️ `@app.exception_handler(RequestValidationError)` 🎀 ⚠ 🐕‍🦺. + +⚠ 🐕‍🦺 🔜 📨 `Request` & ⚠. + +```Python hl_lines="2 14-16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +🔜, 🚥 👆 🚶 `/items/foo`, ↩️ 💆‍♂ 🔢 🎻 ❌ ⏮️: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +👆 🔜 🤚 ✍ ⏬, ⏮️: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +#### `RequestValidationError` 🆚 `ValidationError` + +!!! warning + 👫 📡 ℹ 👈 👆 💪 🚶 🚥 ⚫️ 🚫 ⚠ 👆 🔜. + +`RequestValidationError` 🎧-🎓 Pydantic `ValidationError`. + +**FastAPI** ⚙️ ⚫️ 👈, 🚥 👆 ⚙️ Pydantic 🏷 `response_model`, & 👆 💽 ✔️ ❌, 👆 🔜 👀 ❌ 👆 🕹. + +✋️ 👩‍💻/👩‍💻 🔜 🚫 👀 ⚫️. ↩️, 👩‍💻 🔜 📨 "🔗 💽 ❌" ⏮️ 🇺🇸🔍 👔 📟 `500`. + +⚫️ 🔜 👉 🌌 ↩️ 🚥 👆 ✔️ Pydantic `ValidationError` 👆 *📨* ⚖️ 🙆 👆 📟 (🚫 👩‍💻 *📨*), ⚫️ 🤙 🐛 👆 📟. + +& ⏪ 👆 🔧 ⚫️, 👆 👩‍💻/👩‍💻 🚫🔜 🚫 ✔️ 🔐 🔗 ℹ 🔃 ❌, 👈 💪 🎦 💂‍♂ ⚠. + +### 🔐 `HTTPException` ❌ 🐕‍🦺 + +🎏 🌌, 👆 💪 🔐 `HTTPException` 🐕‍🦺. + +🖼, 👆 💪 💚 📨 ✅ ✍ 📨 ↩️ 🎻 👫 ❌: + +```Python hl_lines="3-4 9-11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import PlainTextResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### ⚙️ `RequestValidationError` 💪 + +`RequestValidationError` 🔌 `body` ⚫️ 📨 ⏮️ ❌ 💽. + +👆 💪 ⚙️ ⚫️ ⏪ 🛠️ 👆 📱 🕹 💪 & ℹ ⚫️, 📨 ⚫️ 👩‍💻, ♒️. + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial005.py!} +``` + +🔜 🔄 📨 ❌ 🏬 💖: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +👆 🔜 📨 📨 💬 👆 👈 💽 ❌ ⚗ 📨 💪: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPI `HTTPException` 🆚 💃 `HTTPException` + +**FastAPI** ✔️ 🚮 👍 `HTTPException`. + +& **FastAPI**'Ⓜ `HTTPException` ❌ 🎓 😖 ⚪️➡️ 💃 `HTTPException` ❌ 🎓. + +🕴 🔺, 👈 **FastAPI**'Ⓜ `HTTPException` ✔ 👆 🚮 🎚 🔌 📨. + +👉 💪/⚙️ 🔘 ✳ 2️⃣.0️⃣ & 💂‍♂ 🚙. + +, 👆 💪 🚧 🙋‍♀ **FastAPI**'Ⓜ `HTTPException` 🛎 👆 📟. + +✋️ 🕐❔ 👆 ® ⚠ 🐕‍🦺, 👆 🔜 ® ⚫️ 💃 `HTTPException`. + +👉 🌌, 🚥 🙆 🍕 💃 🔗 📟, ⚖️ 💃 ↔ ⚖️ 🔌 -, 🤚 💃 `HTTPException`, 👆 🐕‍🦺 🔜 💪 ✊ & 🍵 ⚫️. + +👉 🖼, 💪 ✔️ 👯‍♂️ `HTTPException`Ⓜ 🎏 📟, 💃 ⚠ 📁 `StarletteHTTPException`: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### 🏤-⚙️ **FastAPI**'Ⓜ ⚠ 🐕‍🦺 + +🚥 👆 💚 ⚙️ ⚠ ⤴️ ⏮️ 🎏 🔢 ⚠ 🐕‍🦺 ⚪️➡️ **FastAPI**, 👆 💪 🗄 & 🏤-⚙️ 🔢 ⚠ 🐕‍🦺 ⚪️➡️ `fastapi.exception_handlers`: + +```Python hl_lines="2-5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +👉 🖼 👆 `print`😅 ❌ ⏮️ 📶 🎨 📧, ✋️ 👆 🤚 💭. 👆 💪 ⚙️ ⚠ & ⤴️ 🏤-⚙️ 🔢 ⚠ 🐕‍🦺. diff --git a/docs/em/docs/tutorial/header-params.md b/docs/em/docs/tutorial/header-params.md new file mode 100644 index 000000000..0f33a1774 --- /dev/null +++ b/docs/em/docs/tutorial/header-params.md @@ -0,0 +1,128 @@ +# 🎚 🔢 + +👆 💪 🔬 🎚 🔢 🎏 🌌 👆 🔬 `Query`, `Path` & `Cookie` 🔢. + +## 🗄 `Header` + +🥇 🗄 `Header`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +## 📣 `Header` 🔢 + +⤴️ 📣 🎚 🔢 ⚙️ 🎏 📊 ⏮️ `Path`, `Query` & `Cookie`. + +🥇 💲 🔢 💲, 👆 💪 🚶‍♀️ 🌐 ➕ 🔬 ⚖️ ✍ 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +!!! note "📡 ℹ" + `Header` "👭" 🎓 `Path`, `Query` & `Cookie`. ⚫️ 😖 ⚪️➡️ 🎏 ⚠ `Param` 🎓. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `Header`, & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! info + 📣 🎚, 👆 💪 ⚙️ `Header`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢. + +## 🏧 🛠️ + +`Header` ✔️ 🐥 ➕ 🛠️ 🔛 🔝 ⚫️❔ `Path`, `Query` & `Cookie` 🚚. + +🌅 🐩 🎚 🎏 "🔠" 🦹, 💭 "➖ 🔣" (`-`). + +✋️ 🔢 💖 `user-agent` ❌ 🐍. + +, 🔢, `Header` 🔜 🗜 🔢 📛 🦹 ⚪️➡️ 🎦 (`_`) 🔠 (`-`) ⚗ & 📄 🎚. + +, 🇺🇸🔍 🎚 💼-😛,, 👆 💪 📣 👫 ⏮️ 🐩 🐍 👗 (💭 "🔡"). + +, 👆 💪 ⚙️ `user_agent` 👆 🛎 🔜 🐍 📟, ↩️ 💆‍♂ 🎯 🥇 🔤 `User_Agent` ⚖️ 🕳 🎏. + +🚥 🤔 👆 💪 ❎ 🏧 🛠️ 🎦 🔠, ⚒ 🔢 `convert_underscores` `Header` `False`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +!!! warning + ⏭ ⚒ `convert_underscores` `False`, 🐻 🤯 👈 🇺🇸🔍 🗳 & 💽 / ⚙️ 🎚 ⏮️ 🎦. + +## ❎ 🎚 + +⚫️ 💪 📨 ❎ 🎚. 👈 ⛓, 🎏 🎚 ⏮️ 💗 💲. + +👆 💪 🔬 👈 💼 ⚙️ 📇 🆎 📄. + +👆 🔜 📨 🌐 💲 ⚪️➡️ ❎ 🎚 🐍 `list`. + +🖼, 📣 🎚 `X-Token` 👈 💪 😑 🌅 🌘 🕐, 👆 💪 ✍: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +🚥 👆 🔗 ⏮️ 👈 *➡ 🛠️* 📨 2️⃣ 🇺🇸🔍 🎚 💖: + +``` +X-Token: foo +X-Token: bar +``` + +📨 🔜 💖: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## 🌃 + +📣 🎚 ⏮️ `Header`, ⚙️ 🎏 ⚠ ⚓ `Query`, `Path` & `Cookie`. + +& 🚫 😟 🔃 🎦 👆 🔢, **FastAPI** 🔜 ✊ 💅 🏭 👫. diff --git a/docs/em/docs/tutorial/index.md b/docs/em/docs/tutorial/index.md new file mode 100644 index 000000000..8536dc3ee --- /dev/null +++ b/docs/em/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# 🔰 - 👩‍💻 🦮 - 🎶 + +👉 🔰 🎦 👆 ❔ ⚙️ **FastAPI** ⏮️ 🌅 🚮 ⚒, 🔁 🔁. + +🔠 📄 📉 🏗 🔛 ⏮️ 🕐, ✋️ ⚫️ 🏗 🎏 ❔, 👈 👆 💪 🚶 🔗 🙆 🎯 1️⃣ ❎ 👆 🎯 🛠️ 💪. + +⚫️ 🏗 👷 🔮 🔗. + +👆 💪 👟 🔙 & 👀 ⚫️❔ ⚫️❔ 👆 💪. + +## 🏃 📟 + +🌐 📟 🍫 💪 📁 & ⚙️ 🔗 (👫 🤙 💯 🐍 📁). + +🏃 🙆 🖼, 📁 📟 📁 `main.py`, & ▶️ `uvicorn` ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +⚫️ **🏆 💡** 👈 👆 ✍ ⚖️ 📁 📟, ✍ ⚫️ & 🏃 ⚫️ 🌐. + +⚙️ ⚫️ 👆 👨‍🎨 ⚫️❔ 🤙 🎦 👆 💰 FastAPI, 👀 ❔ 🐥 📟 👆 ✔️ ✍, 🌐 🆎 ✅, ✍, ♒️. + +--- + +## ❎ FastAPI + +🥇 🔁 ❎ FastAPI. + +🔰, 👆 💪 💚 ❎ ⚫️ ⏮️ 🌐 📦 🔗 & ⚒: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...👈 🔌 `uvicorn`, 👈 👆 💪 ⚙️ 💽 👈 🏃 👆 📟. + +!!! note + 👆 💪 ❎ ⚫️ 🍕 🍕. + + 👉 ⚫️❔ 👆 🔜 🎲 🕐 👆 💚 🛠️ 👆 🈸 🏭: + + ``` + pip install fastapi + ``` + + ❎ `uvicorn` 👷 💽: + + ``` + pip install "uvicorn[standard]" + ``` + + & 🎏 🔠 📦 🔗 👈 👆 💚 ⚙️. + +## 🏧 👩‍💻 🦮 + +📤 **🏧 👩‍💻 🦮** 👈 👆 💪 ✍ ⏪ ⏮️ 👉 **🔰 - 👩‍💻 🦮**. + +**🏧 👩‍💻 🦮**, 🏗 🔛 👉, ⚙️ 🎏 🔧, & 💡 👆 ➕ ⚒. + +✋️ 👆 🔜 🥇 ✍ **🔰 - 👩‍💻 🦮** (⚫️❔ 👆 👂 ▶️️ 🔜). + +⚫️ 🔧 👈 👆 💪 🏗 🏁 🈸 ⏮️ **🔰 - 👩‍💻 🦮**, & ⤴️ ↔ ⚫️ 🎏 🌌, ⚓️ 🔛 👆 💪, ⚙️ 🌖 💭 ⚪️➡️ **🏧 👩‍💻 🦮**. diff --git a/docs/em/docs/tutorial/metadata.md b/docs/em/docs/tutorial/metadata.md new file mode 100644 index 000000000..00098cdf5 --- /dev/null +++ b/docs/em/docs/tutorial/metadata.md @@ -0,0 +1,112 @@ +# 🗃 & 🩺 📛 + +👆 💪 🛃 📚 🗃 📳 👆 **FastAPI** 🈸. + +## 🗃 🛠️ + +👆 💪 ⚒ 📄 🏑 👈 ⚙️ 🗄 🔧 & 🏧 🛠️ 🩺 ⚜: + +| 🔢 | 🆎 | 📛 | +|------------|------|-------------| +| `title` | `str` | 📛 🛠️. | +| `description` | `str` | 📏 📛 🛠️. ⚫️ 💪 ⚙️ ✍. | +| `version` | `string` | ⏬ 🛠️. 👉 ⏬ 👆 👍 🈸, 🚫 🗄. 🖼 `2.5.0`. | +| `terms_of_service` | `str` | 📛 ⚖ 🐕‍🦺 🛠️. 🚥 🚚, 👉 ✔️ 📛. | +| `contact` | `dict` | 📧 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
contact 🏑
🔢🆎📛
namestr⚖ 📛 📧 👨‍💼/🏢.
urlstr📛 ☝ 📧 ℹ. 🔜 📁 📛.
emailstr📧 📢 📧 👨‍💼/🏢. 🔜 📁 📧 📢.
| +| `license_info` | `dict` | 🛂 ℹ 🎦 🛠️. ⚫️ 💪 🔌 📚 🏑.
license_info 🏑
🔢🆎📛
namestr🚚 (🚥 license_info ⚒). 🛂 📛 ⚙️ 🛠️.
urlstr📛 🛂 ⚙️ 🛠️. 🔜 📁 📛.
| + +👆 💪 ⚒ 👫 ⏩: + +```Python hl_lines="3-16 19-31" +{!../../../docs_src/metadata/tutorial001.py!} +``` + +!!! tip + 👆 💪 ✍ ✍ `description` 🏑 & ⚫️ 🔜 ✍ 🔢. + +⏮️ 👉 📳, 🏧 🛠️ 🩺 🔜 👀 💖: + + + +## 🗃 🔖 + +👆 💪 🚮 🌖 🗃 🎏 🔖 ⚙️ 👪 👆 ➡ 🛠️ ⏮️ 🔢 `openapi_tags`. + +⚫️ ✊ 📇 ⚗ 1️⃣ 📖 🔠 🔖. + +🔠 📖 💪 🔌: + +* `name` (**✔**): `str` ⏮️ 🎏 📛 👆 ⚙️ `tags` 🔢 👆 *➡ 🛠️* & `APIRouter`Ⓜ. +* `description`: `str` ⏮️ 📏 📛 🔖. ⚫️ 💪 ✔️ ✍ & 🔜 🎦 🩺 🎚. +* `externalDocs`: `dict` 🔬 🔢 🧾 ⏮️: + * `description`: `str` ⏮️ 📏 📛 🔢 🩺. + * `url` (**✔**): `str` ⏮️ 📛 🔢 🧾. + +### ✍ 🗃 🔖 + +➡️ 🔄 👈 🖼 ⏮️ 🔖 `users` & `items`. + +✍ 🗃 👆 🔖 & 🚶‍♀️ ⚫️ `openapi_tags` 🔢: + +```Python hl_lines="3-16 18" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +👀 👈 👆 💪 ⚙️ ✍ 🔘 📛, 🖼 "💳" 🔜 🎦 🦁 (**💳**) & "🎀" 🔜 🎦 ❕ (_🎀_). + +!!! tip + 👆 🚫 ✔️ 🚮 🗃 🌐 🔖 👈 👆 ⚙️. + +### ⚙️ 👆 🔖 + +⚙️ `tags` 🔢 ⏮️ 👆 *➡ 🛠️* (& `APIRouter`Ⓜ) 🛠️ 👫 🎏 🔖: + +```Python hl_lines="21 26" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +!!! info + ✍ 🌅 🔃 🔖 [➡ 🛠️ 📳](../path-operation-configuration/#tags){.internal-link target=_blank}. + +### ✅ 🩺 + +🔜, 🚥 👆 ✅ 🩺, 👫 🔜 🎦 🌐 🌖 🗃: + + + +### ✔ 🔖 + +✔ 🔠 🔖 🗃 📖 🔬 ✔ 🎦 🩺 🎚. + +🖼, ✋️ `users` 🔜 🚶 ⏮️ `items` 🔤 ✔, ⚫️ 🎦 ⏭ 👫, ↩️ 👥 🚮 👫 🗃 🥇 📖 📇. + +## 🗄 📛 + +🔢, 🗄 🔗 🍦 `/openapi.json`. + +✋️ 👆 💪 🔗 ⚫️ ⏮️ 🔢 `openapi_url`. + +🖼, ⚒ ⚫️ 🍦 `/api/v1/openapi.json`: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial002.py!} +``` + +🚥 👆 💚 ❎ 🗄 🔗 🍕 👆 💪 ⚒ `openapi_url=None`, 👈 🔜 ❎ 🧾 👩‍💻 🔢 👈 ⚙️ ⚫️. + +## 🩺 📛 + +👆 💪 🔗 2️⃣ 🧾 👩‍💻 🔢 🔌: + +* **🦁 🎚**: 🍦 `/docs`. + * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `docs_url`. + * 👆 💪 ❎ ⚫️ ⚒ `docs_url=None`. +* **📄**: 🍦 `/redoc`. + * 👆 💪 ⚒ 🚮 📛 ⏮️ 🔢 `redoc_url`. + * 👆 💪 ❎ ⚫️ ⚒ `redoc_url=None`. + +🖼, ⚒ 🦁 🎚 🍦 `/documentation` & ❎ 📄: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial003.py!} +``` diff --git a/docs/em/docs/tutorial/middleware.md b/docs/em/docs/tutorial/middleware.md new file mode 100644 index 000000000..644b4690c --- /dev/null +++ b/docs/em/docs/tutorial/middleware.md @@ -0,0 +1,61 @@ +# 🛠️ + +👆 💪 🚮 🛠️ **FastAPI** 🈸. + +"🛠️" 🔢 👈 👷 ⏮️ 🔠 **📨** ⏭ ⚫️ 🛠️ 🙆 🎯 *➡ 🛠️*. & ⏮️ 🔠 **📨** ⏭ 🛬 ⚫️. + +* ⚫️ ✊ 🔠 **📨** 👈 👟 👆 🈸. +* ⚫️ 💪 ⤴️ 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. +* ⤴️ ⚫️ 🚶‍♀️ **📨** 🛠️ 🎂 🈸 ( *➡ 🛠️*). +* ⚫️ ⤴️ ✊ **📨** 🏗 🈸 ( *➡ 🛠️*). +* ⚫️ 💪 🕳 👈 **📨** ⚖️ 🏃 🙆 💪 📟. +* ⤴️ ⚫️ 📨 **📨**. + +!!! note "📡 ℹ" + 🚥 👆 ✔️ 🔗 ⏮️ `yield`, 🚪 📟 🔜 🏃 *⏮️* 🛠️. + + 🚥 📤 🙆 🖥 📋 (📄 ⏪), 👫 🔜 🏃 *⏮️* 🌐 🛠️. + +## ✍ 🛠️ + +✍ 🛠️ 👆 ⚙️ 👨‍🎨 `@app.middleware("http")` 🔛 🔝 🔢. + +🛠️ 🔢 📨: + +* `request`. +* 🔢 `call_next` 👈 🔜 📨 `request` 🔢. + * 👉 🔢 🔜 🚶‍♀️ `request` 🔗 *➡ 🛠️*. + * ⤴️ ⚫️ 📨 `response` 🏗 🔗 *➡ 🛠️*. +* 👆 💪 ⤴️ 🔀 🌅 `response` ⏭ 🛬 ⚫️. + +```Python hl_lines="8-9 11 14" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +!!! tip + ✔️ 🤯 👈 🛃 © 🎚 💪 🚮 ⚙️ '✖-' 🔡. + + ✋️ 🚥 👆 ✔️ 🛃 🎚 👈 👆 💚 👩‍💻 🖥 💪 👀, 👆 💪 🚮 👫 👆 ⚜ 📳 ([⚜ (✖️-🇨🇳 ℹ 🤝)](cors.md){.internal-link target=_blank}) ⚙️ 🔢 `expose_headers` 📄 💃 ⚜ 🩺. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.requests import Request`. + + **FastAPI** 🚚 ⚫️ 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +### ⏭ & ⏮️ `response` + +👆 💪 🚮 📟 🏃 ⏮️ `request`, ⏭ 🙆 *➡ 🛠️* 📨 ⚫️. + +& ⏮️ `response` 🏗, ⏭ 🛬 ⚫️. + +🖼, 👆 💪 🚮 🛃 🎚 `X-Process-Time` ⚗ 🕰 🥈 👈 ⚫️ ✊ 🛠️ 📨 & 🏗 📨: + +```Python hl_lines="10 12-13" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +## 🎏 🛠️ + +👆 💪 ⏪ ✍ 🌖 🔃 🎏 🛠️ [🏧 👩‍💻 🦮: 🏧 🛠️](../advanced/middleware.md){.internal-link target=_blank}. + +👆 🔜 ✍ 🔃 ❔ 🍵 ⏮️ 🛠️ ⏭ 📄. diff --git a/docs/em/docs/tutorial/path-operation-configuration.md b/docs/em/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..916529258 --- /dev/null +++ b/docs/em/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,179 @@ +# ➡ 🛠️ 📳 + +📤 📚 🔢 👈 👆 💪 🚶‍♀️ 👆 *➡ 🛠️ 👨‍🎨* 🔗 ⚫️. + +!!! warning + 👀 👈 👫 🔢 🚶‍♀️ 🔗 *➡ 🛠️ 👨‍🎨*, 🚫 👆 *➡ 🛠️ 🔢*. + +## 📨 👔 📟 + +👆 💪 🔬 (🇺🇸🔍) `status_code` ⚙️ 📨 👆 *➡ 🛠️*. + +👆 💪 🚶‍♀️ 🔗 `int` 📟, 💖 `404`. + +✋️ 🚥 👆 🚫 💭 ⚫️❔ 🔠 🔢 📟, 👆 💪 ⚙️ ⌨ 📉 `status`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +👈 👔 📟 🔜 ⚙️ 📨 & 🔜 🚮 🗄 🔗. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette import status`. + + **FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## 🔖 + +👆 💪 🚮 🔖 👆 *➡ 🛠️*, 🚶‍♀️ 🔢 `tags` ⏮️ `list` `str` (🛎 1️⃣ `str`): + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +👫 🔜 🚮 🗄 🔗 & ⚙️ 🏧 🧾 🔢: + + + +### 🔖 ⏮️ 🔢 + +🚥 👆 ✔️ 🦏 🈸, 👆 5️⃣📆 🔚 🆙 📈 **📚 🔖**, & 👆 🔜 💚 ⚒ 💭 👆 🕧 ⚙️ **🎏 🔖** 🔗 *➡ 🛠️*. + +👫 💼, ⚫️ 💪 ⚒ 🔑 🏪 🔖 `Enum`. + +**FastAPI** 🐕‍🦺 👈 🎏 🌌 ⏮️ ✅ 🎻: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## 📄 & 📛 + +👆 💪 🚮 `summary` & `description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +## 📛 ⚪️➡️ #️⃣ + +📛 😑 📏 & 📔 💗 ⏸, 👆 💪 📣 *➡ 🛠️* 📛 🔢 #️⃣ & **FastAPI** 🔜 ✍ ⚫️ ⚪️➡️ 📤. + +👆 💪 ✍ #️⃣ , ⚫️ 🔜 🔬 & 🖥 ☑ (✊ 🔘 🏧 #️⃣ 📐). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +⚫️ 🔜 ⚙️ 🎓 🩺: + + + +## 📨 📛 + +👆 💪 ✔ 📨 📛 ⏮️ 🔢 `response_description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +!!! info + 👀 👈 `response_description` 🔗 🎯 📨, `description` 🔗 *➡ 🛠️* 🏢. + +!!! check + 🗄 ✔ 👈 🔠 *➡ 🛠️* 🚚 📨 📛. + + , 🚥 👆 🚫 🚚 1️⃣, **FastAPI** 🔜 🔁 🏗 1️⃣ "🏆 📨". + + + +## 😢 *➡ 🛠️* + +🚥 👆 💪 ™ *➡ 🛠️* 😢, ✋️ 🍵 ❎ ⚫️, 🚶‍♀️ 🔢 `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +⚫️ 🔜 🎯 ™ 😢 🎓 🩺: + + + +✅ ❔ 😢 & 🚫-😢 *➡ 🛠️* 👀 💖: + + + +## 🌃 + +👆 💪 🔗 & 🚮 🗃 👆 *➡ 🛠️* 💪 🚶‍♀️ 🔢 *➡ 🛠️ 👨‍🎨*. diff --git a/docs/em/docs/tutorial/path-params-numeric-validations.md b/docs/em/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..b1ba2670b --- /dev/null +++ b/docs/em/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,138 @@ +# ➡ 🔢 & 🔢 🔬 + +🎏 🌌 👈 👆 💪 📣 🌅 🔬 & 🗃 🔢 🔢 ⏮️ `Query`, 👆 💪 📣 🎏 🆎 🔬 & 🗃 ➡ 🔢 ⏮️ `Path`. + +## 🗄 ➡ + +🥇, 🗄 `Path` ⚪️➡️ `fastapi`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +## 📣 🗃 + +👆 💪 📣 🌐 🎏 🔢 `Query`. + +🖼, 📣 `title` 🗃 💲 ➡ 🔢 `item_id` 👆 💪 🆎: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +!!! note + ➡ 🔢 🕧 ✔ ⚫️ ✔️ 🍕 ➡. + + , 👆 🔜 📣 ⚫️ ⏮️ `...` ™ ⚫️ ✔. + + 👐, 🚥 👆 📣 ⚫️ ⏮️ `None` ⚖️ ⚒ 🔢 💲, ⚫️ 🔜 🚫 📉 🕳, ⚫️ 🔜 🕧 🚚. + +## ✔ 🔢 👆 💪 + +➡️ 💬 👈 👆 💚 📣 🔢 🔢 `q` ✔ `str`. + +& 👆 🚫 💪 📣 🕳 🙆 👈 🔢, 👆 🚫 🤙 💪 ⚙️ `Query`. + +✋️ 👆 💪 ⚙️ `Path` `item_id` ➡ 🔢. + +🐍 🔜 😭 🚥 👆 🚮 💲 ⏮️ "🔢" ⏭ 💲 👈 🚫 ✔️ "🔢". + +✋️ 👆 💪 🏤-✔ 👫, & ✔️ 💲 🍵 🔢 (🔢 🔢 `q`) 🥇. + +⚫️ 🚫 🤔 **FastAPI**. ⚫️ 🔜 🔍 🔢 👫 📛, 🆎 & 🔢 📄 (`Query`, `Path`, ♒️), ⚫️ 🚫 💅 🔃 ✔. + +, 👆 💪 📣 👆 🔢: + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## ✔ 🔢 👆 💪, 🎱 + +🚥 👆 💚 📣 `q` 🔢 🔢 🍵 `Query` 🚫 🙆 🔢 💲, & ➡ 🔢 `item_id` ⚙️ `Path`, & ✔️ 👫 🎏 ✔, 🐍 ✔️ 🐥 🎁 ❕ 👈. + +🚶‍♀️ `*`, 🥇 🔢 🔢. + +🐍 🏆 🚫 🕳 ⏮️ 👈 `*`, ✋️ ⚫️ 🔜 💭 👈 🌐 📄 🔢 🔜 🤙 🇨🇻 ❌ (🔑-💲 👫), 💭 kwargs. 🚥 👫 🚫 ✔️ 🔢 💲. + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## 🔢 🔬: 👑 🌘 ⚖️ 🌓 + +⏮️ `Query` & `Path` (& 🎏 👆 🔜 👀 ⏪) 👆 💪 📣 🔢 ⚛. + +📥, ⏮️ `ge=1`, `item_id` 🔜 💪 🔢 🔢 "`g`🅾 🌘 ⚖️ `e`🅾" `1`. + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## 🔢 🔬: 🌘 🌘 & 🌘 🌘 ⚖️ 🌓 + +🎏 ✔: + +* `gt`: `g`🅾 `t`👲 +* `le`: `l`👭 🌘 ⚖️ `e`🅾 + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## 🔢 🔬: 🎈, 🌘 🌘 & 🌘 🌘 + +🔢 🔬 👷 `float` 💲. + +📥 🌐❔ ⚫️ ▶️️ ⚠ 💪 📣 gt & 🚫 ge. ⏮️ ⚫️ 👆 💪 🚚, 🖼, 👈 💲 🔜 👑 🌘 `0`, 🚥 ⚫️ 🌘 🌘 `1`. + +, `0.5` 🔜 ☑ 💲. ✋️ `0.0` ⚖️ `0` 🔜 🚫. + +& 🎏 lt. + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## 🌃 + +⏮️ `Query`, `Path` (& 🎏 👆 🚫 👀) 👆 💪 📣 🗃 & 🎻 🔬 🎏 🌌 ⏮️ [🔢 🔢 & 🎻 🔬](query-params-str-validations.md){.internal-link target=_blank}. + +& 👆 💪 📣 🔢 🔬: + +* `gt`: `g`🅾 `t`👲 +* `ge`: `g`🅾 🌘 ⚖️ `e`🅾 +* `lt`: `l`👭 `t`👲 +* `le`: `l`👭 🌘 ⚖️ `e`🅾 + +!!! info + `Query`, `Path`, & 🎏 🎓 👆 🔜 👀 ⏪ 🏿 ⚠ `Param` 🎓. + + 🌐 👫 💰 🎏 🔢 🌖 🔬 & 🗃 👆 ✔️ 👀. + +!!! note "📡 ℹ" + 🕐❔ 👆 🗄 `Query`, `Path` & 🎏 ⚪️➡️ `fastapi`, 👫 🤙 🔢. + + 👈 🕐❔ 🤙, 📨 👐 🎓 🎏 📛. + + , 👆 🗄 `Query`, ❔ 🔢. & 🕐❔ 👆 🤙 ⚫️, ⚫️ 📨 👐 🎓 🌟 `Query`. + + 👫 🔢 📤 (↩️ ⚙️ 🎓 🔗) 👈 👆 👨‍🎨 🚫 ™ ❌ 🔃 👫 🆎. + + 👈 🌌 👆 💪 ⚙️ 👆 😐 👨‍🎨 & 🛠️ 🧰 🍵 ✔️ 🚮 🛃 📳 🤷‍♂ 📚 ❌. diff --git a/docs/em/docs/tutorial/path-params.md b/docs/em/docs/tutorial/path-params.md new file mode 100644 index 000000000..ea939b458 --- /dev/null +++ b/docs/em/docs/tutorial/path-params.md @@ -0,0 +1,252 @@ +# ➡ 🔢 + +👆 💪 📣 ➡ "🔢" ⚖️ "🔢" ⏮️ 🎏 ❕ ⚙️ 🐍 📁 🎻: + +```Python hl_lines="6-7" +{!../../../docs_src/path_params/tutorial001.py!} +``` + +💲 ➡ 🔢 `item_id` 🔜 🚶‍♀️ 👆 🔢 ❌ `item_id`. + +, 🚥 👆 🏃 👉 🖼 & 🚶 http://127.0.0.1:8000/items/foo, 👆 🔜 👀 📨: + +```JSON +{"item_id":"foo"} +``` + +## ➡ 🔢 ⏮️ 🆎 + +👆 💪 📣 🆎 ➡ 🔢 🔢, ⚙️ 🐩 🐍 🆎 ✍: + +```Python hl_lines="7" +{!../../../docs_src/path_params/tutorial002.py!} +``` + +👉 💼, `item_id` 📣 `int`. + +!!! check + 👉 🔜 🤝 👆 👨‍🎨 🐕‍🦺 🔘 👆 🔢, ⏮️ ❌ ✅, 🛠️, ♒️. + +## 💽 🛠️ + +🚥 👆 🏃 👉 🖼 & 📂 👆 🖥 http://127.0.0.1:8000/items/3, 👆 🔜 👀 📨: + +```JSON +{"item_id":3} +``` + +!!! check + 👀 👈 💲 👆 🔢 📨 (& 📨) `3`, 🐍 `int`, 🚫 🎻 `"3"`. + + , ⏮️ 👈 🆎 📄, **FastAPI** 🤝 👆 🏧 📨 "✍". + +## 💽 🔬 + +✋️ 🚥 👆 🚶 🖥 http://127.0.0.1:8000/items/foo, 👆 🔜 👀 👌 🇺🇸🔍 ❌: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +↩️ ➡ 🔢 `item_id` ✔️ 💲 `"foo"`, ❔ 🚫 `int`. + +🎏 ❌ 🔜 😑 🚥 👆 🚚 `float` ↩️ `int`,: http://127.0.0.1:8000/items/4.2 + +!!! check + , ⏮️ 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 💽 🔬. + + 👀 👈 ❌ 🎯 🇵🇸 ⚫️❔ ☝ 🌐❔ 🔬 🚫 🚶‍♀️. + + 👉 🙃 👍 ⏪ 🛠️ & 🛠️ 📟 👈 🔗 ⏮️ 👆 🛠️. + +## 🧾 + +& 🕐❔ 👆 📂 👆 🖥 http://127.0.0.1:8000/docs, 👆 🔜 👀 🏧, 🎓, 🛠️ 🧾 💖: + + + +!!! check + 🔄, ⏮️ 👈 🎏 🐍 🆎 📄, **FastAPI** 🤝 👆 🏧, 🎓 🧾 (🛠️ 🦁 🎚). + + 👀 👈 ➡ 🔢 📣 🔢. + +## 🐩-⚓️ 💰, 🎛 🧾 + +& ↩️ 🏗 🔗 ⚪️➡️ 🗄 🐩, 📤 📚 🔗 🧰. + +↩️ 👉, **FastAPI** ⚫️ 🚚 🎛 🛠️ 🧾 (⚙️ 📄), ❔ 👆 💪 🔐 http://127.0.0.1:8000/redoc: + + + +🎏 🌌, 📤 📚 🔗 🧰. ✅ 📟 ⚡ 🧰 📚 🇪🇸. + +## Pydantic + +🌐 💽 🔬 🎭 🔽 🚘 Pydantic, 👆 🤚 🌐 💰 ⚪️➡️ ⚫️. & 👆 💭 👆 👍 ✋. + +👆 💪 ⚙️ 🎏 🆎 📄 ⏮️ `str`, `float`, `bool` & 📚 🎏 🏗 📊 🆎. + +📚 👫 🔬 ⏭ 📃 🔰. + +## ✔ 🤔 + +🕐❔ 🏗 *➡ 🛠️*, 👆 💪 🔎 ⚠ 🌐❔ 👆 ✔️ 🔧 ➡. + +💖 `/users/me`, ➡️ 💬 👈 ⚫️ 🤚 📊 🔃 ⏮️ 👩‍💻. + +& ⤴️ 👆 💪 ✔️ ➡ `/users/{user_id}` 🤚 💽 🔃 🎯 👩‍💻 👩‍💻 🆔. + +↩️ *➡ 🛠️* 🔬 ✔, 👆 💪 ⚒ 💭 👈 ➡ `/users/me` 📣 ⏭ 1️⃣ `/users/{user_id}`: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003.py!} +``` + +⏪, ➡ `/users/{user_id}` 🔜 🏏 `/users/me`, "💭" 👈 ⚫️ 📨 🔢 `user_id` ⏮️ 💲 `"me"`. + +➡, 👆 🚫🔜 ↔ ➡ 🛠️: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003b.py!} +``` + +🥇 🕐 🔜 🕧 ⚙️ ↩️ ➡ 🏏 🥇. + +## 🔁 💲 + +🚥 👆 ✔️ *➡ 🛠️* 👈 📨 *➡ 🔢*, ✋️ 👆 💚 💪 ☑ *➡ 🔢* 💲 🔁, 👆 💪 ⚙️ 🐩 🐍 `Enum`. + +### ✍ `Enum` 🎓 + +🗄 `Enum` & ✍ 🎧-🎓 👈 😖 ⚪️➡️ `str` & ⚪️➡️ `Enum`. + +😖 ⚪️➡️ `str` 🛠️ 🩺 🔜 💪 💭 👈 💲 🔜 🆎 `string` & 🔜 💪 ✍ ☑. + +⤴️ ✍ 🎓 🔢 ⏮️ 🔧 💲, ❔ 🔜 💪 ☑ 💲: + +```Python hl_lines="1 6-9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info + 🔢 (⚖️ 🔢) 💪 🐍 ↩️ ⏬ 3️⃣.4️⃣. + +!!! tip + 🚥 👆 💭, "📊", "🎓", & "🍏" 📛 🎰 🏫 🏷. + +### 📣 *➡ 🔢* + +⤴️ ✍ *➡ 🔢* ⏮️ 🆎 ✍ ⚙️ 🔢 🎓 👆 ✍ (`ModelName`): + +```Python hl_lines="16" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +### ✅ 🩺 + +↩️ 💪 💲 *➡ 🔢* 🔢, 🎓 🩺 💪 🎦 👫 🎆: + + + +### 👷 ⏮️ 🐍 *🔢* + +💲 *➡ 🔢* 🔜 *🔢 👨‍🎓*. + +#### 🔬 *🔢 👨‍🎓* + +👆 💪 🔬 ⚫️ ⏮️ *🔢 👨‍🎓* 👆 ✍ 🔢 `ModelName`: + +```Python hl_lines="17" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +#### 🤚 *🔢 💲* + +👆 💪 🤚 ☑ 💲 ( `str` 👉 💼) ⚙️ `model_name.value`, ⚖️ 🏢, `your_enum_member.value`: + +```Python hl_lines="20" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip + 👆 💪 🔐 💲 `"lenet"` ⏮️ `ModelName.lenet.value`. + +#### 📨 *🔢 👨‍🎓* + +👆 💪 📨 *🔢 👨‍🎓* ⚪️➡️ 👆 *➡ 🛠️*, 🐦 🎻 💪 (✅ `dict`). + +👫 🔜 🗜 👫 🔗 💲 (🎻 👉 💼) ⏭ 🛬 👫 👩‍💻: + +```Python hl_lines="18 21 23" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +👆 👩‍💻 👆 🔜 🤚 🎻 📨 💖: + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + +## ➡ 🔢 ⚗ ➡ + +➡️ 💬 👆 ✔️ *➡ 🛠️* ⏮️ ➡ `/files/{file_path}`. + +✋️ 👆 💪 `file_path` ⚫️ 🔌 *➡*, 💖 `home/johndoe/myfile.txt`. + +, 📛 👈 📁 🔜 🕳 💖: `/files/home/johndoe/myfile.txt`. + +### 🗄 🐕‍🦺 + +🗄 🚫 🐕‍🦺 🌌 📣 *➡ 🔢* 🔌 *➡* 🔘, 👈 💪 ↘️ 😐 👈 ⚠ 💯 & 🔬. + +👐, 👆 💪 ⚫️ **FastAPI**, ⚙️ 1️⃣ 🔗 🧰 ⚪️➡️ 💃. + +& 🩺 🔜 👷, 👐 🚫 ❎ 🙆 🧾 💬 👈 🔢 🔜 🔌 ➡. + +### ➡ 🔌 + +⚙️ 🎛 🔗 ⚪️➡️ 💃 👆 💪 📣 *➡ 🔢* ⚗ *➡* ⚙️ 📛 💖: + +``` +/files/{file_path:path} +``` + +👉 💼, 📛 🔢 `file_path`, & 🏁 🍕, `:path`, 💬 ⚫️ 👈 🔢 🔜 🏏 🙆 *➡*. + +, 👆 💪 ⚙️ ⚫️ ⏮️: + +```Python hl_lines="6" +{!../../../docs_src/path_params/tutorial004.py!} +``` + +!!! tip + 👆 💪 💪 🔢 🔌 `/home/johndoe/myfile.txt`, ⏮️ 🏁 🔪 (`/`). + + 👈 💼, 📛 🔜: `/files//home/johndoe/myfile.txt`, ⏮️ 2️⃣✖️ 🔪 (`//`) 🖖 `files` & `home`. + +## 🌃 + +⏮️ **FastAPI**, ⚙️ 📏, 🏋️ & 🐩 🐍 🆎 📄, 👆 🤚: + +* 👨‍🎨 🐕‍🦺: ❌ ✅, ✍, ♒️. +* 💽 "" +* 💽 🔬 +* 🛠️ ✍ & 🏧 🧾 + +& 👆 🕴 ✔️ 📣 👫 🕐. + +👈 🎲 👑 ⭐ 📈 **FastAPI** 🔬 🎛 🛠️ (↖️ ⚪️➡️ 🍣 🎭). diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..d6b67bd51 --- /dev/null +++ b/docs/em/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,467 @@ +# 🔢 🔢 & 🎻 🔬 + +**FastAPI** ✔ 👆 📣 🌖 ℹ & 🔬 👆 🔢. + +➡️ ✊ 👉 🈸 🖼: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +🔢 🔢 `q` 🆎 `Union[str, None]` (⚖️ `str | None` 🐍 3️⃣.1️⃣0️⃣), 👈 ⛓ 👈 ⚫️ 🆎 `str` ✋️ 💪 `None`, & 👐, 🔢 💲 `None`, FastAPI 🔜 💭 ⚫️ 🚫 ✔. + +!!! note + FastAPI 🔜 💭 👈 💲 `q` 🚫 ✔ ↩️ 🔢 💲 `= None`. + + `Union` `Union[str, None]` 🔜 ✔ 👆 👨‍🎨 🤝 👆 👍 🐕‍🦺 & 🔍 ❌. + +## 🌖 🔬 + +👥 🔜 🛠️ 👈 ✋️ `q` 📦, 🕐❔ ⚫️ 🚚, **🚮 📐 🚫 📉 5️⃣0️⃣ 🦹**. + +### 🗄 `Query` + +🏆 👈, 🥇 🗄 `Query` ⚪️➡️ `fastapi`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +## ⚙️ `Query` 🔢 💲 + +& 🔜 ⚙️ ⚫️ 🔢 💲 👆 🔢, ⚒ 🔢 `max_length` 5️⃣0️⃣: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +👥 ✔️ ❎ 🔢 💲 `None` 🔢 ⏮️ `Query()`, 👥 💪 🔜 ⚒ 🔢 💲 ⏮️ 🔢 `Query(default=None)`, ⚫️ 🍦 🎏 🎯 ⚖ 👈 🔢 💲. + +: + +```Python +q: Union[str, None] = Query(default=None) +``` + +...⚒ 🔢 📦, 🎏: + +```Python +q: Union[str, None] = None +``` + +& 🐍 3️⃣.1️⃣0️⃣ & 🔛: + +```Python +q: str | None = Query(default=None) +``` + +...⚒ 🔢 📦, 🎏: + +```Python +q: str | None = None +``` + +✋️ ⚫️ 📣 ⚫️ 🎯 💆‍♂ 🔢 🔢. + +!!! info + ✔️ 🤯 👈 🌅 ⚠ 🍕 ⚒ 🔢 📦 🍕: + + ```Python + = None + ``` + + ⚖️: + + ```Python + = Query(default=None) + ``` + + ⚫️ 🔜 ⚙️ 👈 `None` 🔢 💲, & 👈 🌌 ⚒ 🔢 **🚫 ✔**. + + `Union[str, None]` 🍕 ✔ 👆 👨‍🎨 🚚 👻 🐕‍🦺, ✋️ ⚫️ 🚫 ⚫️❔ 💬 FastAPI 👈 👉 🔢 🚫 ✔. + +⤴️, 👥 💪 🚶‍♀️ 🌅 🔢 `Query`. 👉 💼, `max_length` 🔢 👈 ✔ 🎻: + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +👉 🔜 ✔ 📊, 🎦 🆑 ❌ 🕐❔ 📊 🚫 ☑, & 📄 🔢 🗄 🔗 *➡ 🛠️*. + +## 🚮 🌅 🔬 + +👆 💪 🚮 🔢 `min_length`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` + +## 🚮 🥔 🧬 + +👆 💪 🔬 🥔 🧬 👈 🔢 🔜 🏏: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` + +👉 🎯 🥔 🧬 ✅ 👈 📨 🔢 💲: + +* `^`: ▶️ ⏮️ 📄 🦹, 🚫 ✔️ 🦹 ⏭. +* `fixedquery`: ✔️ ☑ 💲 `fixedquery`. +* `$`: 🔚 📤, 🚫 ✔️ 🙆 🌖 🦹 ⏮️ `fixedquery`. + +🚥 👆 💭 💸 ⏮️ 🌐 👉 **"🥔 🧬"** 💭, 🚫 😟. 👫 🏋️ ❔ 📚 👫👫. 👆 💪 📚 💩 🍵 💆‍♂ 🥔 🧬. + +✋️ 🕐❔ 👆 💪 👫 & 🚶 & 💡 👫, 💭 👈 👆 💪 ⏪ ⚙️ 👫 🔗 **FastAPI**. + +## 🔢 💲 + +🎏 🌌 👈 👆 💪 🚶‍♀️ `None` 💲 `default` 🔢, 👆 💪 🚶‍♀️ 🎏 💲. + +➡️ 💬 👈 👆 💚 📣 `q` 🔢 🔢 ✔️ `min_length` `3`, & ✔️ 🔢 💲 `"fixedquery"`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note + ✔️ 🔢 💲 ⚒ 🔢 📦. + +## ⚒ ⚫️ ✔ + +🕐❔ 👥 🚫 💪 📣 🌅 🔬 ⚖️ 🗃, 👥 💪 ⚒ `q` 🔢 🔢 ✔ 🚫 📣 🔢 💲, 💖: + +```Python +q: str +``` + +↩️: + +```Python +q: Union[str, None] = None +``` + +✋️ 👥 🔜 📣 ⚫️ ⏮️ `Query`, 🖼 💖: + +```Python +q: Union[str, None] = Query(default=None, min_length=3) +``` + +, 🕐❔ 👆 💪 📣 💲 ✔ ⏪ ⚙️ `Query`, 👆 💪 🎯 🚫 📣 🔢 💲: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +### ✔ ⏮️ ❕ (`...`) + +📤 🎛 🌌 🎯 📣 👈 💲 ✔. 👆 💪 ⚒ `default` 🔢 🔑 💲 `...`: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006b.py!} +``` + +!!! info + 🚥 👆 🚫 👀 👈 `...` ⏭: ⚫️ 🎁 👁 💲, ⚫️ 🍕 🐍 & 🤙 "❕". + + ⚫️ ⚙️ Pydantic & FastAPI 🎯 📣 👈 💲 ✔. + +👉 🔜 ➡️ **FastAPI** 💭 👈 👉 🔢 ✔. + +### ✔ ⏮️ `None` + +👆 💪 📣 👈 🔢 💪 🚫 `None`, ✋️ 👈 ⚫️ ✔. 👉 🔜 ⚡ 👩‍💻 📨 💲, 🚥 💲 `None`. + +👈, 👆 💪 📣 👈 `None` ☑ 🆎 ✋️ ⚙️ `default=...`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} + ``` + +!!! tip + Pydantic, ❔ ⚫️❔ 🏋️ 🌐 💽 🔬 & 🛠️ FastAPI, ✔️ 🎁 🎭 🕐❔ 👆 ⚙️ `Optional` ⚖️ `Union[Something, None]` 🍵 🔢 💲, 👆 💪 ✍ 🌅 🔃 ⚫️ Pydantic 🩺 🔃 ✔ 📦 🏑. + +### ⚙️ Pydantic `Required` ↩️ ❕ (`...`) + +🚥 👆 💭 😬 ⚙️ `...`, 👆 💪 🗄 & ⚙️ `Required` ⚪️➡️ Pydantic: + +```Python hl_lines="2 8" +{!../../../docs_src/query_params_str_validations/tutorial006d.py!} +``` + +!!! tip + 💭 👈 🌅 💼, 🕐❔ 🕳 🚚, 👆 💪 🎯 🚫 `default` 🔢, 👆 🛎 🚫 ✔️ ⚙️ `...` 🚫 `Required`. + +## 🔢 🔢 📇 / 💗 💲 + +🕐❔ 👆 🔬 🔢 🔢 🎯 ⏮️ `Query` 👆 💪 📣 ⚫️ 📨 📇 💲, ⚖️ 🙆‍♀ 🎏 🌌, 📨 💗 💲. + +🖼, 📣 🔢 🔢 `q` 👈 💪 😑 💗 🕰 📛, 👆 💪 ✍: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` + +⤴️, ⏮️ 📛 💖: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +👆 🔜 📨 💗 `q` *🔢 🔢'* 💲 (`foo` & `bar`) 🐍 `list` 🔘 👆 *➡ 🛠️ 🔢*, *🔢 🔢* `q`. + +, 📨 👈 📛 🔜: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip + 📣 🔢 🔢 ⏮️ 🆎 `list`, 💖 🖼 🔛, 👆 💪 🎯 ⚙️ `Query`, ⏪ ⚫️ 🔜 🔬 📨 💪. + +🎓 🛠️ 🩺 🔜 ℹ ➡️, ✔ 💗 💲: + + + +### 🔢 🔢 📇 / 💗 💲 ⏮️ 🔢 + +& 👆 💪 🔬 🔢 `list` 💲 🚥 👌 🚚: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` + +🚥 👆 🚶: + +``` +http://localhost:8000/items/ +``` + +🔢 `q` 🔜: `["foo", "bar"]` & 👆 📨 🔜: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### ⚙️ `list` + +👆 💪 ⚙️ `list` 🔗 ↩️ `List[str]` (⚖️ `list[str]` 🐍 3️⃣.9️⃣ ➕): + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note + ✔️ 🤯 👈 👉 💼, FastAPI 🏆 🚫 ✅ 🎚 📇. + + 🖼, `List[int]` 🔜 ✅ (& 📄) 👈 🎚 📇 🔢. ✋️ `list` 😞 🚫🔜. + +## 📣 🌅 🗃 + +👆 💪 🚮 🌅 ℹ 🔃 🔢. + +👈 ℹ 🔜 🔌 🏗 🗄 & ⚙️ 🧾 👩‍💻 🔢 & 🔢 🧰. + +!!! note + ✔️ 🤯 👈 🎏 🧰 5️⃣📆 ✔️ 🎏 🎚 🗄 🐕‍🦺. + + 👫 💪 🚫 🎦 🌐 ➕ ℹ 📣, 👐 🌅 💼, ❌ ⚒ ⏪ 📄 🛠️. + +👆 💪 🚮 `title`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` + +& `description`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` + +## 📛 🔢 + +🌈 👈 👆 💚 🔢 `item-query`. + +💖: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +✋️ `item-query` 🚫 ☑ 🐍 🔢 📛. + +🔐 🔜 `item_query`. + +✋️ 👆 💪 ⚫️ ⚫️❔ `item-query`... + +⤴️ 👆 💪 📣 `alias`, & 👈 📛 ⚫️❔ 🔜 ⚙️ 🔎 🔢 💲: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` + +## 😛 🔢 + +🔜 ➡️ 💬 👆 🚫 💖 👉 🔢 🚫🔜. + +👆 ✔️ 👈 ⚫️ 📤 ⏪ ↩️ 📤 👩‍💻 ⚙️ ⚫️, ✋️ 👆 💚 🩺 🎯 🎦 ⚫️ 😢. + +⤴️ 🚶‍♀️ 🔢 `deprecated=True` `Query`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` + +🩺 🔜 🎦 ⚫️ 💖 👉: + + + +## 🚫 ⚪️➡️ 🗄 + +🚫 🔢 🔢 ⚪️➡️ 🏗 🗄 🔗 (& ➡️, ⚪️➡️ 🏧 🧾 ⚙️), ⚒ 🔢 `include_in_schema` `Query` `False`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + +## 🌃 + +👆 💪 📣 🌖 🔬 & 🗃 👆 🔢. + +💊 🔬 & 🗃: + +* `alias` +* `title` +* `description` +* `deprecated` + +🔬 🎯 🎻: + +* `min_length` +* `max_length` +* `regex` + +👫 🖼 👆 👀 ❔ 📣 🔬 `str` 💲. + +👀 ⏭ 📃 👀 ❔ 📣 🔬 🎏 🆎, 💖 🔢. diff --git a/docs/em/docs/tutorial/query-params.md b/docs/em/docs/tutorial/query-params.md new file mode 100644 index 000000000..ccb235c15 --- /dev/null +++ b/docs/em/docs/tutorial/query-params.md @@ -0,0 +1,225 @@ +# 🔢 🔢 + +🕐❔ 👆 📣 🎏 🔢 🔢 👈 🚫 🍕 ➡ 🔢, 👫 🔁 🔬 "🔢" 🔢. + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +🔢 ⚒ 🔑-💲 👫 👈 🚶 ⏮️ `?` 📛, 🎏 `&` 🦹. + +🖼, 📛: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...🔢 🔢: + +* `skip`: ⏮️ 💲 `0` +* `limit`: ⏮️ 💲 `10` + +👫 🍕 📛, 👫 "🛎" 🎻. + +✋️ 🕐❔ 👆 📣 👫 ⏮️ 🐍 🆎 (🖼 🔛, `int`), 👫 🗜 👈 🆎 & ✔ 🛡 ⚫️. + +🌐 🎏 🛠️ 👈 ⚖ ➡ 🔢 ✔ 🔢 🔢: + +* 👨‍🎨 🐕‍🦺 (🎲) +* 💽 "✍" +* 💽 🔬 +* 🏧 🧾 + +## 🔢 + +🔢 🔢 🚫 🔧 🍕 ➡, 👫 💪 📦 & 💪 ✔️ 🔢 💲. + +🖼 🔛 👫 ✔️ 🔢 💲 `skip=0` & `limit=10`. + +, 🔜 📛: + +``` +http://127.0.0.1:8000/items/ +``` + +🔜 🎏 🔜: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +✋️ 🚥 👆 🚶, 🖼: + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +🔢 💲 👆 🔢 🔜: + +* `skip=20`: ↩️ 👆 ⚒ ⚫️ 📛 +* `limit=10`: ↩️ 👈 🔢 💲 + +## 📦 🔢 + +🎏 🌌, 👆 💪 📣 📦 🔢 🔢, ⚒ 👫 🔢 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +👉 💼, 🔢 🔢 `q` 🔜 📦, & 🔜 `None` 🔢. + +!!! check + 👀 👈 **FastAPI** 🙃 🥃 👀 👈 ➡ 🔢 `item_id` ➡ 🔢 & `q` 🚫,, ⚫️ 🔢 🔢. + +## 🔢 🔢 🆎 🛠️ + +👆 💪 📣 `bool` 🆎, & 👫 🔜 🗜: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` + +👉 💼, 🚥 👆 🚶: + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +⚖️ + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +⚖️ + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +⚖️ + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +⚖️ + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +⚖️ 🙆 🎏 💼 📈 (🔠, 🥇 🔤 🔠, ♒️), 👆 🔢 🔜 👀 🔢 `short` ⏮️ `bool` 💲 `True`. ⏪ `False`. + + +## 💗 ➡ & 🔢 🔢 + +👆 💪 📣 💗 ➡ 🔢 & 🔢 🔢 🎏 🕰, **FastAPI** 💭 ❔ ❔. + +& 👆 🚫 ✔️ 📣 👫 🙆 🎯 ✔. + +👫 🔜 🔬 📛: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +## ✔ 🔢 🔢 + +🕐❔ 👆 📣 🔢 💲 🚫-➡ 🔢 (🔜, 👥 ✔️ 🕴 👀 🔢 🔢), ⤴️ ⚫️ 🚫 ✔. + +🚥 👆 🚫 💚 🚮 🎯 💲 ✋️ ⚒ ⚫️ 📦, ⚒ 🔢 `None`. + +✋️ 🕐❔ 👆 💚 ⚒ 🔢 🔢 ✔, 👆 💪 🚫 📣 🙆 🔢 💲: + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +📥 🔢 🔢 `needy` ✔ 🔢 🔢 🆎 `str`. + +🚥 👆 📂 👆 🖥 📛 💖: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +...🍵 ❎ ✔ 🔢 `needy`, 👆 🔜 👀 ❌ 💖: + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +`needy` 🚚 🔢, 👆 🔜 💪 ⚒ ⚫️ 📛: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...👉 🔜 👷: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +& ↗️, 👆 💪 🔬 🔢 ✔, ✔️ 🔢 💲, & 🍕 📦: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +👉 💼, 📤 3️⃣ 🔢 🔢: + +* `needy`, ✔ `str`. +* `skip`, `int` ⏮️ 🔢 💲 `0`. +* `limit`, 📦 `int`. + +!!! tip + 👆 💪 ⚙️ `Enum`Ⓜ 🎏 🌌 ⏮️ [➡ 🔢](path-params.md#predefined-values){.internal-link target=_blank}. diff --git a/docs/em/docs/tutorial/request-files.md b/docs/em/docs/tutorial/request-files.md new file mode 100644 index 000000000..26631823f --- /dev/null +++ b/docs/em/docs/tutorial/request-files.md @@ -0,0 +1,186 @@ +# 📨 📁 + +👆 💪 🔬 📁 📂 👩‍💻 ⚙️ `File`. + +!!! info + 📨 📂 📁, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + + 👉 ↩️ 📂 📁 📨 "📨 💽". + +## 🗄 `File` + +🗄 `File` & `UploadFile` ⚪️➡️ `fastapi`: + +```Python hl_lines="1" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +## 🔬 `File` 🔢 + +✍ 📁 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Form`: + +```Python hl_lines="7" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +!!! info + `File` 🎓 👈 😖 🔗 ⚪️➡️ `Form`. + + ✋️ 💭 👈 🕐❔ 👆 🗄 `Query`, `Path`, `File` & 🎏 ⚪️➡️ `fastapi`, 👈 🤙 🔢 👈 📨 🎁 🎓. + +!!! tip + 📣 📁 💪, 👆 💪 ⚙️ `File`, ↩️ ⏪ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. + +📁 🔜 📂 "📨 💽". + +🚥 👆 📣 🆎 👆 *➡ 🛠️ 🔢* 🔢 `bytes`, **FastAPI** 🔜 ✍ 📁 👆 & 👆 🔜 📨 🎚 `bytes`. + +✔️ 🤯 👈 👉 ⛓ 👈 🎂 🎚 🔜 🏪 💾. 👉 🔜 👷 👍 🤪 📁. + +✋️ 📤 📚 💼 ❔ 👆 💪 💰 ⚪️➡️ ⚙️ `UploadFile`. + +## 📁 🔢 ⏮️ `UploadFile` + +🔬 📁 🔢 ⏮️ 🆎 `UploadFile`: + +```Python hl_lines="12" +{!../../../docs_src/request_files/tutorial001.py!} +``` + +⚙️ `UploadFile` ✔️ 📚 📈 🤭 `bytes`: + +* 👆 🚫 ✔️ ⚙️ `File()` 🔢 💲 🔢. +* ⚫️ ⚙️ "🧵" 📁: + * 📁 🏪 💾 🆙 🔆 📐 📉, & ⏮️ 🚶‍♀️ 👉 📉 ⚫️ 🔜 🏪 💾. +* 👉 ⛓ 👈 ⚫️ 🔜 👷 👍 ⭕ 📁 💖 🖼, 📹, ⭕ 💱, ♒️. 🍵 😩 🌐 💾. +* 👆 💪 🤚 🗃 ⚪️➡️ 📂 📁. +* ⚫️ ✔️ 📁-💖 `async` 🔢. +* ⚫️ 🎦 ☑ 🐍 `SpooledTemporaryFile` 🎚 👈 👆 💪 🚶‍♀️ 🔗 🎏 🗃 👈 ⌛ 📁-💖 🎚. + +### `UploadFile` + +`UploadFile` ✔️ 📄 🔢: + +* `filename`: `str` ⏮️ ⏮️ 📁 📛 👈 📂 (✅ `myimage.jpg`). +* `content_type`: `str` ⏮️ 🎚 🆎 (📁 🆎 / 📻 🆎) (✅ `image/jpeg`). +* `file`: `SpooledTemporaryFile` ( 📁-💖 🎚). 👉 ☑ 🐍 📁 👈 👆 💪 🚶‍♀️ 🔗 🎏 🔢 ⚖️ 🗃 👈 ⌛ "📁-💖" 🎚. + +`UploadFile` ✔️ 📄 `async` 👩‍🔬. 👫 🌐 🤙 🔗 📁 👩‍🔬 🔘 (⚙️ 🔗 `SpooledTemporaryFile`). + +* `write(data)`: ✍ `data` (`str` ⚖️ `bytes`) 📁. +* `read(size)`: ✍ `size` (`int`) 🔢/🦹 📁. +* `seek(offset)`: 🚶 🔢 🧘 `offset` (`int`) 📁. + * 🤶 Ⓜ., `await myfile.seek(0)` 🔜 🚶 ▶️ 📁. + * 👉 ✴️ ⚠ 🚥 👆 🏃 `await myfile.read()` 🕐 & ⤴️ 💪 ✍ 🎚 🔄. +* `close()`: 🔐 📁. + +🌐 👫 👩‍🔬 `async` 👩‍🔬, 👆 💪 "⌛" 👫. + +🖼, 🔘 `async` *➡ 🛠️ 🔢* 👆 💪 🤚 🎚 ⏮️: + +```Python +contents = await myfile.read() +``` + +🚥 👆 🔘 😐 `def` *➡ 🛠️ 🔢*, 👆 💪 🔐 `UploadFile.file` 🔗, 🖼: + +```Python +contents = myfile.file.read() +``` + +!!! note "`async` 📡 ℹ" + 🕐❔ 👆 ⚙️ `async` 👩‍🔬, **FastAPI** 🏃 📁 👩‍🔬 🧵 & ⌛ 👫. + +!!! note "💃 📡 ℹ" + **FastAPI**'Ⓜ `UploadFile` 😖 🔗 ⚪️➡️ **💃**'Ⓜ `UploadFile`, ✋️ 🚮 💪 🍕 ⚒ ⚫️ 🔗 ⏮️ **Pydantic** & 🎏 🍕 FastAPI. + +## ⚫️❔ "📨 💽" + +🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. + +**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. + +!!! note "📡 ℹ" + 📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded` 🕐❔ ⚫️ 🚫 🔌 📁. + + ✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 🚥 👆 ⚙️ `File`, **FastAPI** 🔜 💭 ⚫️ ✔️ 🤚 📁 ⚪️➡️ ☑ 🍕 💪. + + 🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. + +!!! warning + 👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 📦 📁 📂 + +👆 💪 ⚒ 📁 📦 ⚙️ 🐩 🆎 ✍ & ⚒ 🔢 💲 `None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 14" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +## `UploadFile` ⏮️ 🌖 🗃 + +👆 💪 ⚙️ `File()` ⏮️ `UploadFile`, 🖼, ⚒ 🌖 🗃: + +```Python hl_lines="13" +{!../../../docs_src/request_files/tutorial001_03.py!} +``` + +## 💗 📁 📂 + +⚫️ 💪 📂 📚 📁 🎏 🕰. + +👫 🔜 👨‍💼 🎏 "📨 🏑" 📨 ⚙️ "📨 💽". + +⚙️ 👈, 📣 📇 `bytes` ⚖️ `UploadFile`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` + +👆 🔜 📨, 📣, `list` `bytes` ⚖️ `UploadFile`Ⓜ. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.responses import HTMLResponse`. + + **FastAPI** 🚚 🎏 `starlette.responses` `fastapi.responses` 🏪 👆, 👩‍💻. ✋️ 🌅 💪 📨 👟 🔗 ⚪️➡️ 💃. + +### 💗 📁 📂 ⏮️ 🌖 🗃 + +& 🎏 🌌 ⏭, 👆 💪 ⚙️ `File()` ⚒ 🌖 🔢, `UploadFile`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + +## 🌃 + +⚙️ `File`, `bytes`, & `UploadFile` 📣 📁 📂 📨, 📨 📨 💽. diff --git a/docs/em/docs/tutorial/request-forms-and-files.md b/docs/em/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..99aeca000 --- /dev/null +++ b/docs/em/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,35 @@ +# 📨 📨 & 📁 + +👆 💪 🔬 📁 & 📨 🏑 🎏 🕰 ⚙️ `File` & `Form`. + +!!! info + 📨 📂 📁 & /⚖️ 📨 📊, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + +## 🗄 `File` & `Form` + +```Python hl_lines="1" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +## 🔬 `File` & `Form` 🔢 + +✍ 📁 & 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: + +```Python hl_lines="8" +{!../../../docs_src/request_forms_and_files/tutorial001.py!} +``` + +📁 & 📨 🏑 🔜 📂 📨 📊 & 👆 🔜 📨 📁 & 📨 🏑. + +& 👆 💪 📣 📁 `bytes` & `UploadFile`. + +!!! warning + 👆 💪 📣 💗 `File` & `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `multipart/form-data` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 🌃 + +⚙️ `File` & `Form` 👯‍♂️ 🕐❔ 👆 💪 📨 💽 & 📁 🎏 📨. diff --git a/docs/em/docs/tutorial/request-forms.md b/docs/em/docs/tutorial/request-forms.md new file mode 100644 index 000000000..fa74adae5 --- /dev/null +++ b/docs/em/docs/tutorial/request-forms.md @@ -0,0 +1,58 @@ +# 📨 💽 + +🕐❔ 👆 💪 📨 📨 🏑 ↩️ 🎻, 👆 💪 ⚙️ `Form`. + +!!! info + ⚙️ 📨, 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + +## 🗄 `Form` + +🗄 `Form` ⚪️➡️ `fastapi`: + +```Python hl_lines="1" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +## 🔬 `Form` 🔢 + +✍ 📨 🔢 🎏 🌌 👆 🔜 `Body` ⚖️ `Query`: + +```Python hl_lines="7" +{!../../../docs_src/request_forms/tutorial001.py!} +``` + +🖼, 1️⃣ 🌌 Oauth2️⃣ 🔧 💪 ⚙️ (🤙 "🔐 💧") ⚫️ ✔ 📨 `username` & `password` 📨 🏑. + +🔌 🚚 🏑 ⚫️❔ 📛 `username` & `password`, & 📨 📨 🏑, 🚫 🎻. + +⏮️ `Form` 👆 💪 📣 🎏 📳 ⏮️ `Body` (& `Query`, `Path`, `Cookie`), 🔌 🔬, 🖼, 📛 (✅ `user-name` ↩️ `username`), ♒️. + +!!! info + `Form` 🎓 👈 😖 🔗 ⚪️➡️ `Body`. + +!!! tip + 📣 📨 💪, 👆 💪 ⚙️ `Form` 🎯, ↩️ 🍵 ⚫️ 🔢 🔜 🔬 🔢 🔢 ⚖️ 💪 (🎻) 🔢. + +## 🔃 "📨 🏑" + +🌌 🕸 📨 (`
`) 📨 💽 💽 🛎 ⚙️ "🎁" 🔢 👈 📊, ⚫️ 🎏 ⚪️➡️ 🎻. + +**FastAPI** 🔜 ⚒ 💭 ✍ 👈 📊 ⚪️➡️ ▶️️ 🥉 ↩️ 🎻. + +!!! note "📡 ℹ" + 📊 ⚪️➡️ 📨 🛎 🗜 ⚙️ "📻 🆎" `application/x-www-form-urlencoded`. + + ✋️ 🕐❔ 📨 🔌 📁, ⚫️ 🗜 `multipart/form-data`. 👆 🔜 ✍ 🔃 🚚 📁 ⏭ 📃. + + 🚥 👆 💚 ✍ 🌖 🔃 👉 🔢 & 📨 🏑, 👳 🏇 🕸 🩺 POST. + +!!! warning + 👆 💪 📣 💗 `Form` 🔢 *➡ 🛠️*, ✋️ 👆 💪 🚫 📣 `Body` 🏑 👈 👆 ⌛ 📨 🎻, 📨 🔜 ✔️ 💪 🗜 ⚙️ `application/x-www-form-urlencoded` ↩️ `application/json`. + + 👉 🚫 🚫 **FastAPI**, ⚫️ 🍕 🇺🇸🔍 🛠️. + +## 🌃 + +⚙️ `Form` 📣 📨 💽 🔢 🔢. diff --git a/docs/em/docs/tutorial/response-model.md b/docs/em/docs/tutorial/response-model.md new file mode 100644 index 000000000..6ea4413f8 --- /dev/null +++ b/docs/em/docs/tutorial/response-model.md @@ -0,0 +1,481 @@ +# 📨 🏷 - 📨 🆎 + +👆 💪 📣 🆎 ⚙️ 📨 ✍ *➡ 🛠️ 🔢* **📨 🆎**. + +👆 💪 ⚙️ **🆎 ✍** 🎏 🌌 👆 🔜 🔢 💽 🔢 **🔢**, 👆 💪 ⚙️ Pydantic 🏷, 📇, 📖, 📊 💲 💖 🔢, 🎻, ♒️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +FastAPI 🔜 ⚙️ 👉 📨 🆎: + +* **✔** 📨 💽. + * 🚥 💽 ❌ (✅ 👆 ❌ 🏑), ⚫️ ⛓ 👈 *👆* 📱 📟 💔, 🚫 🛬 ⚫️❔ ⚫️ 🔜, & ⚫️ 🔜 📨 💽 ❌ ↩️ 🛬 ❌ 💽. 👉 🌌 👆 & 👆 👩‍💻 💪 🎯 👈 👫 🔜 📨 💽 & 💽 💠 📈. +* 🚮 **🎻 🔗** 📨, 🗄 *➡ 🛠️*. + * 👉 🔜 ⚙️ **🏧 🩺**. + * ⚫️ 🔜 ⚙️ 🏧 👩‍💻 📟 ⚡ 🧰. + +✋️ 🏆 🥈: + +* ⚫️ 🔜 **📉 & ⛽** 🔢 📊 ⚫️❔ 🔬 📨 🆎. + * 👉 ✴️ ⚠ **💂‍♂**, 👥 🔜 👀 🌅 👈 🔛. + +## `response_model` 🔢 + +📤 💼 🌐❔ 👆 💪 ⚖️ 💚 📨 💽 👈 🚫 ⚫️❔ ⚫️❔ 🆎 📣. + +🖼, 👆 💪 💚 **📨 📖** ⚖️ 💽 🎚, ✋️ **📣 ⚫️ Pydantic 🏷**. 👉 🌌 Pydantic 🏷 🔜 🌐 💽 🧾, 🔬, ♒️. 🎚 👈 👆 📨 (✅ 📖 ⚖️ 💽 🎚). + +🚥 👆 🚮 📨 🆎 ✍, 🧰 & 👨‍🎨 🔜 😭 ⏮️ (☑) ❌ 💬 👆 👈 👆 🔢 🛬 🆎 (✅#️⃣) 👈 🎏 ⚪️➡️ ⚫️❔ 👆 📣 (✅ Pydantic 🏷). + +📚 💼, 👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` ↩️ 📨 🆎. + +👆 💪 ⚙️ `response_model` 🔢 🙆 *➡ 🛠️*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* ♒️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +!!! note + 👀 👈 `response_model` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. + +`response_model` 📨 🎏 🆎 👆 🔜 📣 Pydantic 🏷 🏑,, ⚫️ 💪 Pydantic 🏷, ✋️ ⚫️ 💪, ✅ `list` Pydantic 🏷, 💖 `List[Item]`. + +FastAPI 🔜 ⚙️ 👉 `response_model` 🌐 💽 🧾, 🔬, ♒️. & **🗜 & ⛽ 🔢 📊** 🚮 🆎 📄. + +!!! tip + 🚥 👆 ✔️ ⚠ 🆎 ✅ 👆 👨‍🎨, ✍, ♒️, 👆 💪 📣 🔢 📨 🆎 `Any`. + + 👈 🌌 👆 💬 👨‍🎨 👈 👆 😫 🛬 🕳. ✋️ FastAPI 🔜 💽 🧾, 🔬, 🖥, ♒️. ⏮️ `response_model`. + +### `response_model` 📫 + +🚥 👆 📣 👯‍♂️ 📨 🆎 & `response_model`, `response_model` 🔜 ✊ 📫 & ⚙️ FastAPI. + +👉 🌌 👆 💪 🚮 ☑ 🆎 ✍ 👆 🔢 🕐❔ 👆 🛬 🆎 🎏 🌘 📨 🏷, ⚙️ 👨‍🎨 & 🧰 💖 ✍. & 👆 💪 ✔️ FastAPI 💽 🔬, 🧾, ♒️. ⚙️ `response_model`. + +👆 💪 ⚙️ `response_model=None` ❎ 🏗 📨 🏷 👈 *➡ 🛠️*, 👆 5️⃣📆 💪 ⚫️ 🚥 👆 ❎ 🆎 ✍ 👜 👈 🚫 ☑ Pydantic 🏑, 👆 🔜 👀 🖼 👈 1️⃣ 📄 🔛. + +## 📨 🎏 🔢 💽 + +📥 👥 📣 `UserIn` 🏷, ⚫️ 🔜 🔌 🔢 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +!!! info + ⚙️ `EmailStr`, 🥇 ❎ `email_validator`. + + 🤶 Ⓜ. `pip install email-validator` + ⚖️ `pip install pydantic[email]`. + +& 👥 ⚙️ 👉 🏷 📣 👆 🔢 & 🎏 🏷 📣 👆 🔢: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +🔜, 🕐❔ 🖥 🏗 👩‍💻 ⏮️ 🔐, 🛠️ 🔜 📨 🎏 🔐 📨. + +👉 💼, ⚫️ 💪 🚫 ⚠, ↩️ ⚫️ 🎏 👩‍💻 📨 🔐. + +✋️ 🚥 👥 ⚙️ 🎏 🏷 ➕1️⃣ *➡ 🛠️*, 👥 💪 📨 👆 👩‍💻 🔐 🔠 👩‍💻. + +!!! danger + 🙅 🏪 ✅ 🔐 👩‍💻 ⚖️ 📨 ⚫️ 📨 💖 👉, 🚥 👆 💭 🌐 ⚠ & 👆 💭 ⚫️❔ 👆 🔨. + +## 🚮 🔢 🏷 + +👥 💪 ↩️ ✍ 🔢 🏷 ⏮️ 🔢 🔐 & 🔢 🏷 🍵 ⚫️: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +📥, ✋️ 👆 *➡ 🛠️ 🔢* 🛬 🎏 🔢 👩‍💻 👈 🔌 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +...👥 📣 `response_model` 👆 🏷 `UserOut`, 👈 🚫 🔌 🔐: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +, **FastAPI** 🔜 ✊ 💅 🖥 👅 🌐 💽 👈 🚫 📣 🔢 🏷 (⚙️ Pydantic). + +### `response_model` ⚖️ 📨 🆎 + +👉 💼, ↩️ 2️⃣ 🏷 🎏, 🚥 👥 ✍ 🔢 📨 🆎 `UserOut`, 👨‍🎨 & 🧰 🔜 😭 👈 👥 🛬 ❌ 🆎, 📚 🎏 🎓. + +👈 ⚫️❔ 👉 🖼 👥 ✔️ 📣 ⚫️ `response_model` 🔢. + +...✋️ 😣 👂 🔛 👀 ❔ ❎ 👈. + +## 📨 🆎 & 💽 🖥 + +➡️ 😣 ⚪️➡️ ⏮️ 🖼. 👥 💚 **✍ 🔢 ⏮️ 1️⃣ 🆎** ✋️ 📨 🕳 👈 🔌 **🌅 💽**. + +👥 💚 FastAPI 🚧 **🖥** 📊 ⚙️ 📨 🏷. + +⏮️ 🖼, ↩️ 🎓 🎏, 👥 ✔️ ⚙️ `response_model` 🔢. ✋️ 👈 ⛓ 👈 👥 🚫 🤚 🐕‍🦺 ⚪️➡️ 👨‍🎨 & 🧰 ✅ 🔢 📨 🆎. + +✋️ 🌅 💼 🌐❔ 👥 💪 🕳 💖 👉, 👥 💚 🏷 **⛽/❎** 📊 👉 🖼. + +& 👈 💼, 👥 💪 ⚙️ 🎓 & 🧬 ✊ 📈 🔢 **🆎 ✍** 🤚 👍 🐕‍🦺 👨‍🎨 & 🧰, & 🤚 FastAPI **💽 🖥**. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +⏮️ 👉, 👥 🤚 🏭 🐕‍🦺, ⚪️➡️ 👨‍🎨 & ✍ 👉 📟 ☑ ⚖ 🆎, ✋️ 👥 🤚 💽 🖥 ⚪️➡️ FastAPI. + +❔ 🔨 👉 👷 ❓ ➡️ ✅ 👈 👅. 👶 + +### 🆎 ✍ & 🏭 + +🥇 ➡️ 👀 ❔ 👨‍🎨, ✍ & 🎏 🧰 🔜 👀 👉. + +`BaseUser` ✔️ 🧢 🏑. ⤴️ `UserIn` 😖 ⚪️➡️ `BaseUser` & 🚮 `password` 🏑,, ⚫️ 🔜 🔌 🌐 🏑 ⚪️➡️ 👯‍♂️ 🏷. + +👥 ✍ 🔢 📨 🆎 `BaseUser`, ✋️ 👥 🤙 🛬 `UserIn` 👐. + +👨‍🎨, ✍, & 🎏 🧰 🏆 🚫 😭 🔃 👉 ↩️, ⌨ ⚖, `UserIn` 🏿 `BaseUser`, ❔ ⛓ ⚫️ *☑* 🆎 🕐❔ ⚫️❔ ⌛ 🕳 👈 `BaseUser`. + +### FastAPI 💽 🖥 + +🔜, FastAPI, ⚫️ 🔜 👀 📨 🆎 & ⚒ 💭 👈 ⚫️❔ 👆 📨 🔌 **🕴** 🏑 👈 📣 🆎. + +FastAPI 🔨 📚 👜 🔘 ⏮️ Pydantic ⚒ 💭 👈 📚 🎏 🚫 🎓 🧬 🚫 ⚙️ 📨 💽 🖥, ⏪ 👆 💪 🔚 🆙 🛬 🌅 🌅 💽 🌘 ⚫️❔ 👆 📈. + +👉 🌌, 👆 💪 🤚 🏆 👯‍♂️ 🌏: 🆎 ✍ ⏮️ **🏭 🐕‍🦺** & **💽 🖥**. + +## 👀 ⚫️ 🩺 + +🕐❔ 👆 👀 🏧 🩺, 👆 💪 ✅ 👈 🔢 🏷 & 🔢 🏷 🔜 👯‍♂️ ✔️ 👫 👍 🎻 🔗: + + + +& 👯‍♂️ 🏷 🔜 ⚙️ 🎓 🛠️ 🧾: + + + +## 🎏 📨 🆎 ✍ + +📤 5️⃣📆 💼 🌐❔ 👆 📨 🕳 👈 🚫 ☑ Pydantic 🏑 & 👆 ✍ ⚫️ 🔢, 🕴 🤚 🐕‍🦺 🚚 🏭 (👨‍🎨, ✍, ♒️). + +### 📨 📨 🔗 + +🏆 ⚠ 💼 🔜 [🛬 📨 🔗 🔬 ⏪ 🏧 🩺](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +👉 🙅 💼 🍵 🔁 FastAPI ↩️ 📨 🆎 ✍ 🎓 (⚖️ 🏿) `Response`. + +& 🧰 🔜 😄 ↩️ 👯‍♂️ `RedirectResponse` & `JSONResponse` 🏿 `Response`, 🆎 ✍ ☑. + +### ✍ 📨 🏿 + +👆 💪 ⚙️ 🏿 `Response` 🆎 ✍: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +👉 🔜 👷 ↩️ `RedirectResponse` 🏿 `Response`, & FastAPI 🔜 🔁 🍵 👉 🙅 💼. + +### ❌ 📨 🆎 ✍ + +✋️ 🕐❔ 👆 📨 🎏 ❌ 🎚 👈 🚫 ☑ Pydantic 🆎 (✅ 💽 🎚) & 👆 ✍ ⚫️ 💖 👈 🔢, FastAPI 🔜 🔄 ✍ Pydantic 📨 🏷 ⚪️➡️ 👈 🆎 ✍, & 🔜 ❌. + +🎏 🔜 🔨 🚥 👆 ✔️ 🕳 💖 🇪🇺 🖖 🎏 🆎 🌐❔ 1️⃣ ⚖️ 🌅 👫 🚫 ☑ Pydantic 🆎, 🖼 👉 🔜 ❌ 👶: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +...👉 ❌ ↩️ 🆎 ✍ 🚫 Pydantic 🆎 & 🚫 👁 `Response` 🎓 ⚖️ 🏿, ⚫️ 🇪🇺 (🙆 2️⃣) 🖖 `Response` & `dict`. + +### ❎ 📨 🏷 + +▶️ ⚪️➡️ 🖼 🔛, 👆 5️⃣📆 🚫 💚 ✔️ 🔢 💽 🔬, 🧾, 🖥, ♒️. 👈 🎭 FastAPI. + +✋️ 👆 💪 💚 🚧 📨 🆎 ✍ 🔢 🤚 🐕‍🦺 ⚪️➡️ 🧰 💖 👨‍🎨 & 🆎 ☑ (✅ ✍). + +👉 💼, 👆 💪 ❎ 📨 🏷 ⚡ ⚒ `response_model=None`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +👉 🔜 ⚒ FastAPI 🚶 📨 🏷 ⚡ & 👈 🌌 👆 💪 ✔️ 🙆 📨 🆎 ✍ 👆 💪 🍵 ⚫️ 🤕 👆 FastAPI 🈸. 👶 + +## 📨 🏷 🔢 🔢 + +👆 📨 🏷 💪 ✔️ 🔢 💲, 💖: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +* `description: Union[str, None] = None` (⚖️ `str | None = None` 🐍 3️⃣.1️⃣0️⃣) ✔️ 🔢 `None`. +* `tax: float = 10.5` ✔️ 🔢 `10.5`. +* `tags: List[str] = []` 🔢 🛁 📇: `[]`. + +✋️ 👆 💪 💚 🚫 👫 ⚪️➡️ 🏁 🚥 👫 🚫 🤙 🏪. + +🖼, 🚥 👆 ✔️ 🏷 ⏮️ 📚 📦 🔢 ☁ 💽, ✋️ 👆 🚫 💚 📨 📶 📏 🎻 📨 🌕 🔢 💲. + +### ⚙️ `response_model_exclude_unset` 🔢 + +👆 💪 ⚒ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_exclude_unset=True`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +& 👈 🔢 💲 🏆 🚫 🔌 📨, 🕴 💲 🤙 ⚒. + +, 🚥 👆 📨 📨 👈 *➡ 🛠️* 🏬 ⏮️ 🆔 `foo`, 📨 (🚫 ✅ 🔢 💲) 🔜: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info + FastAPI ⚙️ Pydantic 🏷 `.dict()` ⏮️ 🚮 `exclude_unset` 🔢 🏆 👉. + +!!! info + 👆 💪 ⚙️: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + 🔬 Pydantic 🩺 `exclude_defaults` & `exclude_none`. + +#### 📊 ⏮️ 💲 🏑 ⏮️ 🔢 + +✋️ 🚥 👆 📊 ✔️ 💲 🏷 🏑 ⏮️ 🔢 💲, 💖 🏬 ⏮️ 🆔 `bar`: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +👫 🔜 🔌 📨. + +#### 📊 ⏮️ 🎏 💲 🔢 + +🚥 📊 ✔️ 🎏 💲 🔢 🕐, 💖 🏬 ⏮️ 🆔 `baz`: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI 🙃 🥃 (🤙, Pydantic 🙃 🥃) 🤔 👈, ✋️ `description`, `tax`, & `tags` ✔️ 🎏 💲 🔢, 👫 ⚒ 🎯 (↩️ ✊ ⚪️➡️ 🔢). + +, 👫 🔜 🔌 🎻 📨. + +!!! tip + 👀 👈 🔢 💲 💪 🕳, 🚫 🕴 `None`. + + 👫 💪 📇 (`[]`), `float` `10.5`, ♒️. + +### `response_model_include` & `response_model_exclude` + +👆 💪 ⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model_include` & `response_model_exclude`. + +👫 ✊ `set` `str` ⏮️ 📛 🔢 🔌 (❎ 🎂) ⚖️ 🚫 (✅ 🎂). + +👉 💪 ⚙️ ⏩ ⌨ 🚥 👆 ✔️ 🕴 1️⃣ Pydantic 🏷 & 💚 ❎ 💽 ⚪️➡️ 🔢. + +!!! tip + ✋️ ⚫️ 👍 ⚙️ 💭 🔛, ⚙️ 💗 🎓, ↩️ 👫 🔢. + + 👉 ↩️ 🎻 🔗 🏗 👆 📱 🗄 (& 🩺) 🔜 1️⃣ 🏁 🏷, 🚥 👆 ⚙️ `response_model_include` ⚖️ `response_model_exclude` 🚫 🔢. + + 👉 ✔ `response_model_by_alias` 👈 👷 ➡. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` + +!!! tip + ❕ `{"name", "description"}` ✍ `set` ⏮️ 📚 2️⃣ 💲. + + ⚫️ 🌓 `set(["name", "description"])`. + +#### ⚙️ `list`Ⓜ ↩️ `set`Ⓜ + +🚥 👆 💭 ⚙️ `set` & ⚙️ `list` ⚖️ `tuple` ↩️, FastAPI 🔜 🗜 ⚫️ `set` & ⚫️ 🔜 👷 ☑: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` + +## 🌃 + +⚙️ *➡ 🛠️ 👨‍🎨* 🔢 `response_model` 🔬 📨 🏷 & ✴️ 🚚 📢 💽 ⛽ 👅. + +⚙️ `response_model_exclude_unset` 📨 🕴 💲 🎯 ⚒. diff --git a/docs/em/docs/tutorial/response-status-code.md b/docs/em/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..e5149de7d --- /dev/null +++ b/docs/em/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# 📨 👔 📟 + +🎏 🌌 👆 💪 ✔ 📨 🏷, 👆 💪 📣 🇺🇸🔍 👔 📟 ⚙️ 📨 ⏮️ 🔢 `status_code` 🙆 *➡ 🛠️*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* ♒️. + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note + 👀 👈 `status_code` 🔢 "👨‍🎨" 👩‍🔬 (`get`, `post`, ♒️). 🚫 👆 *➡ 🛠️ 🔢*, 💖 🌐 🔢 & 💪. + +`status_code` 🔢 📨 🔢 ⏮️ 🇺🇸🔍 👔 📟. + +!!! info + `status_code` 💪 👐 📨 `IntEnum`, ✅ 🐍 `http.HTTPStatus`. + +⚫️ 🔜: + +* 📨 👈 👔 📟 📨. +* 📄 ⚫️ ✅ 🗄 🔗 ( & , 👩‍💻 🔢): + + + +!!! note + 📨 📟 (👀 ⏭ 📄) 🎦 👈 📨 🔨 🚫 ✔️ 💪. + + FastAPI 💭 👉, & 🔜 🏭 🗄 🩺 👈 🇵🇸 📤 🙅‍♂ 📨 💪. + +## 🔃 🇺🇸🔍 👔 📟 + +!!! note + 🚥 👆 ⏪ 💭 ⚫️❔ 🇺🇸🔍 👔 📟, 🚶 ⏭ 📄. + +🇺🇸🔍, 👆 📨 🔢 👔 📟 3️⃣ 9️⃣ 🍕 📨. + +👫 👔 📟 ✔️ 📛 🔗 🤔 👫, ✋️ ⚠ 🍕 🔢. + +📏: + +* `100` & 🔛 "ℹ". 👆 🛎 ⚙️ 👫 🔗. 📨 ⏮️ 👫 👔 📟 🚫🔜 ✔️ 💪. +* **`200`** & 🔛 "🏆" 📨. 👫 🕐 👆 🔜 ⚙️ 🏆. + * `200` 🔢 👔 📟, ❔ ⛓ 🌐 "👌". + * ➕1️⃣ 🖼 🔜 `201`, "✍". ⚫️ 🛎 ⚙️ ⏮️ 🏗 🆕 ⏺ 💽. + * 🎁 💼 `204`, "🙅‍♂ 🎚". 👉 📨 ⚙️ 🕐❔ 📤 🙅‍♂ 🎚 📨 👩‍💻, & 📨 🔜 🚫 ✔️ 💪. +* **`300`** & 🔛 "❎". 📨 ⏮️ 👫 👔 📟 5️⃣📆 ⚖️ 5️⃣📆 🚫 ✔️ 💪, 🌖 `304`, "🚫 🔀", ❔ 🔜 🚫 ✔️ 1️⃣. +* **`400`** & 🔛 "👩‍💻 ❌" 📨. 👫 🥈 🆎 👆 🔜 🎲 ⚙️ 🏆. + * 🖼 `404`, "🚫 🔎" 📨. + * 💊 ❌ ⚪️➡️ 👩‍💻, 👆 💪 ⚙️ `400`. +* `500` & 🔛 💽 ❌. 👆 🌖 🙅 ⚙️ 👫 🔗. 🕐❔ 🕳 🚶 ❌ 🍕 👆 🈸 📟, ⚖️ 💽, ⚫️ 🔜 🔁 📨 1️⃣ 👫 👔 📟. + +!!! tip + 💭 🌅 🔃 🔠 👔 📟 & ❔ 📟 ⚫️❔, ✅ 🏇 🧾 🔃 🇺🇸🔍 👔 📟. + +## ⌨ 💭 📛 + +➡️ 👀 ⏮️ 🖼 🔄: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` 👔 📟 "✍". + +✋️ 👆 🚫 ✔️ ✍ ⚫️❔ 🔠 👉 📟 ⛓. + +👆 💪 ⚙️ 🏪 🔢 ⚪️➡️ `fastapi.status`. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +👫 🏪, 👫 🧑‍🤝‍🧑 🎏 🔢, ✋️ 👈 🌌 👆 💪 ⚙️ 👨‍🎨 📋 🔎 👫: + + + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette import status`. + + **FastAPI** 🚚 🎏 `starlette.status` `fastapi.status` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +## 🔀 🔢 + +⏪, [🏧 👩‍💻 🦮](../advanced/response-change-status-code.md){.internal-link target=_blank}, 👆 🔜 👀 ❔ 📨 🎏 👔 📟 🌘 🔢 👆 📣 📥. diff --git a/docs/em/docs/tutorial/schema-extra-example.md b/docs/em/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..d5bf8810a --- /dev/null +++ b/docs/em/docs/tutorial/schema-extra-example.md @@ -0,0 +1,141 @@ +# 📣 📨 🖼 💽 + +👆 💪 📣 🖼 💽 👆 📱 💪 📨. + +📥 📚 🌌 ⚫️. + +## Pydantic `schema_extra` + +👆 💪 📣 `example` Pydantic 🏷 ⚙️ `Config` & `schema_extra`, 🔬 Pydantic 🩺: 🔗 🛃: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +👈 ➕ ℹ 🔜 🚮-🔢 **🎻 🔗** 👈 🏷, & ⚫️ 🔜 ⚙️ 🛠️ 🩺. + +!!! tip + 👆 💪 ⚙️ 🎏 ⚒ ↔ 🎻 🔗 & 🚮 👆 👍 🛃 ➕ ℹ. + + 🖼 👆 💪 ⚙️ ⚫️ 🚮 🗃 🕸 👩‍💻 🔢, ♒️. + +## `Field` 🌖 ❌ + +🕐❔ ⚙️ `Field()` ⏮️ Pydantic 🏷, 👆 💪 📣 ➕ ℹ **🎻 🔗** 🚶‍♀️ 🙆 🎏 ❌ ❌ 🔢. + +👆 💪 ⚙️ 👉 🚮 `example` 🔠 🏑: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +!!! warning + 🚧 🤯 👈 📚 ➕ ❌ 🚶‍♀️ 🏆 🚫 🚮 🙆 🔬, 🕴 ➕ ℹ, 🧾 🎯. + +## `example` & `examples` 🗄 + +🕐❔ ⚙️ 🙆: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +👆 💪 📣 💽 `example` ⚖️ 👪 `examples` ⏮️ 🌖 ℹ 👈 🔜 🚮 **🗄**. + +### `Body` ⏮️ `example` + +📥 👥 🚶‍♀️ `example` 📊 ⌛ `Body()`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +### 🖼 🩺 🎚 + +⏮️ 🙆 👩‍🔬 🔛 ⚫️ 🔜 👀 💖 👉 `/docs`: + + + +### `Body` ⏮️ 💗 `examples` + +👐 👁 `example`, 👆 💪 🚶‍♀️ `examples` ⚙️ `dict` ⏮️ **💗 🖼**, 🔠 ⏮️ ➕ ℹ 👈 🔜 🚮 **🗄** 💁‍♂️. + +🔑 `dict` 🔬 🔠 🖼, & 🔠 💲 ➕1️⃣ `dict`. + +🔠 🎯 🖼 `dict` `examples` 💪 🔌: + +* `summary`: 📏 📛 🖼. +* `description`: 📏 📛 👈 💪 🔌 ✍ ✍. +* `value`: 👉 ☑ 🖼 🎦, ✅ `dict`. +* `externalValue`: 🎛 `value`, 📛 ☝ 🖼. 👐 👉 5️⃣📆 🚫 🐕‍🦺 📚 🧰 `value`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +### 🖼 🩺 🎚 + +⏮️ `examples` 🚮 `Body()` `/docs` 🔜 👀 💖: + + + +## 📡 ℹ + +!!! warning + 👉 📶 📡 ℹ 🔃 🐩 **🎻 🔗** & **🗄**. + + 🚥 💭 🔛 ⏪ 👷 👆, 👈 💪 🥃, & 👆 🎲 🚫 💪 👉 ℹ, 💭 🆓 🚶 👫. + +🕐❔ 👆 🚮 🖼 🔘 Pydantic 🏷, ⚙️ `schema_extra` ⚖️ `Field(example="something")` 👈 🖼 🚮 **🎻 🔗** 👈 Pydantic 🏷. + +& 👈 **🎻 🔗** Pydantic 🏷 🔌 **🗄** 👆 🛠️, & ⤴️ ⚫️ ⚙️ 🩺 🎚. + +**🎻 🔗** 🚫 🤙 ✔️ 🏑 `example` 🐩. ⏮️ ⏬ 🎻 🔗 🔬 🏑 `examples`, ✋️ 🗄 3️⃣.0️⃣.3️⃣ ⚓️ 🔛 🗝 ⏬ 🎻 🔗 👈 🚫 ✔️ `examples`. + +, 🗄 3️⃣.0️⃣.3️⃣ 🔬 🚮 👍 `example` 🔀 ⏬ **🎻 🔗** ⚫️ ⚙️, 🎏 🎯 (✋️ ⚫️ 👁 `example`, 🚫 `examples`), & 👈 ⚫️❔ ⚙️ 🛠️ 🩺 🎚 (⚙️ 🦁 🎚). + +, 👐 `example` 🚫 🍕 🎻 🔗, ⚫️ 🍕 🗄 🛃 ⏬ 🎻 🔗, & 👈 ⚫️❔ 🔜 ⚙️ 🩺 🎚. + +✋️ 🕐❔ 👆 ⚙️ `example` ⚖️ `examples` ⏮️ 🙆 🎏 🚙 (`Query()`, `Body()`, ♒️.) 📚 🖼 🚫 🚮 🎻 🔗 👈 🔬 👈 💽 (🚫 🗄 👍 ⏬ 🎻 🔗), 👫 🚮 🔗 *➡ 🛠️* 📄 🗄 (🏞 🍕 🗄 👈 ⚙️ 🎻 🔗). + +`Path()`, `Query()`, `Header()`, & `Cookie()`, `example` ⚖️ `examples` 🚮 🗄 🔑, `Parameter Object` (🔧). + +& `Body()`, `File()`, & `Form()`, `example` ⚖️ `examples` 📊 🚮 🗄 🔑, `Request Body Object`, 🏑 `content`, 🔛 `Media Type Object` (🔧). + +🔛 🎏 ✋, 📤 🆕 ⏬ 🗄: **3️⃣.1️⃣.0️⃣**, ⏳ 🚀. ⚫️ ⚓️ 🔛 ⏪ 🎻 🔗 & 🏆 🛠️ ⚪️➡️ 🗄 🛃 ⏬ 🎻 🔗 ❎, 💱 ⚒ ⚪️➡️ ⏮️ ⏬ 🎻 🔗, 🌐 👫 🤪 🔺 📉. 👐, 🦁 🎚 ⏳ 🚫 🐕‍🦺 🗄 3️⃣.1️⃣.0️⃣,, 🔜, ⚫️ 👍 😣 ⚙️ 💭 🔛. diff --git a/docs/em/docs/tutorial/security/first-steps.md b/docs/em/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..6dec6f2c3 --- /dev/null +++ b/docs/em/docs/tutorial/security/first-steps.md @@ -0,0 +1,182 @@ +# 💂‍♂ - 🥇 🔁 + +➡️ 🌈 👈 👆 ✔️ 👆 **👩‍💻** 🛠️ 🆔. + +& 👆 ✔️ **🕸** ➕1️⃣ 🆔 ⚖️ 🎏 ➡ 🎏 🆔 (⚖️ 📱 🈸). + +& 👆 💚 ✔️ 🌌 🕸 🔓 ⏮️ 👩‍💻, ⚙️ **🆔** & **🔐**. + +👥 💪 ⚙️ **Oauth2️⃣** 🏗 👈 ⏮️ **FastAPI**. + +✋️ ➡️ 🖊 👆 🕰 👂 🌕 📏 🔧 🔎 👈 🐥 🍖 ℹ 👆 💪. + +➡️ ⚙️ 🧰 🚚 **FastAPI** 🍵 💂‍♂. + +## ❔ ⚫️ 👀 + +➡️ 🥇 ⚙️ 📟 & 👀 ❔ ⚫️ 👷, & ⤴️ 👥 🔜 👟 🔙 🤔 ⚫️❔ 😥. + +## ✍ `main.py` + +📁 🖼 📁 `main.py`: + +```Python +{!../../../docs_src/security/tutorial001.py!} +``` + +## 🏃 ⚫️ + +!!! info + 🥇 ❎ `python-multipart`. + + 🤶 Ⓜ. `pip install python-multipart`. + + 👉 ↩️ **Oauth2️⃣** ⚙️ "📨 📊" 📨 `username` & `password`. + +🏃 🖼 ⏮️: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +## ✅ ⚫️ + +🚶 🎓 🩺: http://127.0.0.1:8000/docs. + +👆 🔜 👀 🕳 💖 👉: + + + +!!! check "✔ 🔼 ❗" + 👆 ⏪ ✔️ ✨ 🆕 "✔" 🔼. + + & 👆 *➡ 🛠️* ✔️ 🐥 🔒 🔝-▶️️ ↩ 👈 👆 💪 🖊. + +& 🚥 👆 🖊 ⚫️, 👆 ✔️ 🐥 ✔ 📨 🆎 `username` & `password` (& 🎏 📦 🏑): + + + +!!! note + ⚫️ 🚫 🤔 ⚫️❔ 👆 🆎 📨, ⚫️ 🏆 🚫 👷. ✋️ 👥 🔜 🤚 📤. + +👉 ↗️ 🚫 🕸 🏁 👩‍💻, ✋️ ⚫️ 👑 🏧 🧰 📄 🖥 🌐 👆 🛠️. + +⚫️ 💪 ⚙️ 🕸 🏉 (👈 💪 👆). + +⚫️ 💪 ⚙️ 🥉 🥳 🈸 & ⚙️. + +& ⚫️ 💪 ⚙️ 👆, ℹ, ✅ & 💯 🎏 🈸. + +## `password` 💧 + +🔜 ➡️ 🚶 🔙 👄 & 🤔 ⚫️❔ 🌐 👈. + +`password` "💧" 1️⃣ 🌌 ("💧") 🔬 Oauth2️⃣, 🍵 💂‍♂ & 🤝. + +Oauth2️⃣ 🔧 👈 👩‍💻 ⚖️ 🛠️ 💪 🔬 💽 👈 🔓 👩‍💻. + +✋️ 👉 💼, 🎏 **FastAPI** 🈸 🔜 🍵 🛠️ & 🤝. + +, ➡️ 📄 ⚫️ ⚪️➡️ 👈 📉 ☝ 🎑: + +* 👩‍💻 🆎 `username` & `password` 🕸, & 🎯 `Enter`. +* 🕸 (🏃‍♂ 👩‍💻 🖥) 📨 👈 `username` & `password` 🎯 📛 👆 🛠️ (📣 ⏮️ `tokenUrl="token"`). +* 🛠️ ✅ 👈 `username` & `password`, & 📨 ⏮️ "🤝" (👥 🚫 🛠️ 🙆 👉). + * "🤝" 🎻 ⏮️ 🎚 👈 👥 💪 ⚙️ ⏪ ✔ 👉 👩‍💻. + * 🛎, 🤝 ⚒ 🕛 ⏮️ 🕰. + * , 👩‍💻 🔜 ✔️ 🕹 🔄 ☝ ⏪. + * & 🚥 🤝 📎, ⚠ 🌘. ⚫️ 🚫 💖 🧲 🔑 👈 🔜 👷 ♾ (🏆 💼). +* 🕸 🏪 👈 🤝 🍕 👱. +* 👩‍💻 🖊 🕸 🚶 ➕1️⃣ 📄 🕸 🕸 📱. +* 🕸 💪 ☕ 🌅 💽 ⚪️➡️ 🛠️. + * ✋️ ⚫️ 💪 🤝 👈 🎯 🔗. + * , 🔓 ⏮️ 👆 🛠️, ⚫️ 📨 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. + * 🚥 🤝 🔌 `foobar`, 🎚 `Authorization` 🎚 🔜: `Bearer foobar`. + +## **FastAPI**'Ⓜ `OAuth2PasswordBearer` + +**FastAPI** 🚚 📚 🧰, 🎏 🎚 ⚛, 🛠️ 👫 💂‍♂ ⚒. + +👉 🖼 👥 🔜 ⚙️ **Oauth2️⃣**, ⏮️ **🔐** 💧, ⚙️ **📨** 🤝. 👥 👈 ⚙️ `OAuth2PasswordBearer` 🎓. + +!!! info + "📨" 🤝 🚫 🕴 🎛. + + ✋️ ⚫️ 🏆 1️⃣ 👆 ⚙️ 💼. + + & ⚫️ 💪 🏆 🏆 ⚙️ 💼, 🚥 👆 Oauth2️⃣ 🕴 & 💭 ⚫️❔ ⚫️❔ 📤 ➕1️⃣ 🎛 👈 ♣ 👻 👆 💪. + + 👈 💼, **FastAPI** 🚚 👆 ⏮️ 🧰 🏗 ⚫️. + +🕐❔ 👥 ✍ 👐 `OAuth2PasswordBearer` 🎓 👥 🚶‍♀️ `tokenUrl` 🔢. 👉 🔢 🔌 📛 👈 👩‍💻 (🕸 🏃 👩‍💻 🖥) 🔜 ⚙️ 📨 `username` & `password` ✔ 🤚 🤝. + +```Python hl_lines="6" +{!../../../docs_src/security/tutorial001.py!} +``` + +!!! tip + 📥 `tokenUrl="token"` 🔗 ⚖ 📛 `token` 👈 👥 🚫 ✍. ⚫️ ⚖ 📛, ⚫️ 🌓 `./token`. + + ↩️ 👥 ⚙️ ⚖ 📛, 🚥 👆 🛠️ 🔎 `https://example.com/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/token`. ✋️ 🚥 👆 🛠️ 🔎 `https://example.com/api/v1/`, ⤴️ ⚫️ 🔜 🔗 `https://example.com/api/v1/token`. + + ⚙️ ⚖ 📛 ⚠ ⚒ 💭 👆 🈸 🚧 👷 🏧 ⚙️ 💼 💖 [⛅ 🗳](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. + +👉 🔢 🚫 ✍ 👈 🔗 / *➡ 🛠️*, ✋️ 📣 👈 📛 `/token` 🔜 1️⃣ 👈 👩‍💻 🔜 ⚙️ 🤚 🤝. 👈 ℹ ⚙️ 🗄, & ⤴️ 🎓 🛠️ 🧾 ⚙️. + +👥 🔜 🔜 ✍ ☑ ➡ 🛠️. + +!!! info + 🚥 👆 📶 ⚠ "✍" 👆 💪 👎 👗 🔢 📛 `tokenUrl` ↩️ `token_url`. + + 👈 ↩️ ⚫️ ⚙️ 🎏 📛 🗄 🔌. 👈 🚥 👆 💪 🔬 🌅 🔃 🙆 👫 💂‍♂ ⚖ 👆 💪 📁 & 📋 ⚫️ 🔎 🌖 ℹ 🔃 ⚫️. + +`oauth2_scheme` 🔢 👐 `OAuth2PasswordBearer`, ✋️ ⚫️ "🇧🇲". + +⚫️ 💪 🤙: + +```Python +oauth2_scheme(some, parameters) +``` + +, ⚫️ 💪 ⚙️ ⏮️ `Depends`. + +### ⚙️ ⚫️ + +🔜 👆 💪 🚶‍♀️ 👈 `oauth2_scheme` 🔗 ⏮️ `Depends`. + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +👉 🔗 🔜 🚚 `str` 👈 🛠️ 🔢 `token` *➡ 🛠️ 🔢*. + +**FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 👉 🔗 🔬 "💂‍♂ ⚖" 🗄 🔗 (& 🏧 🛠️ 🩺). + +!!! info "📡 ℹ" + **FastAPI** 🔜 💭 👈 ⚫️ 💪 ⚙️ 🎓 `OAuth2PasswordBearer` (📣 🔗) 🔬 💂‍♂ ⚖ 🗄 ↩️ ⚫️ 😖 ⚪️➡️ `fastapi.security.oauth2.OAuth2`, ❔ 🔄 😖 ⚪️➡️ `fastapi.security.base.SecurityBase`. + + 🌐 💂‍♂ 🚙 👈 🛠️ ⏮️ 🗄 (& 🏧 🛠️ 🩺) 😖 ⚪️➡️ `SecurityBase`, 👈 ❔ **FastAPI** 💪 💭 ❔ 🛠️ 👫 🗄. + +## ⚫️❔ ⚫️ 🔨 + +⚫️ 🔜 🚶 & 👀 📨 👈 `Authorization` 🎚, ✅ 🚥 💲 `Bearer ` ➕ 🤝, & 🔜 📨 🤝 `str`. + +🚥 ⚫️ 🚫 👀 `Authorization` 🎚, ⚖️ 💲 🚫 ✔️ `Bearer ` 🤝, ⚫️ 🔜 📨 ⏮️ 4️⃣0️⃣1️⃣ 👔 📟 ❌ (`UNAUTHORIZED`) 🔗. + +👆 🚫 ✔️ ✅ 🚥 🤝 🔀 📨 ❌. 👆 💪 💭 👈 🚥 👆 🔢 🛠️, ⚫️ 🔜 ✔️ `str` 👈 🤝. + +👆 💪 🔄 ⚫️ ⏪ 🎓 🩺: + + + +👥 🚫 ✔ 🔬 🤝, ✋️ 👈 ▶️ ⏪. + +## 🌃 + +, 3️⃣ ⚖️ 4️⃣ ➕ ⏸, 👆 ⏪ ✔️ 🐒 📨 💂‍♂. diff --git a/docs/em/docs/tutorial/security/get-current-user.md b/docs/em/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..455cb4f46 --- /dev/null +++ b/docs/em/docs/tutorial/security/get-current-user.md @@ -0,0 +1,151 @@ +# 🤚 ⏮️ 👩‍💻 + +⏮️ 📃 💂‍♂ ⚙️ (❔ 🧢 🔛 🔗 💉 ⚙️) 🤝 *➡ 🛠️ 🔢* `token` `str`: + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +✋️ 👈 🚫 👈 ⚠. + +➡️ ⚒ ⚫️ 🤝 👥 ⏮️ 👩‍💻. + +## ✍ 👩‍💻 🏷 + +🥇, ➡️ ✍ Pydantic 👩‍💻 🏷. + +🎏 🌌 👥 ⚙️ Pydantic 📣 💪, 👥 💪 ⚙️ ⚫️ 🙆 🙆: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="3 10-14" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## ✍ `get_current_user` 🔗 + +➡️ ✍ 🔗 `get_current_user`. + +💭 👈 🔗 💪 ✔️ 🎧-🔗 ❓ + +`get_current_user` 🔜 ✔️ 🔗 ⏮️ 🎏 `oauth2_scheme` 👥 ✍ ⏭. + +🎏 👥 🔨 ⏭ *➡ 🛠️* 🔗, 👆 🆕 🔗 `get_current_user` 🔜 📨 `token` `str` ⚪️➡️ 🎧-🔗 `oauth2_scheme`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="23" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 🤚 👩‍💻 + +`get_current_user` 🔜 ⚙️ (❌) 🚙 🔢 👥 ✍, 👈 ✊ 🤝 `str` & 📨 👆 Pydantic `User` 🏷: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="17-20 24-25" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 💉 ⏮️ 👩‍💻 + +🔜 👥 💪 ⚙️ 🎏 `Depends` ⏮️ 👆 `get_current_user` *➡ 🛠️*: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="29" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +👀 👈 👥 📣 🆎 `current_user` Pydantic 🏷 `User`. + +👉 🔜 ℹ 🇺🇲 🔘 🔢 ⏮️ 🌐 🛠️ & 🆎 ✅. + +!!! tip + 👆 5️⃣📆 💭 👈 📨 💪 📣 ⏮️ Pydantic 🏷. + + 📥 **FastAPI** 🏆 🚫 🤚 😨 ↩️ 👆 ⚙️ `Depends`. + +!!! check + 🌌 👉 🔗 ⚙️ 🏗 ✔ 👥 ✔️ 🎏 🔗 (🎏 "☑") 👈 🌐 📨 `User` 🏷. + + 👥 🚫 🚫 ✔️ 🕴 1️⃣ 🔗 👈 💪 📨 👈 🆎 💽. + +## 🎏 🏷 + +👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 *➡ 🛠️ 🔢* & 🙅 ⏮️ 💂‍♂ 🛠️ **🔗 💉** 🎚, ⚙️ `Depends`. + +& 👆 💪 ⚙️ 🙆 🏷 ⚖️ 💽 💂‍♂ 📄 (👉 💼, Pydantic 🏷 `User`). + +✋️ 👆 🚫 🚫 ⚙️ 🎯 💽 🏷, 🎓 ⚖️ 🆎. + +👆 💚 ✔️ `id` & `email` & 🚫 ✔️ 🙆 `username` 👆 🏷 ❓ 💭. 👆 💪 ⚙️ 👉 🎏 🧰. + +👆 💚 ✔️ `str`❓ ⚖️ `dict`❓ ⚖️ 💽 🎓 🏷 👐 🔗 ❓ ⚫️ 🌐 👷 🎏 🌌. + +👆 🤙 🚫 ✔️ 👩‍💻 👈 🕹 👆 🈸 ✋️ 🤖, 🤖, ⚖️ 🎏 ⚙️, 👈 ✔️ 🔐 🤝 ❓ 🔄, ⚫️ 🌐 👷 🎏. + +⚙️ 🙆 😇 🏷, 🙆 😇 🎓, 🙆 😇 💽 👈 👆 💪 👆 🈸. **FastAPI** ✔️ 👆 📔 ⏮️ 🔗 💉 ⚙️. + +## 📟 📐 + +👉 🖼 5️⃣📆 😑 🔁. ✔️ 🤯 👈 👥 🌀 💂‍♂, 📊 🏷, 🚙 🔢 & *➡ 🛠️* 🎏 📁. + +✋️ 📥 🔑 ☝. + +💂‍♂ & 🔗 💉 💩 ✍ 🕐. + +& 👆 💪 ⚒ ⚫️ 🏗 👆 💚. & , ✔️ ⚫️ ✍ 🕴 🕐, 👁 🥉. ⏮️ 🌐 💪. + +✋️ 👆 💪 ✔️ 💯 🔗 (*➡ 🛠️*) ⚙️ 🎏 💂‍♂ ⚙️. + +& 🌐 👫 (⚖️ 🙆 ↔ 👫 👈 👆 💚) 💪 ✊ 📈 🏤-⚙️ 👫 🔗 ⚖️ 🙆 🎏 🔗 👆 ✍. + +& 🌐 👉 💯 *➡ 🛠️* 💪 🤪 3️⃣ ⏸: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="28-30" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 🌃 + +👆 💪 🔜 🤚 ⏮️ 👩‍💻 🔗 👆 *➡ 🛠️ 🔢*. + +👥 ⏪ 😬 📤. + +👥 💪 🚮 *➡ 🛠️* 👩‍💻/👩‍💻 🤙 📨 `username` & `password`. + +👈 👟 ⏭. diff --git a/docs/em/docs/tutorial/security/index.md b/docs/em/docs/tutorial/security/index.md new file mode 100644 index 000000000..5b507af3e --- /dev/null +++ b/docs/em/docs/tutorial/security/index.md @@ -0,0 +1,101 @@ +# 💂‍♂ 🎶 + +📤 📚 🌌 🍵 💂‍♂, 🤝 & ✔. + +& ⚫️ 🛎 🏗 & "⚠" ❔. + +📚 🛠️ & ⚙️ 🍵 💂‍♂ & 🤝 ✊ 🦏 💸 🎯 & 📟 (📚 💼 ⚫️ 💪 5️⃣0️⃣ 💯 ⚖️ 🌅 🌐 📟 ✍). + +**FastAPI** 🚚 📚 🧰 ℹ 👆 🙅 ⏮️ **💂‍♂** 💪, 📉, 🐩 🌌, 🍵 ✔️ 🔬 & 💡 🌐 💂‍♂ 🔧. + +✋️ 🥇, ➡️ ✅ 🤪 🔧. + +## 🏃 ❓ + +🚥 👆 🚫 💅 🔃 🙆 👉 ⚖ & 👆 💪 🚮 💂‍♂ ⏮️ 🤝 ⚓️ 🔛 🆔 & 🔐 *▶️️ 🔜*, 🚶 ⏭ 📃. + +## Oauth2️⃣ + +Oauth2️⃣ 🔧 👈 🔬 📚 🌌 🍵 🤝 & ✔. + +⚫️ 🔬 🔧 & 📔 📚 🏗 ⚙️ 💼. + +⚫️ 🔌 🌌 🔓 ⚙️ "🥉 🥳". + +👈 ⚫️❔ 🌐 ⚙️ ⏮️ "💳 ⏮️ 👱📔, 🇺🇸🔍, 👱📔, 📂" ⚙️ 🔘. + +### ✳ 1️⃣ + +📤 ✳ 1️⃣, ❔ 📶 🎏 ⚪️➡️ Oauth2️⃣, & 🌖 🏗, ⚫️ 🔌 🔗 🔧 🔛 ❔ 🗜 📻. + +⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. + +Oauth2️⃣ 🚫 ✔ ❔ 🗜 📻, ⚫️ ⌛ 👆 ✔️ 👆 🈸 🍦 ⏮️ 🇺🇸🔍. + +!!! tip + 📄 🔃 **🛠️** 👆 🔜 👀 ❔ ⚒ 🆙 🇺🇸🔍 🆓, ⚙️ Traefik & ➡️ 🗜. + + +## 👩‍💻 🔗 + +👩‍💻 🔗 ➕1️⃣ 🔧, 🧢 🔛 **Oauth2️⃣**. + +⚫️ ↔ Oauth2️⃣ ✔ 👜 👈 📶 🌌 Oauth2️⃣, 🔄 ⚒ ⚫️ 🌅 🛠️. + +🖼, 🇺🇸🔍 💳 ⚙️ 👩‍💻 🔗 (❔ 🔘 ⚙️ Oauth2️⃣). + +✋️ 👱📔 💳 🚫 🐕‍🦺 👩‍💻 🔗. ⚫️ ✔️ 🚮 👍 🍛 Oauth2️⃣. + +### 👩‍💻 (🚫 "👩‍💻 🔗") + +📤 "👩‍💻" 🔧. 👈 🔄 ❎ 🎏 👜 **👩‍💻 🔗**, ✋️ 🚫 ⚓️ 🔛 Oauth2️⃣. + +, ⚫️ 🏁 🌖 ⚙️. + +⚫️ 🚫 📶 🌟 ⚖️ ⚙️ 🛎. + +## 🗄 + +🗄 (⏪ 💭 🦁) 📂 🔧 🏗 🔗 (🔜 🍕 💾 🏛). + +**FastAPI** ⚓️ 🔛 **🗄**. + +👈 ⚫️❔ ⚒ ⚫️ 💪 ✔️ 💗 🏧 🎓 🧾 🔢, 📟 ⚡, ♒️. + +🗄 ✔️ 🌌 🔬 💗 💂‍♂ "⚖". + +⚙️ 👫, 👆 💪 ✊ 📈 🌐 👫 🐩-⚓️ 🧰, 🔌 👉 🎓 🧾 ⚙️. + +🗄 🔬 📄 💂‍♂ ⚖: + +* `apiKey`: 🈸 🎯 🔑 👈 💪 👟 ⚪️➡️: + * 🔢 🔢. + * 🎚. + * 🍪. +* `http`: 🐩 🇺🇸🔍 🤝 ⚙️, 🔌: + * `bearer`: 🎚 `Authorization` ⏮️ 💲 `Bearer ` ➕ 🤝. 👉 😖 ⚪️➡️ Oauth2️⃣. + * 🇺🇸🔍 🔰 🤝. + * 🇺🇸🔍 📰, ♒️. +* `oauth2`: 🌐 Oauth2️⃣ 🌌 🍵 💂‍♂ (🤙 "💧"). + * 📚 👫 💧 ☑ 🏗 ✳ 2️⃣.0️⃣ 🤝 🐕‍🦺 (💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * ✋️ 📤 1️⃣ 🎯 "💧" 👈 💪 👌 ⚙️ 🚚 🤝 🎏 🈸 🔗: + * `password`: ⏭ 📃 🔜 📔 🖼 👉. +* `openIdConnect`: ✔️ 🌌 🔬 ❔ 🔎 Oauth2️⃣ 🤝 📊 🔁. + * 👉 🏧 🔍 ⚫️❔ 🔬 👩‍💻 🔗 🔧. + + +!!! tip + 🛠️ 🎏 🤝/✔ 🐕‍🦺 💖 🇺🇸🔍, 👱📔, 👱📔, 📂, ♒️. 💪 & 📶 ⏩. + + 🌅 🏗 ⚠ 🏗 🤝/✔ 🐕‍🦺 💖 👈, ✋️ **FastAPI** 🤝 👆 🧰 ⚫️ 💪, ⏪ 🔨 🏋️ 🏋‍♂ 👆. + +## **FastAPI** 🚙 + +FastAPI 🚚 📚 🧰 🔠 👉 💂‍♂ ⚖ `fastapi.security` 🕹 👈 📉 ⚙️ 👉 💂‍♂ 🛠️. + +⏭ 📃 👆 🔜 👀 ❔ 🚮 💂‍♂ 👆 🛠️ ⚙️ 📚 🧰 🚚 **FastAPI**. + +& 👆 🔜 👀 ❔ ⚫️ 🤚 🔁 🛠️ 🔘 🎓 🧾 ⚙️. diff --git a/docs/em/docs/tutorial/security/oauth2-jwt.md b/docs/em/docs/tutorial/security/oauth2-jwt.md new file mode 100644 index 000000000..bc207c566 --- /dev/null +++ b/docs/em/docs/tutorial/security/oauth2-jwt.md @@ -0,0 +1,297 @@ +# Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝 + +🔜 👈 👥 ✔️ 🌐 💂‍♂ 💧, ➡️ ⚒ 🈸 🤙 🔐, ⚙️ 🥙 🤝 & 🔐 🔐 🔁. + +👉 📟 🕳 👆 💪 🤙 ⚙️ 👆 🈸, 🖊 🔐 #️⃣ 👆 💽, ♒️. + +👥 🔜 ▶️ ⚪️➡️ 🌐❔ 👥 ◀️ ⏮️ 📃 & 📈 ⚫️. + +## 🔃 🥙 + +🥙 ⛓ "🎻 🕸 🤝". + +⚫️ 🐩 🚫 🎻 🎚 📏 💧 🎻 🍵 🚀. ⚫️ 👀 💖 👉: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +⚫️ 🚫 🗜,, 🙆 💪 🛡 ℹ ⚪️➡️ 🎚. + +✋️ ⚫️ 🛑. , 🕐❔ 👆 📨 🤝 👈 👆 ♨, 👆 💪 ✔ 👈 👆 🤙 ♨ ⚫️. + +👈 🌌, 👆 💪 ✍ 🤝 ⏮️ 👔, ➡️ 💬, 1️⃣ 🗓️. & ⤴️ 🕐❔ 👩‍💻 👟 🔙 ⏭ 📆 ⏮️ 🤝, 👆 💭 👈 👩‍💻 🕹 👆 ⚙️. + +⏮️ 🗓️, 🤝 🔜 🕛 & 👩‍💻 🔜 🚫 ✔ & 🔜 ✔️ 🛑 🔄 🤚 🆕 🤝. & 🚥 👩‍💻 (⚖️ 🥉 🥳) 🔄 🔀 🤝 🔀 👔, 👆 🔜 💪 🔎 ⚫️, ↩️ 💳 🔜 🚫 🏏. + +🚥 👆 💚 🤾 ⏮️ 🥙 🤝 & 👀 ❔ 👫 👷, ✅ https://jwt.io. + +## ❎ `python-jose` + +👥 💪 ❎ `python-jose` 🏗 & ✔ 🥙 🤝 🐍: + +
+ +```console +$ pip install "python-jose[cryptography]" + +---> 100% +``` + +
+ +🐍-🇩🇬 🚚 🔐 👩‍💻 ➕. + +📥 👥 ⚙️ 👍 1️⃣: )/⚛. + +!!! tip + 👉 🔰 ⏪ ⚙️ PyJWT. + + ✋️ ⚫️ ℹ ⚙️ 🐍-🇩🇬 ↩️ ⚫️ 🚚 🌐 ⚒ ⚪️➡️ PyJWT ➕ ➕ 👈 👆 💪 💪 ⏪ 🕐❔ 🏗 🛠️ ⏮️ 🎏 🧰. + +## 🔐 🔁 + +"🔁" ⛓ 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. + +🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. + +✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. + +### ⚫️❔ ⚙️ 🔐 🔁 + +🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. + +, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). + +## ❎ `passlib` + +🇸🇲 👑 🐍 📦 🍵 🔐#️⃣. + +⚫️ 🐕‍🦺 📚 🔐 🔁 📊 & 🚙 👷 ⏮️ 👫. + +👍 📊 "🐡". + +, ❎ 🇸🇲 ⏮️ 🐡: + +
+ +```console +$ pip install "passlib[bcrypt]" + +---> 100% +``` + +
+ +!!! tip + ⏮️ `passlib`, 👆 💪 🔗 ⚫️ 💪 ✍ 🔐 ✍ **✳**, **🏺** 💂‍♂ 🔌-⚖️ 📚 🎏. + + , 👆 🔜 💪, 🖼, 💰 🎏 📊 ⚪️➡️ ✳ 🈸 💽 ⏮️ FastAPI 🈸. ⚖️ 📉 ↔ ✳ 🈸 ⚙️ 🎏 💽. + + & 👆 👩‍💻 🔜 💪 💳 ⚪️➡️ 👆 ✳ 📱 ⚖️ ⚪️➡️ 👆 **FastAPI** 📱, 🎏 🕰. + +## #️⃣ & ✔ 🔐 + +🗄 🧰 👥 💪 ⚪️➡️ `passlib`. + +✍ 🇸🇲 "🔑". 👉 ⚫️❔ 🔜 ⚙️ #️⃣ & ✔ 🔐. + +!!! tip + 🇸🇲 🔑 ✔️ 🛠️ ⚙️ 🎏 🔁 📊, 🔌 😢 🗝 🕐 🕴 ✔ ✔ 👫, ♒️. + + 🖼, 👆 💪 ⚙️ ⚫️ ✍ & ✔ 🔐 🏗 ➕1️⃣ ⚙️ (💖 ✳) ✋️ #️⃣ 🙆 🆕 🔐 ⏮️ 🎏 📊 💖 🐡. + + & 🔗 ⏮️ 🌐 👫 🎏 🕰. + +✍ 🚙 🔢 #️⃣ 🔐 👟 ⚪️➡️ 👩‍💻. + +& ➕1️⃣ 🚙 ✔ 🚥 📨 🔐 🏏 #️⃣ 🏪. + +& ➕1️⃣ 1️⃣ 🔓 & 📨 👩‍💻. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="7 48 55-56 59-60 69-75" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="6 47 54-55 58-59 68-74" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +!!! note + 🚥 👆 ✅ 🆕 (❌) 💽 `fake_users_db`, 👆 🔜 👀 ❔ #️⃣ 🔐 👀 💖 🔜: `"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"`. + +## 🍵 🥙 🤝 + +🗄 🕹 ❎. + +✍ 🎲 ㊙ 🔑 👈 🔜 ⚙️ 🛑 🥙 🤝. + +🏗 🔐 🎲 ㊙ 🔑 ⚙️ 📋: + +
+ +```console +$ openssl rand -hex 32 + +09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7 +``` + +
+ +& 📁 🔢 🔢 `SECRET_KEY` (🚫 ⚙️ 1️⃣ 🖼). + +✍ 🔢 `ALGORITHM` ⏮️ 📊 ⚙️ 🛑 🥙 🤝 & ⚒ ⚫️ `"HS256"`. + +✍ 🔢 👔 🤝. + +🔬 Pydantic 🏷 👈 🔜 ⚙️ 🤝 🔗 📨. + +✍ 🚙 🔢 🏗 🆕 🔐 🤝. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="6 12-14 28-30 78-86" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="5 11-13 27-29 77-85" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +## ℹ 🔗 + +ℹ `get_current_user` 📨 🎏 🤝 ⏭, ✋️ 👉 🕰, ⚙️ 🥙 🤝. + +🔣 📨 🤝, ✔ ⚫️, & 📨 ⏮️ 👩‍💻. + +🚥 🤝 ❌, 📨 🇺🇸🔍 ❌ ▶️️ ↖️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="89-106" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="88-105" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +## ℹ `/token` *➡ 🛠️* + +✍ `timedelta` ⏮️ 👔 🕰 🤝. + +✍ 🎰 🥙 🔐 🤝 & 📨 ⚫️. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="115-128" + {!> ../../../docs_src/security/tutorial004.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="114-127" + {!> ../../../docs_src/security/tutorial004_py310.py!} + ``` + +### 📡 ℹ 🔃 🥙 "📄" `sub` + +🥙 🔧 💬 👈 📤 🔑 `sub`, ⏮️ 📄 🤝. + +⚫️ 📦 ⚙️ ⚫️, ✋️ 👈 🌐❔ 👆 🔜 🚮 👩‍💻 🆔, 👥 ⚙️ ⚫️ 📥. + +🥙 5️⃣📆 ⚙️ 🎏 👜 ↖️ ⚪️➡️ ⚖ 👩‍💻 & 🤝 👫 🎭 🛠️ 🔗 🔛 👆 🛠️. + +🖼, 👆 💪 🔬 "🚘" ⚖️ "📰 🏤". + +⤴️ 👆 💪 🚮 ✔ 🔃 👈 👨‍💼, 💖 "💾" (🚘) ⚖️ "✍" (📰). + +& ⤴️, 👆 💪 🤝 👈 🥙 🤝 👩‍💻 (⚖️ 🤖), & 👫 💪 ⚙️ ⚫️ 🎭 👈 🎯 (💾 🚘, ⚖️ ✍ 📰 🏤) 🍵 💆‍♂ ✔️ 🏧, ⏮️ 🥙 🤝 👆 🛠️ 🏗 👈. + +⚙️ 👫 💭, 🥙 💪 ⚙️ 🌌 🌖 🤓 😐. + +📚 💼, 📚 👈 👨‍💼 💪 ✔️ 🎏 🆔, ➡️ 💬 `foo` (👩‍💻 `foo`, 🚘 `foo`, & 📰 🏤 `foo`). + +, ❎ 🆔 💥, 🕐❔ 🏗 🥙 🤝 👩‍💻, 👆 💪 🔡 💲 `sub` 🔑, ✅ ⏮️ `username:`. , 👉 🖼, 💲 `sub` 💪 ✔️: `username:johndoe`. + +⚠ 👜 ✔️ 🤯 👈 `sub` 🔑 🔜 ✔️ 😍 🆔 🤭 🎂 🈸, & ⚫️ 🔜 🎻. + +## ✅ ⚫️ + +🏃 💽 & 🚶 🩺: http://127.0.0.1:8000/docs. + +👆 🔜 👀 👩‍💻 🔢 💖: + + + +✔ 🈸 🎏 🌌 ⏭. + +⚙️ 🎓: + +🆔: `johndoe` +🔐: `secret` + +!!! check + 👀 👈 🕳 📟 🔢 🔐 "`secret`", 👥 🕴 ✔️ #️⃣ ⏬. + + + +🤙 🔗 `/users/me/`, 👆 🔜 🤚 📨: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false +} +``` + + + +🚥 👆 📂 👩‍💻 🧰, 👆 💪 👀 ❔ 📊 📨 🕴 🔌 🤝, 🔐 🕴 📨 🥇 📨 🔓 👩‍💻 & 🤚 👈 🔐 🤝, ✋️ 🚫 ⏮️: + + + +!!! note + 👀 🎚 `Authorization`, ⏮️ 💲 👈 ▶️ ⏮️ `Bearer `. + +## 🏧 ⚙️ ⏮️ `scopes` + +Oauth2️⃣ ✔️ 🔑 "↔". + +👆 💪 ⚙️ 👫 🚮 🎯 ⚒ ✔ 🥙 🤝. + +⤴️ 👆 💪 🤝 👉 🤝 👩‍💻 🔗 ⚖️ 🥉 🥳, 🔗 ⏮️ 👆 🛠️ ⏮️ ⚒ 🚫. + +👆 💪 💡 ❔ ⚙️ 👫 & ❔ 👫 🛠️ 🔘 **FastAPI** ⏪ **🏧 👩‍💻 🦮**. + +## 🌃 + +⏮️ ⚫️❔ 👆 ✔️ 👀 🆙 🔜, 👆 💪 ⚒ 🆙 🔐 **FastAPI** 🈸 ⚙️ 🐩 💖 Oauth2️⃣ & 🥙. + +🌖 🙆 🛠️ 🚚 💂‍♂ ▶️️ 👍 🏗 📄 🔜. + +📚 📦 👈 📉 ⚫️ 📚 ✔️ ⚒ 📚 ⚠ ⏮️ 💽 🏷, 💽, & 💪 ⚒. & 👉 📦 👈 📉 👜 💁‍♂️ 🌅 🤙 ✔️ 💂‍♂ ⚠ 🔘. + +--- + +**FastAPI** 🚫 ⚒ 🙆 ⚠ ⏮️ 🙆 💽, 💽 🏷 ⚖️ 🧰. + +⚫️ 🤝 👆 🌐 💪 ⚒ 🕐 👈 👖 👆 🏗 🏆. + +& 👆 💪 ⚙️ 🔗 📚 👍 🚧 & 🛎 ⚙️ 📦 💖 `passlib` & `python-jose`, ↩️ **FastAPI** 🚫 🚚 🙆 🏗 🛠️ 🛠️ 🔢 📦. + +✋️ ⚫️ 🚚 👆 🧰 📉 🛠️ 🌅 💪 🍵 🎯 💪, ⚖, ⚖️ 💂‍♂. + +& 👆 💪 ⚙️ & 🛠️ 🔐, 🐩 🛠️, 💖 Oauth2️⃣ 📶 🙅 🌌. + +👆 💪 💡 🌅 **🏧 👩‍💻 🦮** 🔃 ❔ ⚙️ Oauth2️⃣ "↔", 🌖 👌-🧽 ✔ ⚙️, 📄 👫 🎏 🐩. Oauth2️⃣ ⏮️ ↔ 🛠️ ⚙️ 📚 🦏 🤝 🐕‍🦺, 💖 👱📔, 🇺🇸🔍, 📂, 🤸‍♂, 👱📔, ♒️. ✔ 🥉 🥳 🈸 🔗 ⏮️ 👫 🔗 🔛 👨‍💼 👫 👩‍💻. diff --git a/docs/em/docs/tutorial/security/simple-oauth2.md b/docs/em/docs/tutorial/security/simple-oauth2.md new file mode 100644 index 000000000..765d94039 --- /dev/null +++ b/docs/em/docs/tutorial/security/simple-oauth2.md @@ -0,0 +1,315 @@ +# 🙅 Oauth2️⃣ ⏮️ 🔐 & 📨 + +🔜 ➡️ 🏗 ⚪️➡️ ⏮️ 📃 & 🚮 ❌ 🍕 ✔️ 🏁 💂‍♂ 💧. + +## 🤚 `username` & `password` + +👥 🔜 ⚙️ **FastAPI** 💂‍♂ 🚙 🤚 `username` & `password`. + +Oauth2️⃣ ✔ 👈 🕐❔ ⚙️ "🔐 💧" (👈 👥 ⚙️) 👩‍💻/👩‍💻 🔜 📨 `username` & `password` 🏑 📨 💽. + +& 🔌 💬 👈 🏑 ✔️ 🌟 💖 👈. `user-name` ⚖️ `email` 🚫🔜 👷. + +✋️ 🚫 😟, 👆 💪 🎦 ⚫️ 👆 🎋 👆 🏁 👩‍💻 🕸. + +& 👆 💽 🏷 💪 ⚙️ 🙆 🎏 📛 👆 💚. + +✋️ 💳 *➡ 🛠️*, 👥 💪 ⚙️ 👉 📛 🔗 ⏮️ 🔌 (& 💪, 🖼, ⚙️ 🛠️ 🛠️ 🧾 ⚙️). + +🔌 🇵🇸 👈 `username` & `password` 🔜 📨 📨 💽 (, 🙅‍♂ 🎻 📥). + +### `scope` + +🔌 💬 👈 👩‍💻 💪 📨 ➕1️⃣ 📨 🏑 "`scope`". + +📨 🏑 📛 `scope` (⭐), ✋️ ⚫️ 🤙 📏 🎻 ⏮️ "↔" 🎏 🚀. + +🔠 "↔" 🎻 (🍵 🚀). + +👫 🛎 ⚙️ 📣 🎯 💂‍♂ ✔, 🖼: + +* `users:read` ⚖️ `users:write` ⚠ 🖼. +* `instagram_basic` ⚙️ 👱📔 / 👱📔. +* `https://www.googleapis.com/auth/drive` ⚙️ 🇺🇸🔍. + +!!! info + Oauth2️⃣ "↔" 🎻 👈 📣 🎯 ✔ ✔. + + ⚫️ 🚫 🤔 🚥 ⚫️ ✔️ 🎏 🦹 💖 `:` ⚖️ 🚥 ⚫️ 📛. + + 👈 ℹ 🛠️ 🎯. + + Oauth2️⃣ 👫 🎻. + +## 📟 🤚 `username` & `password` + +🔜 ➡️ ⚙️ 🚙 🚚 **FastAPI** 🍵 👉. + +### `OAuth2PasswordRequestForm` + +🥇, 🗄 `OAuth2PasswordRequestForm`, & ⚙️ ⚫️ 🔗 ⏮️ `Depends` *➡ 🛠️* `/token`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="2 74" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +`OAuth2PasswordRequestForm` 🎓 🔗 👈 📣 📨 💪 ⏮️: + +* `username`. +* `password`. +* 📦 `scope` 🏑 🦏 🎻, ✍ 🎻 🎏 🚀. +* 📦 `grant_type`. + +!!! tip + Oauth2️⃣ 🔌 🤙 *🚚* 🏑 `grant_type` ⏮️ 🔧 💲 `password`, ✋️ `OAuth2PasswordRequestForm` 🚫 🛠️ ⚫️. + + 🚥 👆 💪 🛠️ ⚫️, ⚙️ `OAuth2PasswordRequestFormStrict` ↩️ `OAuth2PasswordRequestForm`. + +* 📦 `client_id` (👥 🚫 💪 ⚫️ 👆 🖼). +* 📦 `client_secret` (👥 🚫 💪 ⚫️ 👆 🖼). + +!!! info + `OAuth2PasswordRequestForm` 🚫 🎁 🎓 **FastAPI** `OAuth2PasswordBearer`. + + `OAuth2PasswordBearer` ⚒ **FastAPI** 💭 👈 ⚫️ 💂‍♂ ⚖. ⚫️ 🚮 👈 🌌 🗄. + + ✋️ `OAuth2PasswordRequestForm` 🎓 🔗 👈 👆 💪 ✔️ ✍ 👆, ⚖️ 👆 💪 ✔️ 📣 `Form` 🔢 🔗. + + ✋️ ⚫️ ⚠ ⚙️ 💼, ⚫️ 🚚 **FastAPI** 🔗, ⚒ ⚫️ ⏩. + +### ⚙️ 📨 💽 + +!!! tip + 👐 🔗 🎓 `OAuth2PasswordRequestForm` 🏆 🚫 ✔️ 🔢 `scope` ⏮️ 📏 🎻 👽 🚀, ↩️, ⚫️ 🔜 ✔️ `scopes` 🔢 ⏮️ ☑ 📇 🎻 🔠 ↔ 📨. + + 👥 🚫 ⚙️ `scopes` 👉 🖼, ✋️ 🛠️ 📤 🚥 👆 💪 ⚫️. + +🔜, 🤚 👩‍💻 📊 ⚪️➡️ (❌) 💽, ⚙️ `username` ⚪️➡️ 📨 🏑. + +🚥 📤 🙅‍♂ ✅ 👩‍💻, 👥 📨 ❌ 💬 "❌ 🆔 ⚖️ 🔐". + +❌, 👥 ⚙️ ⚠ `HTTPException`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 75-77" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +### ✅ 🔐 + +👉 ☝ 👥 ✔️ 👩‍💻 📊 ⚪️➡️ 👆 💽, ✋️ 👥 🚫 ✅ 🔐. + +➡️ 🚮 👈 💽 Pydantic `UserInDB` 🏷 🥇. + +👆 🔜 🙅 🖊 🔢 🔐,, 👥 🔜 ⚙️ (❌) 🔐 🔁 ⚙️. + +🚥 🔐 🚫 🏏, 👥 📨 🎏 ❌. + +#### 🔐 🔁 + +"🔁" ⛓: 🏭 🎚 (🔐 👉 💼) 🔘 🔁 🔢 (🎻) 👈 👀 💖 🙃. + +🕐❔ 👆 🚶‍♀️ ⚫️❔ 🎏 🎚 (⚫️❔ 🎏 🔐) 👆 🤚 ⚫️❔ 🎏 🙃. + +✋️ 👆 🚫🔜 🗜 ⚪️➡️ 🙃 🔙 🔐. + +##### ⚫️❔ ⚙️ 🔐 🔁 + +🚥 👆 💽 📎, 🧙‍♀ 🏆 🚫 ✔️ 👆 👩‍💻' 🔢 🔐, 🕴#️⃣. + +, 🧙‍♀ 🏆 🚫 💪 🔄 ⚙️ 👈 🎏 🔐 ➕1️⃣ ⚙️ (📚 👩‍💻 ⚙️ 🎏 🔐 🌐, 👉 🔜 ⚠). + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="78-81" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +#### 🔃 `**user_dict` + +`UserInDB(**user_dict)` ⛓: + +*🚶‍♀️ 🔑 & 💲 `user_dict` 🔗 🔑-💲 ❌, 🌓:* + +```Python +UserInDB( + username = user_dict["username"], + email = user_dict["email"], + full_name = user_dict["full_name"], + disabled = user_dict["disabled"], + hashed_password = user_dict["hashed_password"], +) +``` + +!!! info + 🌅 🏁 🔑 `**👩‍💻_ #️⃣ ` ✅ 🔙 [🧾 **➕ 🏷**](../extra-models.md#about-user_indict){.internal-link target=_blank}. + +## 📨 🤝 + +📨 `token` 🔗 🔜 🎻 🎚. + +⚫️ 🔜 ✔️ `token_type`. 👆 💼, 👥 ⚙️ "📨" 🤝, 🤝 🆎 🔜 "`bearer`". + +& ⚫️ 🔜 ✔️ `access_token`, ⏮️ 🎻 ⚗ 👆 🔐 🤝. + +👉 🙅 🖼, 👥 🔜 🍕 😟 & 📨 🎏 `username` 🤝. + +!!! tip + ⏭ 📃, 👆 🔜 👀 🎰 🔐 🛠️, ⏮️ 🔐 #️⃣ & 🥙 🤝. + + ✋️ 🔜, ➡️ 🎯 🔛 🎯 ℹ 👥 💪. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="83" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +!!! tip + 🔌, 👆 🔜 📨 🎻 ⏮️ `access_token` & `token_type`, 🎏 👉 🖼. + + 👉 🕳 👈 👆 ✔️ 👆 👆 📟, & ⚒ 💭 👆 ⚙️ 📚 🎻 🔑. + + ⚫️ 🌖 🕴 👜 👈 👆 ✔️ 💭 ☑ 👆, 🛠️ ⏮️ 🔧. + + 🎂, **FastAPI** 🍵 ⚫️ 👆. + +## ℹ 🔗 + +🔜 👥 🔜 ℹ 👆 🔗. + +👥 💚 🤚 `current_user` *🕴* 🚥 👉 👩‍💻 🦁. + +, 👥 ✍ 🌖 🔗 `get_current_active_user` 👈 🔄 ⚙️ `get_current_user` 🔗. + +👯‍♂️ 👉 🔗 🔜 📨 🇺🇸🔍 ❌ 🚥 👩‍💻 🚫 🔀, ⚖️ 🚥 🔕. + +, 👆 🔗, 👥 🔜 🕴 🤚 👩‍💻 🚥 👩‍💻 🔀, ☑ 🔓, & 🦁: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="55-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +!!! info + 🌖 🎚 `WWW-Authenticate` ⏮️ 💲 `Bearer` 👥 🛬 📥 🍕 🔌. + + 🙆 🇺🇸🔍 (❌) 👔 📟 4️⃣0️⃣1️⃣ "⛔" 🤔 📨 `WWW-Authenticate` 🎚. + + 💼 📨 🤝 (👆 💼), 💲 👈 🎚 🔜 `Bearer`. + + 👆 💪 🤙 🚶 👈 ➕ 🎚 & ⚫️ 🔜 👷. + + ✋️ ⚫️ 🚚 📥 🛠️ ⏮️ 🔧. + + , 📤 5️⃣📆 🧰 👈 ⌛ & ⚙️ ⚫️ (🔜 ⚖️ 🔮) & 👈 💪 ⚠ 👆 ⚖️ 👆 👩‍💻, 🔜 ⚖️ 🔮. + + 👈 💰 🐩... + +## 👀 ⚫️ 🎯 + +📂 🎓 🩺: http://127.0.0.1:8000/docs. + +### 🔓 + +🖊 "✔" 🔼. + +⚙️ 🎓: + +👩‍💻: `johndoe` + +🔐: `secret` + + + +⏮️ 🔗 ⚙️, 👆 🔜 👀 ⚫️ 💖: + + + +### 🤚 👆 👍 👩‍💻 💽 + +🔜 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. + +👆 🔜 🤚 👆 👩‍💻 📊, 💖: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false, + "hashed_password": "fakehashedsecret" +} +``` + + + +🚥 👆 🖊 🔒 ℹ & ⏏, & ⤴️ 🔄 🎏 🛠️ 🔄, 👆 🔜 🤚 🇺🇸🔍 4️⃣0️⃣1️⃣ ❌: + +```JSON +{ + "detail": "Not authenticated" +} +``` + +### 🔕 👩‍💻 + +🔜 🔄 ⏮️ 🔕 👩‍💻, 🔓 ⏮️: + +👩‍💻: `alice` + +🔐: `secret2` + +& 🔄 ⚙️ 🛠️ `GET` ⏮️ ➡ `/users/me`. + +👆 🔜 🤚 "🔕 👩‍💻" ❌, 💖: + +```JSON +{ + "detail": "Inactive user" +} +``` + +## 🌃 + +👆 🔜 ✔️ 🧰 🛠️ 🏁 💂‍♂ ⚙️ ⚓️ 🔛 `username` & `password` 👆 🛠️. + +⚙️ 👫 🧰, 👆 💪 ⚒ 💂‍♂ ⚙️ 🔗 ⏮️ 🙆 💽 & ⏮️ 🙆 👩‍💻 ⚖️ 💽 🏷. + +🕴 ℹ ❌ 👈 ⚫️ 🚫 🤙 "🔐". + +⏭ 📃 👆 🔜 👀 ❔ ⚙️ 🔐 🔐 🔁 🗃 & 🥙 🤝. diff --git a/docs/em/docs/tutorial/sql-databases.md b/docs/em/docs/tutorial/sql-databases.md new file mode 100644 index 000000000..9d46c2460 --- /dev/null +++ b/docs/em/docs/tutorial/sql-databases.md @@ -0,0 +1,786 @@ +# 🗄 (🔗) 💽 + +**FastAPI** 🚫 🚚 👆 ⚙️ 🗄 (🔗) 💽. + +✋️ 👆 💪 ⚙️ 🙆 🔗 💽 👈 👆 💚. + +📥 👥 🔜 👀 🖼 ⚙️ 🇸🇲. + +👆 💪 💪 🛠️ ⚫️ 🙆 💽 🐕‍🦺 🇸🇲, 💖: + +* ✳ +* ✳ +* 🗄 +* 🐸 +* 🤸‍♂ 🗄 💽, ♒️. + +👉 🖼, 👥 🔜 ⚙️ **🗄**, ↩️ ⚫️ ⚙️ 👁 📁 & 🐍 ✔️ 🛠️ 🐕‍🦺. , 👆 💪 📁 👉 🖼 & 🏃 ⚫️. + +⏪, 👆 🏭 🈸, 👆 💪 💚 ⚙️ 💽 💽 💖 **✳**. + +!!! tip + 📤 🛂 🏗 🚂 ⏮️ **FastAPI** & **✳**, 🌐 ⚓️ 🔛 **☁**, 🔌 🕸 & 🌖 🧰: https://github.com/tiangolo/full-stack-fastapi-postgresql + +!!! note + 👀 👈 📚 📟 🐩 `SQLAlchemy` 📟 👆 🔜 ⚙️ ⏮️ 🙆 🛠️. + + **FastAPI** 🎯 📟 🤪 🕧. + +## 🐜 + +**FastAPI** 👷 ⏮️ 🙆 💽 & 🙆 👗 🗃 💬 💽. + +⚠ ⚓ ⚙️ "🐜": "🎚-🔗 🗺" 🗃. + +🐜 ✔️ 🧰 🗜 ("*🗺*") 🖖 *🎚* 📟 & 💽 🏓 ("*🔗*"). + +⏮️ 🐜, 👆 🛎 ✍ 🎓 👈 🎨 🏓 🗄 💽, 🔠 🔢 🎓 🎨 🏓, ⏮️ 📛 & 🆎. + +🖼 🎓 `Pet` 💪 🎨 🗄 🏓 `pets`. + +& 🔠 *👐* 🎚 👈 🎓 🎨 ⏭ 💽. + +🖼 🎚 `orion_cat` (👐 `Pet`) 💪 ✔️ 🔢 `orion_cat.type`, 🏓 `type`. & 💲 👈 🔢 💪, ✅ `"cat"`. + +👫 🐜 ✔️ 🧰 ⚒ 🔗 ⚖️ 🔗 🖖 🏓 ⚖️ 👨‍💼. + +👉 🌌, 👆 💪 ✔️ 🔢 `orion_cat.owner` & 👨‍💼 🔜 🔌 💽 👉 🐶 👨‍💼, ✊ ⚪️➡️ 🏓 *👨‍💼*. + +, `orion_cat.owner.name` 💪 📛 (⚪️➡️ `name` 🏓 `owners` 🏓) 👉 🐶 👨‍💼. + +⚫️ 💪 ✔️ 💲 💖 `"Arquilian"`. + +& 🐜 🔜 🌐 👷 🤚 ℹ ⚪️➡️ 🔗 🏓 *👨‍💼* 🕐❔ 👆 🔄 🔐 ⚫️ ⚪️➡️ 👆 🐶 🎚. + +⚠ 🐜 🖼: ✳-🐜 (🍕 ✳ 🛠️), 🇸🇲 🐜 (🍕 🇸🇲, 🔬 🛠️) & 🏒 (🔬 🛠️), 👪 🎏. + +📥 👥 🔜 👀 ❔ 👷 ⏮️ **🇸🇲 🐜**. + +🎏 🌌 👆 💪 ⚙️ 🙆 🎏 🐜. + +!!! tip + 📤 🌓 📄 ⚙️ 🏒 📥 🩺. + +## 📁 📊 + +👫 🖼, ➡️ 💬 👆 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app` ⏮️ 📊 💖 👉: + +``` +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + └── schemas.py +``` + +📁 `__init__.py` 🛁 📁, ✋️ ⚫️ 💬 🐍 👈 `sql_app` ⏮️ 🌐 🚮 🕹 (🐍 📁) 📦. + +🔜 ➡️ 👀 ⚫️❔ 🔠 📁/🕹 🔨. + +## ❎ `SQLAlchemy` + +🥇 👆 💪 ❎ `SQLAlchemy`: + +
+ +```console +$ pip install sqlalchemy + +---> 100% +``` + +
+ +## ✍ 🇸🇲 🍕 + +➡️ 🔗 📁 `sql_app/database.py`. + +### 🗄 🇸🇲 🍕 + +```Python hl_lines="1-3" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### ✍ 💽 📛 🇸🇲 + +```Python hl_lines="5-6" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +👉 🖼, 👥 "🔗" 🗄 💽 (📂 📁 ⏮️ 🗄 💽). + +📁 🔜 🔎 🎏 📁 📁 `sql_app.db`. + +👈 ⚫️❔ 🏁 🍕 `./sql_app.db`. + +🚥 👆 ⚙️ **✳** 💽 ↩️, 👆 🔜 ✔️ ✍ ⏸: + +```Python +SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db" +``` + +...& 🛠️ ⚫️ ⏮️ 👆 💽 📊 & 🎓 (📊 ✳, ✳ ⚖️ 🙆 🎏). + +!!! tip + + 👉 👑 ⏸ 👈 👆 🔜 ✔️ 🔀 🚥 👆 💚 ⚙️ 🎏 💽. + +### ✍ 🇸🇲 `engine` + +🥇 🔁 ✍ 🇸🇲 "🚒". + +👥 🔜 ⏪ ⚙️ 👉 `engine` 🎏 🥉. + +```Python hl_lines="8-10" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +#### 🗒 + +❌: + +```Python +connect_args={"check_same_thread": False} +``` + +...💪 🕴 `SQLite`. ⚫️ 🚫 💪 🎏 💽. + +!!! info "📡 ℹ" + + 🔢 🗄 🔜 🕴 ✔ 1️⃣ 🧵 🔗 ⏮️ ⚫️, 🤔 👈 🔠 🧵 🔜 🍵 🔬 📨. + + 👉 ❎ 😫 🤝 🎏 🔗 🎏 👜 (🎏 📨). + + ✋️ FastAPI, ⚙️ 😐 🔢 (`def`) 🌅 🌘 1️⃣ 🧵 💪 🔗 ⏮️ 💽 🎏 📨, 👥 💪 ⚒ 🗄 💭 👈 ⚫️ 🔜 ✔ 👈 ⏮️ `connect_args={"check_same_thread": False}`. + + , 👥 🔜 ⚒ 💭 🔠 📨 🤚 🚮 👍 💽 🔗 🎉 🔗, 📤 🙅‍♂ 💪 👈 🔢 🛠️. + +### ✍ `SessionLocal` 🎓 + +🔠 👐 `SessionLocal` 🎓 🔜 💽 🎉. 🎓 ⚫️ 🚫 💽 🎉. + +✋️ 🕐 👥 ✍ 👐 `SessionLocal` 🎓, 👉 👐 🔜 ☑ 💽 🎉. + +👥 📛 ⚫️ `SessionLocal` 🔬 ⚫️ ⚪️➡️ `Session` 👥 🏭 ⚪️➡️ 🇸🇲. + +👥 🔜 ⚙️ `Session` (1️⃣ 🗄 ⚪️➡️ 🇸🇲) ⏪. + +✍ `SessionLocal` 🎓, ⚙️ 🔢 `sessionmaker`: + +```Python hl_lines="11" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +### ✍ `Base` 🎓 + +🔜 👥 🔜 ⚙️ 🔢 `declarative_base()` 👈 📨 🎓. + +⏪ 👥 🔜 😖 ⚪️➡️ 👉 🎓 ✍ 🔠 💽 🏷 ⚖️ 🎓 (🐜 🏷): + +```Python hl_lines="13" +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +## ✍ 💽 🏷 + +➡️ 🔜 👀 📁 `sql_app/models.py`. + +### ✍ 🇸🇲 🏷 ⚪️➡️ `Base` 🎓 + +👥 🔜 ⚙️ 👉 `Base` 🎓 👥 ✍ ⏭ ✍ 🇸🇲 🏷. + +!!! tip + 🇸🇲 ⚙️ ⚖ "**🏷**" 🔗 👉 🎓 & 👐 👈 🔗 ⏮️ 💽. + + ✋️ Pydantic ⚙️ ⚖ "**🏷**" 🔗 🕳 🎏, 💽 🔬, 🛠️, & 🧾 🎓 & 👐. + +🗄 `Base` ⚪️➡️ `database` (📁 `database.py` ⚪️➡️ 🔛). + +✍ 🎓 👈 😖 ⚪️➡️ ⚫️. + +👫 🎓 🇸🇲 🏷. + +```Python hl_lines="4 7-8 18-19" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +`__tablename__` 🔢 💬 🇸🇲 📛 🏓 ⚙️ 💽 🔠 👫 🏷. + +### ✍ 🏷 🔢/🏓 + +🔜 ✍ 🌐 🏷 (🎓) 🔢. + +🔠 👫 🔢 🎨 🏓 🚮 🔗 💽 🏓. + +👥 ⚙️ `Column` ⚪️➡️ 🇸🇲 🔢 💲. + +& 👥 🚶‍♀️ 🇸🇲 🎓 "🆎", `Integer`, `String`, & `Boolean`, 👈 🔬 🆎 💽, ❌. + +```Python hl_lines="1 10-13 21-24" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +### ✍ 💛 + +🔜 ✍ 💛. + +👉, 👥 ⚙️ `relationship` 🚚 🇸🇲 🐜. + +👉 🔜 ▶️️, 🌅 ⚖️ 🌘, "🎱" 🔢 👈 🔜 🔌 💲 ⚪️➡️ 🎏 🏓 🔗 👉 1️⃣. + +```Python hl_lines="2 15 26" +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +🕐❔ 🔐 🔢 `items` `User`, `my_user.items`, ⚫️ 🔜 ✔️ 📇 `Item` 🇸🇲 🏷 (⚪️➡️ `items` 🏓) 👈 ✔️ 💱 🔑 ☝ 👉 ⏺ `users` 🏓. + +🕐❔ 👆 🔐 `my_user.items`, 🇸🇲 🔜 🤙 🚶 & ☕ 🏬 ⚪️➡️ 💽 `items` 🏓 & 🔗 👫 📥. + +& 🕐❔ 🔐 🔢 `owner` `Item`, ⚫️ 🔜 🔌 `User` 🇸🇲 🏷 ⚪️➡️ `users` 🏓. ⚫️ 🔜 ⚙️ `owner_id` 🔢/🏓 ⏮️ 🚮 💱 🔑 💭 ❔ ⏺ 🤚 ⚪️➡️ `users` 🏓. + +## ✍ Pydantic 🏷 + +🔜 ➡️ ✅ 📁 `sql_app/schemas.py`. + +!!! tip + ❎ 😨 🖖 🇸🇲 *🏷* & Pydantic *🏷*, 👥 🔜 ✔️ 📁 `models.py` ⏮️ 🇸🇲 🏷, & 📁 `schemas.py` ⏮️ Pydantic 🏷. + + 👫 Pydantic 🏷 🔬 🌅 ⚖️ 🌘 "🔗" (☑ 📊 💠). + + 👉 🔜 ℹ 👥 ❎ 😨 ⏪ ⚙️ 👯‍♂️. + +### ✍ ▶️ Pydantic *🏷* / 🔗 + +✍ `ItemBase` & `UserBase` Pydantic *🏷* (⚖️ ➡️ 💬 "🔗") ✔️ ⚠ 🔢 ⏪ 🏗 ⚖️ 👂 📊. + +& ✍ `ItemCreate` & `UserCreate` 👈 😖 ⚪️➡️ 👫 (👫 🔜 ✔️ 🎏 🔢), ➕ 🙆 🌖 📊 (🔢) 💪 🏗. + +, 👩‍💻 🔜 ✔️ `password` 🕐❔ 🏗 ⚫️. + +✋️ 💂‍♂, `password` 🏆 🚫 🎏 Pydantic *🏷*, 🖼, ⚫️ 🏆 🚫 📨 ⚪️➡️ 🛠️ 🕐❔ 👂 👩‍💻. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="3 6-8 11-12 23-24 27-28" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="1 4-6 9-10 21-22 25-26" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +#### 🇸🇲 👗 & Pydantic 👗 + +👀 👈 🇸🇲 *🏷* 🔬 🔢 ⚙️ `=`, & 🚶‍♀️ 🆎 🔢 `Column`, 💖: + +```Python +name = Column(String) +``` + +⏪ Pydantic *🏷* 📣 🆎 ⚙️ `:`, 🆕 🆎 ✍ ❕/🆎 🔑: + +```Python +name: str +``` + +✔️ ⚫️ 🤯, 👆 🚫 🤚 😕 🕐❔ ⚙️ `=` & `:` ⏮️ 👫. + +### ✍ Pydantic *🏷* / 🔗 👂 / 📨 + +🔜 ✍ Pydantic *🏷* (🔗) 👈 🔜 ⚙️ 🕐❔ 👂 💽, 🕐❔ 🛬 ⚫️ ⚪️➡️ 🛠️. + +🖼, ⏭ 🏗 🏬, 👥 🚫 💭 ⚫️❔ 🔜 🆔 🛠️ ⚫️, ✋️ 🕐❔ 👂 ⚫️ (🕐❔ 🛬 ⚫️ ⚪️➡️ 🛠️) 👥 🔜 ⏪ 💭 🚮 🆔. + +🎏 🌌, 🕐❔ 👂 👩‍💻, 👥 💪 🔜 📣 👈 `items` 🔜 🔌 🏬 👈 💭 👉 👩‍💻. + +🚫 🕴 🆔 📚 🏬, ✋️ 🌐 💽 👈 👥 🔬 Pydantic *🏷* 👂 🏬: `Item`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="15-17 31-34" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13-15 29-32" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 👀 👈 `User`, Pydantic *🏷* 👈 🔜 ⚙️ 🕐❔ 👂 👩‍💻 (🛬 ⚫️ ⚪️➡️ 🛠️) 🚫 🔌 `password`. + +### ⚙️ Pydantic `orm_mode` + +🔜, Pydantic *🏷* 👂, `Item` & `User`, 🚮 🔗 `Config` 🎓. + +👉 `Config` 🎓 ⚙️ 🚚 📳 Pydantic. + +`Config` 🎓, ⚒ 🔢 `orm_mode = True`. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="15 19-20 31 36-37" + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python hl_lines="13 17-18 29 34-35" + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +!!! tip + 👀 ⚫️ ⚖ 💲 ⏮️ `=`, 💖: + + `orm_mode = True` + + ⚫️ 🚫 ⚙️ `:` 🆎 📄 ⏭. + + 👉 ⚒ 📁 💲, 🚫 📣 🆎. + +Pydantic `orm_mode` 🔜 💬 Pydantic *🏷* ✍ 💽 🚥 ⚫️ 🚫 `dict`, ✋️ 🐜 🏷 (⚖️ 🙆 🎏 ❌ 🎚 ⏮️ 🔢). + +👉 🌌, ↩️ 🕴 🔄 🤚 `id` 💲 ⚪️➡️ `dict`,: + +```Python +id = data["id"] +``` + +⚫️ 🔜 🔄 🤚 ⚫️ ⚪️➡️ 🔢,: + +```Python +id = data.id +``` + +& ⏮️ 👉, Pydantic *🏷* 🔗 ⏮️ 🐜, & 👆 💪 📣 ⚫️ `response_model` ❌ 👆 *➡ 🛠️*. + +👆 🔜 💪 📨 💽 🏷 & ⚫️ 🔜 ✍ 💽 ⚪️➡️ ⚫️. + +#### 📡 ℹ 🔃 🐜 📳 + +🇸🇲 & 📚 🎏 🔢 "🙃 🚚". + +👈 ⛓, 🖼, 👈 👫 🚫 ☕ 💽 💛 ⚪️➡️ 💽 🚥 👆 🔄 🔐 🔢 👈 🔜 🔌 👈 💽. + +🖼, 🔐 🔢 `items`: + +```Python +current_user.items +``` + +🔜 ⚒ 🇸🇲 🚶 `items` 🏓 & 🤚 🏬 👉 👩‍💻, ✋️ 🚫 ⏭. + +🍵 `orm_mode`, 🚥 👆 📨 🇸🇲 🏷 ⚪️➡️ 👆 *➡ 🛠️*, ⚫️ 🚫🔜 🔌 💛 💽. + +🚥 👆 📣 📚 💛 👆 Pydantic 🏷. + +✋️ ⏮️ 🐜 📳, Pydantic ⚫️ 🔜 🔄 🔐 💽 ⚫️ 💪 ⚪️➡️ 🔢 (↩️ 🤔 `dict`), 👆 💪 📣 🎯 💽 👆 💚 📨 & ⚫️ 🔜 💪 🚶 & 🤚 ⚫️, ⚪️➡️ 🐜. + +## 💩 🇨🇻 + +🔜 ➡️ 👀 📁 `sql_app/crud.py`. + +👉 📁 👥 🔜 ✔️ ♻ 🔢 🔗 ⏮️ 💽 💽. + +**💩** 👟 ⚪️➡️: **🅱**📧, **Ⓜ**💳, **👤** = , & **🇨🇮**📧. + +...👐 👉 🖼 👥 🕴 🏗 & 👂. + +### ✍ 💽 + +🗄 `Session` ⚪️➡️ `sqlalchemy.orm`, 👉 🔜 ✔ 👆 📣 🆎 `db` 🔢 & ✔️ 👻 🆎 ✅ & 🛠️ 👆 🔢. + +🗄 `models` (🇸🇲 🏷) & `schemas` (Pydantic *🏷* / 🔗). + +✍ 🚙 🔢: + +* ✍ 👁 👩‍💻 🆔 & 📧. +* ✍ 💗 👩‍💻. +* ✍ 💗 🏬. + +```Python hl_lines="1 3 6-7 10-11 14-15 27-28" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + 🏗 🔢 👈 🕴 💡 🔗 ⏮️ 💽 (🤚 👩‍💻 ⚖️ 🏬) 🔬 👆 *➡ 🛠️ 🔢*, 👆 💪 🌖 💪 ♻ 👫 💗 🍕 & 🚮 ⚒ 💯 👫. + +### ✍ 💽 + +🔜 ✍ 🚙 🔢 ✍ 💽. + +🔁: + +* ✍ 🇸🇲 🏷 *👐* ⏮️ 👆 📊. +* `add` 👈 👐 🎚 👆 💽 🎉. +* `commit` 🔀 💽 (👈 👫 🖊). +* `refresh` 👆 👐 (👈 ⚫️ 🔌 🙆 🆕 📊 ⚪️➡️ 💽, 💖 🏗 🆔). + +```Python hl_lines="18-24 31-36" +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +!!! tip + 🇸🇲 🏷 `User` 🔌 `hashed_password` 👈 🔜 🔌 🔐 #️⃣ ⏬ 🔐. + + ✋️ ⚫️❔ 🛠️ 👩‍💻 🚚 ⏮️ 🔐, 👆 💪 ⚗ ⚫️ & 🏗 #️⃣ 🔐 👆 🈸. + + & ⤴️ 🚶‍♀️ `hashed_password` ❌ ⏮️ 💲 🖊. + +!!! warning + 👉 🖼 🚫 🔐, 🔐 🚫#️⃣. + + 🎰 👨‍❤‍👨 🈸 👆 🔜 💪 #️⃣ 🔐 & 🙅 🖊 👫 🔢. + + 🌅 ℹ, 🚶 🔙 💂‍♂ 📄 🔰. + + 📥 👥 🎯 🕴 🔛 🧰 & 👨‍🔧 💽. + +!!! tip + ↩️ 🚶‍♀️ 🔠 🇨🇻 ❌ `Item` & 👂 🔠 1️⃣ 👫 ⚪️➡️ Pydantic *🏷*, 👥 🏭 `dict` ⏮️ Pydantic *🏷*'Ⓜ 📊 ⏮️: + + `item.dict()` + + & ⤴️ 👥 🚶‍♀️ `dict`'Ⓜ 🔑-💲 👫 🇨🇻 ❌ 🇸🇲 `Item`, ⏮️: + + `Item(**item.dict())` + + & ⤴️ 👥 🚶‍♀️ ➕ 🇨🇻 ❌ `owner_id` 👈 🚫 🚚 Pydantic *🏷*, ⏮️: + + `Item(**item.dict(), owner_id=user_id)` + +## 👑 **FastAPI** 📱 + +& 🔜 📁 `sql_app/main.py` ➡️ 🛠️ & ⚙️ 🌐 🎏 🍕 👥 ✍ ⏭. + +### ✍ 💽 🏓 + +📶 🙃 🌌 ✍ 💽 🏓: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="9" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="7" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +#### ⚗ 🗒 + +🛎 👆 🔜 🎲 🔢 👆 💽 (✍ 🏓, ♒️) ⏮️ . + +& 👆 🔜 ⚙️ ⚗ "🛠️" (👈 🚮 👑 👨‍🏭). + +"🛠️" ⚒ 🔁 💪 🕐❔ 👆 🔀 📊 👆 🇸🇲 🏷, 🚮 🆕 🔢, ♒️. 🔁 👈 🔀 💽, 🚮 🆕 🏓, 🆕 🏓, ♒️. + +👆 💪 🔎 🖼 ⚗ FastAPI 🏗 📄 ⚪️➡️ [🏗 ⚡ - 📄](../project-generation.md){.internal-link target=_blank}. 🎯 `alembic` 📁 ℹ 📟. + +### ✍ 🔗 + +🔜 ⚙️ `SessionLocal` 🎓 👥 ✍ `sql_app/database.py` 📁 ✍ 🔗. + +👥 💪 ✔️ 🔬 💽 🎉/🔗 (`SessionLocal`) 📍 📨, ⚙️ 🎏 🎉 🔘 🌐 📨 & ⤴️ 🔐 ⚫️ ⏮️ 📨 🏁. + +& ⤴️ 🆕 🎉 🔜 ✍ ⏭ 📨. + +👈, 👥 🔜 ✍ 🆕 🔗 ⏮️ `yield`, 🔬 ⏭ 📄 🔃 [🔗 ⏮️ `yield`](dependencies/dependencies-with-yield.md){.internal-link target=_blank}. + +👆 🔗 🔜 ✍ 🆕 🇸🇲 `SessionLocal` 👈 🔜 ⚙️ 👁 📨, & ⤴️ 🔐 ⚫️ 🕐 📨 🏁. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="15-20" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="13-18" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info + 👥 🚮 🏗 `SessionLocal()` & 🚚 📨 `try` 🍫. + + & ⤴️ 👥 🔐 ⚫️ `finally` 🍫. + + 👉 🌌 👥 ⚒ 💭 💽 🎉 🕧 📪 ⏮️ 📨. 🚥 📤 ⚠ ⏪ 🏭 📨. + + ✋️ 👆 💪 🚫 🤚 ➕1️⃣ ⚠ ⚪️➡️ 🚪 📟 (⏮️ `yield`). 👀 🌖 [🔗 ⏮️ `yield` & `HTTPException`](./dependencies/dependencies-with-yield.md#dependencies-with-yield-and-httpexception){.internal-link target=_blank} + +& ⤴️, 🕐❔ ⚙️ 🔗 *➡ 🛠️ 🔢*, 👥 📣 ⚫️ ⏮️ 🆎 `Session` 👥 🗄 🔗 ⚪️➡️ 🇸🇲. + +👉 🔜 ⤴️ 🤝 👥 👍 👨‍🎨 🐕‍🦺 🔘 *➡ 🛠️ 🔢*, ↩️ 👨‍🎨 🔜 💭 👈 `db` 🔢 🆎 `Session`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="24 32 38 47 53" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="22 30 36 45 51" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +!!! info "📡 ℹ" + 🔢 `db` 🤙 🆎 `SessionLocal`, ✋️ 👉 🎓 (✍ ⏮️ `sessionmaker()`) "🗳" 🇸🇲 `Session`,, 👨‍🎨 🚫 🤙 💭 ⚫️❔ 👩‍🔬 🚚. + + ✋️ 📣 🆎 `Session`, 👨‍🎨 🔜 💪 💭 💪 👩‍🔬 (`.add()`, `.query()`, `.commit()`, ♒️) & 💪 🚚 👍 🐕‍🦺 (💖 🛠️). 🆎 📄 🚫 📉 ☑ 🎚. + +### ✍ 👆 **FastAPI** *➡ 🛠️* + +🔜, 😒, 📥 🐩 **FastAPI** *➡ 🛠️* 📟. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="21-26 29-32 35-40 43-47 50-53" + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +👥 🏗 💽 🎉 ⏭ 🔠 📨 🔗 ⏮️ `yield`, & ⤴️ 📪 ⚫️ ⏮️. + +& ⤴️ 👥 💪 ✍ 🚚 🔗 *➡ 🛠️ 🔢*, 🤚 👈 🎉 🔗. + +⏮️ 👈, 👥 💪 🤙 `crud.get_user` 🔗 ⚪️➡️ 🔘 *➡ 🛠️ 🔢* & ⚙️ 👈 🎉. + +!!! tip + 👀 👈 💲 👆 📨 🇸🇲 🏷, ⚖️ 📇 🇸🇲 🏷. + + ✋️ 🌐 *➡ 🛠️* ✔️ `response_model` ⏮️ Pydantic *🏷* / 🔗 ⚙️ `orm_mode`, 💽 📣 👆 Pydantic 🏷 🔜 ⚗ ⚪️➡️ 👫 & 📨 👩‍💻, ⏮️ 🌐 😐 ⛽ & 🔬. + +!!! tip + 👀 👈 📤 `response_models` 👈 ✔️ 🐩 🐍 🆎 💖 `List[schemas.Item]`. + + ✋️ 🎚/🔢 👈 `List` Pydantic *🏷* ⏮️ `orm_mode`, 💽 🔜 🗃 & 📨 👩‍💻 🛎, 🍵 ⚠. + +### 🔃 `def` 🆚 `async def` + +📥 👥 ⚙️ 🇸🇲 📟 🔘 *➡ 🛠️ 🔢* & 🔗, &, 🔄, ⚫️ 🔜 🚶 & 🔗 ⏮️ 🔢 💽. + +👈 💪 ⚠ 🚚 "⌛". + +✋️ 🇸🇲 🚫 ✔️ 🔗 ⚙️ `await` 🔗, 🔜 ⏮️ 🕳 💖: + +```Python +user = await db.query(User).first() +``` + +...& ↩️ 👥 ⚙️: + +```Python +user = db.query(User).first() +``` + +⤴️ 👥 🔜 📣 *➡ 🛠️ 🔢* & 🔗 🍵 `async def`, ⏮️ 😐 `def`,: + +```Python hl_lines="2" +@app.get("/users/{user_id}", response_model=schemas.User) +def read_user(user_id: int, db: Session = Depends(get_db)): + db_user = crud.get_user(db, user_id=user_id) + ... +``` + +!!! info + 🚥 👆 💪 🔗 👆 🔗 💽 🔁, 👀 [🔁 🗄 (🔗) 💽](../advanced/async-sql-databases.md){.internal-link target=_blank}. + +!!! note "📶 📡 ℹ" + 🚥 👆 😟 & ✔️ ⏬ 📡 💡, 👆 💪 ✅ 📶 📡 ℹ ❔ 👉 `async def` 🆚 `def` 🍵 [🔁](../async.md#very-technical-details){.internal-link target=_blank} 🩺. + +## 🛠️ + +↩️ 👥 ⚙️ 🇸🇲 🔗 & 👥 🚫 🚚 🙆 😇 🔌-⚫️ 👷 ⏮️ **FastAPI**, 👥 💪 🛠️ 💽 🛠️ ⏮️ 🔗. + +& 📟 🔗 🇸🇲 & 🇸🇲 🏷 🖖 🎏 🔬 📁, 👆 🔜 💪 🎭 🛠️ ⏮️ ⚗ 🍵 ✔️ ❎ FastAPI, Pydantic, ⚖️ 🕳 🙆. + +🎏 🌌, 👆 🔜 💪 ⚙️ 🎏 🇸🇲 🏷 & 🚙 🎏 🍕 👆 📟 👈 🚫 🔗 **FastAPI**. + +🖼, 🖥 📋 👨‍🏭 ⏮️ 🥒, 🅿, ⚖️ 📶. + +## 📄 🌐 📁 + + 💭 👆 🔜 ✔️ 📁 📛 `my_super_project` 👈 🔌 🎧-📁 🤙 `sql_app`. + +`sql_app` 🔜 ✔️ 📄 📁: + +* `sql_app/__init__.py`: 🛁 📁. + +* `sql_app/database.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/database.py!} +``` + +* `sql_app/models.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/models.py!} +``` + +* `sql_app/schemas.py`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py310/schemas.py!} + ``` + +* `sql_app/crud.py`: + +```Python +{!../../../docs_src/sql_databases/sql_app/crud.py!} +``` + +* `sql_app/main.py`: + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app/main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} + ``` + +## ✅ ⚫️ + +👆 💪 📁 👉 📟 & ⚙️ ⚫️. + +!!! info + + 👐, 📟 🎦 📥 🍕 💯. 🌅 📟 👉 🩺. + +⤴️ 👆 💪 🏃 ⚫️ ⏮️ Uvicorn: + + +
+ +```console +$ uvicorn sql_app.main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +& ⤴️, 👆 💪 📂 👆 🖥 http://127.0.0.1:8000/docs. + +& 👆 🔜 💪 🔗 ⏮️ 👆 **FastAPI** 🈸, 👂 📊 ⚪️➡️ 🎰 💽: + + + +## 🔗 ⏮️ 💽 🔗 + +🚥 👆 💚 🔬 🗄 💽 (📁) 🔗, ➡ FastAPI, ℹ 🚮 🎚, 🚮 🏓, 🏓, ⏺, 🔀 📊, ♒️. 👆 💪 ⚙️ 💽 🖥 🗄. + +⚫️ 🔜 👀 💖 👉: + + + +👆 💪 ⚙️ 💳 🗄 🖥 💖 🗄 📋 ⚖️ ExtendsClass. + +## 🎛 💽 🎉 ⏮️ 🛠️ + +🚥 👆 💪 🚫 ⚙️ 🔗 ⏮️ `yield` - 🖼, 🚥 👆 🚫 ⚙️ **🐍 3️⃣.7️⃣** & 💪 🚫 ❎ "🐛" 🤔 🔛 **🐍 3️⃣.6️⃣** - 👆 💪 ⚒ 🆙 🎉 "🛠️" 🎏 🌌. + +"🛠️" 🌖 🔢 👈 🕧 🛠️ 🔠 📨, ⏮️ 📟 🛠️ ⏭, & 📟 🛠️ ⏮️ 🔗 🔢. + +### ✍ 🛠️ + +🛠️ 👥 🔜 🚮 (🔢) 🔜 ✍ 🆕 🇸🇲 `SessionLocal` 🔠 📨, 🚮 ⚫️ 📨 & ⤴️ 🔐 ⚫️ 🕐 📨 🏁. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python hl_lines="14-22" + {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} + ``` + +=== "🐍 3️⃣.9️⃣ & 🔛" + + ```Python hl_lines="12-20" + {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} + ``` + +!!! info + 👥 🚮 🏗 `SessionLocal()` & 🚚 📨 `try` 🍫. + + & ⤴️ 👥 🔐 ⚫️ `finally` 🍫. + + 👉 🌌 👥 ⚒ 💭 💽 🎉 🕧 📪 ⏮️ 📨. 🚥 📤 ⚠ ⏪ 🏭 📨. + +### 🔃 `request.state` + +`request.state` 🏠 🔠 `Request` 🎚. ⚫️ 📤 🏪 ❌ 🎚 📎 📨 ⚫️, 💖 💽 🎉 👉 💼. 👆 💪 ✍ 🌅 🔃 ⚫️ 💃 🩺 🔃 `Request` 🇵🇸. + +👥 👉 💼, ⚫️ ℹ 👥 🚚 👁 💽 🎉 ⚙️ 🔘 🌐 📨, & ⤴️ 🔐 ⏮️ (🛠️). + +### 🔗 ⏮️ `yield` ⚖️ 🛠️ + +❎ **🛠️** 📥 🎏 ⚫️❔ 🔗 ⏮️ `yield` 🔨, ⏮️ 🔺: + +* ⚫️ 🚚 🌖 📟 & 👄 🌅 🏗. +* 🛠️ ✔️ `async` 🔢. + * 🚥 📤 📟 ⚫️ 👈 ✔️ "⌛" 🕸, ⚫️ 💪 "🍫" 👆 🈸 📤 & 📉 🎭 🍖. + * 👐 ⚫️ 🎲 🚫 📶 ⚠ 📥 ⏮️ 🌌 `SQLAlchemy` 👷. + * ✋️ 🚥 👆 🚮 🌖 📟 🛠️ 👈 ✔️ 📚 👤/🅾 ⌛, ⚫️ 💪 ⤴️ ⚠. +* 🛠️ 🏃 *🔠* 📨. + * , 🔗 🔜 ✍ 🔠 📨. + * 🕐❔ *➡ 🛠️* 👈 🍵 👈 📨 🚫 💪 💽. + +!!! tip + ⚫️ 🎲 👍 ⚙️ 🔗 ⏮️ `yield` 🕐❔ 👫 🥃 ⚙️ 💼. + +!!! info + 🔗 ⏮️ `yield` 🚮 ⏳ **FastAPI**. + + ⏮️ ⏬ 👉 🔰 🕴 ✔️ 🖼 ⏮️ 🛠️ & 📤 🎲 📚 🈸 ⚙️ 🛠️ 💽 🎉 🧾. diff --git a/docs/em/docs/tutorial/static-files.md b/docs/em/docs/tutorial/static-files.md new file mode 100644 index 000000000..6090c5338 --- /dev/null +++ b/docs/em/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# 🎻 📁 + +👆 💪 🍦 🎻 📁 🔁 ⚪️➡️ 📁 ⚙️ `StaticFiles`. + +## ⚙️ `StaticFiles` + +* 🗄 `StaticFiles`. +* "🗻" `StaticFiles()` 👐 🎯 ➡. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.staticfiles import StaticFiles`. + + **FastAPI** 🚚 🎏 `starlette.staticfiles` `fastapi.staticfiles` 🏪 👆, 👩‍💻. ✋️ ⚫️ 🤙 👟 🔗 ⚪️➡️ 💃. + +### ⚫️❔ "🗜" + +"🗜" ⛓ ❎ 🏁 "🔬" 🈸 🎯 ➡, 👈 ⤴️ ✊ 💅 🚚 🌐 🎧-➡. + +👉 🎏 ⚪️➡️ ⚙️ `APIRouter` 🗻 🈸 🍕 🔬. 🗄 & 🩺 ⚪️➡️ 👆 👑 🈸 🏆 🚫 🔌 🕳 ⚪️➡️ 🗻 🈸, ♒️. + +👆 💪 ✍ 🌅 🔃 👉 **🏧 👩‍💻 🦮**. + +## ℹ + +🥇 `"/static"` 🔗 🎧-➡ 👉 "🎧-🈸" 🔜 "🗻" 🔛. , 🙆 ➡ 👈 ▶️ ⏮️ `"/static"` 🔜 🍵 ⚫️. + +`directory="static"` 🔗 📛 📁 👈 🔌 👆 🎻 📁. + +`name="static"` 🤝 ⚫️ 📛 👈 💪 ⚙️ 🔘 **FastAPI**. + +🌐 👫 🔢 💪 🎏 🌘 "`static`", 🔆 👫 ⏮️ 💪 & 🎯 ℹ 👆 👍 🈸. + +## 🌅 ℹ + +🌖 ℹ & 🎛 ✅ 💃 🩺 🔃 🎻 📁. diff --git a/docs/em/docs/tutorial/testing.md b/docs/em/docs/tutorial/testing.md new file mode 100644 index 000000000..999d67cd3 --- /dev/null +++ b/docs/em/docs/tutorial/testing.md @@ -0,0 +1,188 @@ +# 🔬 + +👏 💃, 🔬 **FastAPI** 🈸 ⏩ & 😌. + +⚫️ ⚓️ 🔛 🇸🇲, ❔ 🔄 🏗 ⚓️ 🔛 📨, ⚫️ 📶 😰 & 🏋️. + +⏮️ ⚫️, 👆 💪 ⚙️ 🔗 ⏮️ **FastAPI**. + +## ⚙️ `TestClient` + +!!! info + ⚙️ `TestClient`, 🥇 ❎ `httpx`. + + 🤶 Ⓜ. `pip install httpx`. + +🗄 `TestClient`. + +✍ `TestClient` 🚶‍♀️ 👆 **FastAPI** 🈸 ⚫️. + +✍ 🔢 ⏮️ 📛 👈 ▶️ ⏮️ `test_` (👉 🐩 `pytest` 🏛). + +⚙️ `TestClient` 🎚 🎏 🌌 👆 ⏮️ `httpx`. + +✍ 🙅 `assert` 📄 ⏮️ 🐩 🐍 🧬 👈 👆 💪 ✅ (🔄, 🐩 `pytest`). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! tip + 👀 👈 🔬 🔢 😐 `def`, 🚫 `async def`. + + & 🤙 👩‍💻 😐 🤙, 🚫 ⚙️ `await`. + + 👉 ✔ 👆 ⚙️ `pytest` 🔗 🍵 🤢. + +!!! note "📡 ℹ" + 👆 💪 ⚙️ `from starlette.testclient import TestClient`. + + **FastAPI** 🚚 🎏 `starlette.testclient` `fastapi.testclient` 🏪 👆, 👩‍💻. ✋️ ⚫️ 👟 🔗 ⚪️➡️ 💃. + +!!! tip + 🚥 👆 💚 🤙 `async` 🔢 👆 💯 ↖️ ⚪️➡️ 📨 📨 👆 FastAPI 🈸 (✅ 🔁 💽 🔢), ✔️ 👀 [🔁 💯](../advanced/async-tests.md){.internal-link target=_blank} 🏧 🔰. + +## 🎏 💯 + +🎰 🈸, 👆 🎲 🔜 ✔️ 👆 💯 🎏 📁. + +& 👆 **FastAPI** 🈸 5️⃣📆 ✍ 📚 📁/🕹, ♒️. + +### **FastAPI** 📱 📁 + +➡️ 💬 👆 ✔️ 📁 📊 🔬 [🦏 🈸](./bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +📁 `main.py` 👆 ✔️ 👆 **FastAPI** 📱: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### 🔬 📁 + +⤴️ 👆 💪 ✔️ 📁 `test_main.py` ⏮️ 👆 💯. ⚫️ 💪 🖖 🔛 🎏 🐍 📦 (🎏 📁 ⏮️ `__init__.py` 📁): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +↩️ 👉 📁 🎏 📦, 👆 💪 ⚙️ ⚖ 🗄 🗄 🎚 `app` ⚪️➡️ `main` 🕹 (`main.py`): + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...& ✔️ 📟 💯 💖 ⏭. + +## 🔬: ↔ 🖼 + +🔜 ➡️ ↔ 👉 🖼 & 🚮 🌖 ℹ 👀 ❔ 💯 🎏 🍕. + +### ↔ **FastAPI** 📱 📁 + +➡️ 😣 ⏮️ 🎏 📁 📊 ⏭: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +➡️ 💬 👈 🔜 📁 `main.py` ⏮️ 👆 **FastAPI** 📱 ✔️ 🎏 **➡ 🛠️**. + +⚫️ ✔️ `GET` 🛠️ 👈 💪 📨 ❌. + +⚫️ ✔️ `POST` 🛠️ 👈 💪 📨 📚 ❌. + +👯‍♂️ *➡ 🛠️* 🚚 `X-Token` 🎚. + +=== "🐍 3️⃣.6️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +=== "🐍 3️⃣.1️⃣0️⃣ & 🔛" + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +### ↔ 🔬 📁 + +👆 💪 ⤴️ ℹ `test_main.py` ⏮️ ↔ 💯: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +🕐❔ 👆 💪 👩‍💻 🚶‍♀️ ℹ 📨 & 👆 🚫 💭 ❔, 👆 💪 🔎 (🇺🇸🔍) ❔ ⚫️ `httpx`, ⚖️ ❔ ⚫️ ⏮️ `requests`, 🇸🇲 🔧 ⚓️ 🔛 📨' 🔧. + +⤴️ 👆 🎏 👆 💯. + +🤶 Ⓜ.: + +* 🚶‍♀️ *➡* ⚖️ *🔢* 🔢, 🚮 ⚫️ 📛 ⚫️. +* 🚶‍♀️ 🎻 💪, 🚶‍♀️ 🐍 🎚 (✅ `dict`) 🔢 `json`. +* 🚥 👆 💪 📨 *📨 💽* ↩️ 🎻, ⚙️ `data` 🔢 ↩️. +* 🚶‍♀️ *🎚*, ⚙️ `dict` `headers` 🔢. +* *🍪*, `dict` `cookies` 🔢. + +🌖 ℹ 🔃 ❔ 🚶‍♀️ 💽 👩‍💻 (⚙️ `httpx` ⚖️ `TestClient`) ✅ 🇸🇲 🧾. + +!!! info + 🗒 👈 `TestClient` 📨 💽 👈 💪 🗜 🎻, 🚫 Pydantic 🏷. + + 🚥 👆 ✔️ Pydantic 🏷 👆 💯 & 👆 💚 📨 🚮 💽 🈸 ⏮️ 🔬, 👆 💪 ⚙️ `jsonable_encoder` 🔬 [🎻 🔗 🔢](encoder.md){.internal-link target=_blank}. + +## 🏃 ⚫️ + +⏮️ 👈, 👆 💪 ❎ `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +⚫️ 🔜 🔍 📁 & 💯 🔁, 🛠️ 👫, & 📄 🏁 🔙 👆. + +🏃 💯 ⏮️: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml new file mode 100644 index 000000000..df21a1093 --- /dev/null +++ b/docs/em/mkdocs.yml @@ -0,0 +1,261 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/em/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: en +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - em: /em/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - hy: /hy/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - ta: /ta/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +- features.md +- fastapi-people.md +- python-types.md +- 🔰 - 👩‍💻 🦮: + - tutorial/index.md + - tutorial/first-steps.md + - tutorial/path-params.md + - tutorial/query-params.md + - tutorial/body.md + - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md + - tutorial/body-multiple-params.md + - tutorial/body-fields.md + - tutorial/body-nested-models.md + - tutorial/schema-extra-example.md + - tutorial/extra-data-types.md + - tutorial/cookie-params.md + - tutorial/header-params.md + - tutorial/response-model.md + - tutorial/extra-models.md + - tutorial/response-status-code.md + - tutorial/request-forms.md + - tutorial/request-files.md + - tutorial/request-forms-and-files.md + - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md + - tutorial/encoder.md + - tutorial/body-updates.md + - 🔗: + - tutorial/dependencies/index.md + - tutorial/dependencies/classes-as-dependencies.md + - tutorial/dependencies/sub-dependencies.md + - tutorial/dependencies/dependencies-in-path-operation-decorators.md + - tutorial/dependencies/global-dependencies.md + - tutorial/dependencies/dependencies-with-yield.md + - 💂‍♂: + - tutorial/security/index.md + - tutorial/security/first-steps.md + - tutorial/security/get-current-user.md + - tutorial/security/simple-oauth2.md + - tutorial/security/oauth2-jwt.md + - tutorial/middleware.md + - tutorial/cors.md + - tutorial/sql-databases.md + - tutorial/bigger-applications.md + - tutorial/background-tasks.md + - tutorial/metadata.md + - tutorial/static-files.md + - tutorial/testing.md + - tutorial/debugging.md +- 🏧 👩‍💻 🦮: + - advanced/index.md + - advanced/path-operation-advanced-configuration.md + - advanced/additional-status-codes.md + - advanced/response-directly.md + - advanced/custom-response.md + - advanced/additional-responses.md + - advanced/response-cookies.md + - advanced/response-headers.md + - advanced/response-change-status-code.md + - advanced/advanced-dependencies.md + - 🏧 💂‍♂: + - advanced/security/index.md + - advanced/security/oauth2-scopes.md + - advanced/security/http-basic-auth.md + - advanced/using-request-directly.md + - advanced/dataclasses.md + - advanced/middleware.md + - advanced/sql-databases-peewee.md + - advanced/async-sql-databases.md + - advanced/nosql-databases.md + - advanced/sub-applications.md + - advanced/behind-a-proxy.md + - advanced/templates.md + - advanced/graphql.md + - advanced/websockets.md + - advanced/events.md + - advanced/custom-request-and-route.md + - advanced/testing-websockets.md + - advanced/testing-events.md + - advanced/testing-dependencies.md + - advanced/testing-database.md + - advanced/async-tests.md + - advanced/settings.md + - advanced/conditional-openapi.md + - advanced/extending-openapi.md + - advanced/openapi-callbacks.md + - advanced/wsgi.md + - advanced/generate-clients.md +- async.md +- 🛠️: + - deployment/index.md + - deployment/versions.md + - deployment/https.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 +- external-links.md +- benchmarks.md +- help-fastapi.md +- contributing.md +- release-notes.md +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +- attr_list +- md_in_html +extra: + analytics: + provider: google + property: G-YNEVN69SC3 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /em/ + name: 😉 + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /hy/ + name: hy + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /ta/ + name: ta - தமிழ் + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/em/overrides/.gitignore b/docs/em/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a5d77acbf..fc21439ae 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -212,6 +213,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index a89aeb21a..485a2dd70 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -115,6 +116,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index f77f82f69..914b46e1a 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 19182f381..3774d9d42 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -132,6 +133,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index c5689395f..094c5d82e 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index bc64e78f2..ba7c687c1 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -56,6 +57,7 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ + - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -81,7 +83,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -104,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ @@ -134,6 +138,8 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska + - link: /ta/ + name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 7b7875ef7..ca6e09551 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 9393c3663..4633dd017 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 3703398af..9f4342e76 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -149,6 +150,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 29b684371..7d429478c 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -117,6 +118,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index d9b1bc1b8..e187ee383 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 8d0d20239..c781f9783 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -108,6 +109,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 2a8302715..a8ab4cb32 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -142,6 +143,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 4b9727872..808479198 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -119,6 +120,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index f24c7c503..2766b0adf 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 574cc5abd..5aa37ece6 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index e31821e4b..884115044 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -41,10 +41,12 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ - he: /he/ + - hy: /hy/ - id: /id/ - it: /it/ - ja: /ja/ @@ -81,7 +83,7 @@ markdown_extensions: extra: analytics: provider: google - property: UA-133183413-1 + property: G-YNEVN69SC3 social: - icon: fontawesome/brands/github-alt link: https://github.com/tiangolo/fastapi @@ -104,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ @@ -112,6 +116,8 @@ extra: name: fr - français - link: /he/ name: he + - link: /hy/ + name: hy - link: /id/ name: id - link: /it/ diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 19dcf2099..23d6b9708 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -110,6 +111,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index b8152e821..e9339997f 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -105,6 +106,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index d25881c43..906fcf1d6 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -41,6 +41,7 @@ nav: - en: / - az: /az/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -162,6 +163,8 @@ extra: name: az - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ From 6ab811763f2686a1745404b8cc9a399f4b905d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 5 Apr 2023 17:09:04 +0200 Subject: [PATCH 0874/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20databento,=20remove=20Ines's=20course=20and=20StriveWorks?= =?UTF-8?q?=20(#9351)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 9 +- docs/en/data/sponsors_badge.yml | 3 +- docs/en/docs/img/sponsors/databento.svg | 144 ++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 docs/en/docs/img/sponsors/databento.svg diff --git a/README.md b/README.md index 39030ef52..ecb207f6a 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index b9be9810d..6bde2e163 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -24,19 +24,16 @@ silver: - 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 - - url: https://www.udemy.com/course/fastapi-rest/ - title: Learn FastAPI by building a complete project. Extend your knowledge on advanced web development-AWS, Payments, Emails. - img: https://fastapi.tiangolo.com/img/sponsors/ines-course.jpg - url: https://careers.powens.com/ title: Powens is hiring! img: https://fastapi.tiangolo.com/img/sponsors/powens.png - url: https://www.svix.com/ title: Svix - Webhooks as a service img: https://fastapi.tiangolo.com/img/sponsors/svix.svg + - url: https://databento.com/ + title: Pay as you go for market data + img: https://fastapi.tiangolo.com/img/sponsors/databento.svg bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png - - url: https://bit.ly/3ccLCmM - title: https://striveworks.us/careers - img: https://fastapi.tiangolo.com/img/sponsors/striveworks2.png diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 70a7548e4..a95af177c 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -5,9 +5,7 @@ logins: - mikeckennedy - deepset-ai - cryptapi - - Striveworks - xoflare - - InesIvanova - DropbaseHQ - VincentParedes - BLUE-DEVIL1134 @@ -16,3 +14,4 @@ logins: - nihpo - svix - armand-sauzay + - databento-bot diff --git a/docs/en/docs/img/sponsors/databento.svg b/docs/en/docs/img/sponsors/databento.svg new file mode 100644 index 000000000..dfdd9bee6 --- /dev/null +++ b/docs/en/docs/img/sponsors/databento.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From ac4bf3b5eb279650b24971d2c44cd70fae517c9c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 10 Apr 2023 18:16:44 +0000 Subject: [PATCH 0875/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7a9a09eed..d6800c92b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). ## 0.95.0 From 6ce6c8954cf0bf526cc605e8f5eb1e3f2b95c675 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 10 Apr 2023 18:28:00 +0000 Subject: [PATCH 0876/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d6800c92b..44df9205a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). * 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). ## 0.95.0 From fdf66c825ed70da423724180e91b02345aa36326 Mon Sep 17 00:00:00 2001 From: Sharon Yogev <31185192+sharonyogev@users.noreply.github.com> Date: Thu, 13 Apr 2023 20:49:22 +0300 Subject: [PATCH 0877/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20using=20`Annotat?= =?UTF-8?q?ed`=20in=20routers=20or=20path=20operations=20decorated=20multi?= =?UTF-8?q?ple=20times=20(#9315)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: copy FieldInfo from Annotated arguments We need to copy the field_info to prevent ourselves from mutating it. This allows multiple path or nested routers ,etc. * 📝 Add comment in fastapi/dependencies/utils.py Co-authored-by: Nadav Zingerman <7372858+nzig@users.noreply.github.com> * ✅ Extend and tweak tests for Annotated * ✅ Tweak coverage, it's probably covered by a different version of Python --------- Co-authored-by: Sebastián Ramírez Co-authored-by: Nadav Zingerman <7372858+nzig@users.noreply.github.com> --- fastapi/dependencies/utils.py | 5 ++-- tests/test_annotated.py | 43 ++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index c581348c9..f131001ce 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,7 @@ import dataclasses import inspect from contextlib import contextmanager -from copy import deepcopy +from copy import copy, deepcopy from typing import ( Any, Callable, @@ -383,7 +383,8 @@ def analyze_param( ), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}" fastapi_annotation = next(iter(fastapi_annotations), None) if isinstance(fastapi_annotation, FieldInfo): - field_info = fastapi_annotation + # Copy `field_info` because we mutate `field_info.default` below. + field_info = copy(fastapi_annotation) assert field_info.default is Undefined or field_info.default is Required, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 556019897..30c8efe01 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,5 +1,5 @@ import pytest -from fastapi import FastAPI, Query +from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient from typing_extensions import Annotated @@ -224,3 +224,44 @@ def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_multiple_path(): + @app.get("/test1") + @app.get("/test2") + async def test(var: Annotated[str, Query()] = "bar"): + return {"foo": var} + + response = client.get("/test1") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + + response = client.get("/test1", params={"var": "baz"}) + assert response.status_code == 200 + assert response.json() == {"foo": "baz"} + + response = client.get("/test2") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} + + response = client.get("/test2", params={"var": "baz"}) + assert response.status_code == 200 + assert response.json() == {"foo": "baz"} + + +def test_nested_router(): + app = FastAPI() + + router = APIRouter(prefix="/nested") + + @router.get("/test") + async def test(var: Annotated[str, Query()] = "bar"): + return {"foo": var} + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/nested/test") + assert response.status_code == 200 + assert response.json() == {"foo": "bar"} From 3eac0a4a87e2fa5d9c1e56d603cf4bec843f9a8e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 17:49:59 +0000 Subject: [PATCH 0878/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 44df9205a..9ef12da04 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). * 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). * 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). From 221b22200a6e61af3ac2a65aee20717afbf7d9f9 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Thu, 13 Apr 2023 20:52:11 +0300 Subject: [PATCH 0879/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/benchmarks.md`=20(#9271)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * del docs/ru/docs/contributing.md * ru translate for */docs/ru/docs/project-generation.md * docs/ru/docs/benchmarks.md * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Delete project-generation.md * Update benchmarks.md * Update benchmarks.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ru/docs/benchmarks.md | 37 +++++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 2 ++ 2 files changed, 39 insertions(+) create mode 100644 docs/ru/docs/benchmarks.md diff --git a/docs/ru/docs/benchmarks.md b/docs/ru/docs/benchmarks.md new file mode 100644 index 000000000..259dca8e6 --- /dev/null +++ b/docs/ru/docs/benchmarks.md @@ -0,0 +1,37 @@ +# Замеры производительности + +Независимые тесты производительности приложений от TechEmpower показывают, что **FastAPI** под управлением Uvicorn один из самых быстрых Python-фреймворков и уступает только Starlette и Uvicorn (которые используются в FastAPI). (*) + +Но при просмотре и сравнении замеров производительности следует иметь в виду нижеописанное. + +## Замеры производительности и скорости + +В подобных тестах часто можно увидеть, что инструменты разного типа сравнивают друг с другом, как аналогичные. + +В частности, сравнивают вместе Uvicorn, Starlette и FastAPI (среди многих других инструментов). + +Чем проще проблема, которую решает инструмент, тем выше его производительность. И большинство тестов не проверяют дополнительные функции, предоставляемые инструментом. + +Иерархия инструментов имеет следующий вид: + +* **Uvicorn**: ASGI-сервер + * **Starlette** (использует Uvicorn): веб-микрофреймворк + * **FastAPI** (использует Starlette): API-микрофреймворк с дополнительными функциями для создания API, с валидацией данных и т.д. + +* **Uvicorn**: + * Будет иметь наилучшую производительность, так как не имеет большого количества дополнительного кода, кроме самого сервера. + * Вы не будете писать приложение на Uvicorn напрямую. Это означало бы, что Ваш код должен включать как минимум весь + код, предоставляемый Starlette (или **FastAPI**). И если Вы так сделаете, то в конечном итоге Ваше приложение будет иметь те же накладные расходы, что и при использовании фреймворка, минимизирующего код Вашего приложения и Ваши ошибки. + * Uvicorn подлежит сравнению с Daphne, Hypercorn, uWSGI и другими веб-серверами. + +* **Starlette**: + * Будет уступать Uvicorn по производительности. Фактически Starlette управляется Uvicorn и из-за выполнения большего количества кода он не может быть быстрее, чем Uvicorn. + * Зато он предоставляет Вам инструменты для создания простых веб-приложений с обработкой маршрутов URL и т.д. + * Starlette следует сравнивать с Sanic, Flask, Django и другими веб-фреймворками (или микрофреймворками). + +* **FastAPI**: + * Так же как Starlette использует Uvicorn и не может быть быстрее него, **FastAPI** использует Starlette, то есть он не может быть быстрее Starlette. + * FastAPI предоставляет больше возможностей поверх Starlette, которые наверняка Вам понадобятся при создании API, такие как проверка данных и сериализация. В довесок Вы ещё и получаете автоматическую документацию (автоматическая документация даже не увеличивает накладные расходы при работе приложения, так как она создается при запуске). + * Если Вы не используете FastAPI, а используете Starlette напрямую (или другой инструмент вроде Sanic, Flask, Responder и т.д.), Вам пришлось бы самостоятельно реализовать валидацию и сериализацию данных. То есть, в итоге, Ваше приложение имело бы такие же накладные расходы, как если бы оно было создано с использованием FastAPI. И во многих случаях валидация и сериализация данных представляют собой самый большой объём кода, написанного в приложениях. + * Таким образом, используя FastAPI Вы потратите меньше времени на разработку, уменьшите количество ошибок, строк кода и, вероятно, получите ту же производительность (или лучше), как и если бы не использовали его (поскольку Вам пришлось бы реализовать все его возможности в своем коде). + * FastAPI должно сравнивать с фреймворками веб-приложений (или наборами инструментов), которые обеспечивают валидацию и сериализацию данных, а также предоставляют автоматическую документацию, такими как Flask-apispec, NestJS, Molten и им подобные. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 808479198..ed433523b 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -74,6 +74,8 @@ nav: - deployment/versions.md - history-design-future.md - external-links.md +- benchmarks.md +- help-fastapi.md - contributing.md markdown_extensions: - toc: From 08f049208ea27de57464551a9a4fef68b228d054 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 17:52:45 +0000 Subject: [PATCH 0880/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9ef12da04..5a3268fbd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). * 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). * 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). * 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). From 6bc0d210da667e032202d62f7efdc6a4a1b2e270 Mon Sep 17 00:00:00 2001 From: dedkot Date: Thu, 13 Apr 2023 20:58:09 +0300 Subject: [PATCH 0881/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/query-params-str-validatio?= =?UTF-8?q?ns.md`=20(#9267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../tutorial/query-params-str-validations.md | 919 ++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 920 insertions(+) create mode 100644 docs/ru/docs/tutorial/query-params-str-validations.md diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..68042db63 --- /dev/null +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,919 @@ +# Query-параметры и валидация строк + +**FastAPI** позволяет определять дополнительную информацию и валидацию для ваших параметров. + +Давайте рассмотрим следующий пример: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` + +Query-параметр `q` имеет тип `Union[str, None]` (или `str | None` в Python 3.10). Это означает, что входной параметр будет типа `str`, но может быть и `None`. Ещё параметр имеет значение по умолчанию `None`, из-за чего FastAPI определит параметр как необязательный. + +!!! note "Технические детали" + FastAPI определит параметр `q` как необязательный, потому что его значение по умолчанию `= None`. + + `Union` в `Union[str, None]` позволит редактору кода оказать вам лучшую поддержку и найти ошибки. + +## Расширенная валидация + +Добавим дополнительное условие валидации параметра `q` - **длина строки не более 50 символов** (условие проверяется всякий раз, когда параметр `q` не является `None`). + +### Импорт `Query` и `Annotated` + +Чтобы достичь этого, первым делом нам нужно импортировать: + +* `Query` из пакета `fastapi`: +* `Annotated` из пакета `typing` (или из `typing_extensions` для Python ниже 3.9) + +=== "Python 3.10+" + + В Python 3.9 или выше, `Annotated` является частью стандартной библиотеки, таким образом вы можете импортировать его из `typing`. + + ```Python hl_lines="1 3" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6+" + + В версиях Python ниже Python 3.9 `Annotation` импортируется из `typing_extensions`. + + Эта библиотека будет установлена вместе с FastAPI. + + ```Python hl_lines="3-4" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +## `Annotated` как тип для query-параметра `q` + +Помните, как ранее я говорил об Annotated? Он может быть использован для добавления метаданных для ваших параметров в разделе [Введение в аннотации типов Python](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? + +Пришло время использовать их в FastAPI. 🚀 + +У нас была аннотация следующего типа: + +=== "Python 3.10+" + + ```Python + q: str | None = None + ``` + +=== "Python 3.6+" + + ```Python + q: Union[str, None] = None + ``` + +Вот что мы получим, если обернём это в `Annotated`: + +=== "Python 3.10+" + + ```Python + q: Annotated[str | None] = None + ``` + +=== "Python 3.6+" + + ```Python + q: Annotated[Union[str, None]] = None + ``` + +Обе эти версии означают одно и тоже. `q` - это параметр, который может быть `str` или `None`, и по умолчанию он будет принимать `None`. + +Давайте повеселимся. 🎉 + +## Добавим `Query` в `Annotated` для query-параметра `q` + +Теперь, когда у нас есть `Annotated`, где мы можем добавить больше метаданных, добавим `Query` со значением параметра `max_length` равным 50: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} + ``` + +Обратите внимание, что значение по умолчанию всё ещё `None`, так что параметр остаётся необязательным. + +Однако теперь, имея `Query(max_length=50)` внутри `Annotated`, мы говорим FastAPI, что мы хотим извлечь это значение из параметров query-запроса (что произойдёт в любом случае 🤷), и что мы хотим иметь **дополнительные условия валидации** для этого значения (для чего мы и делаем это - чтобы получить дополнительную валидацию). 😎 + +Теперь FastAPI: + +* **Валидирует** (проверяет), что полученные данные состоят максимум из 50 символов +* Показывает **исчерпывающую ошибку** (будет описание местонахождения ошибки и её причины) для клиента в случаях, когда данные не валидны +* **Задокументирует** параметр в схему OpenAPI *операции пути* (что будет отображено в **UI автоматической документации**) + +## Альтернативный (устаревший) способ задать `Query` как значение по умолчанию + +В предыдущих версиях FastAPI (ниже 0.95.0) необходимо было использовать `Query` как значение по умолчанию для query-параметра. Так было вместо размещения его в `Annotated`, так что велика вероятность, что вам встретится такой код. Сейчас объясню. + +!!! tip "Подсказка" + При написании нового кода и везде где это возможно, используйте `Annotated`, как было описано ранее. У этого способа есть несколько преимуществ (о них дальше) и никаких недостатков. 🍰 + +Вот как вы могли бы использовать `Query()` в качестве значения по умолчанию параметра вашей функции, установив для параметра `max_length` значение 50: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} + ``` + +В таком случае (без использования `Annotated`), мы заменили значение по умолчанию с `None` на `Query()` в функции. Теперь нам нужно установить значение по умолчанию для query-параметра `Query(default=None)`, что необходимо для тех же целей, как когда ранее просто указывалось значение по умолчанию (по крайней мере, для FastAPI). + +Таким образом: + +```Python +q: Union[str, None] = Query(default=None) +``` + +...делает параметр необязательным со значением по умолчанию `None`, также как это делает: + +```Python +q: Union[str, None] = None +``` + +И для Python 3.10 и выше: + +```Python +q: str | None = Query(default=None) +``` + +...делает параметр необязательным со значением по умолчанию `None`, также как это делает: + +```Python +q: str | None = None +``` + +Но он явно объявляет его как query-параметр. + +!!! info "Дополнительная информация" + Запомните, важной частью объявления параметра как необязательного является: + + ```Python + = None + ``` + + или: + + ```Python + = Query(default=None) + ``` + + так как `None` указан в качестве значения по умолчанию, параметр будет **необязательным**. + + `Union[str, None]` позволит редактору кода оказать вам лучшую поддержку. Но это не то, на что обращает внимание FastAPI для определения необязательности параметра. + +Теперь, мы можем указать больше параметров для `Query`. В данном случае, параметр `max_length` применяется к строкам: + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +Входные данные будут проверены. Если данные недействительны, тогда будет указано на ошибку в запросе (будет описание местонахождения ошибки и её причины). Кроме того, параметр задокументируется в схеме OpenAPI данной *операции пути*. + +### Использовать `Query` как значение по умолчанию или добавить в `Annotated` + +Когда `Query` используется внутри `Annotated`, вы не можете использовать параметр `default` у `Query`. + +Вместо этого, используйте обычное указание значения по умолчанию для параметра функции. Иначе, это будет несовместимо. + +Следующий пример не рабочий: + +```Python +q: Annotated[str, Query(default="rick")] = "morty" +``` + +...потому что нельзя однозначно определить, что именно должно быть значением по умолчанию: `"rick"` или `"morty"`. + +Вам следует использовать (предпочтительно): + +```Python +q: Annotated[str, Query()] = "rick" +``` + +...или как в старом коде, который вам может попасться: + +```Python +q: str = Query(default="rick") +``` + +### Преимущества `Annotated` + +**Рекомендуется использовать `Annotated`** вместо значения по умолчанию в параметрах функции, потому что так **лучше** по нескольким причинам. 🤓 + +Значение **по умолчанию** у **параметров функции** - это **действительно значение по умолчанию**, что более интуитивно понятно для пользователей Python. 😌 + +Вы можете **вызвать** ту же функцию в **иных местах** без FastAPI, и она **сработает как ожидается**. Если это **обязательный** параметр (без значения по умолчанию), ваш **редактор кода** сообщит об ошибке. **Python** также укажет на ошибку, если вы вызовете функцию без передачи ей обязательного параметра. + +Если вы вместо `Annotated` используете **(устаревший) стиль значений по умолчанию**, тогда при вызове этой функции без FastAPI в **другом месте** вам необходимо **помнить** о передаче аргументов функции, чтобы она работала корректно. В противном случае, значения будут отличаться от тех, что вы ожидаете (например, `QueryInfo` или что-то подобное вместо `str`). И ни ваш редактор кода, ни Python не будут жаловаться на работу этой функции, только когда вычисления внутри дадут сбой. + +Так как `Annotated` может принимать более одной аннотации метаданных, то теперь вы можете использовать ту же функцию с другими инструментами, например Typer. 🚀 + +## Больше валидации + +Вы также можете добавить параметр `min_length`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial003.py!} + ``` + +## Регулярные выражения + +Вы можете определить регулярное выражение, которому должен соответствовать параметр: + +=== "Python 3.10+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial004.py!} + ``` + +Данное регулярное выражение проверяет, что полученное значение параметра: + +* `^`: начало строки. +* `fixedquery`: в точности содержит строку `fixedquery`. +* `$`: конец строки, не имеет символов после `fixedquery`. + +Не переживайте, если **"регулярное выражение"** вызывает у вас трудности. Это достаточно сложная тема для многих людей. Вы можете сделать множество вещей без использования регулярных выражений. + +Но когда они вам понадобятся, и вы закончите их освоение, то не будет проблемой использовать их в **FastAPI**. + +## Значения по умолчанию + +Вы точно также можете указать любое значение `по умолчанию`, как ранее указывали `None`. + +Например, вы хотите для параметра запроса `q` указать, что он должен состоять минимум из 3 символов (`min_length=3`) и иметь значение по умолчанию `"fixedquery"`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial005.py!} + ``` + +!!! note "Технические детали" + Наличие значения по умолчанию делает параметр необязательным. + +## Обязательный параметр + +Когда вам не требуется дополнительная валидация или дополнительные метаданные для параметра запроса, вы можете сделать параметр `q` обязательным просто не указывая значения по умолчанию. Например: + +```Python +q: str +``` + +вместо: + +```Python +q: Union[str, None] = None +``` + +Но у нас query-параметр определён как `Query`. Например: + +=== "Annotated" + + ```Python + q: Annotated[Union[str, None], Query(min_length=3)] = None + ``` + +=== "без Annotated" + + ```Python + q: Union[str, None] = Query(default=None, min_length=3) + ``` + +В таком случае, чтобы сделать query-параметр `Query` обязательным, вы можете просто не указывать значение по умолчанию: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006.py!} + ``` + + !!! tip "Подсказка" + Обратите внимание, что даже когда `Query()` используется как значение по умолчанию для параметра функции, мы не передаём `default=None` в `Query()`. + + Лучше будет использовать версию с `Annotated`. 😉 + +### Обязательный параметр с Ellipsis (`...`) + +Альтернативный способ указать обязательность параметра запроса - это указать параметр `default` через многоточие `...`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006b.py!} + ``` + +!!! info "Дополнительная информация" + Если вы ранее не сталкивались с `...`: это специальное значение, часть языка Python и называется "Ellipsis". + + Используется в Pydantic и FastAPI для определения, что значение требуется обязательно. + +Таким образом, **FastAPI** определяет, что параметр является обязательным. + +### Обязательный параметр с `None` + +Вы можете определить, что параметр может принимать `None`, но всё ещё является обязательным. Это может потребоваться для того, чтобы пользователи явно указали параметр, даже если его значение будет `None`. + +Чтобы этого добиться, вам нужно определить `None` как валидный тип для параметра запроса, но также указать `default=...`: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial006c.py!} + ``` + +!!! tip "Подсказка" + Pydantic, мощь которого используется в FastAPI для валидации и сериализации, имеет специальное поведение для `Optional` или `Union[Something, None]` без значения по умолчанию. Вы можете узнать об этом больше в документации Pydantic, раздел Обязательные Опциональные поля. + +### Использование Pydantic's `Required` вместо Ellipsis (`...`) + +Если вас смущает `...`, вы можете использовать `Required` из Pydantic: + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="2 9" + {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="2 8" + {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} + ``` + +!!! tip "Подсказка" + Запомните, когда вам необходимо объявить query-параметр обязательным, вы можете просто не указывать параметр `default`. Таким образом, вам редко придётся использовать `...` или `Required`. + +## Множество значений для query-параметра + +Для query-параметра `Query` можно указать, что он принимает список значений (множество значений). + +Например, query-параметр `q` может быть указан в URL несколько раз. И если вы ожидаете такой формат запроса, то можете указать это следующим образом: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py310.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial011.py!} + ``` + +Затем, получив такой URL: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +вы бы получили несколько значений (`foo` и `bar`), которые относятся к параметру `q`, в виде Python `list` внутри вашей *функции обработки пути*, в *параметре функции* `q`. + +Таким образом, ответ на этот URL будет: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "Подсказка" + Чтобы объявить query-параметр типом `list`, как в примере выше, вам нужно явно использовать `Query`, иначе он будет интерпретирован как тело запроса. + +Интерактивная документация API будет обновлена соответствующим образом, где будет разрешено множество значений: + + + +### Query-параметр со множеством значений по умолчанию + +Вы также можете указать тип `list` со списком значений по умолчанию на случай, если вам их не предоставят: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial012.py!} + ``` + +Если вы перейдёте по ссылке: + +``` +http://localhost:8000/items/ +``` + +значение по умолчанию для `q` будет: `["foo", "bar"]` и ответом для вас будет: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Использование `list` + +Вы также можете использовать `list` напрямую вместо `List[str]` (или `list[str]` в Python 3.9+): + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial013.py!} + ``` + +!!! note "Технические детали" + Запомните, что в таком случае, FastAPI не будет проверять содержимое списка. + + Например, для List[int] список будет провалидирован (и задокументирован) на содержание только целочисленных элементов. Но для простого `list` такой проверки не будет. + +## Больше метаданных + +Вы можете добавить больше информации об query-параметре. + +Указанная информация будет включена в генерируемую OpenAPI документацию и использована в пользовательском интерфейсе и внешних инструментах. + +!!! note "Технические детали" + Имейте в виду, что разные инструменты могут иметь разные уровни поддержки OpenAPI. + + Некоторые из них могут не отображать (на данный момент) всю заявленную дополнительную информацию, хотя в большинстве случаев отсутствующая функция уже запланирована к разработке. + +Вы можете указать название query-параметра, используя параметр `title`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial007.py!} + ``` + +Добавить описание, используя параметр `description`: + +=== "Python 3.10+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="12" + {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="13" + {!> ../../../docs_src/query_params_str_validations/tutorial008.py!} + ``` + +## Псевдонимы параметров + +Представьте, что вы хотите использовать query-параметр с названием `item-query`. + +Например: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Но `item-query` является невалидным именем переменной в Python. + +Наиболее похожее валидное имя `item_query`. + +Но вам всё равно необходим `item-query`... + +Тогда вы можете объявить `псевдоним`, и этот псевдоним будет использоваться для поиска значения параметра запроса: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial009.py!} + ``` + +## Устаревшие параметры + +Предположим, вы больше не хотите использовать какой-либо параметр. + +Вы решили оставить его, потому что клиенты всё ещё им пользуются. Но вы хотите отобразить это в документации как устаревший функционал. + +Тогда для `Query` укажите параметр `deprecated=True`: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="18" + {!> ../../../docs_src/query_params_str_validations/tutorial010.py!} + ``` + +В документации это будет отображено следующим образом: + + + +## Исключить из OpenAPI + +Чтобы исключить query-параметр из генерируемой OpenAPI схемы (а также из системы автоматической генерации документации), укажите в `Query` параметр `include_in_schema=False`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params_str_validations/tutorial014.py!} + ``` + +## Резюме + +Вы можете объявлять дополнительные правила валидации и метаданные для ваших параметров запроса. + +Общие метаданные: + +* `alias` +* `title` +* `description` +* `deprecated` +* `include_in_schema` + +Специфичные правила валидации для строк: + +* `min_length` +* `max_length` +* `regex` + +В рассмотренных примерах показано объявление правил валидации для строковых значений `str`. + +В следующих главах вы увидете, как объявлять правила валидации для других типов (например, чисел). diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index ed433523b..5b038e2b7 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -65,6 +65,7 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/query-params-str-validations.md - tutorial/body-fields.md - tutorial/background-tasks.md - tutorial/cookie-params.md From c4128e7f5a5c03d7b3696f1912a002185b40a577 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 17:58:46 +0000 Subject: [PATCH 0882/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5a3268fbd..348805df8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). * 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). * 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). * 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). From 1ae5466140082e7cbcd89dd26164f2fe7a97339b Mon Sep 17 00:00:00 2001 From: Cedric Fraboulet <62244267+frabc@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:59:44 +0200 Subject: [PATCH 0883/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/index.md`=20(#9265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(index): copy the new index.md from docs/en * docs(index): add translation for docs/index.md * Apply rjNemo's suggestions * Apply Viicos's suggestions --- docs/fr/docs/index.md | 339 +++++++++++++++++++++--------------------- 1 file changed, 171 insertions(+), 168 deletions(-) diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index e7fb9947d..5ee8b462f 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -1,48 +1,46 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

- FastAPI framework, high performance, easy to learn, fast to code, ready for production + Framework FastAPI, haute performance, facile à apprendre, rapide à coder, prêt pour la production

- - Test + + Test - - Coverage + + Coverage Package version + + Supported Python versions +

--- -**Documentation**: https://fastapi.tiangolo.com +**Documentation** : https://fastapi.tiangolo.com -**Source Code**: https://github.com/tiangolo/fastapi +**Code Source** : https://github.com/tiangolo/fastapi --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. +FastAPI est un framework web moderne et rapide (haute performance) pour la création d'API avec Python 3.7+, basé sur les annotations de type standard de Python. -The key features are: +Les principales fonctionnalités sont : -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Rapidité** : De très hautes performances, au niveau de **NodeJS** et **Go** (grâce à Starlette et Pydantic). [L'un des frameworks Python les plus rapides](#performance). +* **Rapide à coder** : Augmente la vitesse de développement des fonctionnalités d'environ 200 % à 300 %. * +* **Moins de bugs** : Réduit d'environ 40 % les erreurs induites par le développeur. * +* **Intuitif** : Excellente compatibilité avec les IDE. Complétion complète. Moins de temps passé à déboguer. +* **Facile** : Conçu pour être facile à utiliser et à apprendre. Moins de temps passé à lire la documentation. +* **Concis** : Diminue la duplication de code. De nombreuses fonctionnalités liées à la déclaration de chaque paramètre. Moins de bugs. +* **Robuste** : Obtenez un code prêt pour la production. Avec une documentation interactive automatique. +* **Basé sur des normes** : Basé sur (et entièrement compatible avec) les standards ouverts pour les APIs : OpenAPI (précédemment connu sous le nom de Swagger) et JSON Schema. -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. +* estimation basée sur des tests d'une équipe de développement interne, construisant des applications de production. ## Sponsors @@ -63,60 +61,66 @@ The key features are: ## Opinions -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" +"_[...] J'utilise beaucoup **FastAPI** ces derniers temps. [...] Je prévois de l'utiliser dans mon équipe pour tous les **services de ML chez Microsoft**. Certains d'entre eux seront intégrés dans le coeur de **Windows** et dans certains produits **Office**._"
Kabir Khan - Microsoft (ref)
--- -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" +"_Nous avons adopté la bibliothèque **FastAPI** pour créer un serveur **REST** qui peut être interrogé pour obtenir des **prédictions**. [pour Ludwig]_" -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+
Piero Molino, Yaroslav Dudin et Sai Sumanth Miryala - Uber (ref)
--- -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" +"_**Netflix** a le plaisir d'annoncer la sortie en open-source de notre framework d'orchestration de **gestion de crise** : **Dispatch** ! [construit avec **FastAPI**]_"
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
--- -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" +"_Je suis très enthousiaste à propos de **FastAPI**. C'est un bonheur !_" -
Brian Okken - Python Bytes podcast host (ref)
+
Brian Okken - Auteur du podcast Python Bytes (ref)
--- -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" +"_Honnêtement, ce que vous avez construit a l'air super solide et élégant. A bien des égards, c'est comme ça que je voulais que **Hug** soit - c'est vraiment inspirant de voir quelqu'un construire ça._" -
Timothy Crosley - Hug creator (ref)
+
Timothy Crosley - Créateur de Hug (ref)
--- -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" +"_Si vous cherchez à apprendre un **framework moderne** pour créer des APIs REST, regardez **FastAPI** [...] C'est rapide, facile à utiliser et à apprendre [...]_" -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" +"_Nous sommes passés à **FastAPI** pour nos **APIs** [...] Je pense que vous l'aimerez [...]_" -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - Fondateurs de Explosion AI - Créateurs de spaCy (ref) - (ref)
--- -## **Typer**, the FastAPI of CLIs +"_Si quelqu'un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **bien conçu**, **simple à utiliser** et **très évolutif**. Il est devenu un **composant clé** dans notre stratégie de développement API first et il est à l'origine de nombreux automatismes et services tels que notre ingénieur virtuel TAC._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, le FastAPI des CLI -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. +Si vous souhaitez construire une application CLI utilisable dans un terminal au lieu d'une API web, regardez **Typer**. -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 +**Typer** est le petit frère de FastAPI. Et il est destiné à être le **FastAPI des CLI**. ⌨️ 🚀 -## Requirements +## Prérequis Python 3.7+ -FastAPI stands on the shoulders of giants: +FastAPI repose sur les épaules de géants : -* Starlette for the web parts. -* Pydantic for the data parts. +* Starlette pour les parties web. +* Pydantic pour les parties données. ## Installation @@ -130,7 +134,7 @@ $ pip install fastapi
-You will also need an ASGI server, for production such as Uvicorn or Hypercorn. +Vous aurez également besoin d'un serveur ASGI pour la production tel que Uvicorn ou Hypercorn.
@@ -142,11 +146,11 @@ $ pip install "uvicorn[standard]"
-## Example +## Exemple -### Create it +### Créez -* Create a file `main.py` with: +* Créez un fichier `main.py` avec : ```Python from typing import Union @@ -167,11 +171,11 @@ def read_item(item_id: int, q: Union[str, None] = None): ```
-Or use async def... +Ou utilisez async def ... -If your code uses `async` / `await`, use `async def`: +Si votre code utilise `async` / `await`, utilisez `async def` : -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -189,15 +193,15 @@ async def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -**Note**: +**Note** -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. +Si vous n'êtes pas familier avec cette notion, consultez la section _"Vous êtes pressés ?"_ à propos de `async` et `await` dans la documentation.
-### Run it +### Lancez -Run the server with: +Lancez le serveur avec :
@@ -214,56 +218,56 @@ INFO: Application startup complete.
-About the command uvicorn main:app --reload... +À propos de la commande uvicorn main:app --reload ... -The command `uvicorn main:app` refers to: +La commande `uvicorn main:app` fait référence à : -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. +* `main` : le fichier `main.py` (le "module" Python). +* `app` : l'objet créé à l'intérieur de `main.py` avec la ligne `app = FastAPI()`. +* `--reload` : fait redémarrer le serveur après des changements de code. À n'utiliser que pour le développement.
-### Check it +### Vérifiez -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. +Ouvrez votre navigateur à l'adresse http://127.0.0.1:8000/items/5?q=somequery. -You will see the JSON response as: +Vous obtenez alors cette réponse JSON : ```JSON {"item_id": 5, "q": "somequery"} ``` -You already created an API that: +Vous venez de créer une API qui : -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. +* Reçoit les requêtes HTTP pour les _chemins_ `/` et `/items/{item_id}`. +* Les deux _chemins_ acceptent des opérations `GET` (également connu sous le nom de _méthodes_ HTTP). +* Le _chemin_ `/items/{item_id}` a un _paramètre_ `item_id` qui doit être un `int`. +* Le _chemin_ `/items/{item_id}` a un _paramètre de requête_ optionnel `q` de type `str`. -### Interactive API docs +### Documentation API interactive -Now go to http://127.0.0.1:8000/docs. +Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -You will see the automatic interactive API documentation (provided by Swagger UI): +Vous verrez la documentation interactive automatique de l'API (fournie par Swagger UI) : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternative API docs +### Documentation API alternative -And now, go to http://127.0.0.1:8000/redoc. +Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -You will see the alternative automatic documentation (provided by ReDoc): +Vous verrez la documentation interactive automatique de l'API (fournie par ReDoc) : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Example upgrade +## Exemple plus poussé -Now modify the file `main.py` to receive a body from a `PUT` request. +Maintenant, modifiez le fichier `main.py` pour recevoir le corps d'une requête `PUT`. -Declare the body using standard Python types, thanks to Pydantic. +Déclarez ce corps en utilisant les types Python standards, grâce à Pydantic. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -293,174 +297,173 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). +Le serveur se recharge normalement automatiquement (car vous avez pensé à `--reload` dans la commande `uvicorn` ci-dessus). -### Interactive API docs upgrade +### Plus loin avec la documentation API interactive -Now go to http://127.0.0.1:8000/docs. +Maintenant, rendez-vous sur http://127.0.0.1:8000/docs. -* The interactive API documentation will be automatically updated, including the new body: +* La documentation interactive de l'API sera automatiquement mise à jour, y compris le nouveau corps de la requête : ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: +* Cliquez sur le bouton "Try it out", il vous permet de renseigner les paramètres et d'interagir directement avec l'API : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: +* Cliquez ensuite sur le bouton "Execute", l'interface utilisateur communiquera avec votre API, enverra les paramètres, obtiendra les résultats et les affichera à l'écran : ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternative API docs upgrade +### Plus loin avec la documentation API alternative -And now, go to http://127.0.0.1:8000/redoc. +Et maintenant, rendez-vous sur http://127.0.0.1:8000/redoc. -* The alternative documentation will also reflect the new query parameter and body: +* La documentation alternative reflétera également le nouveau paramètre de requête et le nouveau corps : ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) -### Recap +### En résumé -In summary, you declare **once** the types of parameters, body, etc. as function parameters. +En résumé, vous déclarez **une fois** les types de paramètres, le corps de la requête, etc. en tant que paramètres de fonction. -You do that with standard modern Python types. +Vous faites cela avec les types Python standard modernes. -You don't have to learn a new syntax, the methods or classes of a specific library, etc. +Vous n'avez pas à apprendre une nouvelle syntaxe, les méthodes ou les classes d'une bibliothèque spécifique, etc. -Just standard **Python 3.6+**. +Juste du **Python 3.7+** standard. -For example, for an `int`: +Par exemple, pour un `int`: ```Python item_id: int ``` -or for a more complex `Item` model: +ou pour un modèle `Item` plus complexe : ```Python item: Item ``` -...and with that single declaration you get: +... et avec cette déclaration unique, vous obtenez : -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: +* Une assistance dans votre IDE, notamment : + * la complétion. + * la vérification des types. +* La validation des données : + * des erreurs automatiques et claires lorsque les données ne sont pas valides. + * une validation même pour les objets JSON profondément imbriqués. +* Une conversion des données d'entrée : venant du réseau et allant vers les données et types de Python, permettant de lire : + * le JSON. + * les paramètres du chemin. + * les paramètres de la requête. + * les cookies. + * les en-têtes. + * les formulaires. + * les fichiers. +* La conversion des données de sortie : conversion des données et types Python en données réseau (au format JSON), permettant de convertir : + * les types Python (`str`, `int`, `float`, `bool`, `list`, etc). + * les objets `datetime`. + * les objets `UUID`. + * les modèles de base de données. + * ... et beaucoup plus. +* La documentation API interactive automatique, avec 2 interfaces utilisateur au choix : * Swagger UI. * ReDoc. --- -Coming back to the previous code example, **FastAPI** will: +Pour revenir à l'exemple de code précédent, **FastAPI** permet de : -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. +* Valider que `item_id` existe dans le chemin des requêtes `GET` et `PUT`. +* Valider que `item_id` est de type `int` pour les requêtes `GET` et `PUT`. + * Si ce n'est pas le cas, le client voit une erreur utile et claire. +* Vérifier qu'il existe un paramètre de requête facultatif nommé `q` (comme dans `http://127.0.0.1:8000/items/foo?q=somequery`) pour les requêtes `GET`. + * Puisque le paramètre `q` est déclaré avec `= None`, il est facultatif. + * Sans le `None`, il serait nécessaire (comme l'est le corps de la requête dans le cas du `PUT`). +* Pour les requêtes `PUT` vers `/items/{item_id}`, de lire le corps en JSON : + * Vérifier qu'il a un attribut obligatoire `name` qui devrait être un `str`. + * Vérifier qu'il a un attribut obligatoire `prix` qui doit être un `float`. + * Vérifier qu'il a un attribut facultatif `is_offer`, qui devrait être un `bool`, s'il est présent. + * Tout cela fonctionnerait également pour les objets JSON profondément imbriqués. +* Convertir de et vers JSON automatiquement. +* Documenter tout avec OpenAPI, qui peut être utilisé par : + * Les systèmes de documentation interactifs. + * Les systèmes de génération automatique de code client, pour de nombreuses langues. +* Fournir directement 2 interfaces web de documentation interactive. --- -We just scratched the surface, but you already get the idea of how it all works. +Nous n'avons fait qu'effleurer la surface, mais vous avez déjà une idée de la façon dont tout cela fonctionne. -Try changing the line with: +Essayez de changer la ligne contenant : ```Python return {"item_name": item.name, "item_id": item_id} ``` -...from: +... de : ```Python ... "item_name": item.name ... ``` -...to: +... vers : ```Python ... "item_price": item.price ... ``` -...and see how your editor will auto-complete the attributes and know their types: +... et voyez comment votre éditeur complétera automatiquement les attributs et connaîtra leurs types : -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) +![compatibilité IDE](https://fastapi.tiangolo.com/img/vscode-completion.png) -For a more complete example including more features, see the Tutorial - User Guide. +Pour un exemple plus complet comprenant plus de fonctionnalités, voir le Tutoriel - Guide utilisateur. -**Spoiler alert**: the tutorial - user guide includes: +**Spoiler alert** : le tutoriel - guide utilisateur inclut : -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: +* Déclaration de **paramètres** provenant d'autres endroits différents comme : **en-têtes.**, **cookies**, **champs de formulaire** et **fichiers**. +* L'utilisation de **contraintes de validation** comme `maximum_length` ou `regex`. +* Un **systéme d'injection de dépendance ** très puissant et facile à utiliser . +* Sécurité et authentification, y compris la prise en charge de **OAuth2** avec les **jetons JWT** et l'authentification **HTTP Basic**. +* Des techniques plus avancées (mais tout aussi faciles) pour déclarer les **modèles JSON profondément imbriqués** (grâce à Pydantic). +* Intégration de **GraphQL** avec Strawberry et d'autres bibliothèques. +* D'obtenir de nombreuses fonctionnalités supplémentaires (grâce à Starlette) comme : * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** + * de tester le code très facilement avec `requests` et `pytest` + * **CORS** * **Cookie Sessions** - * ...and more. + * ... et plus encore. ## Performance -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) +Les benchmarks TechEmpower indépendants montrent que les applications **FastAPI** s'exécutant sous Uvicorn sont parmi les frameworks existants en Python les plus rapides , juste derrière Starlette et Uvicorn (utilisés en interne par FastAPI). (*) -To understand more about it, see the section Benchmarks. +Pour en savoir plus, consultez la section Benchmarks. -## Optional Dependencies +## Dépendances facultatives -Used by Pydantic: +Utilisées par Pydantic: -* ujson - for faster JSON "parsing". -* email_validator - for email validation. +* ujson - pour un "décodage" JSON plus rapide. +* email_validator - pour la validation des adresses email. -Used by Starlette: +Utilisées par Starlette : -* HTTPX - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. +* requests - Obligatoire si vous souhaitez utiliser `TestClient`. +* jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par defaut. +* python-multipart - Obligatoire si vous souhaitez supporter le "décodage" de formulaire avec `request.form()`. +* itsdangerous - Obligatoire pour la prise en charge de `SessionMiddleware`. +* pyyaml - Obligatoire pour le support `SchemaGenerator` de Starlette (vous n'en avez probablement pas besoin avec FastAPI). +* ujson - Obligatoire si vous souhaitez utiliser `UJSONResponse`. -Used by FastAPI / Starlette: +Utilisées par FastAPI / Starlette : -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. +* uvicorn - Pour le serveur qui charge et sert votre application. +* orjson - Obligatoire si vous voulez utiliser `ORJSONResponse`. -You can install all of these with `pip install fastapi[all]`. +Vous pouvez tout installer avec `pip install fastapi[all]`. -## License +## Licence -This project is licensed under the terms of the MIT license. +Ce projet est soumis aux termes de la licence MIT. From d82809d90da29fb89012eeca8c317a8e63642b30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:00:28 +0000 Subject: [PATCH 0884/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 348805df8..47d4d2ac7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). * 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). * 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). From dafce52bc7b01c1522f43e38abd1c2953d764595 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:00:47 +0300 Subject: [PATCH 0885/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/project-generation.md`=20(#9243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/project-generation.md | 84 ++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 85 insertions(+) create mode 100644 docs/ru/docs/project-generation.md diff --git a/docs/ru/docs/project-generation.md b/docs/ru/docs/project-generation.md new file mode 100644 index 000000000..76253d6f2 --- /dev/null +++ b/docs/ru/docs/project-generation.md @@ -0,0 +1,84 @@ +# Генераторы проектов - Шаблоны + +Чтобы начать работу быстрее, Вы можете использовать "генераторы проектов", в которые включены множество начальных настроек для функций безопасности, баз данных и некоторые эндпоинты API. + +В генераторе проектов всегда будут предустановлены какие-то настройки, которые Вам следует обновить и подогнать под свои нужды, но это может быть хорошей отправной точкой для Вашего проекта. + +## Full Stack FastAPI PostgreSQL + +GitHub: https://github.com/tiangolo/full-stack-fastapi-postgresql + +### Full Stack FastAPI PostgreSQL - Особенности + +* Полностью интегрирован с **Docker** (основан на Docker). +* Развёртывается в режиме Docker Swarm. +* Интегрирован с **Docker Compose** и оптимизирован для локальной разработки. +* **Готовый к реальной работе** веб-сервер Python использующий Uvicorn и Gunicorn. +* Бэкенд построен на фреймворке **FastAPI**: + * **Быстрый**: Высокопроизводительный, на уровне **NodeJS** и **Go** (благодаря Starlette и Pydantic). + * **Интуитивно понятный**: Отличная поддержка редактора. Автодополнение кода везде. Меньше времени на отладку. + * **Простой**: Разработан так, чтоб быть простым в использовании и изучении. Меньше времени на чтение документации. + * **Лаконичный**: Минимизировано повторение кода. Каждый объявленный параметр определяет несколько функций. + * **Надёжный**: Получите готовый к работе код. С автоматической интерактивной документацией. + * **Стандартизированный**: Основан на открытых стандартах API (OpenAPI и JSON Schema) и полностью совместим с ними. + * **Множество других возможностей** включая автоматическую проверку и сериализацию данных, интерактивную документацию, аутентификацию с помощью OAuth2 JWT-токенов и т.д. +* **Безопасное хранение паролей**, которые хэшируются по умолчанию. +* Аутентификация посредством **JWT-токенов**. +* Модели **SQLAlchemy** (независящие от расширений Flask, а значит могут быть непосредственно использованы процессами Celery). +* Базовая модель пользователя (измените или удалите её по необходимости). +* **Alembic** для организации миграций. +* **CORS** (Совместное использование ресурсов из разных источников). +* **Celery**, процессы которого могут выборочно импортировать и использовать модели и код из остальной части бэкенда. +* Тесты, на основе **Pytest**, интегрированные в Docker, чтобы Вы могли полностью проверить Ваше API, независимо от базы данных. Так как тесты запускаются в Docker, для них может создаваться новое хранилище данных каждый раз (Вы можете, по своему желанию, использовать ElasticSearch, MongoDB, CouchDB или другую СУБД, только лишь для проверки - будет ли Ваше API работать с этим хранилищем). +* Простая интеграция Python с **Jupyter Kernels** для разработки удалённо или в Docker с расширениями похожими на Atom Hydrogen или Visual Studio Code Jupyter. +* Фронтенд построен на фреймворке **Vue**: + * Сгенерирован с помощью Vue CLI. + * Поддерживает **аутентификацию с помощью JWT-токенов**. + * Страница логина. + * Перенаправление на страницу главной панели мониторинга после логина. + * Главная страница мониторинга с возможностью создания и изменения пользователей. + * Пользователь может изменять свои данные. + * **Vuex**. + * **Vue-router**. + * **Vuetify** для конструирования красивых компонентов страниц. + * **TypeScript**. + * Сервер Docker основан на **Nginx** (настроен для удобной работы с Vue-router). + * Многоступенчатая сборка Docker, то есть Вам не нужно сохранять или коммитить скомпилированный код. + * Тесты фронтенда запускаются во время сборки (можно отключить). + * Сделан настолько модульно, насколько возможно, поэтому работает "из коробки", но Вы можете повторно сгенерировать фронтенд с помощью Vue CLI или создать то, что Вам нужно и повторно использовать то, что захотите. +* **PGAdmin** для СУБД PostgreSQL, которые легко можно заменить на PHPMyAdmin и MySQL. +* **Flower** для отслеживания работы Celery. +* Балансировка нагрузки между фронтендом и бэкендом с помощью **Traefik**, а значит, Вы можете расположить их на одном домене, разделив url-пути, так как они обслуживаются разными контейнерами. +* Интеграция с Traefik включает автоматическую генерацию сертификатов Let's Encrypt для поддержки протокола **HTTPS**. +* GitLab **CI** (непрерывная интеграция), которая включает тестирование фронтенда и бэкенда. + +## Full Stack FastAPI Couchbase + +GitHub: https://github.com/tiangolo/full-stack-fastapi-couchbase + +⚠️ **ПРЕДУПРЕЖДЕНИЕ** ⚠️ + +Если Вы начинаете новый проект, ознакомьтесь с представленными здесь альтернативами. + +Например, генератор проектов Full Stack FastAPI PostgreSQL может быть более подходящей альтернативой, так как он активно поддерживается и используется. И он включает в себя все новые возможности и улучшения. + +Но никто не запрещает Вам использовать генератор с СУБД Couchbase, возможно, он всё ещё работает нормально. Или у Вас уже есть проект, созданный с помощью этого генератора ранее, и Вы, вероятно, уже обновили его в соответствии со своими потребностями. + +Вы можете прочитать о нём больше в документации соответствующего репозитория. + +## Full Stack FastAPI MongoDB + +...может быть когда-нибудь появится, в зависимости от наличия у меня свободного времени и прочих факторов. 😅 🎉 + +## Модели машинного обучения на основе spaCy и FastAPI + +GitHub: https://github.com/microsoft/cookiecutter-spacy-fastapi + +### Модели машинного обучения на основе spaCy и FastAPI - Особенности + +* Интеграция с моделями **spaCy** NER. +* Встроенный формат запросов к **когнитивному поиску Azure**. +* **Готовый к реальной работе** веб-сервер Python использующий Uvicorn и Gunicorn. +* Встроенное развёртывание на основе **Azure DevOps** Kubernetes (AKS) CI/CD. +* **Многоязычность**. Лёгкий выбор одного из встроенных в spaCy языков во время настройки проекта. +* **Легко подключить** модели из других фреймворков (Pytorch, Tensorflow) не ограничиваясь spaCy. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 5b038e2b7..bd5d3977c 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -73,6 +73,7 @@ nav: - Развёртывание: - deployment/index.md - deployment/versions.md +- project-generation.md - history-design-future.md - external-links.md - benchmarks.md From 48e9d87e7efe7831fd9d833ee3c5eafe58236718 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:01:31 +0000 Subject: [PATCH 0886/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 47d4d2ac7..8cd87d0bb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/project-generation.md`. PR [#9243](https://github.com/tiangolo/fastapi/pull/9243) by [@Xewus](https://github.com/Xewus). * 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). * 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). From 48afd32ac8a16de5688194981ba14f83091ae101 Mon Sep 17 00:00:00 2001 From: Sehwan Park <47601603+sehwan505@users.noreply.github.com> Date: Fri, 14 Apr 2023 03:02:52 +0900 Subject: [PATCH 0887/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/tutorial/dependencies/classes-as-dependencie?= =?UTF-8?q?s.md`=20(#9176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Co-authored-by: Joona Yoon Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dependencies/classes-as-dependencies.md | 244 ++++++++++++++++++ docs/ko/mkdocs.yml | 2 + 2 files changed, 246 insertions(+) create mode 100644 docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md diff --git a/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..bbf3a8283 --- /dev/null +++ b/docs/ko/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,244 @@ +# 의존성으로서의 클래스 + +**의존성 주입** 시스템에 대해 자세히 살펴보기 전에 이전 예제를 업그레이드 해보겠습니다. + +## 이전 예제의 `딕셔너리` + +이전 예제에서, 우리는 의존성(의존 가능한) 함수에서 `딕셔너리`객체를 반환하고 있었습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +우리는 *경로 작동 함수*의 매개변수 `commons`에서 `딕셔너리` 객체를 얻습니다. + +그리고 우리는 에디터들이 `딕셔너리` 객체의 키나 밸류의 자료형을 알 수 없기 때문에 자동 완성과 같은 기능을 제공해 줄 수 없다는 것을 알고 있습니다. + +더 나은 방법이 있을 것 같습니다... + +## 의존성으로 사용 가능한 것 + +지금까지 함수로 선언된 의존성을 봐왔습니다. + +아마도 더 일반적이기는 하겠지만 의존성을 선언하는 유일한 방법은 아닙니다. + +핵심 요소는 의존성이 "호출 가능"해야 한다는 것입니다 + +파이썬에서의 "**호출 가능**"은 파이썬이 함수처럼 "호출"할 수 있는 모든 것입니다. + +따라서, 만약 당신이 `something`(함수가 아닐 수도 있음) 객체를 가지고 있고, + +```Python +something() +``` + +또는 + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +상기와 같은 방식으로 "호출(실행)" 할 수 있다면 "호출 가능"이 됩니다. + +## 의존성으로서의 클래스 + +파이썬 클래스의 인스턴스를 생성하기 위해 사용하는 것과 동일한 문법을 사용한다는 걸 알 수 있습니다. + +예를 들어: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +이 경우에 `fluffy`는 클래스 `Cat`의 인스턴스입니다. 그리고 우리는 `fluffy`를 만들기 위해서 `Cat`을 "호출"했습니다. + +따라서, 파이썬 클래스는 **호출 가능**합니다. + +그래서 **FastAPI**에서는 파이썬 클래스를 의존성으로 사용할 수 있습니다. + +FastAPI가 실질적으로 확인하는 것은 "호출 가능성"(함수, 클래스 또는 다른 모든 것)과 정의된 매개변수들입니다. + +"호출 가능"한 것을 의존성으로서 **FastAPI**에 전달하면, 그 "호출 가능"한 것의 매개변수들을 분석한 후 이를 *경로 동작 함수*를 위한 매개변수와 동일한 방식으로 처리합니다. 하위-의존성 또한 같은 방식으로 처리합니다. + +매개변수가 없는 "호출 가능"한 것 역시 매개변수가 없는 *경로 동작 함수*와 동일한 방식으로 적용됩니다. + +그래서, 우리는 위 예제에서의 `common_paramenters` 의존성을 클래스 `CommonQueryParams`로 바꿀 수 있습니다. + +=== "파이썬 3.6 이상" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +클래스의 인스턴스를 생성하는 데 사용되는 `__init__` 메서드에 주목하기 바랍니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +...이전 `common_parameters`와 동일한 매개변수를 가집니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +이 매개변수들은 **FastAPI**가 의존성을 "해결"하기 위해 사용할 것입니다 + +함수와 클래스 두 가지 방식 모두, 아래 요소를 갖습니다: + +* `문자열`이면서 선택사항인 쿼리 매개변수 `q`. +* 기본값이 `0`이면서 `정수형`인 쿼리 매개변수 `skip` +* 기본값이 `100`이면서 `정수형`인 쿼리 매개변수 `limit` + +두 가지 방식 모두, 데이터는 변환, 검증되고 OpenAPI 스키마에 문서화됩니다. + +## 사용해봅시다! + +이제 아래의 클래스를 이용해서 의존성을 정의할 수 있습니다. + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +**FastAPI**는 `CommonQueryParams` 클래스를 호출합니다. 이것은 해당 클래스의 "인스턴스"를 생성하고 그 인스턴스는 함수의 매개변수 `commons`로 전달됩니다. + +## 타입 힌팅 vs `Depends` + +위 코드에서 `CommonQueryParams`를 두 번 작성한 방식에 주목하십시오: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +마지막 `CommonQueryParams` 변수를 보면: + +```Python +... = Depends(CommonQueryParams) +``` + +... **FastAPI**가 실제로 어떤 것이 의존성인지 알기 위해서 사용하는 방법입니다. +FastAPI는 선언된 매개변수들을 추출할 것이고 실제로 이 변수들을 호출할 것입니다. + +--- + +이 경우에, 첫번째 `CommonQueryParams` 변수를 보면: + +```Python +commons: CommonQueryParams ... +``` + +... **FastAPI**는 `CommonQueryParams` 변수에 어떠한 특별한 의미도 부여하지 않습니다. FastAPI는 이 변수를 데이터 변환, 검증 등에 활용하지 않습니다. (활용하려면 `= Depends(CommonQueryParams)`를 사용해야 합니다.) + +사실 아래와 같이 작성해도 무관합니다: + +```Python +commons = Depends(CommonQueryParams) +``` + +..전체적인 코드는 아래와 같습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +그러나 자료형을 선언하면 에디터가 매개변수 `commons`로 전달될 것이 무엇인지 알게 되고, 이를 통해 코드 완성, 자료형 확인 등에 도움이 될 수 있으므로 권장됩니다. + + + +## 코드 단축 + +그러나 여기 `CommonQueryParams`를 두 번이나 작성하는, 코드 반복이 있다는 것을 알 수 있습니다: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +**FastAPI**는 *특히* 의존성이 **FastAPI**가 클래스 자체의 인스턴스를 생성하기 위해 "호출"하는 클래스인 경우, 조금 더 쉬운 방법을 제공합니다. + +이러한 특정한 경우에는 아래처럼 사용할 수 있습니다: + +이렇게 쓰는 것 대신: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...이렇게 쓸 수 있습니다.: + +```Python +commons: CommonQueryParams = Depends() +``` + +의존성을 매개변수의 타입으로 선언하는 경우 `Depends(CommonQueryParams)`처럼 클래스 이름 전체를 *다시* 작성하는 대신, 매개변수를 넣지 않은 `Depends()`의 형태로 사용할 수 있습니다. + +아래에 같은 예제가 있습니다: + +=== "파이썬 3.6 이상" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +...이렇게 코드를 단축하여도 **FastAPI**는 무엇을 해야하는지 알고 있습니다. + +!!! tip "팁" + 만약 이것이 도움이 되기보다 더 헷갈리게 만든다면, 잊어버리십시오. 이것이 반드시 필요한 것은 아닙니다. + + 이것은 단지 손쉬운 방법일 뿐입니다. 왜냐하면 **FastAPI**는 코드 반복을 최소화할 수 있는 방법을 고민하기 때문입니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 7d429478c..1ab63e791 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -73,6 +73,8 @@ nav: - tutorial/request-forms-and-files.md - tutorial/encoder.md - tutorial/cors.md + - 의존성: + - tutorial/dependencies/classes-as-dependencies.md markdown_extensions: - toc: permalink: true From 9bdb8cc45abd069f6421395851c6e5c0daedfbf4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:03:28 +0000 Subject: [PATCH 0888/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8cd87d0bb..a088f2bea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#9176](https://github.com/tiangolo/fastapi/pull/9176) by [@sehwan505](https://github.com/sehwan505). * 🌐 Add Russian translation for `docs/ru/docs/project-generation.md`. PR [#9243](https://github.com/tiangolo/fastapi/pull/9243) by [@Xewus](https://github.com/Xewus). * 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). From 0ab88cd1a9379b96b98d74e294ffd060773a6b04 Mon Sep 17 00:00:00 2001 From: Aleksandr Egorov <37377259+stigsanek@users.noreply.github.com> Date: Thu, 13 Apr 2023 22:04:30 +0400 Subject: [PATCH 0889/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/contributing.md`=20(#6002)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: stigsanek Co-authored-by: Marcelo Trylesinski --- docs/ru/docs/contributing.md | 2 +- docs/ru/docs/tutorial/extra-data-types.md | 82 +++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 docs/ru/docs/tutorial/extra-data-types.md diff --git a/docs/ru/docs/contributing.md b/docs/ru/docs/contributing.md index cb460beb0..f61ef1cb6 100644 --- a/docs/ru/docs/contributing.md +++ b/docs/ru/docs/contributing.md @@ -82,7 +82,7 @@ $ python -m venv env
-Ели в терминале появится ответ, что бинарник `pip` расположен по пути `.../env/bin/pip`, значит всё в порядке. 🎉 +Если в терминале появится ответ, что бинарник `pip` расположен по пути `.../env/bin/pip`, значит всё в порядке. 🎉 Во избежание ошибок в дальнейших шагах, удостоверьтесь, что в Вашем виртуальном окружении установлена последняя версия `pip`: diff --git a/docs/ru/docs/tutorial/extra-data-types.md b/docs/ru/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..efcbcb38a --- /dev/null +++ b/docs/ru/docs/tutorial/extra-data-types.md @@ -0,0 +1,82 @@ +# Дополнительные типы данных + +До сих пор вы использовали простые типы данных, такие как: + +* `int` +* `float` +* `str` +* `bool` + +Но вы также можете использовать и более сложные типы. + +При этом у вас останутся те же возможности , что и до сих пор: + +* Отличная поддержка редактора. +* Преобразование данных из входящих запросов. +* Преобразование данных для ответа. +* Валидация данных. +* Автоматическая аннотация и документация. + +## Другие типы данных + +Ниже перечислены некоторые из дополнительных типов данных, которые вы можете использовать: + +* `UUID`: + * Стандартный "Универсальный уникальный идентификатор", используемый в качестве идентификатора во многих базах данных и системах. + * В запросах и ответах будет представлен как `str`. +* `datetime.datetime`: + * Встроенный в Python `datetime.datetime`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Встроенный в Python `datetime.date`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `2008-09-15`. +* `datetime.time`: + * Встроенный в Python `datetime.time`. + * В запросах и ответах будет представлен как `str` в формате ISO 8601, например: `14:23:55.003`. +* `datetime.timedelta`: + * Встроенный в Python `datetime.timedelta`. + * В запросах и ответах будет представлен в виде общего количества секунд типа `float`. + * Pydantic также позволяет представить его как "Кодировку разницы во времени ISO 8601", см. документацию для получения дополнительной информации. +* `frozenset`: + * В запросах и ответах обрабатывается так же, как и `set`: + * В запросах будет прочитан список, исключены дубликаты и преобразован в `set`. + * В ответах `set` будет преобразован в `list`. + * В сгенерированной схеме будет указано, что значения `set` уникальны (с помощью JSON-схемы `uniqueItems`). +* `bytes`: + * Встроенный в Python `bytes`. + * В запросах и ответах будет рассматриваться как `str`. + * В сгенерированной схеме будет указано, что это `str` в формате `binary`. +* `Decimal`: + * Встроенный в Python `Decimal`. + * В запросах и ответах обрабатывается так же, как и `float`. +* Вы можете проверить все допустимые типы данных pydantic здесь: Типы данных Pydantic. + +## Пример + +Вот пример *операции пути* с параметрами, который демонстрирует некоторые из вышеперечисленных типов. + +=== "Python 3.6 и выше" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +Обратите внимание, что параметры внутри функции имеют свой естественный тип данных, и вы, например, можете выполнять обычные манипуляции с датами, такие как: + +=== "Python 3.6 и выше" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +=== "Python 3.10 и выше" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index bd5d3977c..88c4eb756 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -68,6 +68,7 @@ nav: - tutorial/query-params-str-validations.md - tutorial/body-fields.md - tutorial/background-tasks.md + - tutorial/extra-data-types.md - tutorial/cookie-params.md - async.md - Развёртывание: From c6be4c6d65e38f86e3ec5d78603742bce1449f4d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:05:11 +0000 Subject: [PATCH 0890/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a088f2bea..e76fd0a52 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#6002](https://github.com/tiangolo/fastapi/pull/6002) by [@stigsanek](https://github.com/stigsanek). * 🌐 Add Korean translation for `docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#9176](https://github.com/tiangolo/fastapi/pull/9176) by [@sehwan505](https://github.com/sehwan505). * 🌐 Add Russian translation for `docs/ru/docs/project-generation.md`. PR [#9243](https://github.com/tiangolo/fastapi/pull/9243) by [@Xewus](https://github.com/Xewus). * 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). From fa103cf1fd60d1f5d7e58e2a9825d8e0b466d9dd Mon Sep 17 00:00:00 2001 From: Lorhan Sohaky <16273730+LorhanSohaky@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:06:27 -0300 Subject: [PATCH 0891/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/path-operation-configur?= =?UTF-8?q?ation.md`=20(#5936)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Ueno --- .../tutorial/path-operation-configuration.md | 180 ++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 181 insertions(+) create mode 100644 docs/pt/docs/tutorial/path-operation-configuration.md diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..e0a23f665 --- /dev/null +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,180 @@ +# Configuração da Operação de Rota + +Existem vários parâmetros que você pode passar para o seu *decorador de operação de rota* para configurá-lo. + +!!! warning "Aviso" + Observe que esses parâmetros são passados diretamente para o *decorador de operação de rota*, não para a sua *função de operação de rota*. + +## Código de Status da Resposta + +Você pode definir o `status_code` (HTTP) para ser usado na resposta da sua *operação de rota*. + +Você pode passar diretamente o código `int`, como `404`. + +Mas se você não se lembrar o que cada código numérico significa, pode usar as constantes de atalho em `status`: + +=== "Python 3.6 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +Esse código de status será usado na resposta e será adicionado ao esquema OpenAPI. + +!!! note "Detalhes Técnicos" + Você também poderia usar `from starlette import status`. + + **FastAPI** fornece o mesmo `starlette.status` como `fastapi.status` apenas como uma conveniência para você, o desenvolvedor. Mas vem diretamente do Starlette. + +## Tags + +Você pode adicionar tags para sua *operação de rota*, passe o parâmetro `tags` com uma `list` de `str` (comumente apenas um `str`): + +=== "Python 3.6 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +Eles serão adicionados ao esquema OpenAPI e usados pelas interfaces de documentação automática: + + + +### Tags com Enums + +Se você tem uma grande aplicação, você pode acabar acumulando **várias tags**, e você gostaria de ter certeza de que você sempre usa a **mesma tag** para *operações de rota* relacionadas. + +Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. + +**FastAPI** suporta isso da mesma maneira que com strings simples: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## Resumo e descrição + +Você pode adicionar um `summary` e uma `description`: + +=== "Python 3.6 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +## Descrição do docstring + +Como as descrições tendem a ser longas e cobrir várias linhas, você pode declarar a descrição da *operação de rota* na docstring da função e o **FastAPI** irá lê-la de lá. + +Você pode escrever Markdown na docstring, ele será interpretado e exibido corretamente (levando em conta a indentação da docstring). + +=== "Python 3.6 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +Ela será usada nas documentações interativas: + + + + +## Descrição da resposta + +Você pode especificar a descrição da resposta com o parâmetro `response_description`: + +=== "Python 3.6 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +!!! info "Informação" + Note que `response_description` se refere especificamente à resposta, a `description` se refere à *operação de rota* em geral. + +!!! check + OpenAPI especifica que cada *operação de rota* requer uma descrição de resposta. + + Então, se você não fornecer uma, o **FastAPI** irá gerar automaticamente uma de "Resposta bem-sucedida". + + + +## Depreciar uma *operação de rota* + +Se você precisar marcar uma *operação de rota* como descontinuada, mas sem removê-la, passe o parâmetro `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +Ela será claramente marcada como descontinuada nas documentações interativas: + + + +Verifique como *operações de rota* descontinuadas e não descontinuadas se parecem: + + + +## Resumindo + +Você pode configurar e adicionar metadados para suas *operações de rota* facilmente passando parâmetros para os *decoradores de operação de rota*. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index a8ab4cb32..0f10032a2 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/extra-data-types.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md + - tutorial/path-operation-configuration.md - tutorial/cookie-params.md - tutorial/header-params.md - tutorial/response-status-code.md From 4b9e9e40b5ee67386d5da594c137dfa511739099 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:07:18 +0000 Subject: [PATCH 0892/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e76fd0a52..b31aef9fe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-operation-configuration.md`. PR [#5936](https://github.com/tiangolo/fastapi/pull/5936) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#6002](https://github.com/tiangolo/fastapi/pull/6002) by [@stigsanek](https://github.com/stigsanek). * 🌐 Add Korean translation for `docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#9176](https://github.com/tiangolo/fastapi/pull/9176) by [@sehwan505](https://github.com/sehwan505). * 🌐 Add Russian translation for `docs/ru/docs/project-generation.md`. PR [#9243](https://github.com/tiangolo/fastapi/pull/9243) by [@Xewus](https://github.com/Xewus). From d455f3f868d2e196cfdf45b31f28f9955f6a7113 Mon Sep 17 00:00:00 2001 From: Lorhan Sohaky <16273730+LorhanSohaky@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:12:25 -0300 Subject: [PATCH 0893/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/extra-models.md`=20(#59?= =?UTF-8?q?12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Ueno --- docs/pt/docs/tutorial/extra-models.md | 252 ++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 253 insertions(+) create mode 100644 docs/pt/docs/tutorial/extra-models.md diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md new file mode 100644 index 000000000..dd5407eb2 --- /dev/null +++ b/docs/pt/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# Modelos Adicionais + +Continuando com o exemplo anterior, será comum ter mais de um modelo relacionado. + +Isso é especialmente o caso para modelos de usuários, porque: + +* O **modelo de entrada** precisa ser capaz de ter uma senha. +* O **modelo de saída** não deve ter uma senha. +* O **modelo de banco de dados** provavelmente precisaria ter uma senha criptografada. + +!!! danger + Nunca armazene senhas em texto simples dos usuários. Sempre armazene uma "hash segura" que você pode verificar depois. + + Se não souber, você aprenderá o que é uma "senha hash" nos [capítulos de segurança](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## Múltiplos modelos + +Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados: + +=== "Python 3.6 and above" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +### Sobre `**user_in.dict()` + +#### O `.dict()` do Pydantic + +`user_in` é um modelo Pydantic da classe `UserIn`. + +Os modelos Pydantic possuem um método `.dict()` que retorna um `dict` com os dados do modelo. + +Então, se criarmos um objeto Pydantic `user_in` como: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +e depois chamarmos: + +```Python +user_dict = user_in.dict() +``` + +agora temos um `dict` com os dados na variável `user_dict` (é um `dict` em vez de um objeto de modelo Pydantic). + +E se chamarmos: + +```Python +print(user_dict) +``` + +teríamos um `dict` Python com: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Desembrulhando um `dict` + +Se tomarmos um `dict` como `user_dict` e passarmos para uma função (ou classe) com `**user_dict`, o Python irá "desembrulhá-lo". Ele passará as chaves e valores do `user_dict` diretamente como argumentos chave-valor. + +Então, continuando com o `user_dict` acima, escrevendo: + +```Python +UserInDB(**user_dict) +``` + +Resultaria em algo equivalente a: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Ou mais exatamente, usando `user_dict` diretamente, com qualquer conteúdo que ele possa ter no futuro: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Um modelo Pydantic a partir do conteúdo de outro + +Como no exemplo acima, obtivemos o `user_dict` a partir do `user_in.dict()`, este código: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +seria equivalente a: + +```Python +UserInDB(**user_in.dict()) +``` + +...porque `user_in.dict()` é um `dict`, e depois fazemos o Python "desembrulhá-lo" passando-o para UserInDB precedido por `**`. + +Então, obtemos um modelo Pydantic a partir dos dados em outro modelo Pydantic. + +#### Desembrulhando um `dict` e palavras-chave extras + +E, então, adicionando o argumento de palavra-chave extra `hashed_password=hashed_password`, como em: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...acaba sendo como: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning + As funções adicionais de suporte são apenas para demonstração de um fluxo possível dos dados, mas é claro que elas não fornecem segurança real. + +## Reduzir duplicação + +Reduzir a duplicação de código é uma das ideias principais no **FastAPI**. + +A duplicação de código aumenta as chances de bugs, problemas de segurança, problemas de desincronização de código (quando você atualiza em um lugar, mas não em outros), etc. + +E esses modelos estão compartilhando muitos dos dados e duplicando nomes e tipos de atributos. + +Nós poderíamos fazer melhor. + +Podemos declarar um modelo `UserBase` que serve como base para nossos outros modelos. E então podemos fazer subclasses desse modelo que herdam seus atributos (declarações de tipo, validação, etc.). + +Toda conversão de dados, validação, documentação, etc. ainda funcionará normalmente. + +Dessa forma, podemos declarar apenas as diferenças entre os modelos (com `password` em texto claro, com `hashed_password` e sem senha): + +=== "Python 3.6 and above" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +## `Union` ou `anyOf` + +Você pode declarar uma resposta como o `Union` de dois tipos, o que significa que a resposta seria qualquer um dos dois. + +Isso será definido no OpenAPI com `anyOf`. + +Para fazer isso, use a dica de tipo padrão do Python `typing.Union`: + +!!! note + Ao definir um `Union`, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico `PlaneItem` vem antes de `CarItem` em `Union[PlaneItem, CarItem]`. + +=== "Python 3.6 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +### `Union` no Python 3.10 + +Neste exemplo, passamos `Union[PlaneItem, CarItem]` como o valor do argumento `response_model`. + +Dado que estamos passando-o como um **valor para um argumento** em vez de colocá-lo em uma **anotação de tipo**, precisamos usar `Union` mesmo no Python 3.10. + +Se estivesse em uma anotação de tipo, poderíamos ter usado a barra vertical, como: + +```Python +some_variable: PlaneItem | CarItem +``` + +Mas se colocarmos isso em `response_model=PlaneItem | CarItem` teríamos um erro, pois o Python tentaria executar uma **operação inválida** entre `PlaneItem` e `CarItem` em vez de interpretar isso como uma anotação de tipo. + +## Lista de modelos + +Da mesma forma, você pode declarar respostas de listas de objetos. + +Para isso, use o padrão Python `typing.List` (ou simplesmente `list` no Python 3.9 e superior): + +=== "Python 3.6 and above" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +## Resposta com `dict` arbitrário + +Você também pode declarar uma resposta usando um simples `dict` arbitrário, declarando apenas o tipo das chaves e valores, sem usar um modelo Pydantic. + +Isso é útil se você não souber os nomes de campo / atributo válidos (que seriam necessários para um modelo Pydantic) antecipadamente. + +Neste caso, você pode usar `typing.Dict` (ou simplesmente dict no Python 3.9 e superior): + +=== "Python 3.6 and above" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +=== "Python 3.9 and above" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +## Em resumo + +Use vários modelos Pydantic e herde livremente para cada caso. + +Não é necessário ter um único modelo de dados por entidade se essa entidade precisar ter diferentes "estados". No caso da "entidade" de usuário com um estado que inclui `password`, `password_hash` e sem senha. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 0f10032a2..6b9a8239d 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -72,6 +72,7 @@ nav: - tutorial/body-multiple-params.md - tutorial/body-fields.md - tutorial/extra-data-types.md + - tutorial/extra-models.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md - tutorial/path-operation-configuration.md From 723d47403b4ea6724db79c5816de0aca006f551e Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:12:48 +0300 Subject: [PATCH 0894/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/alternatives.md`=20(#5994)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/alternatives.md | 460 +++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 461 insertions(+) create mode 100644 docs/ru/docs/alternatives.md diff --git a/docs/ru/docs/alternatives.md b/docs/ru/docs/alternatives.md new file mode 100644 index 000000000..9e3c497d1 --- /dev/null +++ b/docs/ru/docs/alternatives.md @@ -0,0 +1,460 @@ +# Альтернативы, источники вдохновения и сравнения + +Что вдохновило на создание **FastAPI**, сравнение его с альтернативами и чему он научился у них. + +## Введение + +**FastAPI** не существовал бы, если б не было более ранних работ других людей. + +Они создали большое количество инструментов, которые вдохновили меня на создание **FastAPI**. + +Я всячески избегал создания нового фреймворка в течение нескольких лет. +Сначала я пытался собрать все нужные функции, которые ныне есть в **FastAPI**, используя множество различных фреймворков, плагинов и инструментов. + +Но в какой-то момент не осталось другого выбора, кроме как создать что-то, что предоставляло бы все эти функции сразу. +Взять самые лучшие идеи из предыдущих инструментов и, используя новые возможности Python (которых не было до версии 3.6, то есть подсказки типов), объединить их. + +## Предшествующие инструменты + +### Django + +Это самый популярный Python-фреймворк, и он пользуется доверием. +Он используется для создания проектов типа Instagram. + +Django довольно тесно связан с реляционными базами данных (такими как MySQL или PostgreSQL), потому использовать NoSQL базы данных (например, Couchbase, MongoDB, Cassandra и т.п.) в качестве основного хранилища данных - непросто. + +Он был создан для генерации HTML-страниц на сервере, а не для создания API, используемых современными веб-интерфейсами (React, Vue.js, Angular и т.п.) или другими системами (например, IoT) взаимодействующими с сервером. + +### Django REST Framework + +Фреймворк Django REST был создан, как гибкий инструментарий для создания веб-API на основе Django. + +DRF использовался многими компаниями, включая Mozilla, Red Hat и Eventbrite. + +Это был один из первых примеров **автоматического документирования API** и это была одна из первых идей, вдохновивших на создание **FastAPI**. + +!!! note "Заметка" + Django REST Framework был создан Tom Christie. + Он же создал Starlette и Uvicorn, на которых основан **FastAPI**. + +!!! check "Идея для **FastAPI**" + Должно быть автоматическое создание документации API с пользовательским веб-интерфейсом. + +### Flask + +Flask - это "микрофреймворк", в нём нет интеграции с базами данных и многих других вещей, которые предустановлены в Django. + +Его простота и гибкость дают широкие возможности, такие как использование баз данных NoSQL в качестве основной системы хранения данных. + +Он очень прост, его изучение интуитивно понятно, хотя в некоторых местах документация довольно техническая. + +Flask часто используется и для приложений, которым не нужна база данных, настройки прав доступа для пользователей и прочие из множества функций, предварительно встроенных в Django. +Хотя многие из этих функций могут быть добавлены с помощью плагинов. + +Такое разделение на части и то, что это "микрофреймворк", который можно расширить, добавляя необходимые возможности, было ключевой особенностью, которую я хотел сохранить. + +Простота Flask, показалась мне подходящей для создания API. +Но ещё нужно было найти "Django REST Framework" для Flask. + +!!! check "Идеи для **FastAPI**" + Это будет микрофреймворк. К нему легко будет добавить необходимые инструменты и части. + + Должна быть простая и лёгкая в использовании система маршрутизации запросов. + + +### Requests + +На самом деле **FastAPI** не является альтернативой **Requests**. +Их область применения очень разная. + +В принципе, можно использовать Requests *внутри* приложения FastAPI. + +Но всё же я использовал в FastAPI некоторые идеи из Requests. + +**Requests** - это библиотека для взаимодействия с API в качестве клиента, +в то время как **FastAPI** - это библиотека для *создания* API (то есть сервера). + +Они, так или иначе, диаметрально противоположны и дополняют друг друга. + +Requests имеет очень простой и понятный дизайн, очень прост в использовании и имеет разумные значения по умолчанию. +И в то же время он очень мощный и настраиваемый. + +Вот почему на официальном сайте написано: + +> Requests - один из самых загружаемых пакетов Python всех времен + + +Использовать его очень просто. Например, чтобы выполнить запрос `GET`, Вы бы написали: + +```Python +response = requests.get("http://example.com/some/url") +``` + +Противоположная *операция пути* в FastAPI может выглядеть следующим образом: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +Глядите, как похоже `requests.get(...)` и `@app.get(...)`. + +!!! check "Идеи для **FastAPI**" + * Должен быть простой и понятный API. + * Нужно использовать названия HTTP-методов (операций) для упрощения понимания происходящего. + * Должны быть разумные настройки по умолчанию и широкие возможности их кастомизации. + + +### Swagger / OpenAPI + +Главной функцией, которую я хотел унаследовать от Django REST Framework, была автоматическая документация API. + +Но потом я обнаружил, что существует стандарт документирования API, использующий JSON (или YAML, расширение JSON) под названием Swagger. + +И к нему уже был создан пользовательский веб-интерфейс. +Таким образом, возможность генерировать документацию Swagger для API позволила бы использовать этот интерфейс. + +В какой-то момент Swagger был передан Linux Foundation и переименован в OpenAPI. + +Вот почему, когда говорят о версии 2.0, обычно говорят "Swagger", а для версии 3+ "OpenAPI". + +!!! check "Идеи для **FastAPI**" + Использовать открытые стандарты для спецификаций API вместо самодельных схем. + + Совместимость с основанными на стандартах пользовательскими интерфейсами: + + * Swagger UI + * ReDoc + + Они были выбраны за популярность и стабильность. + Но сделав беглый поиск, Вы можете найти десятки альтернативных пользовательских интерфейсов для OpenAPI, которые Вы можете использовать с **FastAPI**. + +### REST фреймворки для Flask + +Существует несколько REST фреймворков для Flask, но потратив время и усилия на их изучение, я обнаружил, что многие из них не обновляются или заброшены и имеют нерешённые проблемы из-за которых они непригодны к использованию. + +### Marshmallow + +Одной из основных функций, необходимых системам API, является "сериализация" данных, то есть преобразование данных из кода (Python) во что-то, что может быть отправлено по сети. +Например, превращение объекта содержащего данные из базы данных в объект JSON, конвертация объекта `datetime` в строку и т.п. + +Еще одна важная функция, необходимая API — проверка данных, позволяющая убедиться, что данные действительны и соответствуют заданным параметрам. +Как пример, можно указать, что ожидаются данные типа `int`, а не какая-то произвольная строка. +Это особенно полезно для входящих данных. + +Без системы проверки данных Вам пришлось бы прописывать все проверки вручную. + +Именно для обеспечения этих функций и была создана Marshmallow. +Это отличная библиотека и я много раз пользовался ею раньше. + +Но она была создана до того, как появились подсказки типов Python. +Итак, чтобы определить каждую схему, +Вам нужно использовать определенные утилиты и классы, предоставляемые Marshmallow. + +!!! check "Идея для **FastAPI**" + Использовать код программы для автоматического создания "схем", определяющих типы данных и их проверку. + +### Webargs + +Другая немаловажная функция API - парсинг данных из входящих запросов. + +Webargs - это инструмент, который был создан для этого и поддерживает несколько фреймворков, включая Flask. + +Для проверки данных он использует Marshmallow и создан теми же авторами. + +Это превосходный инструмент и я тоже часто пользовался им до **FastAPI**. + +!!! info "Информация" + Webargs бы создан разработчиками Marshmallow. + +!!! check "Идея для **FastAPI**" + Должна быть автоматическая проверка входных данных. + +### APISpec + +Marshmallow и Webargs осуществляют проверку, анализ и сериализацию данных как плагины. + +Но документации API всё ещё не было. Тогда был создан APISpec. + +Это плагин для множества фреймворков, в том числе и для Starlette. + +Он работает так - Вы записываете определение схем, используя формат YAML, внутри докстринга каждой функции, обрабатывающей маршрут. + +Используя эти докстринги, он генерирует схему OpenAPI. + +Так это работает для Flask, Starlette, Responder и т.п. + +Но теперь у нас возникает новая проблема - наличие постороннего микро-синтаксиса внутри кода Python (большие YAML). + +Редактор кода не особо может помочь в такой парадигме. +А изменив какие-то параметры или схемы для Marshmallow можно забыть отредактировать докстринг с YAML и сгенерированная схема становится недействительной. + +!!! info "Информация" + APISpec тоже был создан авторами Marshmallow. + +!!! check "Идея для **FastAPI**" + Необходима поддержка открытого стандарта для API - OpenAPI. + +### Flask-apispec + +Это плагин для Flask, который связан с Webargs, Marshmallow и APISpec. + +Он получает информацию от Webargs и Marshmallow, а затем использует APISpec для автоматического создания схемы OpenAPI. + +Это отличный, но крайне недооценённый инструмент. +Он должен быть более популярен, чем многие плагины для Flask. +Возможно, это связано с тем, что его документация слишком скудна и абстрактна. + +Он избавил от необходимости писать чужеродный синтаксис YAML внутри докстрингов. + +Такое сочетание Flask, Flask-apispec, Marshmallow и Webargs было моим любимым стеком при построении бэкенда до появления **FastAPI**. + +Использование этого стека привело к созданию нескольких генераторов проектов. Я и некоторые другие команды до сих пор используем их: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +Эти генераторы проектов также стали основой для [Генераторов проектов с **FastAPI**](project-generation.md){.internal-link target=_blank}. + +!!! info "Информация" + Как ни странно, но Flask-apispec тоже создан авторами Marshmallow. + +!!! check "Идея для **FastAPI**" + Схема OpenAPI должна создаваться автоматически и использовать тот же код, который осуществляет сериализацию и проверку данных. + +### NestJSAngular) + +Здесь даже не используется Python. NestJS - этот фреймворк написанный на JavaScript (TypeScript), основанный на NodeJS и вдохновлённый Angular. + +Он позволяет получить нечто похожее на то, что можно сделать с помощью Flask-apispec. + +В него встроена система внедрения зависимостей, ещё одна идея взятая от Angular. +Однако требуется предварительная регистрация "внедрений" (как и во всех других известных мне системах внедрения зависимостей), что увеличивает количество и повторяемость кода. + +Так как параметры в нём описываются с помощью типов TypeScript (аналогично подсказкам типов в Python), поддержка редактора работает довольно хорошо. + +Но поскольку данные из TypeScript не сохраняются после компиляции в JavaScript, он не может полагаться на подсказки типов для определения проверки данных, сериализации и документации. +Из-за этого и некоторых дизайнерских решений, для валидации, сериализации и автоматической генерации схем, приходится во многих местах добавлять декораторы. +Таким образом, это становится довольно многословным. + +Кроме того, он не очень хорошо справляется с вложенными моделями. +Если в запросе имеется объект JSON, внутренние поля которого, в свою очередь, являются вложенными объектами JSON, это не может быть должным образом задокументировано и проверено. + +!!! check "Идеи для **FastAPI** " + Нужно использовать подсказки типов, чтоб воспользоваться поддержкой редактора кода. + + Нужна мощная система внедрения зависимостей. Необходим способ для уменьшения повторов кода. + +### Sanic + +Sanic был одним из первых чрезвычайно быстрых Python-фреймворков основанных на `asyncio`. +Он был сделан очень похожим на Flask. + +!!! note "Технические детали" + В нём использован `uvloop` вместо стандартного цикла событий `asyncio`, что и сделало его таким быстрым. + + Он явно вдохновил создателей Uvicorn и Starlette, которые в настоящее время быстрее Sanic в открытых бенчмарках. + +!!! check "Идеи для **FastAPI**" + Должна быть сумасшедшая производительность. + + Для этого **FastAPI** основан на Starlette, самом быстром из доступных фреймворков (по замерам незаинтересованных лиц). + +### Falcon + +Falcon - ещё один высокопроизводительный Python-фреймворк. +В нём минимум функций и он создан, чтоб быть основой для других фреймворков, например, Hug. + +Функции в нём получают два параметра - "запрос к серверу" и "ответ сервера". +Затем Вы "читаете" часть запроса и "пишите" часть ответа. +Из-за такой конструкции невозможно объявить параметры запроса и тела сообщения со стандартными подсказками типов Python в качестве параметров функции. + +Таким образом, и валидацию данных, и их сериализацию, и документацию нужно прописывать вручную. +Либо эти функции должны быть встроены во фреймворк, сконструированный поверх Falcon, как в Hug. +Такая же особенность присутствует и в других фреймворках, вдохновлённых идеей Falcon, использовать только один объект запроса и один объект ответа. + +!!! check "Идея для **FastAPI**" + Найдите способы добиться отличной производительности. + + Объявлять параметры `ответа сервера` в функциях, как в Hug. + + Хотя в FastAPI это необязательно и используется в основном для установки заголовков, куки и альтернативных кодов состояния. + +### Molten + +Molten мне попался на начальной стадии написания **FastAPI**. В нём были похожие идеи: + +* Использование подсказок типов. +* Валидация и документация исходя из этих подсказок. +* Система внедрения зависимостей. + +В нём не используются сторонние библиотеки (такие, как Pydantic) для валидации, сериализации и документации. +Поэтому переиспользовать эти определения типов непросто. + +Также требуется более подробная конфигурация и используется стандарт WSGI, который не предназначен для использования с высокопроизводительными инструментами, такими как Uvicorn, Starlette и Sanic, в отличие от ASGI. + +Его система внедрения зависимостей требует предварительной регистрации, и зависимости определяются, как объявления типов. +Из-за этого невозможно объявить более одного "компонента" (зависимости), который предоставляет определенный тип. + +Маршруты объявляются в единственном месте с использованием функций, объявленных в других местах (вместо использования декораторов, в которые могут быть обёрнуты функции, обрабатывающие конкретные ресурсы). +Это больше похоже на Django, чем на Flask и Starlette. +Он разделяет в коде вещи, которые довольно тесно связаны. + +!!! check "Идея для **FastAPI**" + Определить дополнительные проверки типов данных, используя значения атрибутов модели "по умолчанию". + Это улучшает помощь редактора и раньше это не было доступно в Pydantic. + + Фактически это подтолкнуло на обновление Pydantic для поддержки одинакового стиля проверок (теперь этот функционал уже доступен в Pydantic). + +### Hug + +Hug был одним из первых фреймворков, реализовавших объявление параметров API с использованием подсказок типов Python. +Эта отличная идея была использована и другими инструментами. + +При объявлении параметров вместо стандартных типов Python использовались собственные типы, но всё же это был огромный шаг вперед. + +Это также был один из первых фреймворков, генерировавших полную API-схему в формате JSON. + +Данная схема не придерживалась стандартов вроде OpenAPI и JSON Schema. +Поэтому было бы непросто совместить её с другими инструментами, такими как Swagger UI. +Но опять же, это была очень инновационная идея. + +Ещё у него есть интересная и необычная функция: используя один и тот же фреймворк можно создавать и API, и CLI. + +Поскольку он основан на WSGI, старом стандарте для синхронных веб-фреймворков, он не может работать с веб-сокетами и другими модными штуками, но всё равно обладает высокой производительностью. + +!!! info "Информация" + Hug создан Timothy Crosley, автором `isort`, отличного инструмента для автоматической сортировки импортов в Python-файлах. + +!!! check "Идеи для **FastAPI**" + Hug повлиял на создание некоторых частей APIStar и был одним из инструментов, которые я счел наиболее многообещающими, наряду с APIStar. + + Hug натолкнул на мысли использовать в **FastAPI** подсказки типов Python для автоматического создания схемы, определяющей API и его параметры. + + Hug вдохновил **FastAPI** объявить параметр `ответа` в функциях для установки заголовков и куки. + +### APIStar (<= 0.5) + +Непосредственно перед тем, как принять решение о создании **FastAPI**, я обнаружил **APIStar**. +В нем было почти все, что я искал и у него был отличный дизайн. + +Это была одна из первых реализаций фреймворка, использующего подсказки типов для объявления параметров и запросов, которые я когда-либо видел (до NestJS и Molten). +Я нашёл его примерно в то же время, что и Hug, но APIStar использовал стандарт OpenAPI. + +В нём были автоматические проверка и сериализация данных и генерация схемы OpenAPI основанные на подсказках типов в нескольких местах. + +При определении схемы тела сообщения не использовались подсказки типов, как в Pydantic, это больше похоже на Marshmallow, поэтому помощь редактора была недостаточно хорошей, но всё же APIStar был лучшим доступным вариантом. + +На тот момент у него были лучшие показатели производительности (проигрывающие только Starlette). + +Изначально у него не было автоматической документации API для веб-интерфейса, но я знал, что могу добавить к нему Swagger UI. + +В APIStar была система внедрения зависимостей, которая тоже требовала предварительную регистрацию компонентов, как и ранее описанные инструменты. +Но, тем не менее, это была отличная штука. + +Я не смог использовать его в полноценном проекте, так как были проблемы со встраиванием функций безопасности в схему OpenAPI, из-за которых невозможно было встроить все функции, применяемые в генераторах проектов на основе Flask-apispec. +Я добавил в свой список задач создание пул-реквеста, добавляющего эту функциональность. + +В дальнейшем фокус проекта сместился. + +Это больше не был API-фреймворк, так как автор сосредоточился на Starlette. + +Ныне APIStar - это набор инструментов для проверки спецификаций OpenAPI. + +!!! info "Информация" + APIStar был создан Tom Christie. Тот самый парень, который создал: + + * Django REST Framework + * Starlette (на котором основан **FastAPI**) + * Uvicorn (используемый в Starlette и **FastAPI**) + +!!! check "Идеи для **FastAPI**" + Воплощение. + + Мне казалось блестящей идеей объявлять множество функций (проверка данных, сериализация, документация) с помощью одних и тех же типов Python, которые при этом обеспечивают ещё и помощь редактора кода. + + После долгих поисков среди похожих друг на друга фреймворков и сравнения их различий, APIStar стал самым лучшим выбором. + + Но APIStar перестал быть фреймворком для создания веб-сервера, зато появился Starlette, новая и лучшая основа для построения подобных систем. + Это была последняя капля, сподвигнувшая на создание **FastAPI**. + + Я считаю **FastAPI** "духовным преемником" APIStar, улучившим его возможности благодаря урокам, извлечённым из всех упомянутых выше инструментов. + +## Что используется в **FastAPI** + +### Pydantic + +Pydantic - это библиотека для валидации данных, сериализации и документирования (используя JSON Schema), основываясь на подсказках типов Python, что делает его чрезвычайно интуитивным. + +Его можно сравнить с Marshmallow, хотя в бенчмарках Pydantic быстрее, чем Marshmallow. +И он основан на тех же подсказках типов, которые отлично поддерживаются редакторами кода. + +!!! check "**FastAPI** использует Pydantic" + Для проверки данных, сериализации данных и автоматической документации моделей (на основе JSON Schema). + + Затем **FastAPI** берёт эти схемы JSON и помещает их в схему OpenAPI, не касаясь других вещей, которые он делает. + +### Starlette + +Starlette - это легковесный ASGI фреймворк/набор инструментов, который идеален для построения высокопроизводительных асинхронных сервисов. + +Starlette очень простой и интуитивный. +Он разработан таким образом, чтобы быть легко расширяемым и иметь модульные компоненты. + +В нём есть: + +* Впечатляющая производительность. +* Поддержка веб-сокетов. +* Фоновые задачи. +* Обработка событий при старте и финише приложения. +* Тестовый клиент на основе HTTPX. +* Поддержка CORS, сжатие GZip, статические файлы, потоковая передача данных. +* Поддержка сессий и куки. +* 100% покрытие тестами. +* 100% аннотированный код. +* Несколько жёстких зависимостей. + +В настоящее время Starlette показывает самую высокую скорость среди Python-фреймворков в тестовых замерах. +Быстрее только Uvicorn, который является сервером, а не фреймворком. + +Starlette обеспечивает весь функционал микрофреймворка, но не предоставляет автоматическую валидацию данных, сериализацию и документацию. + +**FastAPI** добавляет эти функции используя подсказки типов Python и Pydantic. +Ещё **FastAPI** добавляет систему внедрения зависимостей, утилиты безопасности, генерацию схемы OpenAPI и т.д. + +!!! note "Технические детали" + ASGI - это новый "стандарт" разработанный участниками команды Django. + Он пока что не является "стандартом в Python" (то есть принятым PEP), но процесс принятия запущен. + + Тем не менее он уже используется в качестве "стандарта" несколькими инструментами. + Это значительно улучшает совместимость, поскольку Вы можете переключиться с Uvicorn на любой другой ASGI-сервер (например, Daphne или Hypercorn) или Вы можете добавить ASGI-совместимые инструменты, такие как `python-socketio`. + +!!! check "**FastAPI** использует Starlette" + В качестве ядра веб-сервиса для обработки запросов, добавив некоторые функции сверху. + + Класс `FastAPI` наследуется напрямую от класса `Starlette`. + + Таким образом, всё что Вы могли делать со Starlette, Вы можете делать с **FastAPI**, по сути это прокачанный Starlette. + +### Uvicorn + +Uvicorn - это молниеносный ASGI-сервер, построенный на uvloop и httptools. + +Uvicorn является сервером, а не фреймворком. +Например, он не предоставляет инструментов для маршрутизации запросов по ресурсам. +Для этого нужна надстройка, такая как Starlette (или **FastAPI**). + +Он рекомендуется в качестве сервера для Starlette и **FastAPI**. + +!!! check "**FastAPI** рекомендует его" + Как основной сервер для запуска приложения **FastAPI**. + + Вы можете объединить его с Gunicorn, чтобы иметь асинхронный многопроцессный сервер. + + Узнать больше деталей можно в разделе [Развёртывание](deployment/index.md){.internal-link target=_blank}. + +## Тестовые замеры и скорость + +Чтобы понять, сравнить и увидеть разницу между Uvicorn, Starlette и FastAPI, ознакомьтесь с разделом [Тестовые замеры](benchmarks.md){.internal-link target=_blank}. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 88c4eb756..0423643d6 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -75,6 +75,7 @@ nav: - deployment/index.md - deployment/versions.md - project-generation.md +- alternatives.md - history-design-future.md - external-links.md - benchmarks.md From d824a5ca9bf292fcb89f4be6cf43101aa5566ee1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:12:59 +0000 Subject: [PATCH 0895/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b31aef9fe..56a7b2c47 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-operation-configuration.md`. PR [#5936](https://github.com/tiangolo/fastapi/pull/5936) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#6002](https://github.com/tiangolo/fastapi/pull/6002) by [@stigsanek](https://github.com/stigsanek). * 🌐 Add Korean translation for `docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#9176](https://github.com/tiangolo/fastapi/pull/9176) by [@sehwan505](https://github.com/sehwan505). From d1f1425392b15624d840ef96e4b4c21fbc0912ab Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:13:26 +0000 Subject: [PATCH 0896/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 56a7b2c47..b346ba7ca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-operation-configuration.md`. PR [#5936](https://github.com/tiangolo/fastapi/pull/5936) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Russian translation for `docs/ru/docs/contributing.md`. PR [#6002](https://github.com/tiangolo/fastapi/pull/6002) by [@stigsanek](https://github.com/stigsanek). From 7048ecfa7b685983f6a7d3b924b342f536d706b2 Mon Sep 17 00:00:00 2001 From: Luccas Mateus Date: Thu, 13 Apr 2023 15:15:34 -0300 Subject: [PATCH 0897/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/body-nested-models.md`?= =?UTF-8?q?=20(#4053)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski --- docs/pt/docs/tutorial/body-nested-models.md | 248 ++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 249 insertions(+) create mode 100644 docs/pt/docs/tutorial/body-nested-models.md diff --git a/docs/pt/docs/tutorial/body-nested-models.md b/docs/pt/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..8ab77173e --- /dev/null +++ b/docs/pt/docs/tutorial/body-nested-models.md @@ -0,0 +1,248 @@ +# Corpo - Modelos aninhados + +Com o **FastAPI**, você pode definir, validar, documentar e usar modelos profundamente aninhados de forma arbitrária (graças ao Pydantic). + +## Campos do tipo Lista + +Você pode definir um atributo como um subtipo. Por exemplo, uma `list` do Python: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial001.py!} +``` + +Isso fará com que tags seja uma lista de itens mesmo sem declarar o tipo dos elementos desta lista. + +## Campos do tipo Lista com um parâmetro de tipo + +Mas o Python tem uma maneira específica de declarar listas com tipos internos ou "parâmetros de tipo": + +### Importe `List` do typing + +Primeiramente, importe `List` do módulo `typing` que já vem por padrão no Python: + +```Python hl_lines="1" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### Declare a `List` com um parâmetro de tipo + +Para declarar tipos que têm parâmetros de tipo(tipos internos), como `list`, `dict`, `tuple`: + +* Importe os do modulo `typing` +* Passe o(s) tipo(s) interno(s) como "parâmetros de tipo" usando colchetes: `[` e `]` + +```Python +from typing import List + +my_list: List[str] +``` + +Essa é a sintaxe padrão do Python para declarações de tipo. + +Use a mesma sintaxe padrão para atributos de modelo com tipos internos. + +Portanto, em nosso exemplo, podemos fazer com que `tags` sejam especificamente uma "lista de strings": + + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +## Tipo "set" + + +Mas então, quando nós pensamos mais, percebemos que as tags não devem se repetir, elas provavelmente devem ser strings únicas. + +E que o Python tem um tipo de dados especial para conjuntos de itens únicos, o `set`. + +Então podemos importar `Set` e declarar `tags` como um `set` de `str`s: + + +```Python hl_lines="1 14" +{!../../../docs_src/body_nested_models/tutorial003.py!} +``` + +Com isso, mesmo que você receba uma requisição contendo dados duplicados, ela será convertida em um conjunto de itens exclusivos. + +E sempre que você enviar esses dados como resposta, mesmo se a fonte tiver duplicatas, eles serão gerados como um conjunto de itens exclusivos. + +E também teremos anotações/documentação em conformidade. + +## Modelos aninhados + +Cada atributo de um modelo Pydantic tem um tipo. + +Mas esse tipo pode ser outro modelo Pydantic. + +Portanto, você pode declarar "objects" JSON profundamente aninhados com nomes, tipos e validações de atributos específicos. + +Tudo isso, aninhado arbitrariamente. + +### Defina um sub-modelo + +Por exemplo, nós podemos definir um modelo `Image`: + +```Python hl_lines="9-11" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +### Use o sub-modelo como um tipo + +E então podemos usa-lo como o tipo de um atributo: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +Isso significa que o **FastAPI** vai esperar um corpo similar à: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Novamente, apenas fazendo essa declaração, com o **FastAPI**, você ganha: + +* Suporte do editor de texto (compleção, etc), inclusive para modelos aninhados +* Conversão de dados +* Validação de dados +* Documentação automatica + +## Tipos especiais e validação + +Além dos tipos singulares normais como `str`, `int`, `float`, etc. Você também pode usar tipos singulares mais complexos que herdam de `str`. + +Para ver todas as opções possíveis, cheque a documentação para ostipos exoticos do Pydantic. Você verá alguns exemplos no próximo capitulo. + +Por exemplo, no modelo `Image` nós temos um campo `url`, nós podemos declara-lo como um `HttpUrl` do Pydantic invés de como uma `str`: + +```Python hl_lines="4 10" +{!../../../docs_src/body_nested_models/tutorial005.py!} +``` + +A string será verificada para se tornar uma URL válida e documentada no esquema JSON/1OpenAPI como tal. + +## Atributos como listas de submodelos + +Você também pode usar modelos Pydantic como subtipos de `list`, `set`, etc: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial006.py!} +``` + +Isso vai esperar(converter, validar, documentar, etc) um corpo JSON tal qual: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! Informação + Note como o campo `images` agora tem uma lista de objetos de image. + +## Modelos profundamente aninhados + +Você pode definir modelos profundamente aninhados de forma arbitrária: + +```Python hl_lines="9 14 20 23 27" +{!../../../docs_src/body_nested_models/tutorial007.py!} +``` + +!!! Informação + Note como `Offer` tem uma lista de `Item`s, que por sua vez possui opcionalmente uma lista `Image`s + +## Corpos de listas puras + +Se o valor de primeiro nível do corpo JSON que você espera for um `array` do JSON (uma` lista` do Python), você pode declarar o tipo no parâmetro da função, da mesma forma que nos modelos do Pydantic: + + +```Python +images: List[Image] +``` + +como em: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial008.py!} +``` + +## Suporte de editor em todo canto + +E você obtém suporte do editor em todos os lugares. + +Mesmo para itens dentro de listas: + + + +Você não conseguiria este tipo de suporte de editor se estivesse trabalhando diretamente com `dict` em vez de modelos Pydantic. + +Mas você também não precisa se preocupar com eles, os dicts de entrada são convertidos automaticamente e sua saída é convertida automaticamente para JSON também. + +## Corpos de `dict`s arbitrários + +Você também pode declarar um corpo como um `dict` com chaves de algum tipo e valores de outro tipo. + +Sem ter que saber de antemão quais são os nomes de campos/atributos válidos (como seria o caso dos modelos Pydantic). + +Isso seria útil se você deseja receber chaves que ainda não conhece. + +--- + +Outro caso útil é quando você deseja ter chaves de outro tipo, por exemplo, `int`. + +É isso que vamos ver aqui. + +Neste caso, você aceitaria qualquer `dict`, desde que tenha chaves` int` com valores `float`: + +```Python hl_lines="9" +{!../../../docs_src/body_nested_models/tutorial009.py!} +``` + +!!! Dica + Leve em condideração que o JSON só suporta `str` como chaves. + + Mas o Pydantic tem conversão automática de dados. + + Isso significa que, embora os clientes da API só possam enviar strings como chaves, desde que essas strings contenham inteiros puros, o Pydantic irá convertê-los e validá-los. + + E o `dict` que você recebe como `weights` terá, na verdade, chaves `int` e valores` float`. + +## Recapitulação + +Com **FastAPI** você tem a flexibilidade máxima fornecida pelos modelos Pydantic, enquanto seu código é mantido simples, curto e elegante. + +Mas com todos os benefícios: + +* Suporte do editor (compleção em todo canto!) +* Conversão de dados (leia-se parsing/serialização) +* Validação de dados +* Documentação dos esquemas +* Documentação automática diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 6b9a8239d..f51c3ecc2 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -71,6 +71,7 @@ nav: - tutorial/body.md - tutorial/body-multiple-params.md - tutorial/body-fields.md + - tutorial/body-nested-models.md - tutorial/extra-data-types.md - tutorial/extra-models.md - tutorial/query-params-str-validations.md From 1267736d9fc4668ea52b0b14f332ceafc980f0f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:16:07 +0000 Subject: [PATCH 0898/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b346ba7ca..c6ebe5c46 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/path-operation-configuration.md`. PR [#5936](https://github.com/tiangolo/fastapi/pull/5936) by [@LorhanSohaky](https://github.com/LorhanSohaky). From dfe58433c0774887375fc944abf24d34df5d0216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 13 Apr 2023 11:17:05 -0700 Subject: [PATCH 0899/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20r?= =?UTF-8?q?emove=20Jina=20(#9388)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔧 Remove Jina sponsor --- README.md | 2 -- docs/en/data/sponsors.yml | 6 ------ docs/en/overrides/main.html | 12 ------------ 3 files changed, 20 deletions(-) diff --git a/README.md b/README.md index ecb207f6a..8baee7825 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,6 @@ The key features are: - - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 6bde2e163..6e81e4890 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -1,10 +1,4 @@ gold: - - url: https://bit.ly/3dmXC5S - title: The data structure for unstructured multimodal data - img: https://fastapi.tiangolo.com/img/sponsors/docarray.svg - - url: https://bit.ly/3JJ7y5C - title: Build cross-modal and multimodal applications on the cloud - img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index b85f0c4cf..9125b1d46 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -22,24 +22,12 @@
{% endblock %} From da2f365db476e85116231d8b1bcae65b7acea748 Mon Sep 17 00:00:00 2001 From: Axel Date: Thu, 13 Apr 2023 20:17:42 +0200 Subject: [PATCH 0900/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/advanced/index.md`=20(#5673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/fr/docs/advanced/index.md | 24 ++++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 25 insertions(+) create mode 100644 docs/fr/docs/advanced/index.md diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md new file mode 100644 index 000000000..41737889a --- /dev/null +++ b/docs/fr/docs/advanced/index.md @@ -0,0 +1,24 @@ +# Guide de l'utilisateur avancé - Introduction + +## Caractéristiques supplémentaires + +Le [Tutoriel - Guide de l'utilisateur](../tutorial/){.internal-link target=_blank} devrait suffire à vous faire découvrir toutes les fonctionnalités principales de **FastAPI**. + +Dans les sections suivantes, vous verrez des options, configurations et fonctionnalités supplémentaires. + +!!! Note + Les sections de ce chapitre ne sont **pas nécessairement "avancées"**. + + Et il est possible que pour votre cas d'utilisation, la solution se trouve dans l'un d'entre eux. + +## Lisez d'abord le didacticiel + +Vous pouvez utiliser la plupart des fonctionnalités de **FastAPI** grâce aux connaissances du [Tutoriel - Guide de l'utilisateur](../tutorial/){.internal-link target=_blank}. + +Et les sections suivantes supposent que vous l'avez lu et que vous en connaissez les idées principales. + +## Cours TestDriven.io + +Si vous souhaitez suivre un cours pour débutants avancés pour compléter cette section de la documentation, vous pouvez consulter : Développement piloté par les tests avec FastAPI et Docker par **TestDriven.io**. + +10 % de tous les bénéfices de ce cours sont reversés au développement de **FastAPI**. 🎉 😄 diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 3774d9d42..36fbfb2d0 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -72,6 +72,7 @@ nav: - tutorial/background-tasks.md - tutorial/debugging.md - Guide utilisateur avancé: + - advanced/index.md - advanced/path-operation-advanced-configuration.md - advanced/additional-status-codes.md - advanced/additional-responses.md From 6e129dbaafbc4981d12302d58c6dbbe7da36785f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:17:46 +0000 Subject: [PATCH 0901/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c6ebe5c46..ef4558dce 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). From c1f41fc5fe2bdfd81e28acc6936b3a821c3c2e0f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:18:19 +0000 Subject: [PATCH 0902/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ef4558dce..a47f8af23 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). * 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). From 571b5c6f0c495482713faca8e0ba7d20245a127f Mon Sep 17 00:00:00 2001 From: dasstyxx <79259281+dasstyxx@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:21:17 +0300 Subject: [PATCH 0903/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo:=20'wll'=20to?= =?UTF-8?q?=20'will'=20in=20`docs/en/docs/tutorial/query-params-str-valida?= =?UTF-8?q?tions.md`=20(#9380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 6a5a507b9..2debd088a 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -110,7 +110,7 @@ Notice that the default value is still `None`, so the parameter is still optiona But now, having `Query(max_length=50)` inside of `Annotated`, we are telling FastAPI that we want it to extract this value from the query parameters (this would have been the default anyway 🤷) and that we want to have **additional validation** for this value (that's why we do this, to get the additional validation). 😎 -FastAPI wll now: +FastAPI will now: * **Validate** the data making sure that the max length is 50 characters * Show a **clear error** for the client when the data is not valid From cd6150806f90d24041382f0b20af0607f515122e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:21:50 +0000 Subject: [PATCH 0904/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a47f8af23..54670dede 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). * 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). * 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). From 8ca7c5c29dd8eb1a2aaccd219801a98d857e864a Mon Sep 17 00:00:00 2001 From: Aadarsha Shrestha Date: Fri, 14 Apr 2023 00:13:52 +0545 Subject: [PATCH 0905/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20`docs/e?= =?UTF-8?q?n/docs/tutorial/path-params-numeric-validations.md`=20(#9282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/path-params-numeric-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 70ba5ac50..433e3c134 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -159,7 +159,7 @@ Python won't do anything with that `*`, but it will know that all the following ### Better with `Annotated` -Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and yo probably won't need to use `*`. +Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and you probably won't need to use `*`. === "Python 3.9+" From 0cc8e9cf31ef5f0e8d48df8f257e5a0f4f18c24e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:29:28 +0000 Subject: [PATCH 0906/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 54670dede..a0dbd5c9f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). * ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). * 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). * 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). From 08ba3a98a39859c1a8110d78df58478116863c07 Mon Sep 17 00:00:00 2001 From: tim-habitat <86600518+tim-habitat@users.noreply.github.com> Date: Thu, 13 Apr 2023 19:30:21 +0100 Subject: [PATCH 0907/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo/bug=20in=20inl?= =?UTF-8?q?ine=20code=20example=20in=20`docs/en/docs/tutorial/query-params?= =?UTF-8?q?-str-validations.md`=20(#9273)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 2debd088a..8a801cda2 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -199,7 +199,7 @@ Instead use the actual default value of the function parameter. Otherwise, it wo For example, this is not allowed: ```Python -q: Annotated[str Query(default="rick")] = "morty" +q: Annotated[str, Query(default="rick")] = "morty" ``` ...because it's not clear if the default value should be `"rick"` or `"morty"`. From acd4c11282615f0efd5a0ebccd88853dc29e241b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:30:56 +0000 Subject: [PATCH 0908/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a0dbd5c9f..605ed31ac 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). * ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). * ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). * 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). From fe975b0031ae5c14bfada00ad02cd6baf5a0d3ad Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:33:37 +0000 Subject: [PATCH 0910/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 605ed31ac..9f3cd5a32 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9272](https://github.com/tiangolo/fastapi/pull/9272) by [@nicornk](https://github.com/nicornk). * ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). * ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). * ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). From 75f59c46d5f971529731ac7d689f8cf9e6d0ba4c Mon Sep 17 00:00:00 2001 From: Armen Gabrielyan Date: Thu, 13 Apr 2023 22:34:04 +0400 Subject: [PATCH 0911/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20format,=20?= =?UTF-8?q?remove=20unnecessary=20asterisks=20in=20`docs/en/docs/help-fast?= =?UTF-8?q?api.md`=20(#9249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✏️ Remove unnecessary asterisks from help doc --- docs/en/docs/help-fastapi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 48a88ee96..e977dba20 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -121,7 +121,7 @@ If they reply, there's a high chance you would have solved their problem, congra * Now, if that solved their problem, you can ask them to: * In GitHub Discussions: mark the comment as the **answer**. - * In GitHub Issues: **close** the issue**. + * In GitHub Issues: **close** the issue. ## Watch the GitHub repository From bf9bb08b28c621baca5d567568ad68dd73b58cc3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:34:43 +0000 Subject: [PATCH 0912/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9f3cd5a32..a44153ca7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix format, remove unnecessary asterisks in `docs/en/docs/help-fastapi.md`. PR [#9249](https://github.com/tiangolo/fastapi/pull/9249) by [@armgabrielyan](https://github.com/armgabrielyan). * ✏ Fix typo in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9272](https://github.com/tiangolo/fastapi/pull/9272) by [@nicornk](https://github.com/nicornk). * ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). * ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). From 9f135952478987f8fe6cb7fe224450cd1aec015d Mon Sep 17 00:00:00 2001 From: Kimiaattaei <109368453+Kimiaattaei@users.noreply.github.com> Date: Thu, 13 Apr 2023 22:07:23 +0330 Subject: [PATCH 0913/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20wrong=20import=20fr?= =?UTF-8?q?om=20typing=20module=20in=20Persian=20translations=20for=20`doc?= =?UTF-8?q?s/fa/docs/index.md`=20(#6083)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fa/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index dfc4d24e3..ebaa8085a 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -143,7 +143,7 @@ $ pip install "uvicorn[standard]" * فایلی به نام `main.py` با محتوای زیر ایجاد کنید : ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI From 89fd635925b64195a1fd6e46fd50f940b6fbc799 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:38:01 +0000 Subject: [PATCH 0914/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a44153ca7..37dd65e29 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix wrong import from typing module in Persian translations for `docs/fa/docs/index.md`. PR [#6083](https://github.com/tiangolo/fastapi/pull/6083) by [@Kimiaattaei](https://github.com/Kimiaattaei). * ✏️ Fix format, remove unnecessary asterisks in `docs/en/docs/help-fastapi.md`. PR [#9249](https://github.com/tiangolo/fastapi/pull/9249) by [@armgabrielyan](https://github.com/armgabrielyan). * ✏ Fix typo in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9272](https://github.com/tiangolo/fastapi/pull/9272) by [@nicornk](https://github.com/nicornk). * ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). From 1bb998d5168aaf02e5eb256bf8eaf98b26327d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leonardo=20Marinho=20de=20Melo=20J=C3=BAnior?= <41721777+Leommjr@users.noreply.github.com> Date: Thu, 13 Apr 2023 15:44:09 -0300 Subject: [PATCH 0915/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typo=20in=20`doc?= =?UTF-8?q?s/en/docs/advanced/behind-a-proxy.md`=20(#5681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/behind-a-proxy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 766a218aa..03198851a 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -251,7 +251,7 @@ We get the same response: but this time at the URL with the prefix path provided by the proxy: `/api/v1`. -Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/app/v1` is the "correct" one. +Of course, the idea here is that everyone would access the app through the proxy, so the version with the path prefix `/api/v1` is the "correct" one. And the version without the path prefix (`http://127.0.0.1:8000/app`), provided by Uvicorn directly, would be exclusively for the _proxy_ (Traefik) to access it. From 925ba5c65205a63a03633f5e3a512efc744c33c3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:44:47 +0000 Subject: [PATCH 0916/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 37dd65e29..c57ce39b5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix typo in `docs/en/docs/advanced/behind-a-proxy.md`. PR [#5681](https://github.com/tiangolo/fastapi/pull/5681) by [@Leommjr](https://github.com/Leommjr). * ✏ Fix wrong import from typing module in Persian translations for `docs/fa/docs/index.md`. PR [#6083](https://github.com/tiangolo/fastapi/pull/6083) by [@Kimiaattaei](https://github.com/Kimiaattaei). * ✏️ Fix format, remove unnecessary asterisks in `docs/en/docs/help-fastapi.md`. PR [#9249](https://github.com/tiangolo/fastapi/pull/9249) by [@armgabrielyan](https://github.com/armgabrielyan). * ✏ Fix typo in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9272](https://github.com/tiangolo/fastapi/pull/9272) by [@nicornk](https://github.com/nicornk). From 8df86309c8a09eda02e3ab760ee8d98c83f3f2b9 Mon Sep 17 00:00:00 2001 From: Geoff Dworkin <128619245+grdworkin@users.noreply.github.com> Date: Thu, 13 Apr 2023 13:57:59 -0500 Subject: [PATCH 0917/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20notification=20m?= =?UTF-8?q?essage=20warning=20about=20old=20versions=20of=20FastAPI=20not?= =?UTF-8?q?=20supporting=20`Annotated`=20(#9298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/dependencies/index.md | 7 +++++++ docs/en/docs/tutorial/path-params-numeric-validations.md | 7 +++++++ docs/en/docs/tutorial/query-params-str-validations.md | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 80087a4a7..4f5ecea66 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -85,6 +85,13 @@ In this case, this dependency expects: And then it just returns a `dict` containing those values. +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. + + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you [Upgrade the FastAPI version](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. + ### Import `Depends` === "Python 3.10+" diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 433e3c134..9255875d6 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -42,6 +42,13 @@ First, import `Path` from `fastapi`, and import `Annotated`: {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} ``` +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. + + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you [Upgrade the FastAPI version](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. + ## Declare metadata You can declare all the same parameters as for `Query`. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 8a801cda2..c4b221cb1 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -52,6 +52,13 @@ To achieve that, first import: {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} ``` +!!! info + FastAPI added support for `Annotated` (and started recommending it) in version 0.95.0. + + If you have an older version, you would get errors when trying to use `Annotated`. + + Make sure you [Upgrade the FastAPI version](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} to at least 0.95.1 before using `Annotated`. + ## Use `Annotated` in the type for the `q` parameter Remember I told you before that `Annotated` can be used to add metadata to your parameters in the [Python Types Intro](../python-types.md#type-hints-with-metadata-annotations){.internal-link target=_blank}? From 1ccc5a862b9d8114b549d295a07873f1517bb49c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 13 Apr 2023 18:58:35 +0000 Subject: [PATCH 0918/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c57ce39b5..1c00a1cde 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add notification message warning about old versions of FastAPI not supporting `Annotated`. PR [#9298](https://github.com/tiangolo/fastapi/pull/9298) by [@grdworkin](https://github.com/grdworkin). * 📝 Fix typo in `docs/en/docs/advanced/behind-a-proxy.md`. PR [#5681](https://github.com/tiangolo/fastapi/pull/5681) by [@Leommjr](https://github.com/Leommjr). * ✏ Fix wrong import from typing module in Persian translations for `docs/fa/docs/index.md`. PR [#6083](https://github.com/tiangolo/fastapi/pull/6083) by [@Kimiaattaei](https://github.com/Kimiaattaei). * ✏️ Fix format, remove unnecessary asterisks in `docs/en/docs/help-fastapi.md`. PR [#9249](https://github.com/tiangolo/fastapi/pull/9249) by [@armgabrielyan](https://github.com/armgabrielyan). From 79846b2d2b3718943cb23f30187e857a8b6569f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 13 Apr 2023 12:04:11 -0700 Subject: [PATCH 0919/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1c00a1cde..2b95b167b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,13 @@ ## Latest Changes +### Fixes + +* 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). + +### Docs + +* 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). * 📝 Add notification message warning about old versions of FastAPI not supporting `Annotated`. PR [#9298](https://github.com/tiangolo/fastapi/pull/9298) by [@grdworkin](https://github.com/grdworkin). * 📝 Fix typo in `docs/en/docs/advanced/behind-a-proxy.md`. PR [#5681](https://github.com/tiangolo/fastapi/pull/5681) by [@Leommjr](https://github.com/Leommjr). * ✏ Fix wrong import from typing module in Persian translations for `docs/fa/docs/index.md`. PR [#6083](https://github.com/tiangolo/fastapi/pull/6083) by [@Kimiaattaei](https://github.com/Kimiaattaei). @@ -10,8 +17,10 @@ * ✏ Fix typo/bug in inline code example in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9273](https://github.com/tiangolo/fastapi/pull/9273) by [@tim-habitat](https://github.com/tim-habitat). * ✏ Fix typo in `docs/en/docs/tutorial/path-params-numeric-validations.md`. PR [#9282](https://github.com/tiangolo/fastapi/pull/9282) by [@aadarsh977](https://github.com/aadarsh977). * ✏ Fix typo: 'wll' to 'will' in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9380](https://github.com/tiangolo/fastapi/pull/9380) by [@dasstyxx](https://github.com/dasstyxx). + +### Translations + * 🌐 Add French translation for `docs/fr/docs/advanced/index.md`. PR [#5673](https://github.com/tiangolo/fastapi/pull/5673) by [@axel584](https://github.com/axel584). -* 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/body-nested-models.md`. PR [#4053](https://github.com/tiangolo/fastapi/pull/4053) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Russian translation for `docs/ru/docs/alternatives.md`. PR [#5994](https://github.com/tiangolo/fastapi/pull/5994) by [@Xewus](https://github.com/Xewus). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/extra-models.md`. PR [#5912](https://github.com/tiangolo/fastapi/pull/5912) by [@LorhanSohaky](https://github.com/LorhanSohaky). @@ -22,9 +31,11 @@ * 🌐 Add French translation for `docs/fr/docs/index.md`. PR [#9265](https://github.com/tiangolo/fastapi/pull/9265) by [@frabc](https://github.com/frabc). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params-str-validations.md`. PR [#9267](https://github.com/tiangolo/fastapi/pull/9267) by [@dedkot01](https://github.com/dedkot01). * 🌐 Add Russian translation for `docs/ru/docs/benchmarks.md`. PR [#9271](https://github.com/tiangolo/fastapi/pull/9271) by [@Xewus](https://github.com/Xewus). -* 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). + +### Internal + +* 🔧 Update sponsors: remove Jina. PR [#9388](https://github.com/tiangolo/fastapi/pull/9388) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add databento, remove Ines's course and StriveWorks. PR [#9351](https://github.com/tiangolo/fastapi/pull/9351) by [@tiangolo](https://github.com/tiangolo). -* 🌐 🔠 📄 🐢 Translate docs to Emoji 🥳 🎉 💥 🤯 🤯. PR [#5385](https://github.com/tiangolo/fastapi/pull/5385) by [@LeeeeT](https://github.com/LeeeeT). ## 0.95.0 From c81e136d75f5ac4252df740b35551cf2afb4c7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 13 Apr 2023 12:04:52 -0700 Subject: [PATCH 0920/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?95.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 2b95b167b..dbe3190d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.95.1 + ### Fixes * 🐛 Fix using `Annotated` in routers or path operations decorated multiple times. PR [#9315](https://github.com/tiangolo/fastapi/pull/9315) by [@sharonyogev](https://github.com/sharonyogev). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f06bb6454..e1c2be990 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.95.0" +__version__ = "0.95.1" from starlette import status as status From 07f691ba4b4be7ac6431e799f81ed2e723039c53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 11:25:28 -0700 Subject: [PATCH 0921/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.26.0=20to=202.27.0=20(#9394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.26.0 to 2.27.0. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.26.0...v2.27.0) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index cf0db59ab..91b0cba02 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -16,7 +16,7 @@ jobs: rm -rf ./site mkdir ./site - name: Download Artifact Docs - uses: dawidd6/action-download-artifact@v2.26.0 + uses: dawidd6/action-download-artifact@v2.27.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: build-docs.yml diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 421720433..f135fb3e4 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -20,7 +20,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.26.0 + - uses: dawidd6/action-download-artifact@v2.27.0 with: workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From a3d881ab67a936d8b152f9c8847b9e5e0eb21915 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 11:26:11 -0700 Subject: [PATCH 0922/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.6.4=20to=201.8.5=20(#9346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.6.4 to 1.8.5. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.6.4...v1.8.5) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c2fdb8e17..e88c19716 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,7 +31,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.6.4 + uses: pypa/gh-action-pypi-publish@v1.8.5 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From 46726aa1c418ba2838187678d08860950fc0c3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 25 Apr 2023 11:26:32 -0700 Subject: [PATCH 0923/1881] =?UTF-8?q?=F0=9F=92=9A=20Disable=20setup-python?= =?UTF-8?q?=20pip=20cache=20in=20CI=20(#9438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 3 ++- .github/workflows/test.yml | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e88c19716..1ad11f8d2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,8 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.7" - cache: "pip" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1235516d3..65b29be20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,8 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - cache: "pip" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache @@ -54,7 +55,8 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.8' - cache: "pip" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" cache-dependency-path: pyproject.toml - name: Get coverage files From a73570a832745af8f524ff85c37cde9bdbfc9b1f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 18:26:51 +0000 Subject: [PATCH 0924/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 dbe3190d8..961051e6e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.95.1 From bde03162270bf7df78c4da9dbc697a87f1d17502 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 18:27:12 +0000 Subject: [PATCH 0925/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 961051e6e..891bb47a7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.95.1 From 7f3e0fe8a76a9d3b21c8d68bea25b06f1223244d Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 18:28:42 +0000 Subject: [PATCH 0926/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 891bb47a7..05664d982 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). From da21ec92cf349a79ba8c48d01cfb60570697c3a2 Mon Sep 17 00:00:00 2001 From: Nadezhda Fedina <3773373@mail.ru> Date: Wed, 26 Apr 2023 01:44:34 +0700 Subject: [PATCH 0927/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/response-status-code.md`?= =?UTF-8?q?=20(#9370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Russian translation for docs/ru/docs/tutorial/response-status-code.md * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Apply suggestions from code review Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> * Replace 'response code' with 'status code', make minor translation improvements --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/response-status-code.md | 89 +++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 90 insertions(+) create mode 100644 docs/ru/docs/tutorial/response-status-code.md diff --git a/docs/ru/docs/tutorial/response-status-code.md b/docs/ru/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..b2f9b7704 --- /dev/null +++ b/docs/ru/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# HTTP коды статуса ответа + +Вы можете задать HTTP код статуса ответа с помощью параметра `status_code` подобно тому, как вы определяете схему ответа в любой из *операций пути*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* и других. + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "Примечание" + Обратите внимание, что `status_code` является атрибутом метода-декоратора (`get`, `post` и т.д.), а не *функции-обработчика пути* в отличие от всех остальных параметров и тела запроса. + +Параметр `status_code` принимает число, обозначающее HTTP код статуса ответа. + +!!! info "Информация" + В качестве значения параметра `status_code` также может использоваться `IntEnum`, например, из библиотеки `http.HTTPStatus` в Python. + +Это позволит: + +* Возвращать указанный код статуса в ответе. +* Документировать его как код статуса ответа в OpenAPI схеме (а значит, и в пользовательском интерфейсе): + + + +!!! note "Примечание" + Некоторые коды статуса ответа (см. следующий раздел) указывают на то, что ответ не имеет тела. + + FastAPI знает об этом и создаст документацию OpenAPI, в которой будет указано, что тело ответа отсутствует. + +## Об HTTP кодах статуса ответа + +!!! note "Примечание" + Если вы уже знаете, что представляют собой HTTP коды статуса ответа, можете перейти к следующему разделу. + +В протоколе HTTP числовой код состояния из 3 цифр отправляется как часть ответа. + +У кодов статуса есть названия, чтобы упростить их распознавание, но важны именно числовые значения. + +Кратко о значениях кодов: + +* `1XX` – статус-коды информационного типа. Они редко используются разработчиками напрямую. Ответы с этими кодами не могут иметь тела. +* **`2XX`** – статус-коды, сообщающие об успешной обработке запроса. Они используются чаще всего. + * `200` – это код статуса ответа по умолчанию, который означает, что все прошло "OK". + * Другим примером может быть статус `201`, "Created". Он обычно используется после создания новой записи в базе данных. + * Особый случай – `204`, "No Content". Этот статус ответа используется, когда нет содержимого для возврата клиенту, и поэтому ответ не должен иметь тела. +* **`3XX`** – статус-коды, сообщающие о перенаправлениях. Ответы с этими кодами статуса могут иметь или не иметь тело, за исключением ответов со статусом `304`, "Not Modified", у которых не должно быть тела. +* **`4XX`** – статус-коды, сообщающие о клиентской ошибке. Это ещё одна наиболее часто используемая категория. + * Пример – код `404` для статуса "Not Found". + * Для общих ошибок со стороны клиента можно просто использовать код `400`. +* `5XX` – статус-коды, сообщающие о серверной ошибке. Они почти никогда не используются разработчиками напрямую. Когда что-то идет не так в какой-то части кода вашего приложения или на сервере, он автоматически вернёт один из 5XX кодов. + +!!! tip "Подсказка" + Чтобы узнать больше о HTTP кодах статуса и о том, для чего каждый из них предназначен, ознакомьтесь с документацией MDN об HTTP кодах статуса ответа. + +## Краткие обозначения для запоминания названий кодов + +Рассмотрим предыдущий пример еще раз: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201` – это код статуса "Создано". + +Но вам не обязательно запоминать, что означает каждый из этих кодов. + +Для удобства вы можете использовать переменные из `fastapi.status`. + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +Они содержат те же числовые значения, но позволяют использовать подсказки редактора для выбора кода статуса: + + + +!!! note "Технические детали" + Вы также можете использовать `from starlette import status` вместо `from fastapi import status`. + + **FastAPI** позволяет использовать как `starlette.status`, так и `fastapi.status` исключительно для удобства разработчиков. Но поставляется fastapi.status непосредственно из Starlette. + +## Изменение кода статуса по умолчанию + +Позже, в [Руководстве для продвинутых пользователей](../advanced/response-change-status-code.md){.internal-link target=_blank}, вы узнаете, как возвращать HTTP коды статуса, отличные от используемого здесь кода статуса по умолчанию. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 0423643d6..24b89d0f5 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -70,6 +70,7 @@ nav: - tutorial/background-tasks.md - tutorial/extra-data-types.md - tutorial/cookie-params.md + - tutorial/response-status-code.md - async.md - Развёртывание: - deployment/index.md From bd518323948e9a0324d671459516aba2bc5764be Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 18:45:08 +0000 Subject: [PATCH 0928/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 05664d982..3565ecf6b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). * ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). From 6485c14c3b0fb2c90463a2175906ce5c6e84f90a Mon Sep 17 00:00:00 2001 From: Lucas Balieiro <37416577+lucasbalieiro@users.noreply.github.com> Date: Tue, 25 Apr 2023 14:46:17 -0400 Subject: [PATCH 0929/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20typo=20in=20Portugu?= =?UTF-8?q?ese=20docs=20for=20`docs/pt/docs/index.md`=20(#9337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index afc101ede..76668b4da 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -292,7 +292,7 @@ Agora vá para Date: Tue, 25 Apr 2023 18:46:57 +0000 Subject: [PATCH 0930/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3565ecf6b..15eb754a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). * ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). From 0e75981bd0669b870c2588a465aa449d67c6ddb4 Mon Sep 17 00:00:00 2001 From: Evzen Ptacek <3p1463k@users.noreply.github.com> Date: Tue, 25 Apr 2023 21:08:01 +0200 Subject: [PATCH 0931/1881] =?UTF-8?q?=F0=9F=8C=90=20Initiate=20Czech=20tra?= =?UTF-8?q?nslation=20setup=20(#9288)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initiate Czech translation setup Co-authored-by: Sebastián Ramírez --- docs/az/mkdocs.yml | 3 + docs/cs/docs/index.md | 473 +++++++++++++++++++++++++++++++++++ docs/cs/mkdocs.yml | 154 ++++++++++++ docs/cs/overrides/.gitignore | 0 docs/de/mkdocs.yml | 3 + docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/mkdocs.yml | 3 + docs/hy/mkdocs.yml | 3 + docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + docs/ta/mkdocs.yml | 3 + docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 25 files changed, 693 insertions(+) create mode 100644 docs/cs/docs/index.md create mode 100644 docs/cs/mkdocs.yml create mode 100644 docs/cs/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 7d59451c1..22a77c6e2 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/cs/docs/index.md b/docs/cs/docs/index.md new file mode 100644 index 000000000..bde72f851 --- /dev/null +++ b/docs/cs/docs/index.md @@ -0,0 +1,473 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

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

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Other sponsors + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.7+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.7+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* **GraphQL** integration with Strawberry and other libraries. +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * extremely easy tests based on HTTPX and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* httpx - Required if you want to use the `TestClient`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install "fastapi[all]"`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml new file mode 100644 index 000000000..539d7d65d --- /dev/null +++ b/docs/cs/mkdocs.yml @@ -0,0 +1,154 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/cs/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: cs +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - cs: /cs/ + - de: /de/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - hy: /hy/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - ta: /ta/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +- attr_list +- md_in_html +extra: + analytics: + provider: google + property: G-YNEVN69SC3 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /cs/ + name: cs + - link: /de/ + name: de + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /hy/ + name: hy + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /ta/ + name: ta - தமிழ் + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/cs/overrides/.gitignore b/docs/cs/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 87fe74697..8ee11d46c 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -105,6 +106,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index fc21439ae..fd7decee8 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -211,6 +212,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 485a2dd70..85db87ad1 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -114,6 +115,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 914b46e1a..5bb1a2ff5 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 36fbfb2d0..d416c067b 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -132,6 +133,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 094c5d82e..acf7ea8fd 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index ba7c687c1..5d251ff69 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index ca6e09551..55461328f 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 4633dd017..251d86681 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 9f4342e76..98a18cf4f 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -148,6 +149,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 1ab63e791..138ab678b 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -118,6 +119,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index e187ee383..55c971aa4 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index c781f9783..af68f1b74 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -107,6 +108,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index f51c3ecc2..9c3007e03 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -144,6 +145,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 24b89d0f5..5b6e338ce 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -125,6 +126,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 2766b0adf..ca20bce30 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 5aa37ece6..ce6ee3f83 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index 884115044..d66fc5740 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 23d6b9708..96306ee77 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -109,6 +110,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index e9339997f..5ba681396 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -104,6 +105,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 906fcf1d6..015423752 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -161,6 +162,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ From 0ef0aa55b0f2932c29a85f288c7828c15a2caa56 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 19:08:41 +0000 Subject: [PATCH 0932/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 15eb754a1..d68c89a70 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). * ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). * ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). From eb1b858c4f3f1daa13ea8de9887273fcc2bbbf2a Mon Sep 17 00:00:00 2001 From: Axel Date: Tue, 25 Apr 2023 21:12:00 +0200 Subject: [PATCH 0933/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/advanced/response-directly.md`=20(#9?= =?UTF-8?q?415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/fr/docs/advanced/response-directly.md | 63 ++++++++++++++++++++++ docs/fr/mkdocs.yml | 1 + 2 files changed, 64 insertions(+) create mode 100644 docs/fr/docs/advanced/response-directly.md diff --git a/docs/fr/docs/advanced/response-directly.md b/docs/fr/docs/advanced/response-directly.md new file mode 100644 index 000000000..1c923fb82 --- /dev/null +++ b/docs/fr/docs/advanced/response-directly.md @@ -0,0 +1,63 @@ +# Renvoyer directement une réponse + +Lorsque vous créez une *opération de chemins* **FastAPI**, vous pouvez normalement retourner n'importe quelle donnée : un `dict`, une `list`, un modèle Pydantic, un modèle de base de données, etc. + +Par défaut, **FastAPI** convertirait automatiquement cette valeur de retour en JSON en utilisant le `jsonable_encoder` expliqué dans [JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. + +Ensuite, en arrière-plan, il mettra ces données JSON-compatible (par exemple un `dict`) à l'intérieur d'un `JSONResponse` qui sera utilisé pour envoyer la réponse au client. + +Mais vous pouvez retourner une `JSONResponse` directement à partir de vos *opérations de chemin*. + +Cela peut être utile, par exemple, pour retourner des en-têtes personnalisés ou des cookies. + +## Renvoyer une `Response` + +En fait, vous pouvez retourner n'importe quelle `Response` ou n'importe quelle sous-classe de celle-ci. + +!!! Note + `JSONResponse` est elle-même une sous-classe de `Response`. + +Et quand vous retournez une `Response`, **FastAPI** la transmet directement. + +Elle ne fera aucune conversion de données avec les modèles Pydantic, elle ne convertira pas le contenu en un type quelconque. + +Cela vous donne beaucoup de flexibilité. Vous pouvez retourner n'importe quel type de données, surcharger n'importe quelle déclaration ou validation de données. + +## Utiliser le `jsonable_encoder` dans une `Response` + +Parce que **FastAPI** n'apporte aucune modification à une `Response` que vous retournez, vous devez vous assurer que son contenu est prêt à être utilisé (sérialisable). + +Par exemple, vous ne pouvez pas mettre un modèle Pydantic dans une `JSONResponse` sans d'abord le convertir en un `dict` avec tous les types de données (comme `datetime`, `UUID`, etc.) convertis en types compatibles avec JSON. + +Pour ces cas, vous pouvez spécifier un appel à `jsonable_encoder` pour convertir vos données avant de les passer à une réponse : + +```Python hl_lines="6-7 21-22" +{!../../../docs_src/response_directly/tutorial001.py!} +``` + +!!! note "Détails techniques" + Vous pouvez aussi utiliser `from starlette.responses import JSONResponse`. + + **FastAPI** fournit le même objet `starlette.responses` que `fastapi.responses` juste par commodité pour le développeur. Mais la plupart des réponses disponibles proviennent directement de Starlette. + +## Renvoyer une `Response` personnalisée + +L'exemple ci-dessus montre toutes les parties dont vous avez besoin, mais il n'est pas encore très utile, car vous auriez pu retourner l'`item` directement, et **FastAPI** l'aurait mis dans une `JSONResponse` pour vous, en le convertissant en `dict`, etc. Tout cela par défaut. + +Maintenant, voyons comment vous pourriez utiliser cela pour retourner une réponse personnalisée. + +Disons que vous voulez retourner une réponse XML. + +Vous pouvez mettre votre contenu XML dans une chaîne de caractères, la placer dans une `Response`, et la retourner : + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +## Notes + +Lorsque vous renvoyez une `Response` directement, ses données ne sont pas validées, converties (sérialisées), ni documentées automatiquement. + +Mais vous pouvez toujours les documenter comme décrit dans [Additional Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}. + +Vous pouvez voir dans les sections suivantes comment utiliser/déclarer ces `Response`s personnalisées tout en conservant la conversion automatique des données, la documentation, etc. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index d416c067b..854d14474 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -76,6 +76,7 @@ nav: - advanced/index.md - advanced/path-operation-advanced-configuration.md - advanced/additional-status-codes.md + - advanced/response-directly.md - advanced/additional-responses.md - async.md - Déploiement: From 8ac8d70d52bb0dd9eb55ba4e22d3e383943da05c Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 25 Apr 2023 19:12:33 +0000 Subject: [PATCH 0934/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d68c89a70..29ac94cee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/advanced/response-directly.md`. PR [#9415](https://github.com/tiangolo/fastapi/pull/9415) by [@axel584](https://github.com/axel584). * 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). * ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). From 055cf356ca5fbe586cf5120a4e7cff664ba592ab Mon Sep 17 00:00:00 2001 From: MariiaRomaniuk <69002327+MariiaRomanuik@users.noreply.github.com> Date: Tue, 2 May 2023 00:27:49 -0600 Subject: [PATCH 0935/1881] =?UTF-8?q?=E2=9C=8F=20Fix=20command=20to=20inst?= =?UTF-8?q?all=20requirements=20in=20Windows=20(#9445)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix command to install requirements --- docs/en/docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 58a363220..a1a32a1fe 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -108,7 +108,7 @@ After activating the environment as described above:
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -e ".[dev,doc,test]" ---> 100% ``` From 60ef67a66b1e0cadc2f4b4d6ea9f84baec8768f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 2 May 2023 06:28:23 +0000 Subject: [PATCH 0936/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 29ac94cee..0a167b9c5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Fix command to install requirements in Windows. PR [#9445](https://github.com/tiangolo/fastapi/pull/9445) by [@MariiaRomanuik](https://github.com/MariiaRomanuik). * 🌐 Add French translation for `docs/fr/docs/advanced/response-directly.md`. PR [#9415](https://github.com/tiangolo/fastapi/pull/9415) by [@axel584](https://github.com/axel584). * 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). * ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). From d56f02a9868ae6f90d2233ad5d12b25213774cb7 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Mon, 8 May 2023 14:11:00 +0300 Subject: [PATCH 0937/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/deployment/https.md`=20(#9428)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Artem Golicyn <86262613+AGolicyn@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/deployment/https.md | 198 +++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 199 insertions(+) create mode 100644 docs/ru/docs/deployment/https.md diff --git a/docs/ru/docs/deployment/https.md b/docs/ru/docs/deployment/https.md new file mode 100644 index 000000000..a53ab6927 --- /dev/null +++ b/docs/ru/docs/deployment/https.md @@ -0,0 +1,198 @@ +# Об HTTPS + +Обычно представляется, что HTTPS это некая опция, которая либо "включена", либо нет. + +Но всё несколько сложнее. + +!!! tip "Заметка" + Если Вы торопитесь или Вам не интересно, можете перейти на следующую страницу этого пошагового руководства по размещению приложений на серверах с использованием различных технологий. + +Чтобы **изучить основы HTTPS** для клиента, перейдите по ссылке https://howhttps.works/. + +Здесь же представлены некоторые концепции, которые **разработчик** должен иметь в виду при размышлениях об HTTPS: + +* Протокол HTTPS предполагает, что **серверу** нужно **располагать "сертификатами"** сгенерированными **третьей стороной**. + * На самом деле эти сертификаты **приобретены** у третьей стороны, а не "сгенерированы". +* У сертификатов есть **срок годности**. + * Срок годности **истекает**. + * По истечении срока годности их нужно **обновить**, то есть **снова получить** у третьей стороны. +* Шифрование соединения происходит **на уровне протокола TCP**. + * Протокол TCP находится на один уровень **ниже протокола HTTP**. + * Поэтому **проверка сертификатов и шифрование** происходит **до HTTP**. +* **TCP не знает о "доменах"**, но знает об IP-адресах. + * Информация о **запрашиваемом домене** извлекается из запроса **на уровне HTTP**. +* **Сертификаты HTTPS** "сертифицируют" **конкретный домен**, но проверка сертификатов и шифрование данных происходит на уровне протокола TCP, то есть **до того**, как станет известен домен-получатель данных. +* **По умолчанию** это означает, что у Вас может быть **только один сертификат HTTPS на один IP-адрес**. + * Не важно, насколько большой у Вас сервер и насколько маленькие приложения на нём могут быть. + * Однако, у этой проблемы есть **решение**. +* Существует **расширение** протокола **TLS** (который работает на уровне TCP, то есть до HTTP) называемое **SNI**. + * Расширение SNI позволяет одному серверу (с **одним IP-адресом**) иметь **несколько сертификатов HTTPS** и обслуживать **множество HTTPS-доменов/приложений**. + * Чтобы эта конструкция работала, **один** её компонент (программа) запущенный на сервере и слушающий **публичный IP-адрес**, должен иметь **все сертификаты HTTPS** для этого сервера. +* **После** установления защищённого соединения, протоколом передачи данных **остаётся HTTP**. + * Но данные теперь **зашифрованы**, несмотря на то, что они передаются по **протоколу HTTP**. + +Обычной практикой является иметь **одну программу/HTTP-сервер** запущенную на сервере (машине, хосте и т.д.) и **ответственную за всю работу с HTTPS**: + +* получение **зашифрованных HTTPS-запросов** +* отправка **расшифрованных HTTP запросов** в соответствующее HTTP-приложение, работающее на том же сервере (в нашем случае, это приложение **FastAPI**) +* получние **HTTP-ответа** от приложения +* **шифрование ответа** используя подходящий **сертификат HTTPS** +* отправка зашифрованного **HTTPS-ответа клиенту**. +Такой сервер часто называют **Прокси-сервер завершения работы TLS** или просто "прокси-сервер". + +Вот некоторые варианты, которые Вы можете использовать в качестве такого прокси-сервера: + +* Traefik (может обновлять сертификаты) +* Caddy (может обновлять сертификаты) +* Nginx +* HAProxy + +## Let's Encrypt (центр сертификации) + +До появления Let's Encrypt **сертификаты HTTPS** приходилось покупать у третьих сторон. + +Процесс получения такого сертификата был трудоёмким, требовал предоставления подтверждающих документов и сертификаты стоили дорого. + +Но затем консорциумом Linux Foundation был создан проект **Let's Encrypt**. + +Он автоматически предоставляет **бесплатные сертификаты HTTPS**. Эти сертификаты используют все стандартные криптографические способы шифрования. Они имеют небольшой срок годности (около 3 месяцев), благодаря чему они даже **более безопасны**. + +При запросе на получение сертификата, он автоматически генерируется и домен проверяется на безопасность. Это позволяет обновлять сертификаты автоматически. + +Суть идеи в автоматическом получении и обновлении этих сертификатов, чтобы все могли пользоваться **безопасным HTTPS. Бесплатно. В любое время.** + +## HTTPS для разработчиков + +Ниже, шаг за шагом, с заострением внимания на идеях, важных для разработчика, описано, как может выглядеть HTTPS API. + +### Имя домена + +Чаще всего, всё начинается с **приобретения имени домена**. Затем нужно настроить DNS-сервер (вероятно у того же провайдера, который выдал Вам домен). + +Далее, возможно, Вы получаете "облачный" сервер (виртуальную машину) или что-то типа этого, у которого есть постоянный **публичный IP-адрес**. + +На DNS-сервере (серверах) Вам следует настроить соответствующую ресурсную запись ("`запись A`"), указав, что **Ваш домен** связан с публичным **IP-адресом Вашего сервера**. + +Обычно эту запись достаточно указать один раз, при первоначальной настройке всего сервера. + +!!! tip "Заметка" + Уровни протоколов, работающих с именами доменов, намного ниже HTTPS, но об этом следует упомянуть здесь, так как всё зависит от доменов и IP-адресов. + +### DNS + +Теперь давайте сфокусируемся на работе с HTTPS. + +Всё начинается с того, что браузер спрашивает у **DNS-серверов**, какой **IP-адрес связан с доменом**, для примера возьмём домен `someapp.example.com`. + +DNS-сервера присылают браузеру определённый **IP-адрес**, тот самый публичный IP-адрес Вашего сервера, который Вы указали в ресурсной "записи А" при настройке. + + + +### Рукопожатие TLS + +В дальнейшем браузер будет взаимодействовать с этим IP-адресом через **port 443** (общепринятый номер порта для HTTPS). + +Первым шагом будет установление соединения между клиентом (браузером) и сервером и выбор криптографического ключа (для шифрования). + + + +Эта часть клиент-серверного взаимодействия устанавливает TLS-соединение и называется **TLS-рукопожатием**. + +### TLS с расширением SNI + +На сервере **только один процесс** может прослушивать определённый **порт** определённого **IP-адреса**. На сервере могут быть и другие процессы, слушающие другие порты этого же IP-адреса, но никакой процесс не может быть привязан к уже занятой комбинации IP-адрес:порт. Эта комбинация называется "сокет". + +По умолчанию TLS (HTTPS) использует порт `443`. Потому этот же порт будем использовать и мы. + +И раз уж только один процесс может слушать этот порт, то это будет процесс **прокси-сервера завершения работы TLS**. + +Прокси-сервер завершения работы TLS будет иметь доступ к одному или нескольким **TLS-сертификатам** (сертификаты HTTPS). + +Используя **расширение SNI** упомянутое выше, прокси-сервер из имеющихся сертификатов TLS (HTTPS) выберет тот, который соответствует имени домена, указанному в запросе от клиента. + +То есть будет выбран сертификат для домена `someapp.example.com`. + + + +Клиент уже **доверяет** тому, кто выдал этот TLS-сертификат (в нашем случае - Let's Encrypt, но мы ещё обсудим это), потому может **проверить**, действителен ли полученный от сервера сертификат. + +Затем, используя этот сертификат, клиент и прокси-сервер **выбирают способ шифрования** данных для устанавливаемого **TCP-соединения**. На этом операция **TLS-рукопожатия** завершена. + +В дальнейшем клиент и сервер будут взаимодействовать по **зашифрованному TCP-соединению**, как предлагается в протоколе TLS. И на основе этого TCP-соедениния будет создано **HTTP-соединение**. + +Таким образом, **HTTPS** это тот же **HTTP**, но внутри **безопасного TLS-соединения** вместо чистого (незашифрованного) TCP-соединения. + +!!! tip "Заметка" + Обратите внимание, что шифрование происходит на **уровне TCP**, а не на более высоком уровне HTTP. + +### HTTPS-запрос + +Теперь, когда между клиентом и сервером (в нашем случае, браузером и прокси-сервером) создано **зашифрованное TCP-соединение**, они могут начать **обмен данными по протоколу HTTP**. + +Так клиент отправляет **HTTPS-запрос**. То есть обычный HTTP-запрос, но через зашифрованное TLS-содинение. + + + +### Расшифровка запроса + +Прокси-сервер, используя согласованный с клиентом ключ, расшифрует полученный **зашифрованный запрос** и передаст **обычный (незашифрованный) HTTP-запрос** процессу, запускающему приложение (например, процессу Uvicorn запускающему приложение FastAPI). + + + +### HTTP-ответ + +Приложение обработает запрос и вернёт **обычный (незашифрованный) HTTP-ответ** прокси-серверу. + + + +### HTTPS-ответ + +Пркоси-сервер **зашифрует ответ** используя ранее согласованный с клиентом способ шифрования (которые содержатся в сертификате для домена `someapp.example.com`) и отправит его браузеру. + +Наконец, браузер проверит ответ, в том числе, что тот зашифрован с нужным ключом, **расшифрует его** и обработает. + + + +Клиент (браузер) знает, что ответ пришёл от правильного сервера, так как использует методы шифрования, согласованные ими раннее через **HTTPS-сертификат**. + +### Множество приложений + +На одном и том же сервере (или серверах) можно разместить **множество приложений**, например, другие программы с API или базы данных. + +Напомню, что только один процесс (например, прокси-сервер) может прослушивать определённый порт определённого IP-адреса. +Но другие процессы и приложения тоже могут работать на этом же сервере (серверах), если они не пытаются использовать уже занятую **комбинацию IP-адреса и порта** (сокет). + + + +Таким образом, сервер завершения TLS может обрабатывать HTTPS-запросы и использовать сертификаты для **множества доменов** или приложений и передавать запросы правильным адресатам (другим приложениям). + +### Обновление сертификата + +В недалёком будущем любой сертификат станет **просроченным** (примерно через три месяца после получения). + +Когда это произойдёт, можно запустить другую программу, которая подключится к Let's Encrypt и обновит сертификат(ы). Существуют прокси-серверы, которые могут сделать это действие самостоятельно. + + + +**TLS-сертификаты** не привязаны к IP-адресу, но **связаны с именем домена**. + +Так что при обновлении сертификатов программа должна **подтвердить** центру сертификации (Let's Encrypt), что обновление запросил **"владелец", который контролирует этот домен**. + +Есть несколько путей осуществления этого. Самые популярные из них: + +* **Изменение записей DNS**. + * Для такого варианта Ваша программа обновления должна уметь работать с API DNS-провайдера, обслуживающего Ваши DNS-записи. Не у всех провайдеров есть такой API, так что этот способ не всегда применим. +* **Запуск в качестве программы-сервера** (как минимум, на время обновления сертификатов) на публичном IP-адресе домена. + * Как уже не раз упоминалось, только один процесс может прослушивать определённый порт определённого IP-адреса. + * Это одна из причин использования прокси-сервера ещё и в качестве программы обновления сертификатов. + * В случае, если обновлением сертификатов занимается другая программа, Вам понадобится остановить прокси-сервер, запустить программу обновления сертификатов на сокете, предназначенном для прокси-сервера, настроить прокси-сервер на работу с новыми сертификатами и перезапустить его. Эта схема далека от идеальной, так как Ваши приложения будут недоступны на время отключения прокси-сервера. + +Весь этот процесс обновления, одновременный с обслуживанием запросов, является одной из основных причин, по которой желательно иметь **отдельную систему для работы с HTTPS** в виде прокси-сервера завершения TLS, а не просто использовать сертификаты TLS непосредственно с сервером приложений (например, Uvicorn). + +## Резюме + +Наличие **HTTPS** очень важно и довольно **критично** в большинстве случаев. Однако, Вам, как разработчику, не нужно тратить много сил на это, достаточно **понимать эти концепции** и принципы их работы. + +Но узнав базовые основы **HTTPS** Вы можете легко совмещать разные инструменты, которые помогут Вам в дальнейшей разработке. + +В следующих главах я покажу Вам несколько примеров, как настраивать **HTTPS** для приложений **FastAPI**. 🔒 diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 5b6e338ce..b167eacd1 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -76,6 +76,7 @@ nav: - Развёртывание: - deployment/index.md - deployment/versions.md + - deployment/https.md - project-generation.md - alternatives.md - history-design-future.md From 778b909cc1a2b3472e820fa35ad9d5fec6f3c794 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 11:11:35 +0000 Subject: [PATCH 0938/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0a167b9c5..bcd32ee60 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/deployment/https.md`. PR [#9428](https://github.com/tiangolo/fastapi/pull/9428) by [@Xewus](https://github.com/Xewus). * ✏ Fix command to install requirements in Windows. PR [#9445](https://github.com/tiangolo/fastapi/pull/9445) by [@MariiaRomanuik](https://github.com/MariiaRomanuik). * 🌐 Add French translation for `docs/fr/docs/advanced/response-directly.md`. PR [#9415](https://github.com/tiangolo/fastapi/pull/9415) by [@axel584](https://github.com/axel584). * 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). From 928bb2e2ceee6722bf87d4f4e5b0b9ea3cd1f221 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Mon, 8 May 2023 14:14:19 +0300 Subject: [PATCH 0939/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/testing.md`=20(#9403)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Russian translation for docs/ru/docs/tutorial/testing.md --- docs/ru/docs/tutorial/testing.md | 212 +++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 213 insertions(+) create mode 100644 docs/ru/docs/tutorial/testing.md diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md new file mode 100644 index 000000000..3f9005112 --- /dev/null +++ b/docs/ru/docs/tutorial/testing.md @@ -0,0 +1,212 @@ +# Тестирование + +Благодаря Starlette, тестировать приложения **FastAPI** легко и приятно. + +Тестирование основано на библиотеке HTTPX, которая в свою очередь основана на библиотеке Requests, так что все действия знакомы и интуитивно понятны. + +Используя эти инструменты, Вы можете напрямую задействовать pytest с **FastAPI**. + +## Использование класса `TestClient` + +!!! info "Информация" + Для использования класса `TestClient` необходимо установить библиотеку `httpx`. + + Например, так: `pip install httpx`. + +Импортируйте `TestClient`. + +Создайте объект `TestClient`, передав ему в качестве параметра Ваше приложение **FastAPI**. + +Создайте функцию, название которой должно начинаться с `test_` (это стандарт из соглашений `pytest`). + +Используйте объект `TestClient` так же, как Вы используете `httpx`. + +Напишите простое утверждение с `assert` дабы проверить истинность Python-выражения (это тоже стандарт `pytest`). + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! tip "Подсказка" + Обратите внимание, что тестирующая функция является обычной `def`, а не асинхронной `async def`. + + И вызов клиента также осуществляется без `await`. + + Это позволяет вам использовать `pytest` без лишних усложнений. + +!!! note "Технические детали" + Также можно написать `from starlette.testclient import TestClient`. + + **FastAPI** предоставляет тот же самый `starlette.testclient` как `fastapi.testclient`. Это всего лишь небольшое удобство для Вас, как разработчика. + +!!! tip "Подсказка" + Если для тестирования Вам, помимо запросов к приложению FastAPI, необходимо вызывать асинхронные функции (например, для подключения к базе данных с помощью асинхронного драйвера), то ознакомьтесь со страницей [Асинхронное тестирование](../advanced/async-tests.md){.internal-link target=_blank} в расширенном руководстве. + +## Разделение тестов и приложения + +В реальном приложении Вы, вероятно, разместите тесты в отдельном файле. + +Кроме того, Ваше приложение **FastAPI** может состоять из нескольких файлов, модулей и т.п. + +### Файл приложения **FastAPI** + +Допустим, структура файлов Вашего приложения похожа на ту, что описана на странице [Более крупные приложения](./bigger-applications.md){.internal-link target=_blank}: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +Здесь файл `main.py` является "точкой входа" в Ваше приложение и содержит инициализацию Вашего приложения **FastAPI**: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### Файл тестов + +Также у Вас может быть файл `test_main.py` содержащий тесты. Можно разместить тестовый файл и файл приложения в одной директории (в директориях для Python-кода желательно размещать и файл `__init__.py`): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Так как оба файла находятся в одной директории, для импорта объекта приложения из файла `main` в файл `test_main` Вы можете использовать относительный импорт: + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...и писать дальше тесты, как и раньше. + +## Тестирование: расширенный пример + +Теперь давайте расширим наш пример и добавим деталей, чтоб посмотреть, как тестировать различные части приложения. + +### Расширенный файл приложения **FastAPI** + +Мы продолжим работу с той же файловой структурой, что и ранее: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +Предположим, что в файле `main.py` с приложением **FastAPI** есть несколько **операций пути**. + +В нём описана операция `GET`, которая может вернуть ошибку. + +Ещё есть операция `POST` и она тоже может вернуть ошибку. + +Обе *операции пути* требуют наличия в запросе заголовка `X-Token`. + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + По возможности используйте версию с `Annotated`. + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + По возможности используйте версию с `Annotated`. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +### Расширенный файл тестов + +Теперь обновим файл `test_main.py`, добавив в него тестов: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +Если Вы не знаете, как передать информацию в запросе, можете воспользоваться поисковиком (погуглить) и задать вопрос: "Как передать информацию в запросе с помощью `httpx`", можно даже спросить: "Как передать информацию в запросе с помощью `requests`", поскольку дизайн HTTPX основан на дизайне Requests. + +Затем Вы просто применяете найденные ответы в тестах. + +Например: + +* Передаёте *path*-параметры или *query*-параметры, вписав их непосредственно в строку URL. +* Передаёте JSON в теле запроса, передав Python-объект (например: `dict`) через именованный параметр `json`. +* Если же Вам необходимо отправить *форму с данными* вместо JSON, то используйте параметр `data` вместо `json`. +* Для передачи *заголовков*, передайте объект `dict` через параметр `headers`. +* Для передачи *cookies* также передайте `dict`, но через параметр `cookies`. + +Для получения дополнительной информации о передаче данных на бэкенд с помощью `httpx` или `TestClient` ознакомьтесь с документацией HTTPX. + +!!! info "Информация" + Обратите внимание, что `TestClient` принимает данные, которые можно конвертировать в JSON, но не модели Pydantic. + + Если в Ваших тестах есть модели Pydantic и Вы хотите отправить их в тестируемое приложение, то можете использовать функцию `jsonable_encoder`, описанную на странице [Кодировщик совместимый с JSON](encoder.md){.internal-link target=_blank}. + +## Запуск тестов + +Далее Вам нужно установить `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +Он автоматически найдёт все файлы и тесты, выполнит их и предоставит Вам отчёт о результатах тестирования. + +Запустите тесты командой `pytest` и увидите результат: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index b167eacd1..08223016c 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -71,6 +71,7 @@ nav: - tutorial/background-tasks.md - tutorial/extra-data-types.md - tutorial/cookie-params.md + - tutorial/testing.md - tutorial/response-status-code.md - async.md - Развёртывание: From 33fa1b0927af72f8465344cf8f7c8e35cf03bd38 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 11:14:57 +0000 Subject: [PATCH 0940/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bcd32ee60..9826894eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/testing.md`. PR [#9403](https://github.com/tiangolo/fastapi/pull/9403) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/deployment/https.md`. PR [#9428](https://github.com/tiangolo/fastapi/pull/9428) by [@Xewus](https://github.com/Xewus). * ✏ Fix command to install requirements in Windows. PR [#9445](https://github.com/tiangolo/fastapi/pull/9445) by [@MariiaRomanuik](https://github.com/MariiaRomanuik). * 🌐 Add French translation for `docs/fr/docs/advanced/response-directly.md`. PR [#9415](https://github.com/tiangolo/fastapi/pull/9415) by [@axel584](https://github.com/axel584). From ed1f93f80367d0cc0191fd30ccdc6b51d8c30544 Mon Sep 17 00:00:00 2001 From: Saleumsack KEOBOUALAY Date: Mon, 8 May 2023 18:15:37 +0700 Subject: [PATCH 0941/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20setup=20for=20tr?= =?UTF-8?q?anslations=20to=20Lao=20(#9396)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/az/mkdocs.yml | 3 + docs/de/mkdocs.yml | 3 + docs/em/mkdocs.yml | 3 + docs/en/mkdocs.yml | 3 + docs/es/mkdocs.yml | 3 + docs/fa/mkdocs.yml | 3 + docs/fr/mkdocs.yml | 3 + docs/he/mkdocs.yml | 3 + docs/hy/mkdocs.yml | 3 + docs/id/mkdocs.yml | 3 + docs/it/mkdocs.yml | 3 + docs/ja/mkdocs.yml | 3 + docs/ko/mkdocs.yml | 3 + docs/lo/docs/index.md | 469 +++++++++++++++++++++++++++++++++++ docs/lo/mkdocs.yml | 157 ++++++++++++ docs/lo/overrides/.gitignore | 0 docs/nl/mkdocs.yml | 3 + docs/pl/mkdocs.yml | 3 + docs/pt/mkdocs.yml | 3 + docs/ru/mkdocs.yml | 3 + docs/sq/mkdocs.yml | 3 + docs/sv/mkdocs.yml | 3 + docs/ta/mkdocs.yml | 3 + docs/tr/mkdocs.yml | 3 + docs/uk/mkdocs.yml | 3 + docs/zh/mkdocs.yml | 3 + 26 files changed, 695 insertions(+) create mode 100644 docs/lo/docs/index.md create mode 100644 docs/lo/mkdocs.yml create mode 100644 docs/lo/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 22a77c6e2..1d2930494 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 8ee11d46c..e475759a8 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -130,6 +131,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index df21a1093..2c48de93a 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -51,6 +51,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -233,6 +234,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index fd7decee8..b7cefee53 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -236,6 +237,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 85db87ad1..8152c91e3 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -139,6 +140,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 5bb1a2ff5..2a966f664 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 854d14474..0b73d3cae 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -158,6 +159,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index acf7ea8fd..b8a674812 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index 5d251ff69..19d747c31 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 55461328f..460cb6914 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 251d86681..b3a48482b 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 98a18cf4f..91b9a6658 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -173,6 +174,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 138ab678b..aec1c7569 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -143,6 +144,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/lo/docs/index.md b/docs/lo/docs/index.md new file mode 100644 index 000000000..9a81f14d1 --- /dev/null +++ b/docs/lo/docs/index.md @@ -0,0 +1,469 @@ +

+ FastAPI +

+

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

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Documentation**: https://fastapi.tiangolo.com + +**Source Code**: https://github.com/tiangolo/fastapi + +--- + +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. + +The key features are: + +* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). +* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * +* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * +* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. +* **Easy**: Designed to be easy to use and learn. Less time reading docs. +* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. +* **Robust**: Get production-ready code. With automatic interactive documentation. +* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. + +* estimation based on tests on an internal development team, building production applications. + +## Sponsors + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Other sponsors + +## Opinions + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, the FastAPI of CLIs + + + +If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. + +**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 + +## Requirements + +Python 3.7+ + +FastAPI stands on the shoulders of giants: + +* Starlette for the web parts. +* Pydantic for the data parts. + +## Installation + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +You will also need an ASGI server, for production such as Uvicorn or Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Example + +### Create it + +* Create a file `main.py` with: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Or use async def... + +If your code uses `async` / `await`, use `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Note**: + +If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. + +
+ +### Run it + +Run the server with: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+About the command uvicorn main:app --reload... + +The command `uvicorn main:app` refers to: + +* `main`: the file `main.py` (the Python "module"). +* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. +* `--reload`: make the server restart after code changes. Only do this for development. + +
+ +### Check it + +Open your browser at http://127.0.0.1:8000/items/5?q=somequery. + +You will see the JSON response as: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +You already created an API that: + +* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. +* Both _paths_ take `GET` operations (also known as HTTP _methods_). +* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. +* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. + +### Interactive API docs + +Now go to http://127.0.0.1:8000/docs. + +You will see the automatic interactive API documentation (provided by Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API docs + +And now, go to http://127.0.0.1:8000/redoc. + +You will see the alternative automatic documentation (provided by ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Example upgrade + +Now modify the file `main.py` to receive a body from a `PUT` request. + +Declare the body using standard Python types, thanks to Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +The server should reload automatically (because you added `--reload` to the `uvicorn` command above). + +### Interactive API docs upgrade + +Now go to http://127.0.0.1:8000/docs. + +* The interactive API documentation will be automatically updated, including the new body: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternative API docs upgrade + +And now, go to http://127.0.0.1:8000/redoc. + +* The alternative documentation will also reflect the new query parameter and body: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Recap + +In summary, you declare **once** the types of parameters, body, etc. as function parameters. + +You do that with standard modern Python types. + +You don't have to learn a new syntax, the methods or classes of a specific library, etc. + +Just standard **Python 3.7+**. + +For example, for an `int`: + +```Python +item_id: int +``` + +or for a more complex `Item` model: + +```Python +item: Item +``` + +...and with that single declaration you get: + +* Editor support, including: + * Completion. + * Type checks. +* Validation of data: + * Automatic and clear errors when the data is invalid. + * Validation even for deeply nested JSON objects. +* Conversion of input data: coming from the network to Python data and types. Reading from: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Forms. + * Files. +* Conversion of output data: converting from Python data and types to network data (as JSON): + * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...and many more. +* Automatic interactive API documentation, including 2 alternative user interfaces: + * Swagger UI. + * ReDoc. + +--- + +Coming back to the previous code example, **FastAPI** will: + +* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. +* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. + * If it is not, the client will see a useful, clear error. +* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. + * As the `q` parameter is declared with `= None`, it is optional. + * Without the `None` it would be required (as is the body in the case with `PUT`). +* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: + * Check that it has a required attribute `name` that should be a `str`. + * Check that it has a required attribute `price` that has to be a `float`. + * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. + * All this would also work for deeply nested JSON objects. +* Convert from and to JSON automatically. +* Document everything with OpenAPI, that can be used by: + * Interactive documentation systems. + * Automatic client code generation systems, for many languages. +* Provide 2 interactive documentation web interfaces directly. + +--- + +We just scratched the surface, but you already get the idea of how it all works. + +Try changing the line with: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...from: + +```Python + ... "item_name": item.name ... +``` + +...to: + +```Python + ... "item_price": item.price ... +``` + +...and see how your editor will auto-complete the attributes and know their types: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +For a more complete example including more features, see the Tutorial - User Guide. + +**Spoiler alert**: the tutorial - user guide includes: + +* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. +* How to set **validation constraints** as `maximum_length` or `regex`. +* A very powerful and easy to use **Dependency Injection** system. +* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. +* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). +* **GraphQL** integration with Strawberry and other libraries. +* Many extra features (thanks to Starlette) as: + * **WebSockets** + * extremely easy tests based on HTTPX and `pytest` + * **CORS** + * **Cookie Sessions** + * ...and more. + +## Performance + +Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) + +To understand more about it, see the section Benchmarks. + +## Optional Dependencies + +Used by Pydantic: + +* ujson - for faster JSON "parsing". +* email_validator - for email validation. + +Used by Starlette: + +* httpx - Required if you want to use the `TestClient`. +* jinja2 - Required if you want to use the default template configuration. +* python-multipart - Required if you want to support form "parsing", with `request.form()`. +* itsdangerous - Required for `SessionMiddleware` support. +* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). +* ujson - Required if you want to use `UJSONResponse`. + +Used by FastAPI / Starlette: + +* uvicorn - for the server that loads and serves your application. +* orjson - Required if you want to use `ORJSONResponse`. + +You can install all of these with `pip install "fastapi[all]"`. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml new file mode 100644 index 000000000..450ebcd2b --- /dev/null +++ b/docs/lo/mkdocs.yml @@ -0,0 +1,157 @@ +site_name: FastAPI +site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production +site_url: https://fastapi.tiangolo.com/lo/ +theme: + name: material + custom_dir: overrides + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: teal + accent: amber + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: teal + accent: amber + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + icon: + repo: fontawesome/brands/github-alt + logo: https://fastapi.tiangolo.com/img/icon-white.svg + favicon: https://fastapi.tiangolo.com/img/favicon.png + language: en +repo_name: tiangolo/fastapi +repo_url: https://github.com/tiangolo/fastapi +edit_uri: '' +plugins: +- search +- markdownextradata: + data: data +nav: +- FastAPI: index.md +- Languages: + - en: / + - az: /az/ + - de: /de/ + - em: /em/ + - es: /es/ + - fa: /fa/ + - fr: /fr/ + - he: /he/ + - hy: /hy/ + - id: /id/ + - it: /it/ + - ja: /ja/ + - ko: /ko/ + - lo: /lo/ + - nl: /nl/ + - pl: /pl/ + - pt: /pt/ + - ru: /ru/ + - sq: /sq/ + - sv: /sv/ + - ta: /ta/ + - tr: /tr/ + - uk: /uk/ + - zh: /zh/ +markdown_extensions: +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format '' +- pymdownx.tabbed: + alternate_style: true +- attr_list +- md_in_html +extra: + analytics: + provider: google + property: G-YNEVN69SC3 + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/tiangolo/fastapi + - icon: fontawesome/brands/discord + link: https://discord.gg/VQjSZaeJmf + - icon: fontawesome/brands/twitter + link: https://twitter.com/fastapi + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/tiangolo + - icon: fontawesome/brands/dev + link: https://dev.to/tiangolo + - icon: fontawesome/brands/medium + link: https://medium.com/@tiangolo + - icon: fontawesome/solid/globe + link: https://tiangolo.com + alternate: + - link: / + name: en - English + - link: /az/ + name: az + - link: /de/ + name: de + - link: /em/ + name: 😉 + - link: /es/ + name: es - español + - link: /fa/ + name: fa + - link: /fr/ + name: fr - français + - link: /he/ + name: he + - link: /hy/ + name: hy + - link: /id/ + name: id + - link: /it/ + name: it - italiano + - link: /ja/ + name: ja - 日本語 + - link: /ko/ + name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ + - link: /nl/ + name: nl + - link: /pl/ + name: pl + - link: /pt/ + name: pt - português + - link: /ru/ + name: ru - русский язык + - link: /sq/ + name: sq - shqip + - link: /sv/ + name: sv - svenska + - link: /ta/ + name: ta - தமிழ் + - link: /tr/ + name: tr - Türkçe + - link: /uk/ + name: uk - українська мова + - link: /zh/ + name: zh - 汉语 +extra_css: +- https://fastapi.tiangolo.com/css/termynal.css +- https://fastapi.tiangolo.com/css/custom.css +extra_javascript: +- https://fastapi.tiangolo.com/js/termynal.js +- https://fastapi.tiangolo.com/js/custom.js diff --git a/docs/lo/overrides/.gitignore b/docs/lo/overrides/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 55c971aa4..96c93abff 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index af68f1b74..0d7a783fc 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -132,6 +133,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 9c3007e03..c3ffe7858 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -169,6 +170,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 08223016c..bb0440d04 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -152,6 +153,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index ca20bce30..092c50816 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index ce6ee3f83..215b32f18 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index d66fc5740..4b96d2cad 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 96306ee77..5811f793e 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -134,6 +135,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 5ba681396..5e22570b1 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -129,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 015423752..ef7ab1fd9 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -52,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -186,6 +187,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ From bdb32bfe03bc9cbc5f9a35cbb0e6e49cbd558bde Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 11:16:10 +0000 Subject: [PATCH 0942/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9826894eb..5eb9f4113 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/testing.md`. PR [#9403](https://github.com/tiangolo/fastapi/pull/9403) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/deployment/https.md`. PR [#9428](https://github.com/tiangolo/fastapi/pull/9428) by [@Xewus](https://github.com/Xewus). * ✏ Fix command to install requirements in Windows. PR [#9445](https://github.com/tiangolo/fastapi/pull/9445) by [@MariiaRomanuik](https://github.com/MariiaRomanuik). From 42ca1cb42d61ab78f517ddd04c88d15faaa5f1a2 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Mon, 8 May 2023 14:16:31 +0300 Subject: [PATCH 0943/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/deployment/manually.md`=20(#9417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Artem Golicyn <86262613+AGolicyn@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/deployment/manually.md | 150 ++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 151 insertions(+) create mode 100644 docs/ru/docs/deployment/manually.md diff --git a/docs/ru/docs/deployment/manually.md b/docs/ru/docs/deployment/manually.md new file mode 100644 index 000000000..1d00b3086 --- /dev/null +++ b/docs/ru/docs/deployment/manually.md @@ -0,0 +1,150 @@ +# Запуск сервера вручную - Uvicorn + +Для запуска приложения **FastAPI** на удалённой серверной машине Вам необходим программный сервер, поддерживающий протокол ASGI, такой как **Uvicorn**. + +Существует три наиболее распространённые альтернативы: + +* Uvicorn: высокопроизводительный ASGI сервер. +* Hypercorn: ASGI сервер, помимо прочего поддерживающий HTTP/2 и Trio. +* Daphne: ASGI сервер, созданный для Django Channels. + +## Сервер как машина и сервер как программа + +В этих терминах есть некоторые различия и Вам следует запомнить их. 💡 + +Слово "**сервер**" чаще всего используется в двух контекстах: + +- удалённый или расположенный в "облаке" компьютер (физическая или виртуальная машина). +- программа, запущенная на таком компьютере (например, Uvicorn). + +Просто запомните, если Вам встретился термин "сервер", то обычно он подразумевает что-то из этих двух смыслов. + +Когда имеют в виду именно удалённый компьютер, часто говорят просто **сервер**, но ещё его называют **машина**, **ВМ** (виртуальная машина), **нода**. Все эти термины обозначают одно и то же - удалённый компьютер, обычно под управлением Linux, на котором Вы запускаете программы. + +## Установка программного сервера + +Вы можете установить сервер, совместимый с протоколом ASGI, так: + +=== "Uvicorn" + + * Uvicorn, молниесный ASGI сервер, основанный на библиотеках uvloop и httptools. + +
+ + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + +
+ + !!! tip "Подсказка" + С опцией `standard`, Uvicorn будет установливаться и использоваться с некоторыми дополнительными рекомендованными зависимостями. + + В них входит `uvloop`, высокопроизводительная замена `asyncio`, которая значительно ускоряет работу асинхронных программ. + +=== "Hypercorn" + + * Hypercorn, ASGI сервер, поддерживающий протокол HTTP/2. + +
+ + ```console + $ pip install hypercorn + + ---> 100% + ``` + +
+ + ...или какой-либо другой ASGI сервер. + +## Запуск серверной программы + +Затем запустите Ваше приложение так же, как было указано в руководстве ранее, но без опции `--reload`: + +=== "Uvicorn" + +
+ + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + INFO: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + +
+ +=== "Hypercorn" + +
+ + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + +
+ +!!! warning "Предупреждение" + + Не забудьте удалить опцию `--reload`, если ранее пользовались ею. + + Включение опции `--reload` требует дополнительных ресурсов, влияет на стабильность работы приложения и может повлечь прочие неприятности. + + Она сильно помогает во время **разработки**, но **не следует** использовать её при **реальной работе** приложения. + +## Hypercorn с Trio + +Starlette и **FastAPI** основаны на AnyIO, которая делает их совместимыми как с asyncio - стандартной библиотекой Python, так и с Trio. + + +Тем не менее Uvicorn совместим только с asyncio и обычно используется совместно с `uvloop`, высокопроизводительной заменой `asyncio`. + +Но если Вы хотите использовать **Trio** напрямую, то можете воспользоваться **Hypercorn**, так как они совместимы. ✨ + +### Установка Hypercorn с Trio + +Для начала, Вам нужно установить Hypercorn с поддержкой Trio: + +
+ +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +
+ +### Запуск с Trio + +Далее запустите Hypercorn с опцией `--worker-class` и аргументом `trio`: + +
+ +```console +$ hypercorn main:app --worker-class trio +``` + +
+ +Hypercorn, в свою очередь, запустит Ваше приложение использующее Trio. + +Таким образом, Вы сможете использовать Trio в своём приложении. Но лучше использовать AnyIO, для сохранения совместимости и с Trio, и с asyncio. 🎉 + +## Концепции развёртывания + +В вышеприведённых примерах серверные программы (например Uvicorn) запускали только **один процесс**, принимающий входящие запросы с любого IP (на это указывал аргумент `0.0.0.0`) на определённый порт (в примерах мы указывали порт `80`). + +Это основная идея. Но возможно, Вы озаботитесь добавлением дополнительных возможностей, таких как: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Я поведаю Вам больше о каждой из этих концепций в следующих главах, с конкретными примерами стратегий работы с ними. 🚀 diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index bb0440d04..93fae36ce 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -79,6 +79,7 @@ nav: - deployment/index.md - deployment/versions.md - deployment/https.md + - deployment/manually.md - project-generation.md - alternatives.md - history-design-future.md From f2d01d7a6ad5f921c8d19f413d4a18f61014c2f8 Mon Sep 17 00:00:00 2001 From: oandersonmagalhaes <83456692+oandersonmagalhaes@users.noreply.github.com> Date: Mon, 8 May 2023 08:16:57 -0300 Subject: [PATCH 0944/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/advanced/events.md`=20(#9326)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fernando Crozetta Co-authored-by: Lorhan Sohaky <16273730+LorhanSohaky@users.noreply.github.com> --- docs/pt/docs/advanced/events.md | 163 ++++++++++++++++++++++++++++++++ docs/pt/mkdocs.yml | 1 + 2 files changed, 164 insertions(+) create mode 100644 docs/pt/docs/advanced/events.md diff --git a/docs/pt/docs/advanced/events.md b/docs/pt/docs/advanced/events.md new file mode 100644 index 000000000..7f6cb6f5d --- /dev/null +++ b/docs/pt/docs/advanced/events.md @@ -0,0 +1,163 @@ +# Eventos de vida útil + +Você pode definir a lógica (código) que poderia ser executada antes da aplicação **inicializar**. Isso significa que esse código será executado **uma vez**, **antes** da aplicação **começar a receber requisições**. + +Do mesmo modo, você pode definir a lógica (código) que será executada quando a aplicação estiver sendo **encerrada**. Nesse caso, este código será executado **uma vez**, **depois** de ter possivelmente tratado **várias requisições**. + +Por conta desse código ser executado antes da aplicação **começar** a receber requisições, e logo após **terminar** de lidar com as requisições, ele cobre toda a **vida útil** (_lifespan_) da aplicação (o termo "vida útil" será importante em um segundo 😉). + +Pode ser muito útil para configurar **recursos** que você precisa usar por toda aplicação, e que são **compartilhados** entre as requisições, e/ou que você precisa **limpar** depois. Por exemplo, o pool de uma conexão com o banco de dados ou carregamento de um modelo compartilhado de aprendizado de máquina (_machine learning_). + +## Caso de uso + +Vamos iniciar com um exemplo de **caso de uso** e então ver como resolvê-lo com isso. + +Vamos imaginar que você tem alguns **modelos de _machine learning_** que deseja usar para lidar com as requisições. 🤖 + +Os mesmos modelos são compartilhados entre as requisições, então não é um modelo por requisição, ou um por usuário ou algo parecido. + +Vamos imaginar que o carregamento do modelo pode **demorar bastante tempo**, porque ele tem que ler muitos **dados do disco**. Então você não quer fazer isso a cada requisição. + +Você poderia carregá-lo no nível mais alto do módulo/arquivo, mas isso também poderia significaria **carregar o modelo** mesmo se você estiver executando um simples teste automatizado, então esse teste poderia ser **lento** porque teria que esperar o carregamento do modelo antes de ser capaz de executar uma parte independente do código. + + +Isso é que nós iremos resolver, vamos carregar o modelo antes das requisições serem manuseadas, mas apenas um pouco antes da aplicação começar a receber requisições, não enquanto o código estiver sendo carregado. + +## Vida útil (_Lifespan_) + +Você pode definir essa lógica de *inicialização* e *encerramento* usando os parâmetros de `lifespan` da aplicação `FastAPI`, e um "gerenciador de contexto" (te mostrarei o que é isso a seguir). + +Vamos iniciar com um exemplo e ver isso detalhadamente. + +Nós criamos uma função assíncrona chamada `lifespan()` com `yield` como este: + +```Python hl_lines="16 19" +{!../../../docs_src/events/tutorial003.py!} +``` + +Aqui nós estamos simulando a *inicialização* custosa do carregamento do modelo colocando a (falsa) função de modelo no dicionário com modelos de _machine learning_ antes do `yield`. Este código será executado **antes** da aplicação **começar a receber requisições**, durante a *inicialização*. + +E então, logo após o `yield`, descarregaremos o modelo. Esse código será executado **após** a aplicação **terminar de lidar com as requisições**, pouco antes do *encerramento*. Isso poderia, por exemplo, liberar recursos como memória ou GPU. + +!!! tip "Dica" + O `shutdown` aconteceria quando você estivesse **encerrando** a aplicação. + + Talvez você precise inicializar uma nova versão, ou apenas cansou de executá-la. 🤷 + +### Função _lifespan_ + +A primeira coisa a notar, é que estamos definindo uma função assíncrona com `yield`. Isso é muito semelhante à Dependências com `yield`. + +```Python hl_lines="14-19" +{!../../../docs_src/events/tutorial003.py!} +``` + +A primeira parte da função, antes do `yield`, será executada **antes** da aplicação inicializar. + +E a parte posterior do `yield` irá executar **após** a aplicação ser encerrada. + +### Gerenciador de Contexto Assíncrono + +Se você verificar, a função está decorada com um `@asynccontextmanager`. + +Que converte a função em algo chamado de "**Gerenciador de Contexto Assíncrono**". + +```Python hl_lines="1 13" +{!../../../docs_src/events/tutorial003.py!} +``` + +Um **gerenciador de contexto** em Python é algo que você pode usar em uma declaração `with`, por exemplo, `open()` pode ser usado como um gerenciador de contexto: + +```Python +with open("file.txt") as file: + file.read() +``` + +Nas versões mais recentes de Python, há também um **gerenciador de contexto assíncrono**. Você o usaria com `async with`: + +```Python +async with lifespan(app): + await do_stuff() +``` + +Quando você cria um gerenciador de contexto ou um gerenciador de contexto assíncrono como mencionado acima, o que ele faz é que, antes de entrar no bloco `with`, ele irá executar o código anterior ao `yield`, e depois de sair do bloco `with`, ele irá executar o código depois do `yield`. + +No nosso exemplo de código acima, nós não usamos ele diretamente, mas nós passamos para o FastAPI para ele usá-lo. + +O parâmetro `lifespan` da aplicação `FastAPI` usa um **Gerenciador de Contexto Assíncrono**, então nós podemos passar nosso novo gerenciador de contexto assíncrono do `lifespan` para ele. + +```Python hl_lines="22" +{!../../../docs_src/events/tutorial003.py!} +``` + +## Eventos alternativos (deprecados) + +!!! warning "Aviso" + A maneira recomendada para lidar com a *inicialização* e o *encerramento* é usando o parâmetro `lifespan` da aplicação `FastAPI` como descrito acima. + + Você provavelmente pode pular essa parte. + +Existe uma forma alternativa para definir a execução dessa lógica durante *inicialização* e durante *encerramento*. + +Você pode definir manipuladores de eventos (funções) que precisam ser executadas antes da aplicação inicializar, ou quando a aplicação estiver encerrando. + +Essas funções podem ser declaradas com `async def` ou `def` normal. + +### Evento `startup` + +Para adicionar uma função que deve rodar antes da aplicação iniciar, declare-a com o evento `"startup"`: + +```Python hl_lines="8" +{!../../../docs_src/events/tutorial001.py!} +``` + +Nesse caso, a função de manipulação de evento `startup` irá inicializar os itens do "banco de dados" (só um `dict`) com alguns valores. + +Você pode adicionar mais que uma função de manipulação de evento. + +E sua aplicação não irá começar a receber requisições até que todos os manipuladores de eventos de `startup` sejam concluídos. + +### Evento `shutdown` + +Para adicionar uma função que deve ser executada quando a aplicação estiver encerrando, declare ela com o evento `"shutdown"`: + +```Python hl_lines="6" +{!../../../docs_src/events/tutorial002.py!} +``` + +Aqui, a função de manipulação de evento `shutdown` irá escrever uma linha de texto `"Application shutdown"` no arquivo `log.txt`. + +!!! info "Informação" + Na função `open()`, o `mode="a"` significa "acrescentar", então, a linha irá ser adicionada depois de qualquer coisa que esteja naquele arquivo, sem sobrescrever o conteúdo anterior. + +!!! tip "Dica" + Perceba que nesse caso nós estamos usando a função padrão do Python `open()` que interage com um arquivo. + + Então, isso envolve I/O (input/output), que exige "esperar" que coisas sejam escritas em disco. + + Mas `open()` não usa `async` e `await`. + + Então, nós declaramos uma função de manipulação de evento com o padrão `def` ao invés de `async def`. + +### `startup` e `shutdown` juntos + +Há uma grande chance que a lógica para sua *inicialização* e *encerramento* esteja conectada, você pode querer iniciar alguma coisa e então finalizá-la, adquirir um recurso e então liberá-lo, etc. + +Fazendo isso em funções separadas que não compartilham lógica ou variáveis entre elas é mais difícil já que você precisa armazenar os valores em variáveis globais ou truques parecidos. + +Por causa disso, agora é recomendado em vez disso usar o `lifespan` como explicado acima. + +## Detalhes técnicos + +Só um detalhe técnico para nerds curiosos. 🤓 + +Por baixo, na especificação técnica ASGI, essa é a parte do Protocolo Lifespan, e define eventos chamados `startup` e `shutdown`. + +!!! info "Informação" + Você pode ler mais sobre o manipulador `lifespan` do Starlette na Documentação do Lifespan Starlette. + + Incluindo como manipular estado do lifespan que pode ser usado em outras áreas do seu código. + +## Sub Aplicações + +🚨 Tenha em mente que esses eventos de lifespan (de inicialização e desligamento) irão somente ser executados para a aplicação principal, não para [Sub Aplicações - Montagem](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index c3ffe7858..023944618 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -92,6 +92,7 @@ nav: - tutorial/static-files.md - Guia de Usuário Avançado: - advanced/index.md + - advanced/events.md - Implantação: - deployment/index.md - deployment/versions.md From 3f5cfdc3fe4fa4a06c5910d93fd4fdd83a12be91 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 11:17:05 +0000 Subject: [PATCH 0945/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5eb9f4113..c45469e3b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). * 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/testing.md`. PR [#9403](https://github.com/tiangolo/fastapi/pull/9403) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/deployment/https.md`. PR [#9428](https://github.com/tiangolo/fastapi/pull/9428) by [@Xewus](https://github.com/Xewus). From 490bde716941f23bf93d0a9e17cb57cf049db225 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 11:18:04 +0000 Subject: [PATCH 0946/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c45469e3b..f44a2b3b1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). * 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). * 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/testing.md`. PR [#9403](https://github.com/tiangolo/fastapi/pull/9403) by [@Xewus](https://github.com/Xewus). From 724060df433daf0550d2c3853a7149f60fc5d626 Mon Sep 17 00:00:00 2001 From: Bighneswar Parida <102468247+mikBighne98@users.noreply.github.com> Date: Mon, 8 May 2023 18:02:02 +0530 Subject: [PATCH 0947/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20Deta=20deploy?= =?UTF-8?q?ment=20tutorial=20for=20compatibility=20with=20Deta=20Space=20(?= =?UTF-8?q?#6004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lemonyte <49930425+lemonyte@users.noreply.github.com> Co-authored-by: xeust Co-authored-by: Sebastián Ramírez --- docs/en/docs/deployment/deta.md | 323 +++++++++++++------ docs/en/docs/img/deployment/deta/image03.png | Bin 0 -> 52704 bytes docs/en/docs/img/deployment/deta/image04.png | Bin 0 -> 49760 bytes docs/en/docs/img/deployment/deta/image05.png | Bin 0 -> 33130 bytes docs/en/docs/img/deployment/deta/image06.png | Bin 0 -> 105270 bytes 5 files changed, 228 insertions(+), 95 deletions(-) create mode 100644 docs/en/docs/img/deployment/deta/image03.png create mode 100644 docs/en/docs/img/deployment/deta/image04.png create mode 100644 docs/en/docs/img/deployment/deta/image05.png create mode 100644 docs/en/docs/img/deployment/deta/image06.png diff --git a/docs/en/docs/deployment/deta.md b/docs/en/docs/deployment/deta.md index c0dc3336a..04fc04c0c 100644 --- a/docs/en/docs/deployment/deta.md +++ b/docs/en/docs/deployment/deta.md @@ -1,15 +1,22 @@ -# Deploy FastAPI on Deta +# Deploy FastAPI on Deta Space -In this section you will learn how to easily deploy a **FastAPI** application on Deta using the free plan. 🎁 +In this section you will learn how to easily deploy a **FastAPI** application on Deta Space, for free. 🎁 -It will take you about **10 minutes**. +It will take you about **10 minutes** to deploy an API that you can use. After that, you can optionally release it to anyone. + +Let's dive in. !!! info - Deta is a **FastAPI** sponsor. 🎉 + Deta is a **FastAPI** sponsor. 🎉 -## A basic **FastAPI** app +## A simple **FastAPI** app -* Create a directory for your app, for example, `./fastapideta/` and enter into it. +* To start, create an empty directory with the name of your app, for example `./fastapi-deta/`, and then navigate into it. + +```console +$ mkdir fastapi-deta +$ cd fastapi-deta +``` ### FastAPI code @@ -37,14 +44,12 @@ Now, in the same directory create a file `requirements.txt` with: ```text fastapi +uvicorn[standard] ``` -!!! tip - You don't need to install Uvicorn to deploy on Deta, although you would probably want to install it locally to test your app. - ### Directory structure -You will now have one directory `./fastapideta/` with two files: +You will now have a directory `./fastapi-deta/` with two files: ``` . @@ -52,22 +57,23 @@ You will now have one directory `./fastapideta/` with two files: └── requirements.txt ``` -## Create a free Deta account +## Create a free **Deta Space** account -Now create a free account on Deta, you just need an email and password. +Next, create a free account on Deta Space, you just need an email and password. + +You don't even need a credit card, but make sure **Developer Mode** is enabled when you sign up. -You don't even need a credit card. ## Install the CLI -Once you have your account, install the Deta CLI: +Once you have your account, install the Deta Space CLI: === "Linux, macOS"
```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh + $ curl -fsSL https://get.deta.dev/space-cli.sh | sh ```
@@ -77,7 +83,7 @@ Once you have your account, install the Deta ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex + $ iwr https://get.deta.dev/space-cli.ps1 -useb | iex ```
@@ -89,95 +95,144 @@ In a new terminal, confirm that it was correctly installed with:
```console -$ deta --help +$ space --help Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh +Complete documentation available at https://deta.space/docs Usage: - deta [flags] - deta [command] + space [flags] + space [command] Available Commands: - auth Change auth settings for a deta micro - + help Help about any command + link link code to project + login login to space + new create new project + push push code for project + release create release for a project + validate validate spacefile in dir + version Space CLI version ... ```
!!! tip - If you have problems installing the CLI, check the official Deta docs. + If you have problems installing the CLI, check the official Deta Space Documentation. ## Login with the CLI -Now login to Deta from the CLI with: +In order to authenticate your CLI with Deta Space, you will need an access token. + +To obtain this token, open your Deta Space Canvas, open the **Teletype** (command bar at the bottom of the Canvas), and then click on **Settings**. From there, select **Generate Token** and copy the resulting token. + + + +Now run `space login` from the Space CLI. Upon pasting the token into the CLI prompt and pressing enter, you should see a confirmation message.
```console -$ deta login +$ space login -Please, log in from the web page. Waiting.. -Logged in successfully. +To authenticate the Space CLI with your Space account, generate a new access token in your Space settings and paste it below: + +# Enter access token (41 chars) >$ ***************************************** + +👍 Login Successful! ```
-This will open a web browser and authenticate automatically. +## Create a new project in Space -## Deploy with Deta +Now that you've authenticated with the Space CLI, use it to create a new Space Project: -Next, deploy your application with the Deta CLI: +```console +$ space new + +# What is your project's name? >$ fastapi-deta +``` + +The Space CLI will ask you to name the project, we will call ours `fastapi-deta`. + +Then, it will try to automatically detect which framework or language you are using, showing you what it finds. In our case it will identify the Python app with the following message, prompting you to confirm: + +```console +⚙️ No Spacefile found, trying to auto-detect configuration ... +👇 Deta detected the following configuration: + +Micros: +name: fastapi-deta + L src: . + L engine: python3.9 + +# Do you want to bootstrap "fastapi-deta" with this configuration? (y/n)$ y +``` + +After you confirm, your project will be created in Deta Space inside a special app called Builder. Builder is a toolbox that helps you to create and manage your apps in Deta Space. + +The CLI will also create a `Spacefile` locally in the `fastapi-deta` directory. The Spacefile is a configuration file which tells Deta Space how to run your app. The `Spacefile` for your app will be as follows: + +```yaml +v: 0 +micros: + - name: fastapi-deta + src: . + engine: python3.9 +``` + +It is a `yaml` file, and you can use it to add features like scheduled tasks or modify how your app functions, which we'll do later. To learn more, read the `Spacefile` documentation. + +!!! tip + The Space CLI will also create a hidden `.space` folder in your local directory to link your local environment with Deta Space. This folder should not be included in your version control and will automatically be added to your `.gitignore` file, if you have initialized a Git repository. + +## Define the run command in the Spacefile + +The `run` command in the Spacefile tells Space what command should be executed to start your app. In this case it would be `uvicorn main:app`. + +```diff +v: 0 +micros: + - name: fastapi-deta + src: . + engine: python3.9 ++ run: uvicorn main:app +``` + +## Deploy to Deta Space + +To get your FastAPI live in the cloud, use one more CLI command:
```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - +$ space push ---> 100% +build complete... created revision: satyr-jvjk -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 +✔ Successfully pushed your code and created a new Revision! +ℹ Updating your development instance with the latest Revision, it will be available on your Canvas shortly. ``` -
-You will see a JSON message similar to: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` +This command will package your code, upload all the necessary files to Deta Space, and run a remote build of your app, resulting in a **revision**. Whenever you run `space push` successfully, a live instance of your API is automatically updated with the latest revision. !!! tip - Your deployment will have a different `"endpoint"` URL. + You can manage your revisions by opening your project in the Builder app. The live copy of your API will be visible under the **Develop** tab in Builder. ## Check it -Now open your browser in your `endpoint` URL. In the example above it was `https://qltnci.deta.dev`, but yours will be different. +The live instance of your API will also be added automatically to your Canvas (the dashboard) on Deta Space. -You will see the JSON response from your FastAPI app: + + +Click on the new app called `fastapi-deta`, and it will open your API in a new browser tab on a URL like `https://fastapi-deta-gj7ka8.deta.app/`. + +You will get a JSON response from your FastAPI app: ```JSON { @@ -185,74 +240,152 @@ You will see the JSON response from your FastAPI app: } ``` -And now go to the `/docs` for your API, in the example above it would be `https://qltnci.deta.dev/docs`. +And now you can head over to the `/docs` of your API. For this example, it would be `https://fastapi-deta-gj7ka8.deta.app/docs`. -It will show your docs like: - - + ## Enable public access -By default, Deta will handle authentication using cookies for your account. +Deta will handle authentication for your account using cookies. By default, every app or API that you `push` or install to your Space is personal - it's only accessible to you. -But once you are ready, you can make it public with: +But you can also make your API public using the `Spacefile` from earlier. + +With a `public_routes` parameter, you can specify which paths of your API should be available to the public. + +Set your `public_routes` to `"*"` to open every route of your API to the public: + +```yaml +v: 0 +micros: + - name: fastapi-deta + src: . + engine: python3.9 + public_routes: + - "/*" +``` + +Then run `space push` again to update your live API on Deta Space. + +Once it deploys, you can share your URL with anyone and they will be able to access your API. 🚀 + +## HTTPS + +Congrats! You deployed your FastAPI app to Deta Space! 🎉 🍰 + +Also, notice that Deta Space correctly handles HTTPS for you, so you don't have to take care of that and can be sure that your users will have a secure encrypted connection. ✅ 🔒 + +## Create a release + +Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Data Space cloud. + +To do so, run `space release` in the Space CLI to create an **unlisted release**:
```console -$ deta auth disable +$ space release -Successfully disabled http auth +# Do you want to use the latest revision (buzzard-hczt)? (y/n)$ y + +~ Creating a Release with the latest Revision + +---> 100% + +creating release... +publishing release in edge locations.. +completed... +released: fastapi-deta-exp-msbu +https://deta.space/discovery/r/5kjhgyxewkdmtotx + + Lift off -- successfully created a new Release! + Your Release is available globally on 5 Deta Edges + Anyone can install their own copy of your app. ``` -
-Now you can share that URL with anyone and they will be able to access your API. 🚀 +This command publishes your revision as a release and gives you a link. Anyone you give this link to can install your API. -## HTTPS -Congrats! You deployed your FastAPI app to Deta! 🎉 🍰 +You can also make your app publicly discoverable by creating a **listed release** with `space release --listed` in the Space CLI: -Also, notice that Deta correctly handles HTTPS for you, so you don't have to take care of that and can be sure that your clients will have a secure encrypted connection. ✅ 🔒 +
-## Check the Visor +```console +$ space release --listed -From your docs UI (they will be in a URL like `https://qltnci.deta.dev/docs`) send a request to your *path operation* `/items/{item_id}`. +# Do you want to use the latest revision (buzzard-hczt)? (y/n)$ y -For example with ID `5`. +~ Creating a listed Release with the latest Revision ... -Now go to https://web.deta.sh. +creating release... +publishing release in edge locations.. +completed... +released: fastapi-deta-exp-msbu +https://deta.space/discovery/@user/fastapi-deta -You will see there's a section to the left called "Micros" with each of your apps. + Lift off -- successfully created a new Release! + Your Release is available globally on 5 Deta Edges + Anyone can install their own copy of your app. + Listed on Discovery for others to find! +``` +
-You will see a tab with "Details", and also a tab "Visor", go to the tab "Visor". +This will allow anyone to find and install your app via Deta Discovery. Read more about releasing your app in the docs. -In there you can inspect the recent requests sent to your app. +## Check runtime logs -You can also edit them and re-play them. +Deta Space also lets you inspect the logs of every app you build or install. - +Add some logging functionality to your app by adding a `print` statement to your `main.py` file. + +```py +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int): + print(item_id) + return {"item_id": item_id} +``` + +The code within the `read_item` function includes a print statement that will output the `item_id` that is included in the URL. Send a request to your _path operation_ `/items/{item_id}` from the docs UI (which will have a URL like `https://fastapi-deta-gj7ka8.deta.app/docs`), using an ID like `5` as an example. + +Now go to your Space's Canvas. Click on the context menu (`...`) of your live app instance, and then click on **View Logs**. Here you can view your app's logs, sorted by time. + + ## Learn more -At some point, you will probably want to store some data for your app in a way that persists through time. For that you can use Deta Base, it also has a generous **free tier**. +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 and Deta Drive, both of which have a generous **free tier**. + +You can also read more in the Deta Space Documentation. + +!!! tip + If you have any Deta related questions, comments, or feedback, head to the Deta Discord server. -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: +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 Space: -* **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. +- **HTTPS**: Handled by Deta Space, they will give you a subdomain and handle HTTPS automatically. +- **Running on startup**: Handled by Deta Space, as part of their service. +- **Restarts**: Handled by Deta Space, as part of their service. +- **Replication**: Handled by Deta Space, as part of their service. +- **Authentication**: Handled by Deta Space, as part of their service. +- **Memory**: Limit predefined by Deta Space, you could contact them to increase it. +- **Previous steps before starting**: Can be configured using the `Spacefile`. !!! note - Deta is designed to make it easy (and free) to deploy simple applications quickly. + Deta Space is designed to make it easy and free to build cloud applications for yourself. Then you can optionally share them with anyone. It can simplify 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. + You can read more details in the Deta Space Documentation to see if it's the right choice for you. diff --git a/docs/en/docs/img/deployment/deta/image03.png b/docs/en/docs/img/deployment/deta/image03.png new file mode 100644 index 0000000000000000000000000000000000000000..232355658f54ded0b634f0a6c360c1255d94968e GIT binary patch literal 52704 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

=LSK00=3RLwUj@T%n#Y`A{xX=IP|8zJLtDvdSqm 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

n5@->Ml; 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/deployment/deta/image05.png b/docs/en/docs/img/deployment/deta/image05.png new file mode 100644 index 0000000000000000000000000000000000000000..590f6f5e407ed2d569e77a44629521f22d7bb7a8 GIT binary patch literal 33130 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/deployment/deta/image06.png b/docs/en/docs/img/deployment/deta/image06.png new file mode 100644 index 0000000000000000000000000000000000000000..f5828bfda66e8812bfee15a9e2966fdd659f7e32 GIT binary patch literal 105270 zcmc$FXH-;8w`QI0Cg+@!k|jw-5E=o&KokTdDoHXZIUWN!Dk?~jRwPJPl7c`B0xFV3 zat6sc$EK%!zdLikS+i!%%>6TK(bVbMyPh3t@2cvmIxz-%+EnE1|A+0FV+M zi4I7Sgo4X+sG3l08(cTm()PN}$-(ijzx~}UHdfY+wI#e%;pY1C%;><**4oy_>hAW2 zw3OuZNFO^J+iSkN`#YOFTwG`P_{2m-qaz*#2l}rq&#$j82?_|#OpSl8t*QD@`r}(` zNn!riFZBk7h6{7k%Zsx;UF~VlQ<4+nZrfNtgu`6!Iw~nD=H+0rGt;#+H7(3c`hWjY zS5>*^`kil(}8!0<|z`~5d&?2$P>rtW?6*E*CctFB+a`fCIC-t;Joz2UCSQkI7g4!kYTmBh(JrX8bkC@)?@aGgc7g~? z(qGa!w@Si%#dg!*IbULUs7nTbmwNO^KaR-2c+_v#o0hngn%mb91Xc{m-SnReV3*y2 z2LKEueX=@ae5gQK%?1FPOv>mSI$`cFLG#tQuuF)xlnu)N^_$i;4InRqaoD5edx6147nQ#aF+pb62?Y?< z$s*VR(6c(F$mp&5D1tn&{`Gxd-3z<~+AW08kb(hDIx|5J&)VSW2*l3o5@ZGQEMSlD zj8(pXHDvl6w!@AN`fKQ$KR{ayQP6`USYRwpRKOKT2WAO&Pd;Sgy|8r!{rfM|88&n) z2b88w_Ak;?CNvqWq6Y^134jsO@O1=l(nlIvoEiZ>!U;^r{>yP}Sf;;K=v`^}+&@lT za`-2=03!J^7;GZ6ZIOi)82;sOY5V`6VF~qM@+lSgYZe;loduL&(wHP41gPD0#QdHg ztR#m4_GQoGXD=LHg7t_Cr*9cEJ20P#VSuKq3+J1eE<=76x{7#foG;su0s~&)Rl26; zfqSQB0Ei5z(c}a<*T9sRc5dDe{PZZrM#I;(*V5n;5O(U11?cy*+ebV}wgTAT4`~`A zMBN8>5D0?2jD7s3k^)-;In^W=U3;$Zd&uh+5(OZ*W)08s?%XU7IRZsGfW6^#b8AV5R*$qs%q*A%b z{mEl8@KqMs?n7*!xE~4u95f|bOMsRPn9U7Q*Js=@FI}y4I{!#6Xj!dJ$NGE)^1GlW zz&uNM@J0%X@lbM5JN5)akhM^Ff&ArDNs1PdxZ*`SE5WL#bo=Ic=Ccq?n;J5bDQ)vf#GJ@0 zc1^9^WWDj_y1&c|{8zw9!=dpvo02QdnV8`Nvgp*4jnDKno^wS6Cs>X(me%6G^anK7 zr6KH}!sE7cw;0(`;D=@i4Z5QC9GIKMH}Pbd_SXxU5vNu3>);&r8^!SEXJHTj{(D1g zq~O^#;4@a}g{zS`g`#KcAN7W?@yg8~oY>UZ_~47Qz&0sw?;Y$*_xC1rgfJ<DVy%p-t)i9(yu7Jb-lM9r!h!cW;@G}sEMs^2T#XE%2OSuM1@ zw*I^T2Hp4+LUXM(yj5^@9e&->2I{_Y>NCv;6i}A{fP}xZ%hbyf6GEYtCD`dC+!)pU z(lmUxH$$lyfhN#x0hX4)<%3rnv^WSJs>Mejb{t6w-AoFQ@EyY+d;sA!ecD)8O)X^w zHtLSG2?2p75TgmsdN{z^8!V3R@*_h#^%Ck)gwIV-PENq{is`wzjIXm$8(~k*JznRJ z`wub-<3&dRk6W*lu{d|CTm~s-0=LRjviAsH0w^y?)&C?&cqs0!AG(#=cotF&;j?u* zcVy#?{b_%EhG6brHiUs@klL7q%l|}y-THucSZ|C6zZ?GrLg0V_%0)_dun_FcGl@l1|@nZx1z^B?2=j(SfoRD7xU;Jf) zAHJzFZO7dt3Jn#R9L|D+l!bug{OP~^`;Qt#P7MfK!_oPc@SLOdcyPg$4!d0S??MWS zCy-uBKPGBfy-e`Ib;tjXe$DQX7VChLl040*3U@`6XPx-d{nHEoNSBE3vBkYm8|yoR z9Us2xHM_?V3< z)ctmCsCs?mwms85?LxbsR1X*8v#I`vZ3+vHBN_Uqt7}1Ly6;ezGb4((@M~9yu-#%~ zehsRss$uR2PkiS@pYzj1slG0c6mKJ&JyhlR8(IHYF4X4ZRR1Ls@ww@U5|eTdXX6Kz zN9HUhEoWpLXJpj9=W(7NWlX1>9{r+dz}_*nQ?2xBhYsGxt#brgyR&|yMtBV&Xg_)Q zm?L-}Kbvlm>|6gjZu*_8>=GEK9msa?I~=3mn2y9cBMIX?hDuxQ^mws3iYEMIbFp`q z|M!O%-z|lzrxvEX>G$>m&WVBa%Ih~c551{;`{l)B2pN_t z23}3LMD%Ep!HYq+e@_U{HQl=?Dc#Z_&N=u%f}j|k4NB3h3rO>WcL-Pv!E5YYk3URW z-yi6h+a0q@6M+osRHe6Bv0k$Ejh`|!aT<7Ya!Bg!;^qFBLbo9luf?^}YJ1x6XX^rd zuX@jLePO8=M$r0429A8T%F1yt|6qWi6<1Wx3-ngmo z?e&;P)RXIesm`kbB=-kh4j#mSOhe%K$apv00`ZFj9Rx`gPwk9!(M+trYLFkc;mH!# z`1z`sYX|8aV#UzBnAE6t!XAY?A%g|2y7qG#{N6k?KLbh4ozH%9iZM8rlTAAL{LA>5 zuUqLXqZW9U9B94pY3>RWdeyOQ6KT!~c_F)QKE9ni0nYgTaF!QRHztLWZ>l7FogEvl zA&Iyc6yUOZyxdq3nG+Rt1(V1%=23+!3E_Po*)uZ@!6;(s>RQ>wpS&IL#{*@X_$?u@WRQJ&SM@Qjmop}p4t{%JvFZG~ z@~NOy!qCg=3pLBy=~kmYinqVAy{1L*ikX~-=8ySJqmLhR)o~`i$`)!cVT7U)b+eTm z371DUH6%&5W=>fy=$ibNrRE=Q(vf)uJR( z=vK0g0D3+!uPiW9;egXuAU3-UL`;DrBkXKs@hEUUTV&Vj*(_)NQ2_2sSNIqYJM{Jn zcv^GhTb3A(vUQbsYB~KfQ>_WDPuIcm9#qo7(xkBL>fZc({>aGOYVY>U*Cah=M8Wun zAx{0D4@u$W1n%?!|Jdc_xz%PTAsgyRW^M3$|J~-s3FG$3#-LZzfZ)-+D3Epz+Q?HY zZJUb2Nz%dF32#SY=X1bHioT6PM^Ba0*-SC8wAN+R<;Rp|3nLE{ITJlRpM&T-q-fdn@ssTd z{NdJoc6iv~7H(uLV0!-DcmzG%gCDBvUz4p8KjIsY=U}jZ97U|I9&<3w7o^mI$<|Y4 zqvSfoH_nxQlIOiV!q7XsQ~4`V{%O5J5-#n2M}5CEmDQTYQIz6NQxN(0!Piy;tt7{X7f~hrrot{7JwqOK@*TEjo zrSoPiS=UuKQ@%F^Q%Geq!4nanAU$>8-BsR2lG#1_#VDg+3ZkiadEHy3@F_U<3)*5xk}6nQB6g<)#{(U{xf{#^+qs zLF~JS!WYu-vkua=D(h~fr;9OZaOcmGzpy(me)q0jt|&#%S6Zh7(X_U{$9yS`kC`d2 zUAe&N)v9sRvMW#vFltt3t5dDt9yc%LIB}cSb<9vwR~ z4*bXtpuk3Mlzr*(evD$?@5$!$oYc1&o#X6^NOD1MmDwIy{7xK%r4)?aL zK8k_wP6n^i^}SwBFggyp2L4byOzYvSxi>*g-@|+kBs^&GmvJF^^5Zu7M+XXjnZ3kV z(-SIExEQpts9W+WSx}9XO6D}vhyqeJ(UL~|=h8>FwmPMZs`HmoXvd=UOojywQuvK( zuA}a_2}~?64^e(kqKG$OOehD}8Vhx~K4ib9Kc9GRMeWur=nmt-Yx)q)^untqv209X zOZGtwoYDn4Cbv!EPNuJpK7O5P04>}yx3u58Cto~x7n4N4a)yoVAW30v z6nBsqJ>Me>B5d-TG#j;`?B+|nB%%=rsn=HnL?Z7A0<|uZNnWzSA0W6C_RIs?2ZWWH zY^13K^~k7-im=`*Kav3*s*m@aGMd!&d-_)^2kPrSx%+utFeHNRj#6JL9XVU8o2D^S zqO3VPSD4E5+=0F6eyNj^L!gaTQR?8pj!8&+-_lV}@xo}gDE@}=Y zC|SwhTk&JPJGI5lxtKD^Nnvp<2%Z)oA{8-4B`qmu9)~;h!Yo)&0BGUuq(VWn=12a4?(xe zdEJ!V>89UNX`@1lymvxq1kjJk7S67SGSI_ykHkEuAB*dIqOQRKe$3Z}4)0o`o?Ovi zJ@(ElHatI9_cSv?SSkOVE@t{qPCP}g87qtFnMl3alqd#KV1bZ|*z2iMkd1~84o6pd z=v%mb=u4ECFB+;-Z853|JxT3S__X4_S~)55vwHg7lGha>wqoCG zpYnB5WTlKkx~4FaW4E_g1^RNyWi{1-QBARm+e_J9Z>I6=I#>E!Ors_gRICx(f#bXl zA9L($DsKID_|g5ZpueBnFcqlUM;URH&#nR99?TxW^J*NQw z&|c2UyR#{Hegr0(3Vw7|RYP8zq}7f5$1yp~61VY%waG(d@EP=7k8-4}|AivY@mKk3 z6#8az=Chye+nIQPz^)Acl&=y9U7fsM+{a7Q6Jo4`ELuAo*;)Bz^n0~zztU)@?nmk% zLuI++rZ1GNk0q(p3puHg{5s#~wfe%CRSH^fxTISKP*}$<&A)iArH%Nh%ml7HWSiOQ zpBsL#_=~)GmT8YP`r{uP%iv~?RM%UlmbW_2Sl@VIoX25w)-VfT5V$RuPnLdV3%9)_ z-t=vJ7eEv0J=+=TEG!m3}}OCp9kOG&pZFkAkZRNv64d~sz%qvB&3SR zbF_?_7ChQ#Nk^7ea9S#eB0z`B(1PWs7OrY(X4=PyZp@XKjPn$Tv0Wg|wK$OFx(an#Hg+FNO=t-%ItC~;Oo*AC*RY%?U zzz<^Sh>SO;J}ZVqBVd)0$CPqHDosAU{L#$G^dgyLH+fM^f+79yIM#LJUXO$Dg(T*M zA>rG+F@uM09gxE0tcj+f5=xB(Zt}en#5|tdIRg!5~T}Rh$9^ z_g8ew&DW}acp-7ywz6}qdyLdA-^h#kYx;cpeywWFShBUYkttY6HTITgAG0uiA_ubd zN`5M3rN3PJRT@&Dw}3Qo`}2$(3n8%N;Nrra_pBY)H$S+Nqt!t{&T3fyWnSai5IOB8K>q^tGX#J zZkUq7VhyhgW*iG-_=xej!wB^A&bTb`1-;>SBLQSeDdo|KkLjLXGNk@DzMR<*hQ`j9 zKnW}h99xZe`Ma)5=Q&+M{Hcht0KVQ=1{Id#*xu1 z68pSBlPADiC@J|l?c3lsNw*Bd z+xm2$z4^t4qV3B`q6``nL2TErJ&(Z~Hzf{G}6Kn@dZ<5A|p8D_IuzhjnNpcz8<{iH)Dw zyP2~*j&P8mkKL&4Z&4KpoVsIpif?p0-icqEq!miL$JA^s+&yTUe3a2U=0w?SrKea% z47_f>e(-v>-GJ@83@|WB)IvRpe87l|pyhlhED)BRD%!;?dY^s$EzoSYCkJT${m&-R z(*ER;lKmG@^gDrkNmu4=AANmHi?%s{^K?tjfbM#cO@u{mUHc)GG;tCDo=n?zR@ml- zE3g^@T*w(c4ot$o8esxO{@-~}#uNtWH%$Qoa#|P`^MN!Y)jz(vJw*nni@_h1lzuO? zebt&*f#ioAj1&P}s!6>;tC3!c9C-~a zdxpN6eVIOOiTjk;mj!sw1V_mjkp}JLKDPn|ZsRIvTgtrO@E*Q*?%BU3asW1prdlsi zPoKyy-nopPf*Dn%fv^u5Yt{b9UHv}iC+pfHAGmL23{4u&KU__Iu|8{3Yfthq@~%(E z_lGzk6L~7u=uM5&p&jJL=YrF@d_ojoCq5N{yu?5^3TWnDlWU>3pI?msbCAsbwaQRZ zIAW#c9z1PB4152ex%EO$=j^8!2q1lWkL-urhCBTfb@7S(+ZfEKq7XVlQ zJvx3a>2-lS!1j;P?P^7*mAu=LmphA=E+6E3{ni~u-T%fBrzjD9^~nb{-8;VOJqOlb z1s;%ZzW{J_#*gxIlg_wsVsx8B+Bp~RXMvCSgzT}}t2;lYUb-gDOmctc%el;84X4fW7 zho3R_8+rqDgwP?_f-mE@knk^q%u^;I^z-){D6mcpp9mQl{0ExxGO&4M{YD+cz ztSe+$=A^qO;f(3;C`6z&o}Km>aX-nt*>j5=+?B5kp1F3Ob4|J1fI0e(hQBTK8mpY< z>}1N32RVElGB{bA2oP)%YSCYpxuYb(R9zLu`RRUFjtNx%sN<2-W|(rK((*F^--SLr z_)3m|w@u6t;2B2{2?}(=_p(&?l-T3yLD*~XHt><36?9j!c7tJV>}mW5CMkd|3?Rz} zlgS|28@)Mk25HO%ogVs2q7HEEFalulXPNt~uaN;CLw%KJYgR`qQtuhSvuS%7BKRo+ zPEPsRxYwAY!i$8pwwh-g5orN!ypfVzhUy3JDp^1<4eyirEz*|)2F~SF!1xmrH#q~t zaWfJwBw6?|($!N=4Pu9>M_FCVpL9`#9F{k1OfehW0Q^8$lt;psBo~V9-o-KjSQAN) zBFcII!KshT@CthK$D53>y*Axs%Jo0m7j{A(JM~D~#oQj&;{tCBGPSl4U_(&uo=exQ z!%@dr_Qb=$Lljw}o1u zTL1_PfdCtDK!jjS0a2#lZs=f^ncKbFBe*$=)J}*x-$wH2#-&iTr*bnRz;?>7EiHbn zUiW?x0?S|_`{a%<1mrU$RXl>}K||T}?=v4!pk&^Whnh1aW86|j4WsAH0WdYrZd*l* zujw|rFYda!m}q2Gq$dh!{SWfR%m6);hb2u!A+3f)z#6J-6e70=NIZd zcAg9@)j@X>p6)o-4E%aoaQFn`9U1IOcyxDOIQ=w+9lk`zTRf)dU3R(IGeSI#imrJZ z$0`~O3A>Yloq)-sUtE(MVUPQ1ce*7SWQalh4QSGv3FNq-G+B;g#2*Jd7l5T+v>(c+ ze7TT_sGZ8z&LIazvR+Cn?H<4!X?vy>4MuJc91$UdR&{#T5_O;Vj0Q zr%A;K+;DB_qiSjreM&MjJH(ZC(5?J&v)Cgo;SCK8fSNvJ> z%JcUwz{PF66F69BB7)Z#s(&*9R)wmw{sB&877Hl|FBFmO76nR}9xX?*VR{4j0`zqG zL~T&o@Wop;6RH$Ma9w?O;zaGoAeh0VzuD9XbRQ3Rjy2V#3ficx-vkoYo!0=Sc5Llp zl0-`iIS45P=xx7SoEH!1`KfY z$xl-j0bo!B42Gx-o>9_*0^%Uzmhq-K2z>>zT`CIR{utv4ipC<^oefN(W;CDHU zL`Fpf-li;{vw7ge3)lE{|XYUcr*fB)??f%KMb$Hbo>~E!y3cKT5?)u`)MG>HF2*Q4?nh*=e00(`{MSfUe07T0lS452m!1s77C)B>J z1r}W0;Y#0Y3{$H4sM<&l2Fb8t4Qp>x@;h)$@MUEk?VW-Ltg7=Oa4e!b`xR6A3f61JSWB!pKwSTRzdOOMvb@6X4@lqlx1A~VqX%>Kn_yz5pgQ2a6mZvd5YwS(D7MY=q;5X;M zw5rL8Mo<~z)v7svv+;6Cz5JX@GTA{6+VGu<@sl&k8GZ3Vv|H}{B;tyQL`x5(7R}yM@Vi6~>^z+gV05UV6PI0fhsj6d%DQLcR^i*M8?RjfJG)A-0a9vM z>0bD${yPR+R)@F^O2SVVFFZROiGqor!M8c9e4D!;4Y&eQ2FtLjd{Ou;0)BQf@TH(+ z*=Oqr+3WN63|7|G{NnpeZJ9=g(k4>*Ta2eWwFs;O?DN?!_xR+k9^90fc!t+gspNjR zlAd-cDdiOFAMB-&-4_XtVb`FAjH26|IZHLl>tokT+F4aq$HpsXyi&qxzk2h_qb{@A zlGxKaIyM&Vq}lH1fbOc>c!n5zw9{__nV>X>ZZc}fbzgGB8wUmtx z6TDzJpOvk9uF7W8FX=keec*s;Id?m$^>5CCv=dHzW`4~8tF47NW{_c03Scm-Wmi;+ zNR=DyQ$&Dt>lW6cxpmfHeyD4BSpez`5U6Dro8oDgyiY)elbgroh z)XgR*Bfs4xLZ>9Rpluh$4~neK$oID*2cENf*hDkzvhnP@p5JQUHtqgCNnEje@D+gDLLs zF@FjpC69dZym~>T*Hks`vYkPrn}2ENcj5aTHvPC|rBlkLfr`uDv=8|oHmnb9R|~Ic z&!>-nTK?^%Voz#EojA$<;X$#$Ngr2?G?}T@(lu9TRH0zKFBdK;0lX$28>G(1uEf|} zB#pc63_bPfAfj*G3p{w93(=!vb4Yuhe4{g@#AZl=nwPtc;<%y9h(9ZM2}3}F1Vxy% zHh|&9P^d{2A$!yWG?tJ~0Q*$wC`QFhHIFb4_|xpmawEaco-H5dEkEk_pI|q`op+31 z$v5AQcU&Ex)d0Y@5KN5CCBF21c{$se5rVyzw5wGnCuU8ctCZj&&5y4S_#pT<6y1IS z#5hp2UJ`*ma{+W#gJHX~O+N{hbX>s}gP^MdE@7nI>~_Esmy;%vE)FS|<_=)Tw+~Nm zma@|79iMHao>`8XMv=WgdKFiA31%;`o=@h4ajbCX(7O>A4Y7QMtV=CsjN1a&1~T?; zBA&a~1B$*1&V06&M3Be2O{0p`AGDvbTzf_Ld<6J#W-AR5JS7sO^+mz-KD( zkx3Y1d)Us4Nux>*f?jo?6cvfF_Q#?SYzy>I;M-4@Lbq~8nq;*NbukF$gRmb^z&0aE zOB_B+-DxL)?GCagH_PNnct}6#vpYE+<$9ZaM!3#KPTUk-&ju8agaNj|?1(?!zpuZj zoE|$&MhLKXDB*zPSb=AtM4J^nNre2qk3ml#KZ@7W-k*zDn^rma=QuNd#aBsIX6((yj`naaHdOaXq{_UOfHQy zwFjShvigS1_cMpdUOMtzQkW*^GQY5JQAtUAPi_AL`()L*m&g2*Prs;Y!^(QoKcNuY^qPsY4A=;mLxx_Ey*x858Hx=D(nB(Bot`biX7qHmN6 zoWP>uVHK`U&f%=YXkIv`!PRhvJa~x=;ulW(UAY^B0?Y&JF$_CP>gMll1ZktMJQ-lQ z-q4MVe(KokB5&xD2EpGGBa_b}({*kX78gNR$(xMspgjp44AY}syWhVU9^~1vt4E8| zEVWmvB*j!3W^4aK53bHxO?Wb{B<4yIhn;1H@Oi6L#O;IhAwJ=g+rO`=U2nKnDYZlL z;ZM0j5ls{F64(d8KP5sxD%{2Afa^FRZ0)*UquI#qioph+A7*%E&u;FqDCa+TuA~Q6 zKEu9w)R?=!{D|EMtD}svgbli-Uqs#BW|^LUy8No~Q2-o8$}L$;_Kw+`u^h`axOuhA z)@dQdNUC2`5Bar{1vO3`%U(45dBVqbVtfAM&sI$5$k@-+B87S)MB#JBjb-`fI`Nu} zgzSD+Ofv1q9F9p_VDLm8rjmFyMOK~t9MsKbV4@l$2J)`TI2CJq>@BvhFf8~q&MmsGNXG$MBX2m6WM>9zr@6A#id-RV4^f>^9{W1SH(@}k`q@0 z_>w5rri>A#BQ!g%QMH^XfeXUXI}^r|w66OFUHk8n;sYtn0V$WYbGDxHWA%}&P3gSv zy-{7?_;!|Mz0yq)A>E1kcVjlcd7Jh{-EjKu)Ts@?y}@sH22`!=eguTfl-eszo-HLV z|J5)}{>AFj>Z+<_iy?kTR(v#vYSU1~ipd$?kqvX*r2}Bd!*D8~?aPIejesi=+?9cB zgewXw+Kt5%O;sWBIl9EQt-~Urtt52eji1D5abE+HWXQzn&SICqyT`Fr#DMACq@rg1 zo6o?L2)q@!9Q%#BI!X+EFZsz+uj%*iT3>fHCwWa#nt-Q2Cc&-3)V)TI6`6W1~4GXFmJ zv9^})y%f5{vt#E$LFnS8r^hEne#8nH4k{2ZcoW%R$CBOn%W7=AuZ86;Rr$+1NoORb zS9_js_a zx8@#1KFCjhp6Y%K0qNiNi8UO5c-4a)?-^v;ux4jVK!y(4EwvE|KnAq-y zSboIfPgRk{UG@9eZ6kBz!SKVE_9+44@05kt_(*z>P^1f%_7xp9E78vo$iOpQahpsU>Qpkm*`O)>?%~MhZszcm| zQ~q(CGYQ@nJ4&nVm($Nuo5SAh7b@ZxzAA}a=N@1a!Tua5RX*F$K~upB~12~NEY(8M!icbE!WmpNzTo^6T?pO5OM+dGcMY^{vhe4(T9u-A;rnEft4 zkl(AtZp-P-_>!e$U1&y+(k^3hy5Oy>6iH&q&&h7rsZIuc29n>NZ`-W6o~TSS=aIg& zh}zTiO29b-Sj`?qo&2SmoW#3%GT>nw^}Jgx#2SB3fg!r(^qIrW2@fK03IMHmSY=zg z4g`n*wc@juZANHinpD^egqQV!S8u=`03$JW%A{{}P^pe(0m|y{%g|lQ@l?&_JK*e% zkso1B*XLM!pE=N8LfAN0EB18YcgM%Gk7PdP2c%!L zTIC@I8}hH+m%}`Mm5z0Z<)tG44=G`XPbQw>ue%g6IGOCO0e&06ZXJcwY*{s*JR|QZpsIp%Se$CKsOi+by0iPJFhv%-}66xpYUVvK{&zbx_=F_ zeuhZdP=_dh5#(v}tKpH5JtrE1 zPIm2S;X8jj{vRFw2Eb%Zh4@t7uH@-%H6xc-%0Bc~9jt#N!mtu_G`<1Qe|0rhmOhoq z#w&+6T%?9x?LsGx0JA7Ap}l_%chUXnf9OUqgdBYKId&pBw!wg3!mtX_hCMshlNelA zPWx)O^~$B`U)uenCBR#1SxcH~pLnlwDn_uJ3-a@88K8IZTaBT3Z2P9yGssTCHlY z#s70JnQ&9HA*5PmC78%IP!djD13sG>{DeEM2s9vtCHs$$8^gE7;naGHN$<8BFrk(g zpkM;MWV=!Yb?{+^4m;mr0qN%Z5pKm2sH6buF5!Yx@6p)KQ^6S>T=1eXq4J+52#iL> z?Cr*U;Nb>-ssrpI3~4LeW`J0kun-!qi0~AjX z-yKdnYzaQR0`U?wfhbc0gaHa|K&*c}PPCA>8H?RkPyjEE!N1y~N9MxCz|R+da87_x zlB6;afeB;53=I>btB4H3ZvE8`4VndtT|x6*Ua$f}%1>-8@Crzb3PLJ0m;}oPAQ+JH zgNMV2Xd*C6jUAb3Y&e~0fTXO(iM@PLm|kQrU_4!<;_*n+v!LE<6 z97EY2*^N+Rr?)F)_Z*lB;_^WzL2dC2@s#8J#8Y>RQ^{7m9Zm!kwS;X`qCMmrnF+V9 zp~*8a1wdfjD`ppuN?OA2pLlYEH1TyvO1uU);0Lz!JB~P(1)Hlt!=rgo-zG&E{O7zkeDd{?xlRFg70c_5#TzzGcgP^z|Zw01wc1D}wO7oQKGAp;g`NU3Yu z{dfdC`6sDjK7<_ZS5v!D#e2azr{&r2s!Gx-ZH7-0DH=&43#yt!Sx_Hu_ zMh716!%tXXaWE{8Kh_We)U^fsUZLT;U9&YyB6xGe?>2nv79mK>#tv(0@#|u`y-$`2 zk_>pHm}#o^z}=A(f5c=tTW>ahP7khccG+R?Ree3PT4>jVbz^;A*~G6l*(?^%D(19- z@phVu#caWphD}L8x-I!<+8RI;`jMTT;RLA@eaA}N6JxlBbGmDSa#vQP?6*q!uL#aG ztZTL-u{BZ6JQ8m{W=~Mr@Hk0KkkaheL!HeQetZ69wpQJ;VzkvD^UogyO>Q{%yLjafHVZ zP7whZv&Qpq6D9y~q3%>o3h)z{0#Cw42~!QIv_IuJ2UZ|}_HQEqpb-E~F^4Ctfg=b2 zb4z$v1R4pj-}_I6$lxFp0BwE;C(M5p0`PspKQ#FJsu964giC&aFc$qK0D@Me|0mM_ zBm2Jz`M*K`H}?P3>A$4sU!?zp_&=KUzX(PX{PABB{Qu2CCtc(9;<+V}5Uc8E^1mSuCJv=--$SMU2cZkLBk9qAFaayJ$s12wUe zI5F{?@9(g+Q+#);POMpcQ_N-Xh6`?;a`ubbUTL4bC+9r3>_gXZ`)~~vwQ#j2|Dj^M z>dTS=@1P|(Dao2JV88rQp(wX1}vf;KK^9eRD{u(Eo^9Q$a7r*JiU zX8gm;wxI?$$tuYXYP679SI&>!8s(~X4^L@}%;O(l^*$`Q*AA#PT->a$W1|)n{1pzm2@Wm$Rv`C_iv+P#H7XUR_(8(xy^>_`OptKaQnhoymgvcu*%CU z|C}%`P_<;@Kbpb6c&z=8y2{AOfa}Ujas%pt5}z!da=~ITYx1Ys&&RsfR=>k{EQfeV zVLRQEg|aO%!pM)9v>}C;_SXVFSU+>3!Ei$54HqE;chkuB*$@78JYI%uU!ACy_aTF# zMEOVkd-XYlc?dBX4+{Du%1X5(miJPe^7;y!7mH=Pkiy@o(c@*KocJV?UX?59?VHXt zm?Jwy<>kH<8YCu2;b^_Dm*TvK` zFufZMrFcJJS?xa)Hs!T@(B4)?wvd?b?Oa-G6A20w=EnBCtH%xpf@N#MCtKF4 zG;mP;DOsE7?pEUiR7lfpv{SbGjQP`_j+^o$;o~N~ze1#jDYx}I;~N`sJwt6*TrY@l zA8#oZM8)|W4_v}|trEQ0MF9kCbnyM{`F zSwTi%=Bvw%;$Z?eo3kg@k+zkt4b?dYH2V8nvr(_+A%oFAwT1yd<;HG7ZhYBp;}gLS z2cOT8PL*zgi}_60AUQW4ZsM!{^phWPo2M1c=F&=^Z5ADSZJ0@PM?#o%w_0q5gQDl# z)$doc0Dl%tk>)0D>r=Ev(fHcY8oF{U>$4YiVb4(h$gbFTixQQWB{M<;O*Frh^;JI} zApG%L3wBEhyw_%CoK!+QdOq9yU0M9S3-IHB)_J#P705QtD-@op&^F;VFPtcNK1|LW zNAQhg@Aq;_2AP7Mz$e!|tZfY_+#Up@v09$UgLzEbUB3P-CHo*Emx zKy_AADR+4`O$Gag2tCR;Y#G@Zbhmk1G=*%( zFP^{WV<`M$e!P%LQdQ*PK+04>owLM`O5HF;TeXul9v-_I^NqbDCF`rXp%)8R{ASe8 zi6>%?#!g+1&Ci|A+>le}b{DO8K9UO?JoN*d{cT$5G_9ulsrhkFcMiMIFRrK7H}`F{ z&4e{7q|G^d;qMzH<)?7;&h2e(WmI|K-@R<3dC{&%TO-)BD8pS^+L&IJ?Lm%{0I@_n z-xG#aCl*D>on9yiwX1LP&TqJX46Qo%R(`fq@J$V>4J^@pKK z|46~c%XW;pj4CR-VY|u8PPRTZ|&(sO5;s@qu(tA zN-uNdx4n?^NP6mfy9alZ`c>W^vJLywL}CcPK9&JmTlZ88;WsicPnoI~W42tdy7)Qh za>&%lqiYJ1%-fcp@lCWLn^(mj-d~zpb5(q%`oQ9o#Y@2-pY17Ul%Zq`5R8zbunfI; z!3Y`f;L@b*oF7AitK4yp%w^vvHT3SgDw%vyYG-G;z2o1xzGy@>^u1IzlX#acuofSh zo?)N1v&pm_Y5`3?5?#`HK{&IT7e<*ihN&D@nN!)aZ2xMLo72loxB)MPO2>(Axly3A zU$BBz#%I0Sj!W)@9Sjv^_JDgNm&sO^`PfExcBy)oYsj!72}{ueDg_$7)YfwpIZMZj zmeaeDzpL$og!502cID&-T4D!6xpkK zY~VWM~Tk6xERV~q>BC5gg1c*QXA**2 z2m}c>xCM8=^ZUPg^*-GD<-Mx+>C~K>?$h1Xy?giGYppKMqS4lG+O4RViWjREKzVy2 zBx#YwA;A6pxE_L}P1$+oyztM<*M0J*NPVf^Ay4P!MMXyTjMSw#uU^p`a(DjY$)lDqKAaT%H!~h_nNLDO;-*I7$LF|4aV< z10}<(JHavtq@mb z5mUJO39N|8V$XzUq!N{GS1nz!9kVk5b?hJ{!8wW3F}(9)?^24KC- z@=fG?`MAYD7U}3Hx9>xR?&;LO_wyQaD4#3DUeoW#p{P14MK%x3#_D~t-$i48y-+2{ z2MQE&{(N7du@erHyM2NR<5reX#^b_(UL<|U>(t&_q62>u-C`=eDrmMX_&B-LF7_|J zf4xb@^=Y~CV}YllfHMC13)4G)4v?7f;5>ZxWN;!J1K@lNd7m%A07{>Ri7#TpI`0ME zCeOehU#pJQo4O;%H8FhJt1ZrK5j9Dfe^W}aWkEAu5$iLE9i>r`6hadqw%%#1pY(cM zMb{)HYhfs3v-%eY=dczh8+_}Gq-eTL$hPS&1lcf??&|SQUaRoLao@CNrMN&8<<9FR zec(@H1HP8WI|EneOES)Lhe5)Qog8`)R8%Ihui7l30s*-V^|+xwR3#@qQJ^$q&)J$t zhLca4r-*V0o`*zbHEA2zZ3UC!ovKQb!hX$*Z#+L$R!m#g0Oi%>lk;EKYik!Y2EDTY z#&=lr>ZQ(Ij65%#)?gOjn0Cwps^%!)rrwRS9fow2yY=AR0U262GdRIIL37`l?v?^q zx^-=NRlnri(%YwIpf7)2l*{p3w>Z#(CRQS4SVzLA-A3K=`~=(px4v3L9LX&ejt z#B9<{MlAU@3h7(*RlT<*C-3f`RolKr`CNdax~$9kQa6|B!mvo8l(2^`q5TZko+)09 zui+Mj!R(17_!lWKR$$pFgEy_=lTLEhQt29Lt|s~97aIIe$Lvm+!j2*ScqsnE=h%0n z^ck2?C$Cj|>#^LWi7y_rdV@N9wewB)iR|HsDE78|O!6A8z*I!lfR7v`K*?npi!Ghl zfcnpxF3w7kW(w{B!}DRPwzKnVVhy3z=R3uh#0W8y@HMufu8dbjHxN`w3^bE7N1pET ziY;ly^okI^XnDQN>xrJg6Ja9)1~mn>KhzD`5bGXU0VF8fS-nlt`lwCg&d2c#y-1FBIL8%<8?Er) ziYa<`b;welV~pBhyM8Fg18W4jo-pUIPY81z0rFL_U>AWL{JCGg=%q@w10#6ClAB1N z5RF|UDfspGi}if0JUNyPQsZuli)K!n(?&Z?2=ViZoX+TZH#f=so4euM=--{RPMb?B2dp-XY6t>YrfW1 zX!3Ld_s|ms+E0G6=}=mpmDJ|PRE(?Kt!#@N$-0lzoW7=8TVF&DPuJ`O2QQ6+OH8Nwu`#(;lNrp;D+P@eKPX ziL!dc!lzDcHxAx!kFgp*30(Rh$rr>tH1;{@Iv*`}91p#F?U!z{$7-Rh3T*aAbu$Xf z<0yX|onjJqO>}C-JEjC=A4{`%gsC@fd-p`B*7P(#u#epZ%MDIJp{|0)DN8Ag?uTp@ z;j{WuPx-Evz7xZ-E$NNw_CEzzj{-(&3FNOoO=1oOJni$9)!>9&<3C?L<%viG+*Bv^ z@nfKarKRmX2O3s5%I$vk&tA1-h9?kYJ=p*f*|w5*?ptuS%* zm~UHUGZ6O^4WD&%BE_*n7EG7%$9Bd<0tBRF1%yumFUA^LHH|TQXg7(7U85_0FcL%& z+~OhRjvi|k9Y3MaSan)CZcKkp<4XK#Y4disWZALtS!3e(o7l%pdB?}OcZ;z^P|NKp z>IvM(?`Nr=0%EDfOzk_=Yp;Ny*bcd0XRyj8o+5Y=>4ymz1qzw|6q;cgLeH?ot2Xk0 zXbi2(*J5RDj=vT{JlT8vrwTfh)9Wa0N&Ndu2BKUv)t1_xRtFX?2P6)%zv~AO2|_yM zucTyFDe|V=O7*L4H!VTFCfGd35K!xbx3$IHf33+h+x6#?Qed({*K2<9hm;p zH{BRQJGLsTBf*sy_4yt9)8ScRLVk;n;#Qn!R~I;L}gv{kfd{|ze0Iaa*LcVUpaPG!{+b6j|(>q+gpq2oT?>XE?U@n~{0ZE6mz1 z1@SDGTLlR8xF*xJt;OkUfWB(?LfS(yJ~X^gqxC4sxHg!=V(bn1H7ek)Don=BH&GkD zF)&W}soQd_0ciTWZ)^R+KT-@k2^Y-5hhO1(A+?X~fBpRiBL;q9qtrgwpG1@QUL-X& zua7Q!V-O24jApu$4bi7*ywHblJc|{O;?DVv)b|??aJzlu9bd&NUZ61h1TVCld$krK zgc;q&U{zM!n=hD$C;#IhBjSECym^oLywAJo@KNi>AEDkRGwdyFZx5WCY7c=Q`&Old zoHj$jqdn6Aut1TTUhZvT8oBn(z=k_gd{8Wdo4HdUzhLzSUx{$xdj{|*5=)k(U@vJv?Db7Q3r!}xG_sgK$Q_V1hfOi8YZ_QYqx%tD=o z!Lu0LuciDHoO%-TA|LnKb185{iyV?(4g?78*h>5m0A_lW4>!u#RD!5fGf{-4+;591 z9f`rhE*X9S3pFF@w2b>d2HKF5(O=@rQ|#8t@Qa+~Co9iUVNO&QzF+i)&I%ePTPv7s zZZy)wD@#6t+}IfTVRzNRwM^kNA%KS=}fXFXp(-wE3z>1h4a3`1=)<0|m^VV}Um-lme$6 zrDO+vEn*vRNgEPUxqkQTY&}9mqBWs0RNEc$;scJ6DJyWt5f-ey-^K&Q9uR4zPi~Pz zRqyTzLEP?>YzZ%=S{3adfkw?kGLbV?RWewhCM^370L$2hcuRuk9uRJ1R6hjuSk?ir z1lPURmoIQW|I?v10*!iY{OqsZ&S5u;w!hQ<&-m<>7p`vxf@2l?DoogKugM%+Y;M&> zP0EI-Ry7l^AJwYkc~k2(0xQ;*;@x(424+= zR@&#%>OjnT%sqtxY?wFndsBcM`z>L{fnOoemoHLz{HJ@Oiat4>v7^e}IW$y1de;0A z(sFVaAQAqO%k}R`m?;uyuaUn_J7Mo0NKaEH7A1oAA0u?p6hc!wQK49n+#ql3bn@e` z*3Jp;PiTYZ!k;ub$8I!2`Wwf{S2VwrSpz^*%DkAfppb+EbEf{wOf9nz;(!hWJ>6g z45P9{7IdKSlL*7n9=Gy!on?e3L|>nN=pZ@GGoAYBL=lUi1qA7OhthHjBp`~NfniCGqup?pRAF@K)PA&=cMjU6NT>2O{hlf`CE%6iu@~fSCOtR0sVJ0UN1ytw$e?Tb!k{RUHP!5m zW`l*`@P!u5kO`!^)Flt&B~pbAVru+v|Za2E_qHv6^X5XL`lD^pyZkntX_0oD7 z+6|gJ>pL!}^aV(@NZL7f?hcIEcDe78w-e-|K$N{4O767UZ|2Z>hY zbX`N2Z#^zBL9S-@m8+e*+oH}yezr0>LX3_Q=|%~Wb|cO zANJ(rTCoD0 zrVy)dnH6NGR1vrATsnSwfpPw;yPgdejs;@_&fH>R=`_;Ne*%-;P1HxiP<%zG_1YB| zkmT*(ZPHIJoey9Jv1=t%1V-<%a$8WL-#XxxMRCWTwFQ2k3s|@C~?$s*=Kep-rUXo-4}Yw0W|aSzFak&YXr7grAQhz=*i0c`N(| zsX0i{<<(dA-Lrf$#3xC@4{TqcoIYQTj%1UD6Z&^Df>?bE3MLwQ8fgnWuOb8i{xM5D$V@LKa`bW zf)0%;Xv6ar@Gtw)Mtzyk&3a z)g<0OM}=G8oPOE44c=M2gn(1j-L~t$jmOyFm&hky8^M27;k5r5e3oz<-5viYW@%1x z(07a>vmMrE@8-2`Lj$1j3>=+LsfD=HQsX3S)c;+134zfmEu zu86&}$+%#)=6gW zyM+gD>%ikt?(^B88}(c7u!Vj|LSDCw&(Cf&1gxXD?Dy_=$3pmVhkj-p3{UIc-@F0 zAJwBv&fU~Fy`^K*)<0v}_Z^;Hm>CI|7VrR)wQKOAZw?L5DywX$cRN%u8aW zoO+xe7)?)#`wjhRhBg!5Y>s_`0EYuR(;sZ-IXJ+fJ^RRtx547eQPQZ#4#I@)x8Nu1 zTH{0A$nVK~ebABCzogCg?`Ra~rJFt)4MkL*(}2kzJf7EZuQuw!=-6s5GQR$MN8uo{-w^&OYXmIDJoFb;C0j<5+jj-V1FTLoCz@-;N1H!{H+6r?T_n9YbfacA5B|?OkVwOANWTaPnV79{v0H3tFp?@3Z zS`GmilDn&PtQiE;?q;R;s-;I(TJp*esMEa$M3B7UTYvcH93@wcpYCC$Lm)0O6$yG> z?M?`A$Q>L<08=W1|&)6pFo+TB++x; zUp3EGDjj>ao{o|rJ39~MlG1t^c_DERNUWM6zwm%yscQ>D@1%L|Tzm1x@Qoprx1b>s zAKGURqxpOD4`K4!G&JQOXh27A8IqGHnpy13=jw}C{o%qr(ogrJA&N-vPgk@jX^jUd8IhZYVMKPAV-MH_a>P{8|s1T5&nl`LETV<)k#E|nUJ2svE zS$Naisn2M^C*w1fDrjH%HXJ}MHps03&=nG7Ze$RM;Qx3berR;l@x5?Y%ae$w3P5ma z*#O-Q8c^c}%nzf$tOxVg%?gTwMk&|gdUdd=Mt9VLq*p+h>V6u{Zw`d@s}Bk_?ilE+ zt2rl@$jw=P2VA`j6Y54)-BJXcHL;d%m_W-rKxM8e34!@ixVd9fw=U(5WohdU@b?JX zuF0&rwr73;*1hmgg5p4_7I`eX<))8^;ZNeyq?mgR>t`TK??70F7O>P_xPq);UT^MdNkQ#*kc8l~SJ=OPd*X-$wL_y*N5F^nY@!(~`7yVK z<)_9bou3UtI^vloPJ9QJ>PQ!lP}?N1>7dIuT%&hsRe^}iTJx0{5M0fI%&xs!d-@7P zMgnYM55lI}Xf(>a>)c-@J|A^`wa$dGW_FPDDw9WwVfhR3?bz2Cn@VJ;Oh)TAZy}e( zs4Et-nNb^E7&AQVd03B=NJU4Q#_#95b#Au2eDSN7PS(JZ(A{+*kHM~6mHcHC<}f2{ zF53!gtlJE*O47xVxc?hf2F{!vWOku0gdlSa)#p=AvgW)e9UQ>E*}r`uJH;sZ*L9X% z6{>6Sobvb5a2CWYv@sKH`=xf-R^2a^F*lNBt}ogQIPM>#@ZbBQ0jg|tP>llC#Ly%I znmB;u%f)#k$G{boS{t46DQ-2tpYjCMso@W)IItl;*z2Z;eJ5n#f_jAK_1|K_ISJiS z%%>~+>KmBhtJlID7WwJwh5AQ~K~tNZcg?C}QV~z|n6o&UtdN0@1+Nr_!(D5%j%Fn^ ztNeT1Uu4~=%+I$K_0y1q>+}U4s@&f7twGwyq41JTd4jqqKM!?i&36WUab-CyyElCj zqhPzLPR>GhgGNS7FX}77V|7E^M-;IOEImz&*_gB-kb$4q( zugkv~a_}6aBu{b;QUJk5Icd|s*st-w11cPU20^4z#?}wX&yW`u+#RXSQgOmG!xH^P zZvy@BV6H~7-%D`sEi-rUeMjGBv7Njf{Dk}}@M4&^LiFlqx*iT<#$@thltsnqdK!!K z6QTnd141M1an2eq0M<;nkN{P5(47;{7FuwZO)p02@KdsI)GB$!Mtvq8CH9luEJ!Z( z6P5(q%do}{GGh`Y|C@PRxFQm?9J>&Yj=~4)W53CV0VXB96Ms|VC$`PHzz3EAB#t}~ zB-OA;EI1yrMmaF+#MJPlROq3vuL~hD!%KF$B1SgLhMir!7$oIyQrd^iF5HN)0&QK0 z_ZmKcEBl-}ki%#6a-+rnXX9s4li@el(v{Yles5SSlZm0^#yDh*qC4)VG}6z1zoq?> zzOpF33lEg$aQWMO^%%~}jX{o)w4Qg5T$;MX=y&Yz9cu7^d!KE#byVEX^<5k?RL(8J zV&c(fx9cb4NV6O%G*|xepK4gWkGY+zShqog8urF=8{d*jJO%{Y(r4jX)eA)pwnt|=aYVBZU zGBdOxLIl@l#Q(uA&J`D26gdZ*bVM+yGA&ed_pa0onIYHJ6!`DJ#mB}`&UUiu_8x( zWFSLdW2Yk!_n&D>{vqV?WMNFfC+2a5)6==4Zn>? z{g}2p&9>3!D+))M6}YY&USNaFD&BC3ltABd68qr>oTj;Y#^nKV#5@islHm(C^dKcdTYwpepLA` z+N#ZQq;?F=?M>c5L@eAHBJG8Q%-)lMOiYa?#_$(ZGa5qw9mj19FA)zt)&TFDJ;i#HHs0+D~$$&oJ72umKqwT?7#VrpZvFA3Fb`8>?J( z%Vbpvj}4+W7^&;1tQbp*10Wy?0`q~T`Tt=-plxlrf7bAT)hUokH;b)%*~|NRQU1I) z6~JUGh5gDx3b$W7R`J+x90Dku>4RX0=YtW>SBFCPm!QNC{mB>3{d$ztSSNa}#|+^F&nF_f!hyGCV3 zaW9)xjZHIRaBRubs5UkDaEVc+Ij5`fpgLBLAA(*v6LvfUtI zgY`VFPP+&zTzR%^>Sn~Cg09BbF;D+`nh?4fS1&!YnK*sp*PDSZ>SR|#?OaQqVWq;l zgUW5#c_xKMXP@{vZ)9fm&MS8Rw`SZ@@OVjMhxTh6=tS}!QtN~mbG{rIlxg1GdIebj z4B25h#69Cdrc(lXuQu5~nQ@sr&xnLb=-xxX_Pg@9MoKzZI?u!dNfE6=h|~r(GK5!v5J3~4#tKb$_`+? zC(jbxuU$D%W|GYbDX3!qbgoVDInc$W_`zv(16y=&O+aN=CH)6YUiUY-fH*2yTx|BGeu@uk~6YGk>W1GtZF#&gLUsdDR zeakc{l}!vTZj@g5b=Z)@iBI$9;$_a!EE?oxcCrT%2#J{Y`j_UQ8xvSQc+s%B-8?PfWjlN3E z&dyij*4bAK=hd*+n=#h2!T{{w2H|=p#X*>?vcE-h zl+TB+YI%anp)*RkBJtt#w)rYU@aIC}Pa`WxSNODIKj`jWI6Ts$?&&wn4HWCyY2j>Q9J8MDi-OY(*mP9Hk`Kc}vXZK; z9?yl5TP84QQNZIDSMRiMu&Ctzn8DI0?57k{%uc$d-P-qPpmIh<$N|j{Q|Y(QH<$VI zflpkJ-1m7F87x4uH_tHZB{k`&J?*ElfL10TxRLZ2C}kgZcq`LP|BRuPQe5gY6%X5R z*o&*7h@cyrQAi6L2$rT3IovB!#SkZYtNj53^`2h8SCa+?nuWmI?6O!_uwu2LHmAlY>>JL}3sf*MLv;94Lq)u10S1}uT}JhV_?MtILeM`P=mYMQ-O5{)&&u;WHpi$s zC=n~<>*}T1NMK3jmz?nz?=0rr5+d^N%K@v#)5)(Lr>|ZqBv$govja(U=!lz11KpI9 z{S|_-q9(LKSxXB~Q=M#ZyRwW~oB7whFmg*_ua6ly%!O|Ib>gnhzQamFb~ZuElk{^K z`v5v}ug~1?Zms_Y<|q~RjeWkZWBbXHXV$@Jq=keAKSd*GCa^fjn{MdD+8nlh_3`i~ z@+V0ZN!jS`C<~bqN}={HbHD2TaT2DjGmmA%v3;sH zOKygzbrsNoZ+T%gIyv9Uz0#1$(E!RprE0IotW~u3$%|Z-g7Y%RbW$sf*bgWiwE#4V zO#2K$?&$t!wrF>8W(BDz;d82zr=`y^CSFyYA2R`PHM$w`ldq?We$7cOv%c(nbt=euXJc189}%@mG+&3LHeim(wv_N(LeK%%;qr2& znXuzKc_$;YvWM_CNRV{3!nq#rud6B^hbmm1o9JC~0F$fd?@Yn#3V)++o(%6_xzV}1 z|6~<9JQIOT0u)>cKSBaAfEpS+Nn7n2)$%)6fyJX&wG>>Z?5^|G2@`;sKM6459kzUX zVL0 zCF8#%2H=g%HX-l*(%;}c7TalBSF6Yhri?Q9lY@n`0o}{^68Te;^M`%_0za>2N-^cf zw@vI{o!|oQNHprN|Gb|)Aa*~YjU-bvAfMHmvL&dwuJ4nfXG4jw3g_X(+k`_sp8 zeSS@vRsI(0uiu%~F3}{1Y@qjvfx40e=Pq5Y=v@vlIy6l+hg0c$)$pVdlCW7E)6t{# zSH&Nu)e(DiO|0h)XHS!|vUZ>kt%km!yqYYAYZBAr3vlGOj^BrOEhduYZ-3_fONr~npaXxSx|?7I!;(jlBa$L1(amE4$S~4-*x9xy`r~oKBtp} zT`j&kX$CN3yQ-GUP}*iUMQftrDN}sAMZZAH9)1G2%$8h%7}Vdt^_11dKrG4$VJw5OAX*XO+)QoJjJf@dNBbXEgBmy0ucxv?NtR@ba+%% zUD(9cw4|g^4*p7#B=PcNg#%OWvmd4P^@qYYqV1$^Qqo)avl8iQUUr!lYZpCUXY-rA zw2%NA;8?A^f4vemmsm)o%9$$OZ2`xH7;aoWSR z;cYNlGMW`Yj2muJBQh5iVAs4$#Zl+wnY`%BhrP^ts21>+lVb8PCN`xrN}$ow#@XX* zM1bfa$KUAiqIRc#D~$bCy9Uz!TX)fJZ1~O14#f}cMFDB18$bcgKRngD z?BBVrK`TlO`gbl}J23pX3O&^Gy5EcneX{1BtQz%)x4d!C_07IGCxvE|za>Q67nD}_ zEZ?O<;N{;On>YX1H@2QpdcIg1DbMTalE~r}oaOJYAQjUSBbOLK+7=kEDKn}rsrZ}U zy8npq&nWhdNf$`iK)ctj~7vz{3%6c?#E5`UPLmw)@~t}gKz-N~#- zkN%~%qDqOcEHQBV=*)?`Px*x6FSTQH^Xo>glpNqyW+%8pk-j*R;c} zoEIEofuGx#+)W==kHfKtDT$u;unn833CHf?RrB6Uz+U5n4fj`rsqtwfc1&A+Yqnh| zJq(ZM8_9#I8_fH}x8|cFS#K&P)41fXkwz;n?8x~M}GPE!?7o08VxK?6W-~~TW7@~1>l#-)Q?yHNQ3aIa?XAW zs0uAMo28Ie>!F_@AF%A)C?d_J*&oi45Z8~EmkVaB7(LlKy%|dG@1=3N*-wjQ|F3)D zPNSMy(HkW7oczP?mlw;{P?k^3hQoM*d1KZHfx^5RPqgSF0wtatJX_@M)5F2Ds zIwSe=J?O2 z$#JKlSq027%k{%LUDR(9ND_>R9>d=9pQ!N&32*fFtt*SXZg(rd#dp98&$0V;j3LR3 z0#$O!H=Ql4@N@rgP0HnIdb3TcWa^{5GknWRjH-I#+i`)?i?yui?Uz7gC3brCf&VQ> zY>;fxQU*UQqlr-?sTi#RRN@)%6DoNQI=C9nNKRdv48vwM6;d*6PhHZ&+o#>jw}o&C5*fJN!;LmN{$dOI7(lFF!(1p!k(f)i);HD(cB*7>An==^#*Wlyhd5Y~4C{7Wp_iQ1|Mv+Egqi!98t;yLpbvljr3F zJ{>rctAm=*N&*Da3xNOqfnd2JNVSNd!~gro|5YE{i~02qDTxoFW586~Lg@O(?>aB& zf1xvs|Jb>v44|-rOXa`XyGmU%3r#P(XDR>pX8ixtw*PaN|Bv7Le_sB79S%gf|5LvL z!d&#kY&>d6J)@f&hxbj_Mv&Q}L;mMW{ zFD7`rSk78}IrnEdK=Os!$6*G}m@g@vCTBJLTx!4h9(t6GFiGR@V#e3H&dy^$7P(NE zR7vpVPI>Jt|1+9P$Y>++Z_zFJ%ao@b4+Ik992h@^Kv^FiXR6Uqyj3P=#(F>BCzXcI z8h+fb3S|rs!V``9^>EsTZ79o!Lh4pXoo~A+vGZ>4C5Z-Ii>0TDO5jWW9BADy61bL< z)6dk`%*9L5DwqF~$r!Kl%~xN5>~*U|v?9*?JLC`{^K=)vhJ4uDhWoe zwf()NGm1Xzv_?c95hm)=vqCOv+gO7ovDwC$Z( zL`lbfxOvJe8r_CxexAo=;K3jrY%Ee5YURMDCo^SuS{O&@u zQt7p)lbEdwwRish=VMVBrwnU^gS?mqX7_2>!NS<(BBlQ~S<~rk7i)^!O9B9G`_762 z8U~A4T*h5J=N;Ot^N-?HOi5hWXB~q_f+BT4h!2mJZ|Cf`0l?%8XIRSbMaq?$e~Scs z#jcSiy02!xKqilCTJ-729GkyMANinw!FFN&PO$xr&?A7f*M;hp-oiCckG0SRT5T;`gPL-1}b|r^y>WPkJy0urL;Ur;7H){x3@?zF(a36lC?w_mgM4*7712>#8*zzJC^0685;C9rl^o91k_xU6hFx;uW$BR*3`9)wSt$2WnlQN3MH=JA z@!9+xAhw4N)45x%`Fp+v%z}S0V_su`3YK8k>wmtFjs1?!Fv^vgaaCsDcpaA)4}#1PhXV$=`?0nD5`OAs%Ydnqir1Zg|W1_7_uZP z)pwl#+L9pOUaEiRqWZf>kEXqhrMg|8{>tdJXSNK0Zo&`CO{H$rA^Lr5eC7U+o#Irx zW6M8BBgbl>%Pe5MA-fa)>!~*;wA&vIYC;TtHSNo}G(3X<(YH=aLWE#W8TbwLJ`du8 zIGb7`Wd3KyxE8JuwN2&m<1gI_%j+iSFh}&{hwmLDWRk%!C8DMo9MC?@vqp>wWfADG z1P|%U+8DrkOGLsoaHDqy2liAYqxLqUqPQsYHwkHPdP+4c9TjGW27iTol=@$3m@>FC z{qip>i~EGs#BUU^KLtL`%PFV=eDEGzB{3%+%V_fjSGy^c?4l@(GO;YfFp`pd{Xc)S zO@Hl$W)cuv49Ws}`g(G%Vg%P)s~hKS;n_q=4iz)-OzX zqQ_$w*2<{nlWlpbhVsOMbuRC_{{2b1gEmPMow_@rz}7>K6WCZl+eU$j}3>uW~i32$ap9J$23g5bm;zD*c1)ndafL~!)u7MEnRKP zRkL~2RfP}!FQatcV|@VqY=7_m>NI30I@lQ=#!h+|N<#iwgxp`vgpOR?Yl05})*#yb z=Z$X3wQGyxAP&zT6+x4GRU$2q1!dM$v5MS*OUJ<;1|rFaA_-5skhFhiFD)_^RJ>xc zgGZlgz+*X_8_?lB_}Hj#l%d%Wj3|4_5%GJSTqb7)2m#-m5G=8Eixhhd6&Ah2xTpZp zD48Q@ET|Y28B1GYJu3tRvBpP90zOTRD%+H6NzWP9-oE>RT-q4O?Zu;GL|K<_>(+!0 zV4t`8M1iHd?cl~RN+rvg%A5rWP4C~(j*ro+iu&n=9H>y*5*ZXzg}*S_7}uhaRvp>4 zV-pEwd$Z}2BXKi)%P$SdVY}1C0kA=v1(5)i_BUj$rv(g?D|Vk}ZOH{NrwhV{nyo-F z5jSPc)DsfR1Lk1XK^rHZizEjz4j^tj)o|5Dn5_&7=bc3QD{q5Wf;=)v)Ixs<#st?6 z%_)4RX}2KT))K8wMW$^3%#8Qrx4E#EeRfe%DOt+FH51gBp^Z3+XrJIGS?^ld`_AX- zd*slZ_8WrFrf}9nq^QnDn?k4Clq$RrqRceGOO^~T@|WpW|o^Z{(V-377|%-Mn6Zt zKtB+#BXw_%0!#Zuu*FPPb9rk0d`Vls?ip7Iae^Bj zRx0mH?%vIz5&f4^(qGUihp5zRa}xEIBwoH`w;8D#*8=}u?P@A*J~yD>B}t#HZjjMb zPqzw#noJ7f#V0E6zd(hl)$@3+E38|*kK>bxyXzm4&mLbWPcE|Nd_z4EZW%%}s!I@& z>U!uMqPl7;xR2x@KqKVTY7Qks5ycG(v4b4lkIqc!kz{8>1d|j(+ad}k*I%o{JnmyYwSTo|!8M)HVOYfX1#+WBy4c#>`=5iuA$}QjAkh9{eGn&w>h@ z7VX>0ewLQ84VSqwBmWw$E%hvoOh?rz41dLtp*x9JvxKKmX^5U}7hnA|-Kzpa5r+Q3 z_rEeG%4|7}pNVfZe7nTLj@8=BY-1Fu!|e4``pX!vba1JG1vREl72ewTXaBLFKU8W3 z)N{BwQs{(U+cV4h@UUVRmoeGmSxJko;DyG)8qQVd%Dr?Ca@%F|b7g#=hWh!jPobU& zmJ>r5%&kQiSti^MjvS1LV7o=YjsScnCGBqldUAc zDT7&-H%zn@TwQr>Wnzi5Dfg5f@Sj-2=;M0$_+;GNOWPe!X+mS@KNq4!%5Gd9K(WYw z{d!v38(#gf2H&+o&_OJ=z_8)Bhvv?1v0U|BO#PR4AMTknSP*vtR2^273v;;I7+iMw zkbWR;RV(?JsbCk1D$OcQW*787LfRJs|Q_R(-{xj_qt>J0?4u?m@gJB>2Q( zr3fZ>;aN{o9h`f(!{_LuXAolmMuXv4%AFG6W6f9#ttnYbd8eOM_>@uCCh_S0SpM&# zp`nlQoWc6OftHlMJFiwM6{UWatG`kwXo=0*q=WZ=4wk@#HowHS=0qhXdiPD*{DbW)cTXo zq975wZDgwrLvS^tkH@Xuqf)Z^J^83ihFdx&ZeNU?qPMH(l8p2RwmB|vg-sD|>cPb% z2PCLCL$eCalK)oX>yFMzFTJhq&1J;Gd~4P;qrFO{LG`7 zwvu#Ye>awqv(FN-7zz*EUf8+fw)_|rqt>Qqb7Jo2-VWEYoQ=6woDQ~Xd+;F9i*2H@ zgV_-+MO8jcBn36Z>f_W;UsFS)*`1dt{~nP-YFi2}kmx24wy4_Wz9hONe0k1N=O0o> z0%G5G(}`XF2t$Fr-Ravdjw2@*&Tu4AFh>*U%T@~Zcz$H}lK}*HPqvK;^^j_t$*)_F z*3A@k3B=6q+$nr0+qc%NWX}Zk>7e*5np*QS zfR{RNZul0|eMtecAvL2K-Y>Icbhc+w{A)K_TWiM|c(CK?x!mp>hW2#>0~IzuSvCkk zCpPr7=n3=EKZz6171M3}aT%r+>k4L^KL_h5k|Bu0jQK^lRstx!GkP}_%H8IP^^YDE zllPBttEcG~X)IQ2J(f5R+BvcHQx-2&8pl#ocI}mK4oRp|X714pyVzw2VfYq=Zhqby zjK;9)X;-6eC_eYT&+7ep zm8dWj0S@;9--j3!NtQ)2o^pROV_Uf^0-^weg=b9Fy+Q6a)h3|Bv}ylOX)BUiiyZRn zkfa-_O}H?jNN9hBQvE@fXnEy#)>7P+XFwwvV2yrDxUMAz6AxHd+V6fvP1lj7y^iI8 z>mZycoSZAtqH)c1lzHm7-^#X*8rI^M;3Bn8Mz6;EGP~m=&ymSbKFViWuZ6FxEFiTX z**Lby6&jYa6(N!vExZTM3bzxMF`&HlVeR$A(Q#r=@V_BL0kt=S%om1)Vk&I=QU9j3 z9>1$^E#6Gf`)%wA4kYLpBwu}51yM@E9dIYUe5-+Z_ky$e4HDfQ0od0dq+r&hC*f2B zcYdl_YcTF-*E60b_cGK_SqmnNp8akW-=EBi`MiBo8Z!oIx1+B6aZ`+r6qAYsFonOE zQFb3j*xM+&4>bL2?>Yp8%AOPUch)+wJ~R|+a}X)Zb&hdh}EFnCSVcHp0|x983c_TRLLbeO~szXglk-sJ^h> zub~?W326kRyGx|IyQBrAK|&BlkdOxHM!G=(X$BCGZfT^uyE*fH&wKua^UGX&_RO{S zwfC%9Ywc&<_x&Nn2lbrjiy4$|erRx00-IJDMw7Lp{1IhQ`pTZ~XOBuBimPBCh!}5h zC@(JhR>$t{0;dP=S=dryRpVv4#$`FjxbN&nN>m}Ro8%hmW=8;oQ`Jwql88{hWfF8K zbro3}Cf~Dd&9L<)m|5`Pd-)Md6`^E=r7v5*TFcNUp0{OQwRlv;r{&UkH|0<9WSGcH z3+5UGOhc9rIhCVUYNk#YS57qFALFQeXV-?u`yU5?UOCgua5x;fvL}mEi1hZ43>21W zXnRGYer5~*Q)@_CifMnlI)@+!4>=I{N{;0KqRv7hhU~>)5wqtuoxW28eI{Wy)`?Qx z#LFA%9?ke>9s9TBA48fYRl!VFYZew6<@Gbbpu}Q6gVXTVi#6}CPTHiBOfk?#pAxb? z%sI|cODo*|3jO<_AdQScmcv#>F;1!Z0~tlHaZwsKXXTz%PGw)kM6nl|T&5EAxc==g z2^G?7ckSFp|F*%tRK0P;1E;jdUwD9{z-88}^-c`%ACVL@zHgdsl##nL)I+vzE!G$9HyT&okgH}nA!y+a&S#LbG+Q9 zB-(ydx4B-I`Ge3cLX&rCI0NIho2ID4Z$N~8arF^?(zf|If*l!_@*E|MkIV9b3<4{6 z9SwAzOlQhZ0=utmy81}|74k@bVr7<;B@fE?3Up)rqQHcpkRrh6W#ar`tZ8*3bN`42 zn0{-tU>`h9LD1ens*p(Z=KHbIMDObnIr$L6j+iu5cy8rD6&iwGvoU;;P5fyBftuXD zalqR!&W3IV!o|+y<}-oZ<^|_gwnQKJnU2$fp6$3tOHxKq@+{vfX_+|=Je@POXky+i zi5r=T^}Z;`x+ea0&!(mKq_vuY%bB-o@oyXmVXy57jgG;9>*vkwN&L68hA4yBq*_p^@P-hyk zSR*J^Z)^nVvUTC*>`sgIBYvDz#N2#(?VVkw0emQlStAZ-375pfc*d%kT^tE_v8=f* zPYY~)3^xXk|KYB9u}+2EyhQXj2KU?fuzFj{ z?#y)_YOt=Tt||M@4l_(L z_CPCQ>yTH3Ifqmpy5HNbvZ4W0$^7F}!<6@d;Ut!rXoNl(5@O`pF zP_DYO%McoQSkTL|Fw7D7d;plxf#hyU70LAxl)td_nL|-PqA2i zv8L3KzM6<>xS8-SKHpaTJAz!3(825P=%Ws^FP3MZVWD$@8%!xbbu2Ho0L(PcocPm< zNZf@|a6Wog{l=37k4I5Bp+0xy3;xXj;bgZoiF4HFHw2iwA_8S#*xtWTg!D% z)tJGH5#sa*3FL!sw3Ut@jUkMkixoq=OGx@Pc)FzHbvD<|h%j|tCY`SXrSnE^=-^Ag zR)k6g%!~t->BXM`i_4PI4K?|XM#HxaTGJ%`ODa63k$flw`N!+qC{|txFMqw&1?!H` z`^w;%NO#hPY+7vu5I^`VB4CKgRY}fjR3+nFz)RNT_#yvc*%!K?)u3RGF$djo`i#gD}Z%j`mwiLJ|``sZ81V#o#^aorm3cb;T9{!A}_ zZPETY;D=S>sG9bLt9`i|?$F%0zTJk#D~*LU6^2Pu(YY;h`c6Mjlwkf!otM86QpF5# zJ;P3dGj#U(=I3EeFPF;FV1S#FO&Ul)KQ@Cwg7chm6mVGhfnR;>JxGLXLR2_ zn7jDXqv%~%?qyrQ5uHO}++-H{@8@Kgf;cR_d$Rsd)@U*>k~jZ$E18nik`QA#72{wsz;N8t6nS^W3a)25Ds^8_`)@kP6VdGIE|bwBCU zs|>B@6BEZ@Q510c0%KCX)*{ShG;aTAojK%*$MB56uU~{Ak_NRkDO#0H;_^*2qRNGi z6FxnNt%p3WVZiLlD0SQ7!^n4Ck#mmkVm@Y2IU{s0FeoBTwjnN&#l9Gyp|1}~5 zSB?Dl)9-E!Imc?ce`ND?DmLbb5@TEJ>pW*`a?!4uUAyz+MP-V@kA}BVE6`x2^`i~5 z=eg=vJ+s(39MB|nRh=mLp#3xEb%o`(5U!+$-*?zq2vBjuIP9oTeo4fsHE0Ym!;sPW zIg{@}bJneYm;=-y`Vc_dhoJF&G@Q{NuMQ>NGIb#n(TsYD=Cb4pF4#V_6XSAcychVZ z&!-fBe!E>RrJs+mx-#OW0#l{D_HfgBKkF+*oQ3_a_>%#|8vq__oT2fzMJJkwh!Qf) zb=aKu%QC24;e^2hicN;zu8OvQHl_;{><`oED(n)y5P=HCd*2cq_aAt(>?g@sAP1dM zyMJKm00m6iWcR?u+JAl?m$Ab&8m*^_aMo)EP0^;=DV*OU0G3gR4&PG~CpyS%I0Fgy z9!_1Nk$MP7@98UR;QWUakQ>_EZWDZ}r0e*-GQm)zpp{Xc@E@6H`*d;4+b_5Uoh_YX zD?$J+L;OZ{F?jM0P_w&47k314w<6)5x?Ch(AGBxT+sHZd)p=2_kP@xm)7=dpNmMYc+%?4;%DvCo0)o0SOtHzJc$+ z6itj}x5v80nx@l!)#>OYZMt}?X2V;@;?8f|vx1`a?^m?i@5ag1_+LeNHA>mp7SdHa zS?r;JG{ks8iXZ%m?ongc@N5ieGE=E6b=!wSguisGAruiq?YDd>syE$Z5Y0<-4NL0F zUvqB}VOP(g&jC!?CSRHK>$(Cm6iA4l7@&Y36?Smf%GQt!wDB=oTfPvQlI8ELf<#V3W&qOgw}2uKuL58N_$jGm=iOj89I!dsBK zKQRRh0jJ3kkeJ@=soMNuJp?bo8gsvN-7**Pl+doqXa^v)Wk|)XtJin@%u#Q=zr!HF z4L_%tZ`e4g>UGTRT@VJ?W3^r@-87!~?O156SH3`(| zfsO%AEie0RPniQ8gg8P~p?TYl3x!JI1**T!$uIF|X4c;X7(mQi9VGQ}5ymt3h5&KL zOWm4uFT)7Q8HrDXo~t^mL0hG&UmoBBeXFZF+~z1osp)8pxxX!l;DQg>G*AmG0x;65 z%F~X^u6JE$;xi!QV+|zu2^$_h{DmJo#|;fah8$m0h>|2Zhx`N&((aZQCi&`x3Gl-@ zr=BV{hU&f5UfdNna2IT(InWayOJF&hd(o8c2if8qabSHXh~S21YXkjb-+|35;370l z^=9!aV}kb$x8R_++u&(5sBR=np-caR2Y8Dx_J4B_$&_~L9lo6^*Y4<5n5cUTM>AW) zv&Qg)_R5J9-mlNcah_JEu_Xbyv`KYgFI)e+N=ou2?; z85*yD_y(-JjF+S@nZ6XfCx@KJvM+*1n#t-tQQhJuHj39%8 zr>8n(d1TEbDYYR=&yUfT2E51!xVX5nBk1Yk$N-uY?UXoLrKMFA!FeFVf@QgY_HzV1 zH4SP5Ks#$VYkKk&+(RRZJ8+3%ze(&sH||5&w~AM0MO}7f~+YSst>isDOLFL z?GqaCAEWV>GQ9bXfF>)J4zDTWrCBUpM8EVB?R9l{`yDLL3HQ)_UFF?)hErpS^t4d5 zZIrS3-Zbb<#mPe#Wd>(q(#;o0?3h!duSQM=wK@@ia+nm(5$|@fCTjGn@UtzPNx58b zK)`BFZ1C<#~ z^`d(;;3PIYuw961cDGKekvjCLc%c%J__K2saURjNeODi~K*NTv+*UeFNXVV=2o1FW zQQ$%fC8z3po`_c&8x~+cE`QDX?5Cn?)mtCnH7S>0DQyYn{UcR z16Dk^F-^?e0ArV+NU0>kOG6Z@6E3&*90F!ga}7(WxB&yvi|w<{X@TAu^V~OSiteRs zV1^w33GR%NdCv8((BKx!NAHWpGq2g&G3;8k;uf1pNwyb`PPvh4xH9Uqv zplm16u2cRPV0*`Hd*?@AoKePyNcYc%oA4x_%c(^;i@*Qwjhuq_C04W~2T%PQf%_=*U_Ys!t15J|dj5IWs15on2Jo zZ!_S5oWq<=J0s+&iRwJlQfq8$$R#{8;{`42d1oF+ivVe6R2{qaxEe_}%^Kd-80*hD zdJ5lg56dS}L^J+0sz!ygNJxX^end?WO=wVmnl&8T(5fiq{P+(7R6$D=#A?!9u5O78 zfZX`HHnV_==OK9aJMWNyV(RQI175>)B|J(R?~Z3U*|}&tm{~E9E#Wx8+(VQs1}vi^ zLa(?Re4~oDx*&K6i0Tr8sSr?jTCPJQ@f@$Y$lnJMIBBzViO{2|2=1BPu7nQ2iNY49 zUt=|K$nALC*I&YUKlpx$hLW)f)L+C90DVyY5XS6gmM28wTgboUu5;k=;8$bJzm^YG zl8RWaO;N$T(w=F!C{Z0Ko*&;#(Rx05P=p122iti&mtX^q9X@|5Fp}4WrvI|-$emt2 z8gQ+rf4IhGltu!jRIcMRrQ{)z_$}zDfKxt;qlA_lMC_XY(A;x}28w$hX%L~fY5Ym% zo>e+rj#8?m3bhc>Nh6dINdaPYCUOT?5SiOiSZ7S5L&Tluc6^hnhmx`+QFLK?-o0>? zIzDmix%b%mw73OuQTjVK;0w%>_||fj1x*%Kaf*~!TYd+k0!MqJh~`;%zZ9=`H##N` zM`PZwd6y9CDU+XYf$F}i)VJgl4h!!K^I*jSH<7^**mBCPG%o!#nZ5)(l>S0}wsh#i z!q9hC+70dw=L|ZH_`v^*fui`Nt=(bzsQ#DelvmsQ0GesE{$|>(bwcH zWqU#P(P_!o_Rk=aIHe@YX-3<91KSe3fV4UW>4*Alb3%=dBfqQU`%QM}YdBz>09sLe zG(rN%nGC!%k$kn@xf8=MKtDV_g)`edRBmv2R2;yvjJ`;j>gdG!X#!43^}|vlkG zt*PsnM?29JUL%L7&riM1G)==ZLxWLETwvZeap2@501ku{h`>wY*sNl))}`3_Rd*8Ki_@_$J#b{+@iKC< z2VUhG64l)$#;DkgT*Sg}=2<8(QziluSgx)HjYbF!7lN8Lt^#Us)(Mu$;Q+oaPW+v& zl4#4qK+s+cfC<$%9$Eejw<|<#;FiB1O}Fu72f&|fVem6juut(Z*xXq{J_oWbUQL7W|G9h(!;J=p_rqt@$vbw>n zsYL)C?>XJaN7~e1;#A+L>NBQ)KwElM#ABkNX+9368Z8Q2Ga9RW6h)Nqbs#~6Wp2Qw zKuqb(*UISc93+K*A%kJ~kGFmA@uV!^+Kx0zY)aRyFdu3C7Ph2D4-L{sh>Z((D?+*3 zo11BAc;)azPE0}QGMFb>z=(6{HVVbey~>{*^5+NuW0JstqQzE-<;=e%aP3OtkP#|i z_KORw=)C|gVtgHQ0eOuvQnb7*_x*2qKsi!uj7{pdYAv{R*xnkA6;Mj@0Kr8>AUG|e zdxOf^DBj(YNc!ErPZv2ZPiRxx&7EMNi`7|zJBFol;#bLNhp6G;x?46E(D8PO@Tood z4)Ly+_-(p8rG6!=MxO+}JK4*$S7PtXTtQhQY#0Gp=9r+-UE6>_ z?_UBq2m<`DFx8l2km1T?ZHIS#;cLHUX!@38;S+(hf&gbFfaE)Sqe3vhi_%bKZ&Rnn zyU@Xj{bM2f0|)%{?bzVhV`_EP8?_YyB(w)~sO29rxWcido$Pj=Y@dE-@YU|0Kkm{8 zl3|0Kic)X=g_J2I6BW#oH)17cdT?6Xiuj+*VYJlREO_C*jX)5dB3t!Rhz-J8llY>l zO$%YB(V_Q<0Ki7noA6r~4OKI4?5oV6F0=@@0+aw35A&!;jC041p$Oq%wY2u6 z3$@0M(ZZG`-zV@M#{zz!sI|rVKV0z{K~D(dqiHOLnqjH{fS!mxvv5w)!a2bJx;RSD zAiA!CL(j6NP!gqMLo}_z15F{&x0X&yHAGf+)AR?04|o}}g7oj<|FwNcr?|`pzeil6 zLo=v;8b5ZGZY?)XT{8PEeyM1fs=bk>L<7&9)GC89RnnyB1)|9FoBxB&N-1-uMD!kZ z)b*5^tvy@c>}O8CE61eXDHJ!##azaK#OF_kB3mGa=-|#S8}X_$|LTm&-(ATZdQU8& zH4}PqkkQ{7?;+Yl_Eux6@Be85-~m>I>~TW8#goZ&^MkH6gWsuP_NsrKe)2Psmk?z%fh2 zhn-L?Xm%?+*v3Qb?E(fsn0oa5X2F}=V<$aW!1x~m z6svBiyUUhXc%rK>y@NhN!xcau+sukhl)e)%C9y-P>4_YLEOL>ehw+TgmN-E~Q!0 zXf{pAk|JbDd7);lL#oSu%9du*!HXNqt2xDc{h~m%zIxgn6D*IG*JQi{-(&bd13fQk zXJMDn`U!wqYdap|kz3n64q<_Vfv2{H10qn1Ba|nJ1qH)lxz_O_bl<&cHv$@)n;5KJ z89vq+B{8mPA)eA?pVH7W(&{`uwO?ND&>@k(OKF{n_>`SqQRjh{sk)Uv@oBjA#BU#K z;vk-Ewk=m)-1ytnv%d1 z>0EflU9fi&n-7ki>d3EF9{$Nn*20=f2tUbYsgGL*?7qPb}TdJVlMxxXx23+Sn#!`69)sQoCr#bMNgK)Kcsi;UdKuqv;ICi0h>k zRR>$@Fg40-U!E*21Z3>^q1=nOAFm6d@J-B#7J;*qBiws8Hq$gAQ*aDe6O*bJlm-Ro| zndE%G8)+M4zj9-G?_uDVgDpG*QX#=i)erL#Yi6_A%%ANOm#LrYHuMV%MGIUQ49n%K9eyc>BtFr)IQxCHCA~FhNAIqu zzQ+LG0#w$QbO835Em5W*$y&^gG^qZKy52>O;2LJ53|Q{5Y@CMA9SZYoWn`-OOiUSZ z<>C^Sve}{Q*gb*vEup7WGf~n;ANq?&j$*shW>3ecj86na%L3axI7l zT6)-a?me_&6c7ocb$qtbMtKCFO2*4=6`Nb?q96Jo)dvR9tJleMOa8@_`AkZ`_4(-3 zRO#aAbBu=Yftv<1F!~V9=Ex|eWB;;>8!T^PO3IbaG69$|Z&?-M`sR-uw7hDqcPNrR zLaCO{L&jf~pP1~UaIWz4&n;5SLXqn73yPtnukb2AbbeUSZSMmuUQ5p0fLI0Zoxu|$ zqS?HgY$@-&L|ELVSmj_c$-%e-Q}iPah<*8h-ScN`IwiKrw)V=P>Ta!7k>52UYXFs^ zrk@5|ZX*e9KR0B@`}RA)0zU69WPC1F0Uusy<>Dp{H~hr~t!6B2YETs#S*fxBI5)$P zeQ`f3%s?n$yg~I=^g;6lFTV-HQViyOxe%eIxa=1h2te|0?42;<)n9Gc zo!pPpi^eGgdYK$T9bS;5Zry)Z8N{<5vQBC3--Ux8-?v78{0RAxWd10F3H7AStyhvV zL0^aV`@Jaro%|9o%JnG2Q+0Z^itx{fD#&i<%h)wvQ5vbj%=depu@S%-OW;<`U{+*m-8^Yj5aC;%w7oX|E(kvZi$0)JFo zpBu_4R$2wXp9JkXB(4&&bck&0u3XB>=rYb{4j@+q7rcIzCT~wQiR!d}v*;F)?#Po?0RIYG$3- zXLtz&WqZOK`3qKvx#1i^imMF_cX$-FpWXI{HOdqFMv ze_FPD!Fi0{N1~~Ws2hA?7|AiG4cDiCP-}8$e9!qJc4G^&YxZ`3FmN3x*Rt)N!iN-* zw}pMGZ10&6C2-!efM@v2;@<~7j}c{_iP%GCw%EH$j1fJn9d3KPvpRofZFTgjMDUqd z5{L-LBRjP07Yspu)xoN|0~gtBIzskNqg!OUue7;ITt>0z(&vSE4jE!`wmH$ z9_L|&n&)77ZSJG&*mx-4$2T;JUz_aipBfbq-|V&#qH*WFlUutA zAw1HWdQ~YfZV!FsKcFRT!+0|(6Gpk~`6_uxUc;Rgft5D&)ye`EJcx^3j9p~p8grYq zJ?FMqDZ{kob4yV}&4(R=SJ;u;8yEojN(wI}&-0}wp$}f>Zqu%vEDF6L#y`%{j%Y9W znosbixgTR>gd|04+KG-@f((TmnxYo>gNKD!?%oBim`BW8v9{Uf)q8s7BiOky2h}Sa z5f|-!X*T#E!7P7M!~5|MnwvUYoBMzJP%~eK*Pk&Pry>nk;18D{N=&$(f5JRS#hgS@ zg$Rbtl2NHu^P{-Mo!83yBLH>X1?x_=KNwTrOotSrs98rkZ7&Y^6zq;hq+)&i(fJV# z8D!e}duoeO077E6xgr0(Bnsf-Ro`sbuYf-%=@Dx)^>z$xKwqPUji2jzxhz+{T9E~y zgH=r_u1EWy#y?-?NOm{br{%774IkmvKucP3O4~v7GkZc)UFrw%^Vr|p$s-t zx2B+52KieV#;V_JsItO1ar&%vom#8VB$m_XOcDi3AI(!Eyxbz?YLUkQXMFm!G#tv9 zSqIL&BIUq2n!h8blYt%U16`cWL&2{~Ueaj^$F@P?hcV=vhbzpp}Fa z)KbwI=ZeZ#vP}z5f;Tri#Akdqi_Uf4NEX!BL*h=msj4#@fAZjN5*#&F0%h?+VN(SF zjTV>Bi}NB^`|MIk3OKvVEPMBUd|$B$>snL^%G8%3ly{Rs%I_KNk(HP@7Dtpg5eT1{ zxOgiAo-hFQ@8(PQ!ZzxwbAF>f3%NIxyQ8jzJwd-r(P_<`IOcH{G6;!20}xBO_v3>s z)kJa^z7hAqW`_-h$0&-L-G|{)N!(B4j@x^E<=D^eV%mp71b@GD*Mg@XE+Z}d4Vz$; zJrX9Vju;FTcUP3q3g2Y40TMz>8%t2r6e|bB+I`{SUvJ{ZpAiK9`L=<~>g;qDjXHss zlK(JegvxabK9EsIYZztUsKimvjb=Y@4T??T8uF5ii)}j=sD6)xXb+CnCj4GKL#_*er*5@7g$a9S%t7FA-CI?>|uL(>&@oY;# z)I}sb`b&CfOA|hsoqdZDOHbuz{e@QWU<3VcLLq7pi2?p3_eu0)df0|C9I|J;b!|ZI z{Y+zRjKc;ji5v-#g6rT$n)aL>g;G1Fxe-!uNAhA_SpmPN$MF24VZD;+$dW$q;d4|R zo-LVp);b9HNoFI$89^$eboTydGXf#b&1eQDwlWTaAnPCf`p3u>307`2#k`1s@qCUu zPrIdS@d5!xC!G>rM+ceB>9bt#&YI{LCP9HEr2|2Y(oL*ko8k0~Ee@5_hA& zUz4lq3EJk;-O(z#tR1hvlj6;DN=F3TAxc4K>&vS(3HaEd*-w^7hTa6dpN|z1)$Pc1 zw?wJwXue3qOa4)UBPZSq@YJ8>sLwe4YTnqKQ1FubHbWWHn@z=Y_5*8w_-E|^a`oAZ z^ZtvD6fpt9*ZHqN4vjf)oUF}F*p=AO9#12=Wz07w<|7ZpwB%IWUo9x5A`~U;q>izx z(n@g);48~M8TF@N-nr)AO+OILX3Qlv2}1qolBfAbrI?s%Rml_pXQp>;jqa(_)<@3sV++;P0|9jnUJNti(OcR$JvXfMZXPGwL=l5R+=(aJxvj$;c#af1YBP!D z7V5w|8E~9{fb}M+d0SDYe?1xv)^66G!^)AtmX(xgH2eY^+aRAaOI!gV_T~*1;&Wy` z-!y?}>#Tne*hF&CtJgObT#x`vh>KA#Zus}l0ckt6Xu2Z(Z-SA`w0?6rSy{3&gv#^8 zwgu@C%8(g(E6ZzSONx`fBVim=-TWvpctl$Mt70xzK#M?L7Bdx;e zy9clRQ#$JC;z`vJZZye)oNyzmG<>b0bP3aHyt)J8 z38wpSbP>76`nkHO!=lp1_tT{{Gm>p@d0E^jVWRsT+?UpmInVt}58kgj&L;<~E#3}^ z$;U4LWhop;b+D@Ldc)A=U~kt8%bg6w{wpDJJ{M{aTl+baXC~wbU#3+Z-E$kHrW`3| z^Ka^5W7=Bo=9vwoE53rXly7 z$JS$~O%5y=?;+F)iH{cMa-Io1_Zyu!olu$>!1+?GBMEz@wd=}s3k{5Jb}TIWdeFcA zAin?!tL|Fin4lHac7uy6RKYOQQ7{EC@6fFHyo6M9G(rG%7Xb-5avx;K_X{`@VPCD9 z9I6MIiG4iL);g$5{?PzjYsrs+7#O=!XPM-$eqd*P^2_^Lt=^STRcp#l@MNvIy|w^> zRG;?8oJgaH2UcQCqd^Jfq*et@=>TWbxW9)&1lJ2%77`s(dSw=}6f%!4-$deC(MfUg zI?ivHW)wzhdvec~g)(*uYmzvaou1~D+dbnzBnz=;7gEewUQw=AKsFZqnW!uI%{3ug z;VV$6Q{=lZKdL%AwYsv41ERPl?Ot$0WJ!qx@BneIVo?)|@VRiDg+!`zPO7aThGr^4 z=6=;aAQ4nifBA-yL=hT&>Npuq%IEarI(66fMKn(aHbx8@w9XTUPV<^6U)mu@AC<2M zVcg4Y`vTQX^aaCUrAaC!YxvS{NDkuu5>baVeGnNbj6S@l%1s~593<;goL`Cnr5DV+ zzbKeLd0vK};oFPMF)hp$h;hV}i|aZBD|{b50y)X@gcW~#e$7LmsoxRqgrgV;jP;$r ziIa{Fz6~aMsDwG}D{hjoFzP#H5tC74`ZI5de8$^sVK9UR4y(m5vvDYE{j|knbA8ar z@+M;tBr&HT8-gO*hkBHiUsmh;W;w))7cxJK(U>>q`3LkC$F2pe4bISXn@mFb-XnqD z01O-JpZc&~*UN8ve-=*GY!TnbNhQUECL35#yfid)c&`N;75*|RZEH#3<*TT^MvG>R zC32l0KJ(kyQ;p-z->j7&l*C4F1oWQVNb-=gP6k*or;Vk)keG9h9k=afgj_6irTx zN3-%j?6ZSh8H$V96PDCQu|qE}vwy{--VmdM;dq?L&LJc#Tounr*S7LyEEfGg7@nrI z5q+FPq%uR8=|dS->ISzjsKb^9CN*+`GDWgcOq!LO5CI+4xx-hY;VH^OJ#?tHX@YiD z3$Z8I{?Zv#`eq0&Mf3t+jPiCe^0+@5eOklHizdq1G0Xe9+nLXf+E+_Pmy8B|MsimV zxhRxy)TD~FmmSSTmM`!eA(!o27jm5^;83#!ECnA&)uQXCDUjyeewf(gwZJW zV)BrD;f=oSbE+@i29=4Mo32()<$O(7mavAPrY!X!f4c{m0}AI>g;}*KV=?()<7VSD z`!hf~gWEfez+5z-J)Jo*%`j42A z*2NR_ZvzfGUd)+n49~2NV-$p4c6!y0v9tWJbr(hHe)-_#1z4+_ilfLXeDgdmJ!>tDoQ)P$dF$ZbGtM?=4q!l-RWrPN{1_($~Sc?8_D0OY2|r?l@4A&FCNAEz zZ|XE@Z7vPwro8<0hxcz+cyej{2Zpjts7{cb@0JG_D&xOsHcY4Ba`J}&VOu=jao6o~ zgcScxLFUXK%I^EwDg-*_-*Aj|JcKy5$vlij)ds#x=A6k)jpso=`i#SMj4~g^$X2(I ziok$5$AEpkdeRc-uYM%^g*v5@SM^gQOq_l9Y|r^l9kv*4aQ5it57$gd^cBlC=<7;5 z^X4^PrPIX|cs(#D&e@1$|IA96zh&uTT5Tr666%#oX0!lNYi*L2o5WmxNc%m;s7I1Ph1MIsTW8F%2Rt zlZs4&^3NqSEOp`+#N#O1G4JS47v(WI+8K28H0nGZIE3>uu_E~g{p}2QiWx`8Jh=xs ztV?mUxba>d#Ks@RZ+eE1XXy|RihN77z>TuO7wh>l_93l40icHg{yuvd6lQLd*_J>=XP z$`y|2ayh>m6!qA6N4;@4`EGMj^7oe4c<2YcjN*JD9ArP0q@p^4>}eub-#4#&UVj~l zn`1*%G-lZj!eDeFIi{K-KiOM5zvX!?UK*R7^R)UCwePO}g!nb9_|MI3>N2Hll3qxI zYs>xLuMZsXq4ymc^aghQr#pNB-CDC?p0ZsLv?0-Y-TxU;bR{-%i36M@kxv|z_|SGD zWzZJvZ{9+yXk%oQ4Hwftuj3!vqMmDO@*2ZtAOI*q9Y|1gm>#?6^f-kW+T~aCrTg)0 zdf;sniqm^XtFQC-6I@FTKOPnXfFBCqakuJ^NOtGgr*9PC*Wji4o`nKw_F>*jZ&YZ9 z87IUH;W%+ZwNc7>9yYHhKn3n^C;*F>=y|=&mc*JW8u;HMQPF{tX>VoVultLWan+xBfdTeD{AB_5be+k4Rt4 zNI6Nac(QyXcv1kT6Ay9WP8ur;%9^T~p=cJVGC2GgNmk`!m)bsju2N<^U2t(#KgoR7 zVjXnZ$00BL!*ZSfUT#q|>vjY(pLZJ+-W zO2ly;nJS^S2l>x=qHxZhZ72JGbV?t3kG`G8F?5RCO*|g_YaE>TW9gh5oi47r7amX) z_Ki=70a;Gq4vAxzi+y&H(erhP+|kdXU!_h8x}t{W_{bfm2{Y)hm&PrTSqe{hNI3ym z2+*LfZ<%h)dz}MX!qN|0)Hzk0T&faW(pmR~k4hZc+-K&`d_t=qNFl|$h!^Wdn3(zs z+1*#PK!&$X`k^YJUwv75xJk<+QBc;44m_@0@1TRLH5cVuR2Ht*xV+1~2?8k0PX8yo zNZ@4F^>RA1QLd{>8o0bFD{XE`?s0lvV7IhbJqrCWw9ee`)^OXH85Ve{8P zBCj)hvhe|4{QWB=*vs7Qb^^$?5k^{g>D@c6HM`<)bFq2^yi3U=Iq-Pd#exJHwRku` zV5@A2-hW~}dN28Nw}u%wZxlb=?jwN@GO~MZ^W39!_@|$7C;a&kmM;T)x;IE!zL5Pp z3per>sN!Z`^7ntJv1w4z9ldb%@HO?_s?G#{#U+@pp9D@`#M^yM-+e-r&0O3&~`wS8f;A%U~?($3mgQKaE)r)T)P>!0p#~P^VXb`28lLLq_{@Sc@7? zzEyh!FfIdT>`HIuPo9N@CjB-&+3pq4fYGnv)=#Ta3KdR0hHt0>PXre^1SsNFJ?gCp zWQGMi$;Jb@`vQs`0Rrgz=inFdx{GKulfpcggXPl-TPXD|RmDsj0kldH9IFD#WN<&9 zeyR#CF<`wRn|V4webma)o37^1X$z5S#>IqFVJ6r|GOPOU{G3#X9nJ*CD}DW*!9>7aL>FtJ6w=-~ zX^0b+Au^0RGmH-V&Onw#Y^t7f!G!VB%&F(yQ1mTtE=A)_;DYS(_^#)GU?XD*8Z5Vx zd1`RjQyBI31y&V~wljt&5nJBRxdbDP-4YZzHs2Q>sMc?KJENv@Z5vtbgWPfoC%8AS zBpo8MsNUj3&y|T_6^>RCZ<3Ly^FLJS*}bJi)BTO;5uNsV=oxgn%jTuW**MWmhiR8Z ziuu_RzNBj2N2&K(Z(o9CpZ=*t-^3GFdQcXw8$t_u8(8-pFj;qngpBCiNIH{Cwix4g zyS|Z;jL~gTj|DBiZoAF=@K?wRme_&ey(xolfz1(4Mn0THjwq_Ti}5@4AG_0+NIBNx z6{XXj@>;W9{x)}WD;XfWLaK)XBZN@bRiRcN^diN5ypeCKAHUB#@w7V~Uft^U88;Ye zH}f!nprK^9)hd|(Mbe~rAa)ZN&_|FWwIX2m&WrWLv4#yxv|OCCoTG5O=9rM6Yn8?M zEMqM&kL6B+aQHbBWmOGZYL8?#<%nsH)f?r}M|9-WBRXEsP5W$uyZ~VXgEt&RrVeQw zwvh(JV1U5mJ_|*(ulWD`xrxm;r}0$mwC}q3|G{u+>1Xe!IOr@T6$x4^&ynbvqJH{w znnw80{Pn^W=3odcq4rE?u3D4#x*Rz?(a71re=+?bAH^RqGIUo4Z2 z7LuATlP?-GoAeWLanNL6MH-3+Ck=T%XN{KN2KN@-!q3e3deCSnax~#=4d!C zv6+;-`P@^k`HSCP%O71%Oou@TfoD=7pN-MaCL+6%m1hd=n@r9F!)KL2;+kFZt<8v1 z`8sJ&|EqZ5U}e`(-o9;jLJzs#Biuc#`81R@!q69f<6EPjOJvYOAqDum_`fx^OvgHy z$-S9=H)r~y$i8*H3&Ik0XJvi9h5Cwcss}VB4Y3#d7)GTsBKL>%f%o(Vmsh$%Of&>+xDYMYGzo9vM80IemEzo z+%Jyd;j*lmKwd|}DRG}!jjs@6$?pb*!4qS#yrzYhSN;q?9=l&dP;g;Ygx+`PIU&UR zhd2#J^mB-JVO(?$$#*xn;lb9_qjCXbV06#&E(eE>bZyH_Y|$4-MPJE|mVnbxev%#% zDUj%I=Ci`1%H9RKsTX+P^seU+hrnwa*lC(F3hsd#taGx)s&<+vd*3 z`GlarRBI@Th)T70sx-OqTTV&cz7T$Y^ZkO0& z^Eoh=bnpxptekzTLb2PxwM%fe#daz2z{0hh$7mn(Ew>-b`>M|~tO)pjlt&fw^U{^? zPSv=|fXA~gR-W;@vN@8M73ut6+g`beM5Q&sFczLceP&n?M!jw9M%b{N2VDve3X)L- z-w4-9S8B;qB;1WQe7ZeJ17;g?gec5cv}+T}U|468J3*UuZo3QOh zW-IUb=h?s6FIXdqJI|e1p0P%-ijIuh2jSI!)@(joc}8itcyF&{!{?)zg7Gz%(9WLx zHIn4z?H)87+}nM!U;uKU4!IYZ_%yWyUPJoqMB1zAdoL6NbE5Z{(_qkxi(ILvX2?6t zWF>P)d^USAmX1e0lqm{7>yR zD~V5hI|FCWI)lQn2+Pb@{e7Bu>$_zbZ>XYr9ZYdK@;EO<8gNT`pK81uRh&-c&uNUb z@&;#&Fwq=7$969oJC@!*J$lf{ZMlcd~dPb2aNJvSN_?olyuCw?WGf(*MimR8x(N8=Z?>Qx;<1QQYyLYw8pn4Bjyg7tJ!NDdZZPF>PUkPk zp}`jV=Y_OvSVS1#118pqh@y+v6LqesTIbf+Ch(xtNXwX5@_5QgX1V;qb7>xt2pvLm!O3i?xuUB^0)=DCf z{>Ix+`hwZ}u7v*CYp;V-6y%eqUSizUoiXjP!R(OqAH`R$^EA52gTqEopNX##E@$+;z|M2YvvZetF zwV07nf*r9%S}rRkRpOWb!`WMgMb*Xq!fWX6lGw*$= zKhyeG;_8Sev$f8D=ZM0Zjso=%GhSn+<~shP-0TcSyOg=MXKG7M?Fd%7M(b-1MiBmk zz4e|2gvg)+K~d7E=afI*nwW^xAJ+&?e`;YxXM!yl2^#IVn(ov4_*7`m`@Br!inbnZ zqE)9&{d6{g2xaYpwxP*sjV zdK-(i8rd}gt#Y1Z7*&)Ti({@NEQ0IvCvbjjA!8b{wYt%bY`0*eP>4MtO}JKL+V$QtIeRPuZev{r_tM>;G^N@r8%#F1j&*(-%^f) zGR=FRyu4ZzdO1uk5~@TiJ-Uw|Fq+-5N)dPJv@rRsV6K2grBZIe{p?l<0>c|D(VF1I z%v6tS-mTUXd*P0^j0t;`#V7VSbJ~w#Wlg)QL53Y%7+g_5yR^^x=tKS`a7ni~>YH_e zD*17n#U*{p!Eoj~{yCF#HG&r$yU%cNhfa-?LI`23yzqW=l-1DL)X{&QM>svpY`+PK z!KSW^KexXs+qatig4ygMjmobfQW1m!dww4=Vrl?jc9kEk<>{O@ZYGD9B|BTUMYWr;9 z?HCmZhRDr{wfvRXy5hU`eR4m}Uck}}^6#EQH)x8aW_V*cX%xbv);fQ%-$dEOen`Ee zA$jq|hsy0_nV1puT2AF%M%Fzp;q|yhzp{$l{}?P4T=Vu$cPs7O*@vpcptWViN$e_s zPeISR*XKonY(y-61qUArU%TONnQkUv@6$h`wi<%JWUz9jjBlD5_?j9bQe;z79Q$vS zoOz(i=-r7v9TwrgI6i|j$Ulr|)E43KINY8euJ$b~fSpiUWqQfidUx*&4NGz%wQ6x_ z{bvn18}zh^J*cK#Cn>@2Y?htgTX z{2Ik9ZnPbvd%MZ6fdO2pPqt9ua_Q2xm|@8d|JV|3Hps! zjT!P-EA7dJCz89JAt>X?pNp&POHS}DB%R|ep04Mcq@Pd%+=CJ}wJ%)S1B*4?tl$sc zeg76Y8xh5s0K|;uPFfN937klX*;zj`D%Z&Dn6)f=HQd}eR;nmSK!b4q}b zE5+U=SA$j3(MNQ;M8s-o2LkcU^SFw#cEHWvD`5>`1(MRZg$zn})Qv2_dO7=8>JM@_ z3WPEFIG?Oz6E@_XYGpICW*k?GV&-Czn!6GuJ&jhJK!|dae-u|WCOUZJ5Ajml*g<@M zM%5sHwXbll^kxJ@iiQJ+3pf;lH^|SVjDNqzWh<|_ZnMez%Jwnl{C8HS6gw2|JhEp$ zl19X5M#IB|>gbd_a_dXH%jc-?@v8DVvdaKtW@WABF;>fA`%&5G4k25+Bhjvp5uCkn zvBu=I8}W=|d5m9AenF&YXoBnnTd%l-GE(D}Nkt!W5l)MrM-iLqJh=j{na4AC6tH0R zD}DV!D}759Y`w9VT3v-ZqOY}#)xT|J*@mpcZM`W?>;;nLo4B!xKIdN37J?zv6yZOo zh4P8*OxAytGG#N=y4k?IXTW8hYsDpGI#K*eLbEm&vq$k5xrNNh_0#URpIRqOQMNi$ z#*Y!U^ey(iYY|v3)jtnE3y&sa*YP3|DCk#SUSb|nu~Mfct8;N$h457J2HC7NXE9%s zzURqX4YH?s!IG-uUVNFNy0&pEqc~^{2m$3%GqtwH_}0dCEksHTz|kOz#pt{a6eo$X zENWGLcT(HUwOgp$9MV+=9&Es-&FA^{_mhI3CHorQlAK}=eA#zas=Ud%@0OE`p4}fd ztWo8C{>w=6Gu6mC*ObaX2$$gY2r9`~qYJ_WFvMale*uJFK}Gza{{y})3Q$Vdp3SFL~j$p#Z>!(8(KUm-kF9_ zb2@Zt<5x(f?fj&8x`cwE_Vl-}=HRc1E*>qgwsW1uQixDS0R|OrM>@R%q&(ed0Q)N8 z`(35yv6N5~a7F(yT%vw;icfI@!N+=rJQw3|=gvUHPZ_Q{3HfFbr9z77@|?Nz)Q_Aq*A`2+@&%WDpCPn<9Exe$k zTT3(BXorp|d&vuNyy`@tMRkLCb-rIWrrI1BXGKx&g}nw~yqM0R7b0Nz5*fz%Rzxwm zA>P5i<_lG$KN1eZQ+PGLY6)yr7OW)_sekjO+$gey(;(l=k^;QcZjJ3gNOXCHL_tNc zu3We4W%}rm59b7k$ud)mjc(1xM+-p{{}O@){!m~x1yT)eNCI-@HBM#Dmeo6YwI>Gw zMS-i92*yobPxm*X$Xu>tVOQQ<0>X%7bkHJO&$cw%Co{t$Dr@x>0qECwdw>z9h>$~| z14026cxeK+cQs2!-_)XO4U4mldXZDmBoJOFFLD!{A0-hKFMtcYqdw|YjLW83cVmJz z3~a?P1eCXkhNV7PmPh_Wm;ZE;)U6%m9?v1!S!bYeGeqhQw$q0Cj^f`&7Ta?7E_9d? z9UaqyB2A@8Qb!^d%shE=6hQqQIdl+Kyxw&^xogEAB)8_fCSNu5r6E)dxc-LC^gW}l zfPhqA{Z!vbmVvlnd}yI%0&f1RmW!D;NFbLdsygMyy6_u3^n{`YZ1BamO#jhj z4BvkAiuoP8a^OM=bAdpcp5!7R^IE;p&5t0a5m9(}Xchz0_%(vQE;Rx3pFBo_&Rwx8 zWpT@V!-M=Tn_j5P1@spu!oCTi zMv`zb8+8?(qt}^~gAwX=NoBK5%esRv$ed}>t9!Lg^(^@o)v38%(3Sa?UKTgdg$g5DzY5GyqnhP=>62Q!|$rPjalzM-^**~!hPhUcdX=zsY$Hf zFw7kPW~51rp8jNoraz?y$Dx`kt_kLf5q8??Z?Xlpl^uAs0NUErI|%u(Gb@ZCg3TvW z>K%J+f%c0&z)YNMX~C-v%KXXT>eZ!`9u6RmNvDmF$degk@Xl{KY(x+G0eP+kdc zvzVr`IGdt)y7(8QUJHSI&#x$hdZ;~aFL6vK)Mje~tf)FvzJR>a$%J8th+*zy{u98}`RTZA+f_ z3uwF=w!Y{9?j@cp}{bN0ww>IC)?AdWXox;PRTrAe3bS@ zw0xjQumb@;aaT7u+2#NVGAxXI6Bd*^Sc~p-2|u-@<@4y|J&oYi9EAP^b;{xFeA>c; zl+W_Rw;V+(zi=Ge0`*U@n}*vFu%>U>^}g?OsutN??Y6k4qu?ezeJVAd_QFgYtxj6f zy_uZ;$NoR)0DOWQx^--Bs!aKY3xS@?n~$W|+yF<%bILN(HAhjNk$U%AuDn-2kK|zr zUJHUfTu01RcCP}t>6s}|5oR~AWc>YZ-K_Qj4>;X3s&!ulD#}xYKwtsWdG4Z%z{kOR zf1-Ao`Ic5Za%wW-hs_s7D&y6QAPj}KUD(Gr;PIt+|xS`Qk7(Sfs3 z`bpw#SnHQ3?@GUF0^Najgty?E5hX0VA;qvyQXhDvIE6p16(I)sN?sNRnh?kuz9#Ue zWp54avA4=)-7N3AUZ(l5A+@3XP|Pp-!LBG9mt|~IBrJUn>-f^Mi|=Q8>CTfv)dGJ=3{70a&3U0}J&%Q7NQP3q;5=;9rS502EQ<;e#Fh!F$8 zR}}%nuXw2iBu~1Kfz$6w+tA%p6Hp`)@?nJNVayK+U<@-~t{_9dzYq4y8hQ%Mj4{-G z-Ig*>NYYTf-HbW*eHuUBJ$Y1z^n9P?75BZ+kdY^2=EVYC)j#LRUB5JgHRGLAl@MgG z#Nl^zaZipo_ovh;+cuTJiJ|e36u%$^`dZhmmbdGka|ZMX>XvT2w6f+W)Kx^V@UOn* zYEPyTl@Q&=1P5c-S7)B#H*M5G#=fZxz^>rFi(|3Qvd;J06klB9yr;}&fNyd`t?1zt z*AN<5FpZYb97#In2yNV#vlcz30s6A=*0xUvj;ST(Uy;Ui9}q?P^d*3q0|O*EIa|#) z#QP%%W5lc}vxqSzq#6_(0fHxhdS6$QX&6JT*tVhO;dD|W_j2^N1V!UQF8p^-;4^B6 zZ%Gm4CC2dDroFC;T^xg)!eZ6keK)Y*?!)3M4@*6drs+nqA| zqiuZJ5fu`IutC|s7=L^)Rd8rk3?T-R4-NnAE_A?*vqmF zPyUj3tOo(3vgf*!`^@)eF1!#RJdghhvBa{nE3CI$rv`P}Xt^zzU@S82rpB2Q+z1Gx zF`P`4f1f9@dQ??>ss&M?f{jaS13u~c2K{Sw%MiW}Thui@JZv|wHvp)h*)})7ZwA0+ zYQrxv4+%PB(kn1}k&Ry!d4Gljg4Sr_CzpiGNPN<#Ey-FZPiNIwHcy^_F$lriOX~T$ z6A=WFXT3fe7;ASqR>hWgbFkXV_143hi zUnMn}?uo@6*7(uRpOLpOnh>HtcKm(5!g=MuOqZi}n;20>4q~vQM`b~R)WfB47(sD~ z1DtLiLC-14N*_7>g=?KeKE`Z-9izT*{`pGqSwRpv*;hgF#V27P#lKsEy@ zLFI+a1{UlCGCWTmWSZ=x#&0om%@7bn@(GD z4Km;*8fCJ4GtzOz&`Znsa%V-$&*%CZVu1{>CV;xuRR|Pc`pkdlM$Hex2aZ@km9$EbeM0k$^4&^< z5GwSC)uXgJnCf_E@7Pya%UM>mj|D?~Q2lkL^QL15aD-yYn<-MZm)_isz5o~Fn>`J>i#IWNN9A%M9^eE0jLd+&hFWPc2M4hhV?1~3&%F1yKl2b}2eBxG4~d*SNOE z1GIl15DkS3-X0y;u=#zpWVdt{MYmNejTrIfIQ{nFcj-w6Rj56uTsk`kU*ZCthX#Xi zSv(V%?UiL&N;=n0R`UR{+ti>+tmnzPLBLJ#u8?3?vzOBHr&{c$@o$1gff@job#5f` zdF*sArpiPVNn~tK0GO2xb!N`Y*CJH4(R<6os#hLoPi<0S+4v_4Ip4C zMw2Xt2^f|h&MYF<=+Xt*D-^7_U7|?&$~rr0$9TeycQo9190wPIUl7_CWcKWOyE!TM zi?t-+7i4W=EQT6p!;NlM9sN`kPy=VnyXkd|wPzWfH5{PS|A_1Pr1|`+Xw?jEJV2p1 zB+dea7c*@A8q(4_!&5^p3c4G6R$jT~0Nn`n_b zGEv|a{p26xPJ%c}Nom`3XN?8s;-(~mzxnNpd8zvEc+i-{Z1QZTCqWF##AovE6GFQX z{9NHe2c2hyNt{hlzv2NGTV>j$4JoL@gElQuMjN{+(#MpGH6;>hZa5C$^Evs91%yM7j1suYs3i#R(`vnt(OKW%zR^l1@ZDRS$b;8#P6fnK)t&NoYR1ZtypCWt zV-S#T{3`;hVcgj}Cjy#AEAbGApK6uHmQ4}7{w|MPGy@SSak`TFE%yA~goL0g+kW=4 zb}238tSfi9c7`L9zo1qS8TMn8_SI{3PP-591}h$>{YrqH|5Y0nSkb*s@T0?m9vLaO z8XGlN0R6_?BZ4Oo;3(O7>QEts3QHH=8g8xO>5mMylDnD$uFlo}FBaf20~uye zSMl?9DZG0SZ+uNZau#3mh7hb}+FFj6ilKul2--kzQ6ItEh&j(tXIljE+5p9f)SN6` zOl6=}ZaL~*)$iYGdu8d3Sb&S6aPXbSd@M(k5ir|}hP-#kHou$SwbHfqfYSmJTU!=| zj4V)Bj`VnoIJ1C-y{03^;dWvkZXexnvoJWK6p_UKoV>&9JXJ&dym2xM;kL~gaPb6z zBybl|v3c)(!quextSD|204;RZ?&?8wm~oJU$kS%Gdihr6d~SY^8^*6Maj!AW=s^C= z^zo0!>8e-n6@0zwzR3--=3Y*!rIY z!;3wk0Jha0Y?+0y;WN9JEVTfd)u0*wXNdJU1qp`t>q0#)&VU2?uN3Ilguqxi)m~>K zLu1w<4I}0S&C1&dp#j)L@pdf-(67eGCe1!%Rncc;b6A;F#eb@gyH~cr8__+cTm~fbmhxRE|e28N&Ck5&zesBUX zcE9k!C55+L6yGXK3gP`eF)Y|O#f&Asm2vqR)y!-*_;Xb?5)6yKx%E;SoLoa-5$*1C zRrOSbCsNa2sG`yc;xG#JY}Ug|Xv83RaJ5{9^fHL92C*Bl?rX`SYRcyeR;1{0Nzd4x zE*0RA7X@yi?EH9d5@m)GCiK0`tb;$u0iQ2u?xu;@i!wweRLmj!Bi8jvzwq_Rx?bG0 z+;FN8^$b#95$;?{tZvWJ`EZAAdU?`59RPu|@@zer8If#=d9O~QcE{vuM;sMOWg152 zn`(uONh(?+vjpgxg8kjW-GmepOzgdM8%a$CL%R5qNIcv?uqID3#!NY5-Sr*)!&elm zj_8#}U&t#8mP6Nlk%e>R6A!OP<)6tr{K;F~A;R3;pUZH+?{(^=pp29r`043P>-!}P4JTa_&4?;U5uZ7nAWtKIEZ4H z2>mlrWK1XlgE0vSzs@-V{m?9i=}RTK{cG{j;a^>46BC0{?NODd*m5%p?00~)r4}|& zUN0!}ek|4Q`7vW6Vk5MwzH})7(LWqAW+8I?jQX_NSJ*8Jj_aOY7B^UfnfkoV=aB=U zd!$6JmHJa0AJ7`@rUd5=e&vutK|0xGF|36A(UlMKGJN<54mhcZ%=zF<5PH9n?FY}2 z?;&7@2~sZJ3JdzuyGoJkPIMmV`M68~GI%Q&KM+1%yFJepc-wWY)Rq4!uV~lHw^e#iW*f8Qm||>%K4qv1tucQGN9IC1;Zj>6!P^Y32<}JfiGJ17 z(DweZ^4xrhR9B)b{;coSN#90w?Rh79&znOf0)jDh_pR~ZC`~556yd3~6iqli$guA8p#I8youyx_Hs=Pk6AdU17KnaMd)eHrdGEPf>|% zI6r8}*|Wny$7qxsIq%X@Z-PIzO0lH=ieII`oof8Dv8%-U;0td5@PdbQjGY1GlQ^$I zk55C2OmRhYl@LQg;X{3#oqZLSLiyXVeh}h9LWl`Mc_hmk{k zr=S^iHyOaGTeck4<0W+W3t9M-e%RKfL)EK*nG7SGV)~QKb_xJDmJz9*SP_QMBsZ}xy2Tx4KKm+XTJGYk0AqNg{eFZ#TS%Xr zIk8=1HM^U3Lsl-7<}1Oc{q?uW4y#@-6Vu}qn5^=O4LI*C?30vfUj+vp_(SU<53Z*| zxUWTyCBfX0g|+^Ga$jm|{3Z-@el_I@rNiur3{gI_ zF6K`(!$X`z8{RHo_I}9q`8Q341f5Q_IwC@`Tl)n~`@pv*j~N%gUEhhOn3Py~CJ0IC z@Jn!1yFAnp%pw#Er1_2AnXARfnZdi`-+5K}UZvV{;PVK|o&`qFRULW1d(Br^0A)^! zGsnBjq=*cx8vPp*)C9TV(It!zi~bI^7%)|_XIY;)2&Wy9@+m)HJK735g-!5EBJyKi z#AsR6@%_^Z9VV<^W8A99Y;&8HcJFvQo(=G2y%-2kperMPYk42?M!%r=_Cf;JlP6$k zdVys^tK!G3#OJ&|ke|T^nLjdwu7%Ci|JWrJETsO}#a;6SyL>=QIPQWR>i+K|xY-s{ zY}LGQyz{eJht!Fe|C*hmSy5RLF-%PpMp2hmvn1)fW!c_R@jKUPMmi~@<}b~9n0u{- zG~f$JXcIbnr#uVH&*ATY6Mm_rU{p=XjK@DF$Vqd$fRaEc-AW2~&@rTECDiCvF;S?L zeLZoB3+J`Q87o{}FCjG}+f-@eS)e@VQj5!F%v_h-E;caf6|PTWmDgEC$C2!aQP5`9 z9M-akL6|bO-b;fHCsd=-ygDO31kk9;GPk51;PWUKfW%d#ctVCdBFUCPwN?&R9d z0a26wDtrr5G|7&oKoP z-PPg=lStumfJcXjw~#|QF+#(DOXj@d!5pXm^Kh|GdYSnAtHa)_Hw3Ul>m5FN0ZBWX zDl~mA>OhEyrG`@$|D_C?@}y((Q+aj6hg1-l-Ix0Gox55(VxqHaD;U~6Ey!q+1)6WxU{d72;Bbuj}Hw3c7}B)I39AYsp^H(is16}SC9 z6(CHP>NQY)b1ijVfty83zJ+oe;XOr4V$XL)-FecvfPns*kDH70=d=)6*t|?u*w879 zO26kSRn`%GlBtH7iOTTuc?#4}(_&`XG~48`{rg~j_t3kngoi_xU&X}zZbyU6*j@?v z!Ug^Dh^fAZ3w36cB8SAll`{T3{x9xu%+j~QuX%-iqqFS80TXqwQgw z7E*Vw3?_CA3!lo)rV}lj)Z}C~EcpVo{-WW7`8S_7=Yl=I zf2~vb)L5AApP1>KE1nv&%wAoQmh@EZJiMdlBgfaRb*Nz6CZQHJ+Pb+(OGw()88Q_` zz4FlAtu8W-UkX%vYI$K`arauD`BUh9(_&liej$dqle(%UQ660u^c5Q}|3EFr;vk{G%Rx~JwjN*)GPMWyt%){FFrJLXX_lp$6GCK zuu8zTSIYQcXrMrOPE^kE^V%qWnwNK;4FfxrLyr|cfec~yv%-WvP1{&(`XT)3aMz9Y zntL|gE^>HFR|5*}0++a51g9E>Ke_pd7-605Pn$PH(;KU+YI-)bIjK-k&m(j5MXhSe zre~_uQ6a6>*6AkQ$(+#g>4ao`8Y>v$=ZI!*wfbf^5bIVnl~Jd}*hvPNC#;EyvUzF5 zHloBAV};5eAFu0r?q&ZB?=~^9sw$+KYFnKc)~|i6WQ(jleF^jGC8$L1;I zGRNL3#GJ>z{z}!QRw7`H6#FR=WPjN~G;{XqT=~(8LTHe@4+`w{pYj2POT->JUi*Xs z86ODIcL`{2H+F=!VPNTEzJT^f=31(?R|lv{K74`0ua}gND5h%OO<+llrWukDeaiTi zHK_85rby;i!tn^tA#*bh0~BymGS3U}DJVJmM|@g=ihs7jA4r$LrkDQYZItxGI`-3x zYi}(R%H!O_Y4LC-&DPWq^_z5c2$p2P!@>J6B?B9QSW{+G(Z{)F#3^6O7E~TPbTVh= zHKjvDgD&s%DIJu3?TyPbG&b+VRZ~-i&EJo+4{qQ5t;~3oqE+j9D}}w5pV?Jw*tsH@ z61=1|u$^?Ca&m5!Z+t6p6kA%RHoM5{R6cmMABmqibD=4XNvFx_^ufW)Uk$#ytL^`% zH9NjI-NYew;(3Hlw&Vee$=4SlXI<{rR~ox+Vq$b}T>cI^=%W3>oiv*FHfn%i?QYp^ zX0f-IOW!{HO{vpyZrXjGnz9*wA8y50wfuxWc}0_v7k$twI()N#el7CsZ+KOeXBlPf zNz;yZeI`l&#l2|q26NS2Bd<^gcZTz5V|FZ7|Mtv~PFsUKTZ1*ZuA87if4TR{2ip(> zUn0vHOHKCi$(6FfPklZ(FV;?K2B&WMdNllkhgaBRZdcL|Pd&Z9cpVHBxef%sO!-Ygs*rkB4T_b9@(?^>@j=LzeQw`aO5U`2hpb#koUX z+UE|m?FQgKk9vP!E@(1lC32RpuM*w34H@X> zviP<%Xi8=N%k5+zyu7CNY-~5&3<;0t2n}G<*_jEN?$rP8CZ;EZyf= zYF|yw)}4wM%~zBdHKZILxj)9S{U|@#ou_9SIa5F!C_3>@Wbqwj(_W_1(thcjswFr0 z)^5Wa3}06~zakxHL7GAC`n_j6C5P=)dU#pQHg4H%>S+tf2;et#aPeCF7x2$#NtL^^ zJy2?3%f4&*dSn&Tm8^O@5A}o#k7u#cxOc=BB5}i4-`Ux@e*6G?pm|uT_&N}Y23bQ- zPW-d8HhWjzO*nt@842*t9{fhr`0VB0q}S~wJ>+V$WKT`|y>FUst~t*V++fm@AP{}} z2kLq}vz+gfir%OVe2r_74+A|#R9TuOy4XTR#*}(l$Ew?l^BqIXmqnc$NAWGWoCr56 z4Di&RYceKecWS5TVV*)90wb@XraQKo9II^aymQ*SY8<*qx<@1a7k*ml4^V)9Ge}U? zbSU0%z^`@TVL`|ui-Uy#+m|E$MdK0+0hhtu0=WSaGVVkbzjjy5i*5+eBoXHzRYqVZ z{>iTA#Fmn62Zvf55%A3}*L-`FxAo=oVt<7OrCx*h+bK1L2Re5num5g9>KQ`n*)z)<+Wk0x4ylM0A5pj7lRhflb9=a`3VI5(2Rh-e zovlx0ka}+I;N^dWg)xBIoc`4YbB;v&i_xeL4$C5~ZrFkP`6wmC&F7I%ncwSP56HeP z(<%K|RH4HDJA9kNxb~%^=Z|VaVc*)R2y-8&0TmV$U?!Qxh_8)`0uU7q5cL2E5Esb* z?(%=0@V~nJFJJjzR#8CK|K$At^8{O;ENj4ks0?4ve|cXYtCUcDv#eoX7XC|9csZICGP1Yeg%XC5PyAn+ zsrl|JPi|gR3T%M*f9v9nWm{JcP(U1PgI76l$H!R-Bn*If8__m=0e-0l7y%5d-M>NX z9U}D<9>TYq^Zbr_O+O*B4`XwJPtI)e=C+Q81R+*VD6ld|I9nXtF}J?kbeqA92U+pI zn~->Tq*fdr-#Ef&=~4a?=BI-t{QtU04Kn>(xKOntDbf}(?7zKs2|lo72hX{$HA1Pv5=>eh) z_*kGH8!W)R;y`#SphNxdj$m>g;ItqQO#aKx!(j;Gv>g7N5z>NyW&zjQ!(IX?Ihgs! z2s8ihc}S9=0hpnzmBu0=;tDbHkZb`8NS){`Ktvj3ZG{#<%+6~d|5HcZ3tcU)_^`PB z|37I!26A2W|4FwhkMQn#yW{^4p|*?xhA)7f%q0LPh|Um#@?QiXuy@}D5kH_{3>lGe zda#2iEh3UH5UCNrp@`tdM|3{m|6g>EA;SQG(f*f%3q(?oj}F_W1cmO^?poCyuCOSF zugY&(>X}TwvFPq>S8*Xy&M+hT-{6s3txw?6Z|X8>J@jSy;c>ptO-bhqSju)KDF+|# zQrMhoi_DGR;wuZiB!S|?z==CD)W;JcvoJG*0QF{EcfUQnpK)p?<5qZYdR{jx_T~lT zP#0g7>eTO6fr*wo8Dr~F;z?tcZTrIqCaiq)H4dbn&CRso;aAlsZK$x|vf!4>QE4M1 z@osc+qutS$Y^U$~?ZiLlJ-89z>#55`wqL&4ck|%^$&MvQNQXY!mD%Kv9n^UT$6rwq zh40xwhT7eC2i}r}h3Gy(`hxmeQfURFp5>vOW#;c~e@+Qf4<9~2H8OhHd5i{QZ|vM| z)%@%kD}+&FHo`5OFXQC#WL|8QADqElqGmX=Oi&s{UFD;<89 ze@45zrNrKbDH2MZ_T}lNVZY2(V}LoyfSb)Ux8t2_3out1-vz(Y3=F(Dnht^cUO&PS z+oUdgLwLdLACwjSg z{6`Jt?YubKwgDg|9u!It!cNOsu}%=aA-$KD)gJ-;g>Rkq^KQreI_YU@z1ff?K)(rD z3=wxb5x|7*W!+0c%42tkkqB5?D|B-0Gff|LAy{ONVKdPkCNZl-cyK95`n@a)G*f(d z?kXZvcT-2G%H48-oakcW6>o?$8m!Pg_Q6k8N0hem=ibIqfIIkiPN+2C``wND3KC$BbiAnS-g7wbauMR^`YADBrge) zF#D==eQBt{t8Z-NendMv7yQr59ME7VA%YYq6d#|z9>IJM(F@VS%Vz;g5h|Z0Ttf|( z@9~gt7$G9?d6cyg!w5V@G7kT)ll2`V@x2fgG^jg?Ww}nOW*DWSK8o^Hmw0`essHX~ z{2*AP5u;~cJ4xI-hv8yyp}SV6bdw%R*Y0#vb!R1*^`@}75D0+e2Hb=LgzL1@M!%nj6Ko6R95$UO{3do^F2a| z4Na!kQ!xKciT%dg-)!s9nU}0Ul(U2lNV6itn$JKu^-6!z!AGi_*!)1Ve)sL=kIL6{ z%2%I#U_ovzxIrQh3LtDGdLF5bzbLTp>ytdv?Pc9b+k4m3tB!rnBO;jizcQc6--ql} zHk5##QCu^m#o(yQ=~8le%=*#m-c>xI)-?aV5(RdmsgmOGhZXjF3KiOUdnRU#7VH%I z{o9mzEwXYFY23PNEHAF9`RmN$@n_N1A{vDk5p6e6hFI1vB8uEQMPRBk3hIeUoFofw z>+uc${=n&pdi|Uza2*2^*!~;r-;0C%)Z_lM_ptcBFv;$S4|9_2Q%UsC1e0H-5H3cO z&HHDfnsx*)V~7$>KHiI9^6I>|Xo-FAikmlsnw5Ujf5}i%-G_Es-t-{d^L zTnBh9q4f8GOJA;&moi>6baM%wudWRC>EqrkqLmNV3OsN?*0Rn=$fP3_?`0V9szho{ za6<)EzeRjIXE6!o4!hPvL&W&1q}TP9<75q@eyU!`y?AAJ>ujAxYIc}7n=;y@W44Z;kUH)|<~nUG{Yg%0CiTZK$lUz~JzQ8-0(lPmc2BFzX+3h-4U+(GKrG4M%bhI8S)UMDjT#A;bx{z z5X1fpzn6%y`wH#-_?GXHG1dtJ{Z4=exXs@8*oB$AOJ{HCUyp-s-uI9VyhSqaUNnoK zvzfeTlj?l`jRyjilm5d3&f-(!TsQbfJG*+_bCoaf$}(jm>TGYLBCPLFa7Igs4Gd4_rJPVkC&-zqC%Ix zDn^?+Pf+8sPN8=ARvz~g#K>@Usp`Tk@o$Ke9h>`d_a8a$FRKGFZ%CHy( zgB#2dyO}h}IXfntjTx2bf}M)Blpx!A4vhITf0}+)7x~1WcE7c$u2)AdS~A;UJn6?r zJ=BNvPjrj7RnM@nPESQRFQg&Z*io1Lw-PuKBb@0-gY_%MMc-eGMyK3_=25gh-O=h8 z>md$osHVsGB$8h9FC?(@Vq#eB7f{DG3+mwQL#ELEj6=X=%?yxZKb+ z+Iv$+uD{4gbp_@3P!uzj7ko&pKAYHdgfK}$0<6!;IGqx6#L4xJ3#l9X{)?}0eXg6jr;_808EM%j7hhIh+ zQmEI3LA;wZepuq+TcIr3dfY2J_NCDw4Tvnif$@#Q1VYk~J;5Sn?-SyRN> z!MY|&OU`@u{;3|-Clz{F8kb#(bL2Jso%tU`YPhR)h zu@c$M6oI? z9V>;PX9oWmKg^}e9rJ_*i7!#M=pV_68DXHYe=rlc;d~T37t%RzPU3;2Q)>0L`jMq$ zPx4YiT;GP9Qlx6JylIh=q)KLCb$Ob8leVRC@R@|6@0r@=daN~awuqkeTjXRpwg~!w z)>Mwz>(|e(#nV7u2Qr_*>p10@BGQ>N)K;6P+EFc7#MKiqcrh3@_cFH!&%?GY%-f7* za@XCMKPzq7-oGH}2v zLB^%uQI73&?o>(ve$UCG_OQy zlpX*qd&Ii5V+*%3yxt76jPn6uX=fJ$hR+uv0I5rg*-8o8nuG9>fXu<+o^AiYL2WV3 zA8mYe`cW;DaL^Mj)pZdw8?7CB&Ek7@vvYEu(M3>B9QN`h@88-b+(&0JXie#a^r1b} zO$y#MkOJSo@&DiGg#6znTQ(<(w~w$U3@TSTh-r&Enl4Q^R<+I>B0sG#nY z0@Rs(Tkj+V1yl&55H%9m)2gFriZ_S9WIy&rgxcPD0c0F-y?KxE;0ebC*_Z&QmU*#I zUYLX~*ZwV|F?Nn=@hiVCr@{7qBnUVR0on2Pq8A4CtuR=kx)9F+N>uRb;9M-=+(KiH zkBfc6Ii|H1M0htd7I`s~Yx*-D^oV(E!6m|CnhOIUfC>^y{Bf^1;&}b=UKK!F=f`n8 z7SX_kH5-lStz2r#=Yo+wtLBIkMh}u6tc}~J4x zi3|?~^TTz$N6&UH78)*qMjN2VJ)C<1k@dDVY*Dr&)i4^A`EXkGT7mr{F)LRPXK>wl{w!#o)-al^u$Ta2Oa*raA6qz}nuNRsoAU{>bX z@xZKQUJvAlpz!c$;ERmZ$<`+S17>wGL8}Oior~o@nR2q-c%^-pP>v0IY2_H1CU#IO z4mS5l#m8F&S%IwobG%OA{k+xgM;tak4G2hlckqjizpN~av@6Z&3HgGZ)=({^$LWx} zh$}|B3>ysO3LyAz!dgkM+?ePEA#Dbm=7PVQT`%0v|NPs7kwD0t(>E=f;G^$QW3F{T z1TQDhxf-Z=+#QOU8pFSuuB?3yK#qZ|fnET@?24?15C!bYj7GaRwXp3!zUO(t`Vt+x z(A6%D^a8P&=Mi6tzWa%qoct5ogGU(>)HtxFIB?AXLXU(BdN37t>bnZI!x6MH2|WN+ zG;|?FY+aO(1|i?A#Pl@DtiPQ^2K#aR%?K2>r;wx3P|~st-Cfg$2-*EJ?8iRi-WQL2 zQ5BoV*9fNtv_B@#(j>LUs_*4dp|mcqHA@qWJifVdw~$}(&Qd73Z4{oNG=WkYNB0Yo zMHO62wvROJ1Z%B^fG?f-H+xj`*is8-%g7h%pM$J?Rfi~PQdOOHq!!lHsM5#tdb@RddYkXj{4Al^>_bAaPNCgoJUB`KddI-Y+Ip(OtwY405s)enV> zW`>S51*BI|kgn3}paOzYl_tISD!t861f`4gj)?SLrH&#+0RfRBbr9*jcjg`a-MiLX z>%F_yefQ;$nMqDgc6PE)GAG&jzHicS^wqnP*b`+^lna_51Z=OVXVz*mn5^)O>^FA0U{*C&YQ>T)>Zq@wczt+2aFoz92`Qd|KcS0RSR-*}GUBEyyG>nv)(X zy}x*?9q((eN&|H#v3?^NIHC;!Dh+zyaxg}G&bTK=c5#;^b4d_@LcDknNNYUvOcb&yRgZ2g!4z$Fjx2$;E48-ceYM=o|g&K7+ z9OzL)T3G~gc*Cn3Q~_p8Knjr)qX>>k0qg5*fd-hFS&p=qmu#lrRoSM@8y8zF09}D| zaB;@*f&lH=IESmdG0uajbeETMdS`6|b?}Zs>dz3~8Bd>x#km3#f3Z-dzffgf!wJ}8ZYj6!OZ*&q& zu69jT>QF@u18+M2;+fzuwJAy`=r|G+t%IZt!q1O z7svNU^)iC9w@3F{iV|Zas8GZJU-=qC#z!VUJM}MDFMmdc_)yHRL`+t0--f27yLzhT zp}@1nG9B>hR(h*(q7`{Cl=bi=dR3GmMAzjRw>dUUB(Vf>{rt62N%9SET}Rdzu=3~^ z*N}2t%iSU}1RCBB6-Xr*>-SF9oxN)n!xC+#FBsa3DbL@_qbj%n#QMebx8GRx#yj{S zo|sY=kfx0e!Qum}{Nj_9r_P3S*Z16?i|=^CQJXaV?C2bR__JG&y~g?BjI4mGjR2K0 zJbhM~RSkF)JEiF*d*PY+&%E z>L*!<(|vl)6ww{28i=&ZN}%EC|}8SaOj#X;Y~V3B}zKs)TvK~l)*qe4|-`49|*Brd)ncSPAC88EE# zi&G5%IHJ6z)X0H%G0%0Y{YN}X2CPn8auso{FH4a5fTU~t@Y$nvT;T+zBzFJtPlFbE z--Gc1`jN^0vVpYhD@Y>}F!Kn8o)Gn1XZy;izingjP=KS-+!8lbro$}g(cwZ-h|3K< z%J7zCfS6+0$;Uh=_|53vr1R?&V^%no?`P6r_JPL(3~Nv?HMX*(o$oBOMKLTZ2x1+X>kDDcK?H4S$0^+5qioRw*!fBxHkBTE0G+0ol+e{ZURwP$#(ad@3 zIQsS68;5PrSiz#3zJV#N8MyuS(6h1=AR)cHyDY#RT z8wh#qgkMl|c%^iSQk94teCP`HYhM_YL^ z0ViU;*-v(`qKghh$Fp0isAVN3}VE zS8Z2!FGNdIA0{v%O8c>^=abK~E}jEbKGbhw4h><{tN|>J^E|7sC%Cg3uR8TqGl?{%*G2l`m8o2K z?1njr&wZ=2Qjnca7b%(%M^1;Un{H%JqqBs>1{gKonf++A_5hCJ#TVKjfz#oR(VtjF zWlA#~6pU0FlUZ08{`9W8j!vB=yi!hCn0)JAk9vv_PK<%6`T0 zAPq_LW0?Q+j|B3$X+Hc7%eQYO)#ZdW)nV-w~$@|s*{?YP5j;{%h(*+rOs zY^e?$Wm#gSpD}yV=Z{KdYpA6pb1ci^%6N zv6?QUR$mRl2jGBnMZ|f8V56k0+3D;#0V+r11w(b4OWBL`J|+@qYs`fjNF$6!EBP|) ztG{dwn?1@Y0)ISs2YC+qljn~tI%O`Z_x(=o0_=hMw1td#%xgwpt1NGEKFo0TU2Efs zxUG^aRDsWp4O|+CQO~aTF@^Gc9N7_5;}l8q@XL>O)1ME|2x&PH`f3<4#slsmhvq~2 za=91`maSGOcW;ZcEqx_AK>_uZx^FR@Sp5E}6UTzZ)U9CG70BW&V_TkEKhF%y^lcs1 zX#HrL#Cwlw@z02AO0UXU-iT49=-Gng#WT523UW(gp3xCRH}$>ddV3>$c*-Pz z8{>e72!kCKytEg6R6x6oH*J-2Wv$vrF%!R)R=tew+vT$x5>5@B<|sf`1CQ$qK8h2a zu1@%q96OPE2hbE-zy0KwT7aj*lG3g$8sEEGMvW3nHAkyoAU%BprH4^=J<0d40p_H! zvtRN-&{H}1;+%st;CS!0kCa-2ar^V0H7$aXX6EZE%}H;!een>p+TM*yGo{wNF*>R} z$uzM%=lU>oHBV>irRb_i#fRuZ>oz|RrZi~+;1N*Tgnpi4-%#?@gDpd9-$~22E$?*X zaKlmmAV%0)_qlDX;cl~2xo=5%7sFjslwsP#5G!3rod*pJWvzN@WlJBQ9(a2AuxPE1 z!BK|v3Wg0&M;vW^n>dZ7_49Y$>8%b553uO)`Y{&nzZuqldGNt@6#@vRcHCSQHD=)R zyUP7s^}$Wnx`VOG;nJ^zk#8i}hCTHI+!|O^XyJqy1P}NsCGMtkxuP-4JFx%t1KcF3 zH937c)rfC;dFcG1SJS|Y3lE3IuCG&F1J4+;k{0(8FOfTY-S2OvPN0uYCgze;?{Dq+ z?FwbfSUF6vr%SDMqN+(%A{c&{_M-7ytC%O8(+*QE3O|T{urO&;r5udyQ(d~1eD3XY zU&i82j(qBr>lIVk@xwl!B5IFT%tD7-N6$*ShF|N@bk3AAetBZ>CW3jeMwzl;?O4fr zbZ;qDZsnGbaDdbbm+BDwE92ST6SD9=czE7~xJL)ww=wJUQG$DlTDGWz!iWsq* za{l73u8a}CTkQ{)e#@>&CLJy|LuUWFH8UzwRGOqfXDUF|tHG_XbKq^5 zRE0x^)VGGJh4k2u=e~8hI}_GMj9f;w>*CVd8R8Bt6QS#>^T(Z!+xMw3&2UtmIfzSi z79An5P}1M4HoV=hydo5sek2d^|OLOD|Z^D8OPfq4hUi@Pu4qHYj^dAE)F&1i?qK7b-dpW&bl zn8~1p53yh{cm-dpVTv6pfEsmlW_o=g7b_bK)ag%Ef3EKQqyy@(@m|cyz@2Fk&tZ~=6H4!}%Ak2p|ISFyMMMaBREE|$;mpXBu~&-q|+Am;ScI1KC-zyU7G z5RU$(OoXCG4;fx!Whb%7JS+iJhwnF0mhc=x7ykw96YK!gB&?u?4OlqAQ*aUZVlm&1 zjV~(>A{gMR|w-pOQQRXY>`Da*9=e;o)q}S0+l;2LY1p1A^X>TXCwR0MJ zj{q%WSbl*rAsh^5`UUi!ObGnSaCApoi|{cBJ-3B{!M}OdhZpisWA6o``E?+8eh{o ztZ#^z&|iu3W74myY#hL!$e&TCqv08yQJpKb*s|2|wAG&5B7?MLIIEbk5aQk}!8TaG z)wXwF*%rusC$r^(Bz-o0;7x_AAxOVjR8iI!miN^`!1gmgss6poO>o{ZGg7Bqy*e1~ zlXN98Nc|PLo!;|zGC%*aw+$7~w3Zyn^i2&5CP!ZhyI1fbN-53i=A93mR$$u_hK{UD z@~T>UqeC}=bq0tLBb~Wf?Yx|HqLLbLl^z+{)A5*QM)PfMr;h1QDZG@w0fyGWLwUSj zyCfHvHg6wb^{S&{S|8sVUz3zm{{3Y&L?E0xpmf#P5dYDyvK}>apt==ppND*E^ zEV7}$K!F4pi3bZ+jc@UQh=lr!%OIhzCG~S9#FAS5XXnXlLkWqPcV#LUxrVcTY9A6t zDf|gHR5Gdh<@siZ^D7AoEN|?ki{60(4r&aJ@O*yk{5rMPpnq#AjzKV{mm$E?NVE8b z8@6|Jciqf@As1;g<^@ADubWxU5XzE$pOKqLNPTY}Fl-xqHFA6Md{O1elP7%u^YU8I z=l!G3HSD_9`1V<!&GG+M(oE+v(OJyh7e8;Gi!cmX zd6YKn*&DeTIvQMWy_{wSbw241d=Z+9L*+<)pdS7cUE#dpz@cKNKkFVFTwI(d{9kMB zgy4Xl8WzV5HDrNq#v;n^W!ho6zsJw_*>1WLGi|HCq6o^$mX2?o?SE_iJ)Q3wFZUeN zP&-+1+l~~5ekwaL-qw2(cj9>UV&3xfe7mMPdE9iJ=G`d_CA7-Z{GCb0-8;qS>HG?FRi-yRo(|P5~b)tleXT9U9!`*@Iy<~6@YYv90IjzZ;22IDzKZWlkFILbH zD7#E($KF32P!QZcr|^0{c99fLASPdQMoAXMlAFlW!` zO<3nk3|{Km&&c-FJWZ@P7O;J7*3RriI%}k(_o6`1?ER?HPr$V#=|@)Gu2KFf*uz#X zSg1LQ>THZsWh}+u;95$LF+FSRsd{+EhFC*c&OS{`w19QOj3|>SffPrxcp|Ber0lhK z1bo^n{hqR^)>GrOmd@&plbx|I2waATW~C4L87+*1vs7lA@>YyK8F9+J62&#*Dp0UT zY~r&`^AFJsaxx2$_wTt@z^GD_Y7ZSvoG)*G`Bu=|6+W5i{Vj+hWM_K*;pa8Aqnmr$ zYjgm5OrD%$fJyJQyU*_i2N_DN5NSSq`~B9#C-rIbW)3r-PslYMkm+}IOdo}lU1fgz zcS!#03vj0^^r<6CRzx64z5{!U^U2b*bm{fGzTSWSD2+v}7u3G`;q(*~+97oFp|zsO zjm9D~ULamqZax`Gv%37)=4myRXG`2eX1b3O6LrNrei^ET7kbLt$8PoxfBYV4{Q3UH zt2s=o@j-hb8qp-CbuG7O*xIAzr%Lw?{EV7=@5e`+F^9f(RCwO8Z^9{bO_&w0C|>#f zV~Y626`$ozp>K@X=ach@cYnB>mYN%%e%Lo?!uK?BW>ISv>t zZjBlL^`^{|7GTCDO(+?krN=ER+S7estJns#_$>A`^{VrHjj962F0PrjJKEWsROXTz zyVsbnc#y~+_#S>7%(~*73<8-_vaWr6cx0oc_(V%JlUk^167I zy2Jiwk_pk~Y7uo|Efcv0KTjb~{~b8_m}L6w`nzA9`OYwZV>C7E#WgC&<#1e!Zl*c9 zb;1ufntwQ)RbK@;lgU-wWzk$1tkCtGiq(?`Fk%bIi^`=uF1M?#RlbGs>WAVrDZBk% zlL|c_rpdDLdb&3l9a!P`V2*?U!T*SCl^C3pfWPC|-$(O-@!BI_>JLrVkh27Cnmr03 zw#}bvE`|8p?!~{1jx(~t`D*i%r|lcr4aX<%e21kta?6buyNhbSp9P)=n!H7NR8ruk zfir0V1C2BSX6lVVTXk3Yo%_D0v||1}gcc-bZt|Y)KbD4K;yy2ZlfFq?@-W=a5+?U# zAVup_4!Ru1x9k_6HCmy>8VNUzo{M6HTp^OtcqmmWpc!YbbjtM+YnR3R{M?sZy4+YI%jM*V~RLi?XJYpew%sWDOB`k7PYD?Spe-**jZ;~a@CtZ8{G2_dEq1i z$Hb{|!{5kLwQ(^P+54@at}$|$xU%I*D&QT#(O>7yPH1r+MhQz@#WPH;$HD8O z$_osYYgz}J4}AM!sW<`3r!KT1IG^en{*vl||8yB8>TNpnnG%H^e;l3gb_3{Ffb za@wBd@RA?6eYnzl`*6QVe$GyluoZG{m1P!`IP;C#ooZHeO}aq-YK}MTcBS?1(W6Be zfjpcJ0{b2OUIHw(f;2M0K|C{+obb!19G2a8x|)Oi?0UTX_Y-aqb`;Ka4v9Isn9Uaa zO4f;M^+JB>{cR9;eqp=I`SewID_h-6XeBr1&bJC(YzjGbYp<0hMaeaaey$-~yU=MZ#Z#M5$+u9Qr zZlHOrl-QM(n#AF#gS;`=vzK@$+3q9zmNaZRW}Ok#XNF) zz6!rsCyFhD`aehR+%`a*B=7sw76>e}phYA3rjyJdoA{H9IN4&lr zK|tKBqFfZ5DyGFB*u?4)W2|FrN(s0?0#V~Is-bzN{&S}n75#l-h0G7L zWr*#1e)&)TIm`dW{eg#2cuNvsm{H?7+eP(}Zvvgfw)zipgfikdUH0zl5^~~jp@+z~ zxfLFR)hb@2vow%eAbpGX=WVx+{tvDpd$d=CN9e^8w$NMV_Vq-GDQ&M!FG8!oinRVD zbEo+VPjvA0xd}TXxSr@0TT(t=xVF z&f;FlWWRS`+@v^%AyoUNw>$AvRZ4RH9cA|omG;^@(dfF%$5)GHa9#kH5Oaof%0E(m zhRz$bFmw((R&yrEHz?0J>K^Is28|H?9#Nul>sakqk15HV%`svAd4Cmusu6JHn;-pjEc$Q`#CKUrgRcVLZsYR}u! zGV_{sGSCrT$a2qC%#osGVSVTQVzgUeHu&CJw z2#I?Bvr;oz0#NSMMsw5?`yByw+p{p3D9fL_RttG`y!pgtr7iVn zlH2bTmC{~1MFlnTX$}m$yf1z}ii@Ut>BsG;&6nH-8`@bD%pI@-tT(xs3xih9d0)7n z?LPJ5Zdr0|e)B+P4mG>X<)W5*FmF(id^6eksqg)lC;iG&U5H{YyjUbL`+ye7D#1~ZXhwV0LzrihRQaw(-TAf=MXEIO^BuB)YxXuI(TJWC@rWaVyv4y|(Mg`E;Qe ztNS51C(PRDfxl1x!?S!kO}L3;IT3;FQIF7^px>OlO+UwomC}z=L9<&Ax9#$_mhS%RZKk%@SWN?bpoB}fNpdl z$hkFp(>0x-q%k8RJ3+sCkpKG3fpOK1R$eF3B{pio~GlbvS>#p6P z-rM_|y=%A4n{6Yy?)}Oo1V4&o2DI3GT<$Gsoo)6j6tu)D4Ti}$kMHFn`8(<&MRqeu zUf*q_HoW$h;VlXky1T)t@bx4aWAMR9ewpfw5WM#=X{vnO>euvyLZCM4s(;`0!rIf{x=Du2EmqX11nr&@wgYCXH)2+V3p!lX z9#TrjuQm8DEpPq;woI?+z#KgXHOhhE(O#|M*wt6_MUN!ZxLhbrQgPM)Dj`b((2A6~ z$ja&KF*sWWG6T`Cn|A97^o>4@glvV~6%w+oPLXn3-xoQmX=hx>)4x1iaNZPq#Cg^^)l-AQ=ZdV%hy*04X;JH@$dE}R&pg-mGBsvb0D%`6Mh-!%gP%S zAwGrJ&_!-P>rgp-<@M!9kRpM{pQ>r*``^aRR$%|lQTPA@YP;Y6tbNXW2Zv67*M}-B zYk7hk9}ke34&h<8wd=^sk>=XGoD0@&ZOWM?9xuqi1XKT31i+K}@nd}DS%j_qG>y zuRiWMM>$#=-xL2bSzYeGsSAQ8#QrqjowE8$-a-d*49PTICl}?Yx6YS!M3E zL!hZ-2wBPFXKr#^KP^Rhop+^NbS8_r_7A1mh`nb%ouY?Ivb=Ly$S!NK&?zAKhk+%4 zBmT1}U5-@nh;Y}2aG<0}-+AN(erYduk?1omUQ17$IdlKxbZPuA(0VP+>|zSUfgWi!{3G3Cv`za^AbJgshhijk8Uqg5`IE`8Qp$p`u%&(tpW2_ z*6ZhV-|i`&=zqK_dzC7s8$HlfiYdE+v20zELp9RgTQ6Di8Th1sNuCMlUw}=S&cD+& z&tW7mnc%u+oL28hGQ}P-lsIwSO(gn-mBIB8I+<&hZWB|re7#fq(l5RU_IzNmZM9b@ z?0s~Lr2{4R%Ij7f=MUZcf&Eu@Y;_;0p6C;y4s4v;{G);NV)4B>Nxq*g zOD+aK$*!-Xz6*1b>$7ArD~QDWQG5y=lH(0`E&X}VsGt>wP=Nbq*$a9lPeh5|KhwMR(@(uBi$xo$6@Avxbb6Bh z@O~&I_JDbL`-Uuopv@reyXWq=!v=pGNryogGp7i1`VcI`w%lrKKA*EzE}-JkE*;;3 zgVeO64Ce7ouwj_$XzTU#ZFU_BLn+Djcb)TqRM z*pmQ+?7m?+d~lp+vb^N0sHQhUTr? zQoG89bt90!8>SWG3G8LgTF=WMMRlu;@j;WMRA)F<=9%No~N-rZFlbxcb zIV$n^+5C6o~yR8Y@}GK0TlWV7=V8lP^XHmzV+d)T|CD~rLUoIOO{Of z!is-YYI7TZ&s!nqg1vqgzBT9SN4>M6=_6$VoDke_wlQNiE0c7lP!H zGw0iu^YLX1D``p|7sjr;=Wn+UD&|Ml${@cV{aFW&i1hwSrb9_+B?X|vxfhi`jnxRgk8iq}dRy!pMp)z@KM z?|&@|f<+U^`?1(^9h>E#sxrk}KcYH(=yGuR$%U+!r^?Q3JU~o#B+*J72yP6N!oc=d z9CY<9UVHUg|A-a?2~n#3<3zvR)#_ULp{@3)-_^y=TJ_aG*0slV8V=tKS$%A`F>KMX z|1Cu0Ba%rLHt9_MB;Ib<4qvY8FX~~BrcUQoR z#aGi)fn?@7w|{&>g}-jUl&-M#8pY*p>}A1m$??1obOZ-ukBd@gYv6G?`7tFF*vhqa zIn>Y=6?oR!e!eXhPVFm^1ik3l(b8>g#n7MoJ^mblbFs5`1-uibznH+zo|gM7AnV5q zR+Lh*bOM_R^X?5?xzTdQ>(IVCcN9*c9hg;L{gMuHZDRxL*P~8GBQ0fP7vD>N4>OIJ zagg#&k%^j!JYq8QzU8)O^nfAG<%+4 zE+2>=?TYltjyFq)hF^@jAA4$%JF}$JO98^|n>dL0vD3P_0v=eud!qqPj$ol>&57UE zyFBl#Xpz`)tSw2Z_3%WS{rdOrJdfV9y3=)(*FH5Y2b-0D(GfwKH*_gY0x|-@s}i5q zLa*#i`AUuIv^>$mk?<3XY?)2FEVcO96-wj2>Q62M#BI~Nu70kM9;ekzaHY3b1Z~t7 zV_7gfhd_wtgdmuT6gO@_|An8XR7xVgex za-iQJe}d#)ZHEL@;G-hDyJs%?H)@T0t@fJ|` z9Vu}u0dy4(Ldw=U#8v}K9SFRgbuS-y=BZ?j{Dw$FJ zJxl^zC77pXq-QT4L*s(>%IrRTimc1lJp#sR6veXHai=i{RC>`iHX>=iI~a1a^R z3PqM z_O?y;rf7xvZ{7dGQfI;7hI@HMc6}4f*Xv?liom57Q0R0VV@H>!*29t%7T^> z&K$QxM?XePx$xz&d{8VEh(F0WYSh9GBqD*g7jC107d+Gy)otV$pP>L={n&A@xintB ztblguw=aRZ_#_FAfx)>y?SL8w=sOkFKf+q`-vsMREknOb3BB*7WI{L9r1z7c3O0Q@ zEpbs7&%QUStwEj{qVsPFfoom84;)jpr*b3<*4K7-?eEB zu8+M0fB+x0dEhIKiOnMPPJjTf_0{0(270)JfXv;qE{kBWxmT49IB3I`14Rh{AbWPw zmP$A-`4R|Dl)?uOu9*NaF_7V*6A3ex8dYNh2BkQNJaz!kLHs<^{bxXx7wAGb6=Y-} zIojhwHX3ZVkmW=YfN=W^Z3vYC0G8|E+XxEEa_sgOkOlaE0wz$C|AUIiAwWPm{v{Ls zz4pKOKx2PNj?Fv_;vzvVhjE)wwvf8Dd&6*duF1y5aS?@Lv9X@`cJ^MAWge{~ZEC~O z&nrJnx@Gw+^+9p~0^0EU8*u*$_#6Efpa&lcf87h5;IEe~?lZbvebq!=MMPHhJ%7q(1~4GsW7b0Q_y)e4u*hIFwf_a`p-{Z}=zTR&?Sz{O6z~)(R;6>Kb^M6tGZE!pVRgOET0pOB8n4?W`3KrGiU^P5!T) z_Tk$<;JO>iSNPLlE&DJbovnJ9vQkpu-m0-0Zi3G(#9zgdFqz5pp+u-iYjAdW;1u+J z9@{O6>W6dutJG~+rSVf60=UZx3C8krVMQ8-QL6eSGZPM@;+Z?<|4&UhV3hnW7YFkj zzC96CBy@Q>U`h$R*umsoW2RZWf1Q1g7)>k*4CFvUGBxcMAwqkq6Bi+`XVL9Ui2K(V zYKVa+X!NE*w^O2cTUlVo75}4bMpHGQ{zic8qyYR`SagDr9IpSX*bpu_I3C5ts6GeT zlMomVR0b|!WCxuWKaqCe_FpTF11J_w5c@Yf0uIP1QHq5B%pvZdegBVS80tUsCW0=( z|5g=nK&|}0&iDwh{?F@>z`qa5--50|)8c;xJpyF%x4ztJPA6mYE;0SDoInH|v4$KD zybkoMn1b!dp{$Tyw{n7*cMM^R+QLp&^g$or20hrrX||;={ul}}mdboY)8hl3Vr9-! zp8i#C8|ND6WO_MdP-1{yR-Eh6YSX4o_8r*uy@iY6J!pRtDWuYyM*_KrB{2}9GYL@~ zyVZU3%7ow=>gmwWvrKP?%5+b`LxQ&z#HbRVY41`wKqk zfCzo3FF24kCcIaG5Vg|TRv?nuz&sQ=-&yMfH=u7V5(xo}xMiy)7Tw$eKVmuI-=yX^ zUTb4{Wj{oWc}N9;3`6knR8-K<>fd?2L@5lLmPHOiada1ykfa23uQy%<7^6}|_sp)BB<4PoM*?gj$zmF&& zM33PQ+6n9$zcTMl|K78cXFC#Edh!grN5t_}Tk#?gGr`$jyZkh^ZSzfVKLu!<$2BQZ zIS*?8fQLMF&jI7TtnZV*sao(JJqVFYml3OCB1GSTZ}X`2swg?%is$%2d@;dt-1G;j ze;4*>{LR2Y^K0N+ORihosR$QP?G__`4-|`-?JMjWZ}aoqhj5LoFc8G@TU*F?D7PobDAoe zB6znZ2>X(2k`6t1E`GRETO-LHrdF|hwZ%gv+0=iNeH*Bxr?FXoUgQ?^u6$PCmxBpy z?NeuzGv%aN@0%l$BN4=8JgYL?G}A64Z}dz}g9zBOm6AjKtOZ8h*ICX2Ba6_V)0t&e zMX#@E|9pzQ4qiL=RJ(*}OGC4dEGtZK=@NbSL*ypVkn&L3hAidV*()oC$IU< z2M}`Dn-4Nu{EGt&uwVDeM9EeJ3%|*UJ6Y|w*j#NG$9d3PUeU1=%tWC=;>8Ibhh)iD zWCkvi_VXN3rrmIC&wnFi*q@SV0O}7SfGa0RzpHjll>)bZxqXTVv@TvT2rhP=4HtQd!(v`u}Mj)FAB_7e7v~Xr>T+s~H3+94nqJM{fy!=uR7isle zAMqm^Uj1HC)pRt=DC^Enr4Ynraf%@Y3t+%b8wqYMUt z=duxt`@&+)&^*s@xD-~Bp$H%!Q;D{LdofzbRGN6Z`F3kq9EZ_wz1M2@R8QsGxoX-z z`MzZ}iYpR&Wuev2UP-z9VC3}k1RFOjE;Mvr{Ak5%xK{h_Hfcm;25mbKi?_K?P%C#C6 zpd$VVq}d4^w)#+c-6-3H+pNAVXYsj^_xnBrw=o6|0d}Mknc6Ltp9j;_VS}Yk1#1HD zdaBuPf$~LCi4Bj&%5ynu6P4ly3SCucR&oCz){_qHeE8j=!PS7M zgXZ4$mwF4t>pI;l@*D3xZ)0AnmBqPk_ z#Zd?C>CK%**WZcM<$gC;z3lcM3zks&X42#f#iL^bu0t@M=Yy?%Yo%~8Gun?q#$Sa+ zP^dc`cC8!z=>oHSz0x*VI1nO;31Fs=+3~Q6y4N9flV~wWOSANGh) z-+46p2N2BarLWqo#*A@;v+2#+$<~{&EKffNVi&2+7)&S400o){h%JE()e1W~EGq-y zXH>^b%2(X>j=&OAzPYt4#Na#gi%Y?1Ogt`E__}pk&=k~=Ow@n~gk8OPTdKDgH}HGb z2+7IK6eJge#Qu0U=V7(KO{s}0?>8UOeM|PGqutzc`)zEBx_TaI?N1Uu)xKP91wBtk z+&}O#vXEc+x7QjA(`%!drS2`knXNxDaB{r2u_FtAFX-2DOam%IbAm_<$4Pw_qicHC z_9*zlB3&AUMjt}M3S)IDVaAjBWQrpCDK%ZsnZ8c00pg>|*Mwa1L|&pM?KYeNn#3l^ zOI!ZQcfFftHJR(N5(pV)>U^<7PhL?y{Q`v(xG$(ba?**incQOK2?k5i@pLsj=Rw4jjii4R&K_~jrrbz+eCD?G~p z;zzt%HRkMIH&;nnrabD%s_B{$>L`*>NP^St8ps14mOG0uh&Z@awrzvSpDCCwloFgn zOGu$djtBU_hYw#Np*u5Qcc8@OUe zSGEs-nq~S{IO)DImeJ8EHWI|Y)hKQ-O|atyGpXOz zL2bIlvj|^W{PJ?U^SEe==$q<8gDHWM)5DO_wQB;0v4V6D<_V0cbXT@b{twU&Q1$Eb z*1j(UmeQr%9UJ=H<%YQmce_9M5&##LQqi9z--|AOylsb=nTMoCqc99HlmuFPxH!~+ z^Si6TPg9Dlsi?2uf#+1ZnV9HA zEz3l`e)XEYA6@-&b*vlVXSepz^F0YN+$X{zd7e!-Ow^{>vD zKw-*q^ zbAan~DVNShQ`m|#6Nb6g;oz46Fm1%>CpeEaZ@mRekpnQ`ar*P<(Hk$_2f9sM%p4|q zeC`f-uVN{wzL7ev{8o5iD%Ttfot`=P5+d0CRzsrXT+q_RovgiqLb#EQQo@BxTQeKq-j4gg1C^Z?u= zXwGWPRm!H1A14B!e+Gu;b_{honTOP9py(Ol|J3mFX@h6X zh#-1Uap#hy2O^ttA`idV``pN}*viZz$b_*o<^}~JR219U=fNOn6_D!jRL&aG!-`sg zDgeDiV6zyI$jcMn=DEirwiSV!4ufdQJ&a#EMn>8)>Y2o>&GYNN1i+i0l;>0(=N0pz+@#XfU{|8e=g1? z2_+o-#`Ahzk(6AD$ILCuAp@AnO&FRz&&sAIQSlW^%K|BPWH$+xvbwKtElkVh_ZJJn z&f3l$i?`ESut(JjihRaeyQyaVks=T&?loMLkjpejnRx#-_>MA5-$rPqqMh#+V9jgd zYEc8IO0uUw003fOv!3;N{+gL{=I*mk+O>VZM@^Z-F6mVBb@8yhIs=q>hV9RNdF}HW7h^gbJb7W@p_rChF?@Q{F`KgeqxhlP5z|}K7oTA- z7Df2e76Dl=&*~3b|4@b;lBS}`5 z-2^&6_!9%wOk8k@2O!GmbWu4N2nt4)OoC0b*WRA_C1WzPd3=hf({lXGepo#o7|(sA z_Is>UYq)=s_qX2zJ)eHIn zy6rte1B^l9`2~xrdaqo!ABJGir`!y2Zvx86V$M9%%;G3?oJ3hiD2@; zfW4gc!FFfZ@hJm*eE4|iwP`{(Kzibo>C3U8LKA9R?}LTC(3}SGVt_+#;ubvBk`*fJ z8|@VF3TYa>XHk_3&8{JA{-MwTk>154wU6Rh;LcEPMkZY1Z&y@mfEH{G0mk8AoENyl z0TlzjVTF|INZD{d7IHG4Uh=ypgz#EsMuvp@Ybtbop$Dz7F$G16<6>28WZkd5A0Fk6 z$S9XbDxH`F(W}X3C4kNi7(Z%q@nz^dvzVe8OT2~yWrjuU`yLl02H>IY8_R90zesy; zsexyc#6}xn`SR+zfJh=?LAmCweG4ArWM$9Oo*TX4?jLEBmjux4Y8Rx(4nQ}thMyzW zk*ArGLh;JRO@|LTa*BaCqTyLY2?kC!C2FZxvVrs$`hg&2pD19^gN41!A`y?m^IX|p zo%9=xvDYyD73^!7{krcl0HHkBNg(@PsV49z0w&m5*~-^ap3jOhF@SUIFhtSRvry={ z0aFco8>^1V(TUzDc?~g_OiIn;qIc7erPXL>r``mZnk{~qpG+M?yYx6W<00)aEjGz4 z$dx_b^6<-APGOd;?DX_fX~MzpA}2+@bhrdQ$YuoE^tM7~8DV06^C50&84%#}cPI7c z15b5aC&v8Ts&?gDe3w=*jW1tWo}2%!Hsu}?{IKee2eKV}O+c)U{)3AgUX(x6XT4MC z>8g5y{px}gB$5KBBG4NUOf~Qti(d1tJ&# z$z=S^zObP7yXob8(dJ!nbpO`%@r4dK;Kf9xux?+u3NOA4ILEvPs4yB*O}`a{vLng2 zWYQkQse{UAp8_)zxT3z%xZ6Ip z->zIG^*BEC=p4N1%S*fyTI8nf`6o1{1>*y^`kPn_>DOnH5~>p^n>k!6^+5mEkF;_Q z3_9aiSS&xb)uHa{3fl@}T=_}$m=>nY8c*$)(Y6j8&ReFcLoc+R`%kYP0jJ9|5OIY4 zX$0FE{nYzkDC4isKGtHLr{vyJ)+H_S-4{I6_42{f5df%9m?Cc+RA%Qgw+=9@7XDrq zAv~>FB(!gt3GN^4)<(I=I9gqr)FRS|CvQPlchBARdntcb$fW6U8uE+|LG>CI7e?8g$D&3C?8#A>^ONN z>*5U@+TJ@@f|?pi=ez;!{-K)B6B3yXO;jY5+Z4Q|HSf?svlH_xhu3+-Qv>y zknMI2fu#f(Iu{9|7hi4&zLnM23?K*muM!r>W}`PAJb*<@+Xn@mp`LE@h|HQcT>{CAgC6-R1Y0dBV8*V-3DWK$x&_TWhVxr&$CxI~R_9WbOhrU?hRoZ{euFq|jM?A;G|7p6`ugo)1} z<3Cpd2v|5O8wmEA&l+7S#oRd4K3g$mJ|KJZI6TT?{T3hzQCT_l^GghNXn;fHRE?u& zk3bmq-7Azp`p!ztM`-bvxRR1DHK}QtG`*F6r(a2!JHh@iQREa&4jN~=&bGG$+|bnE zMgx;E6-YE3P%(JjuLD-`NAV4g7K;JUZDYKrB=&QgoUQ2esE)9pXuR&wycOEmC^}z_ zx3rl3Xs?3!Stu~$PN9na6;UJTXogQLE zhZ%R=^;y8=>7aA*^~5xmqlzKey%?rrTftN@n4+J*h?xv(|Doi^{Ep67AGYS%+^OJY z=$qazO^hT>#+6&% zXwOR4`))d2*<;LS60c?ndAxm~*eAl1l0 z+y0;#7_JUk8p?P8`<+$$Zl5jsh7Y7pSd|6%GBAs)L|3z3SL%v{*;F``*9FuzH0Tjj zx0{2=?syQdqjSnSk`~nxb2!nwv*#hsf#TBwaYb_+SS>my-iTdFY2Jsj*D_5)Y!_Z$ z_jg^#XnVa2R$(U9XQ3ON9O zEZ!6iY_y}CCwGbH#VtRU+!z9gBL#gMa1bL8ZhS1i%v@F-%XplgZP_)^B(}JUT@{G< zuNpU{kz;o}9SIWa`AzKZt{h7I=*INY)6%j2_zIb8`c!c&Cc4l4SN7!<7t;NsTgR7? zmJIvTO|>UZbh9c4wZFOpcTj_ijI!tmSFH`392xe6kWM>T5W-ggemWut)G|>x;R}j?o_{tyGUl)yVaf>Q; z&t+(`9?4cXRFC9`Bv}MS)tY>PH>THO;hbFkHSMjv2OUh&;Bd%|7eN?|D*B$ z<7e3|m=g;WQ~#%1SzpP2zT7+j4qQTCKna&4=D!N)_YRj36mUZTl$HTZj+FTz2@km$ zQ5S`a?2dpKP4=XL$sh#}D0fY|o zA+*d3*Z>JA#7ElQ9F3rl|FUk6o^OZPVbs^wCy(7+2YEme_hGU940zce{+9E4{iFm3 zNBa@t{r*0u6-pDBWdlSI0$>j}=L@fuZ6+R)1zX}o!|3qJ1An>q;4TWQ2VJ`3P(t~$ zp%7~840sv_&!*Pllx^Js+$Bv{CwS-Q=e=z+wYwNKwe@ukVvbyU1`hxb69aKq>_8u( zeQ<@gz9QaEc;0lqWZmb;HR{WN8d+&mqC!*t?5KleI&HtYs=U>}t5PB2PJ>$QzkVm& zKXk4RK09+<0~UmFHIzClePdRI9HN0d>K<~T#>_6S?O!jJKOCRKK$)GIsB$r^$myyy1&t4YE%=MZ z0m!^!*z3a(EbW^;@%|n(CP3khft=R6s2A<%&(=5G0K7^Dy}Id5-Y!qF>Q9?Q-Y_X| z{p)g}0ftp|ntTGlTFvXZ9(u@aiT(>kYm7&7EWARWBr+@8SU^AL3ya`o8jb4^7K9P} z2e$~90N~#uhJHhZ(A@+PuKe$cz`v`I;6LyuMgSy0-mp->KRN%f08j)Dx-p z{8#}=C*lPYM0$uSR?iU0S`h+q5?*48;W)^EWHjv!OnVa%Mxl>!mI*Vy zAV%GP2vvAsAq&x<$PdnsiK;tAr=c;*ZfwB^$WMR?mc&7tDu~#EpM^k8an1>VqNdTi zt1#aVPzJpOMQOvW@Q|knhRj#zczo9_n4Xick|+TGN$! zlmBOy#I!OYSj>#_mkOheEuxSi{$8kkVEhgnJy9u6x8;7)vfM-_X)LmKa+y0}a|#Ur zy28V)!TwO>Xh+&F{;6zWBY_SdTn@=~g$|hRmg;g(6;$|BtRf`QQ^r{!&ZbL2+{Ijl zf#fo%bzML3<3nV0tsAuu>U4fS6plt<{UK9^JmEy3K(16n^Kdm@Dm2XN1~>LWThWg% z`4;S#{D1Nkk^*>ds-oyXKy~iVHTl12nYiu%0SWWR+bsnO|Kj$PVCq z`lM>lt5f(K8$j)yvn~bPLw5*g_9wB(`oXMIz>6#)BU`(X_p%ZZXIj2X*Ai=eu&s zbb#cr$}=>Vf;)Q-TlN!h)&WBm)z@!|k6gI&h})J)c{<%|FhnCbBhsbir3{TzNdtey!1tEb-qZwvHPd44 zM3NK2G8MC4hb0y?XeGL@M8KZ`>SmEyb*>M!-vRGCRWEUc7o=RuVCd^zCeUfXaf_`^WaC?^-e=?rb}_U7$YN`DNwwPR`{I9UE3av6#d_fs*h zr_$k7GyNT|PqzwPJhpD1wEInQ@lr-XjFkVDItUa<(Vcw{3r&o zVf`a>uL@NaFwL!qyijTQkn+L|SrXs4^Nl&VO9nTU6DiIkS2uM0;l3;P@JdH({)*k2 z{b6|h$|T}ZJsFD;#VI==352s(Hxx=ilkSKGxDNRKew*(58ivNb%AkZiS=yarjWhcB zHDTc&cx5*CY_PxxMygHI6J%Kc=6Nk8Kz-zz$lUS4PAxw^daZbJa*}_f0FZv^4VF%C ztm3yCR*-SPrI&vKgzfnq(aS}nlBhI4iCA}v9qtuEo zxl%uaUN1pWiM<+s>9JR%y1jMC`$2bTj8=ifeNfh5>zF(=`-9rQ0RyZNN~8yB*)QWV zes$D&KLSH#0q{F=^^oeA^ETK=dolr@Z>#kuSiqs&IBY3-tAO!FQ>AgEvL>hTiEq%} zyO-SA(V@VY$63CgIa~{ddJn2AS{gdv{O%5pXCIHq4GDQkp1;0mpWCFgVpJfy9T-r6 z8C;e^`$)*bNBsRZ_8*_RzsdPkFmRyEMQY67hNkgQ63&>W(PsJ?vj3YuYQyj@<;#Q+ z?WL$KL~i}*3w0yz4rH7u%RStXu3!A|o3BX^HaD1oYE5f$x8TPEleiCx-!YIn#aT*f z2zvQ#USY0tnBsgSMx5s$36I%#KLMuAh_9M zkr0>8@`AXFn(n7QUj=tpp^Jn{R7mudrcJ0X!_KnmX@v1rEP1feb`X`XFAmUfTKYp4 zMLVqOE8haoqc9Lj0&;g%Zj*cjSE@xJ;jUlSAJEHY>98UwEoRya_w*BLwxg(U6b<^4 zE-J0)^7Ckj1>Y|>(SH4qqu8$aY=%hxqhsHyCqOFs7*g8rEWDTx*_J6{TUCjbkNUgj zCzCJrTcB!-iuCmY8%*Pq_0bh+F!apGf}H8+j2H7-BcJAlcTD7=yNUtCZn@|Z%^Pr~ zE};j5Ew#!a#|92zAV)!?Hr9hCLa>~L30jEZAp4Wm!_}9r`dnM@5YzhKgC}3T`CvvQ z8U8r7LeYI*=1n`1$r{OY@Lfx>48y_h8AXhgnb@hZw|(N|@H-LE9{mT7>8hNN7lc|J zUUQuen`og4Ck5FM*1lxzf?1?+@3^l0-eBBw_;Z3jHAZA|^dx?K^q`8T`^Ku^gRhmYgJ!*SgaR$Zu8rux@`Wm9l!=wG{I3g(X>JA zF?;r!pWIn*o^AGBJp{)2!peA2XIy6od-Y_|e17i9jx%rQyPR>7IfU=8j*cWnhvt`a5|WpMqJLH2$V8uOrx+ygh}JH|6Y!$RMNY+{mJCOOm|gu z4#-~5s^J+4hw=#4w9DgaoxX+-`j7KTy=0S$_V;Si)*vCv^*1$8zyK6Ya3u|^c`k(c z4y^n86w&;Xsw0KLk~lT%8bx+4{b$)$S($76)5R>YnEtRKX!mgmz^6-0mM0d}-8z`P z6EAMsH$9^tH!2s9vm$ze+?=DAgJrNgQX0>RKqDdO{5ky>EN||kErGGc1GY9bg_8A| zAH=9;p1Ge}iu;o(LkaF}c2BVyeDNMLZ2=xo<$otC3TsRP*GV7e^}TaAH!F0pq(XuD-b*}Ev)^TX0*;x)Y@Ky zP;R034s^Z+d=|W^VvGUE2~ z4d&{Pf%hB(yi)>T_g$nH%hz`EGl=j17cq(;u9&rKVyNAgYMMHCZk7y?%K4VVzyOW? zQqDj^znAVV4BRua>B49HNG=V#H*Vtqh-&$?q{m4-PG72F29wPQeONnyz~w^OE~>5; zgN6bzs@+67?%q?0FCYpY6Wm{o6pTEBJ`roZQNPg+`_%CXae*v=;@>|9PPP!SXfU+5B= z{qbl)v|@)I7;pGLf)o1sKh2-M&?}a9TE2fmTO{}+PZ5v1U-PIac8s_u=+t!(GtbQq z`PDuumOTh9?xR()^pFVQ$l=@-#mgt2_?C@Sj_&G)xY}T(or=_gFZY~?d10nyhYhi0 z?BV9&<`xOYF+c2PE>NHjb{_J|_VA2Nmp8HwlX%c;8%Aau=k0R<5`9kEhX!v6fl9RDvquKlLV(^>TQ7BSUc-J&uM=C423!l_YcAK zXhB>s#*F`bm21*gVj1)y;Wp1Cffn<7+p9bruvSvh9qvl-z^W~ds#xpKyB~v#uOEMH z#X?$>1)HL7Q|{)|QV+Mfb1dKPeFH-V;DE>pQ_ygMj@i9HbHh*)ivrZMx~d@>mDK_h zgBo4(WX`^%$Lm*1{GLOU4)^g5kYUk8EQ_(@7}>~gh7&B_<>`wbaOao=9d#FBS7=Ip z8)8-gwOIxRJV4SQ{mbVyfuz2jSL!+s6P7oIunC{rAA{fR7ajoI{Zos>IHH6U%x|B~ z{_S2g{ItNX`Q&)e1@l``{LG)Jo#wS_?XNjh%kJOr4ctB`FYBdrTkerfF=ILG=Y52J z!hARRZIWmCgu+ulMf-DWc z)+41V>56yh;cY|`ngr-GWEZq2zx_h!^VTx%bOt`S8nTeEC6J&yXpFf*pK+qm=6Lgh zv87yC8rFReXG5OM8IWYhS9o%z_!RdjMW2>CuL-bsEIjugUz{I#rvk@?Ia@EdX__vz zau+yzd1!4U0iZ<2ww^&wC`V@2V-5%8q{=F$u9}!r*~^|XP^g-Kw2Eb?x_?Rcpv*5KYQfrAJ{ukdSy@yQChwRt4DJWpVv|`8UTXVB=HwFTu_$sVofK zb&I!l$_-@rBEPTrYPN3+TdyY_`9@l&KgHru+`rdm={>3Cgqv7f6<9;EuRJHb42-|L z{!;7a<>$|UbyZe}Da;2lVJPn}q9A#v2*U46?RWrG(CAOa@vkqj&>Er8OTJn0HyZLa z`O#PYRUSp<`p3& zPf~_Y1dQz>|B8mt(J<#R}6Bm1P7jVcnjz@9BY@`BfryH!=<2WTT0z?Zwl@%r&rNU6Er*e}Q1d$2 z>!6zc)^R>LLS8)9WI(XBkWlou+WmFsV;*sBK6_yoMT@9Z)ww7m3G4ij6MAM1GbHi>R_QL&q(@P>ESJhTsT zh4~twG=Es}c+9Y7j#nH0SoO=OkRVHaB?sZVUW|k)D~>D@JvLV$bug4wV{0>fHC1ri zTbs1}b3^eh$Nj*-_V#c>acn?ub+^g)%klEF7oM%29^PYjF^n&#(m?jI_3c{a*-#1| zZtE)=9ohJRENSxBeuq9l_3qnu$l-V8C7igZ%)tpV=7oU}N-&R)g^UBMp0^bEp=tBv z0j9XH#8r&qR~mfYCDleHG}_a*N@U{x4L$1Uds4@fev3~JsY`iPE@ff`BY%a>*pY$O z+lE9mLAHcKUCiI#y#26JQ%(uJ`EMhusU)tM(dbHoGDgs8PuhANJAcbDOjhKewj0q7 zQ+)8dck1)-#b*pvYyT94`Z(fq6AaAn9;a%+S2<_;?E%0mK6H@q4*tcFn|~j+9vikKeHrILAk>+KKi6M+!S~~V!n7~9|jV~0O~kzRo}t1C-A%AGY^(6mr!}2t^QIeK&FCuX+vH{ zj*cD%4=T0qc+vd7v5mWU@S-CV1(`%5j&?kAq&;9rBLvVR$cBz*o|T{ju)x)AAa|1p zr1Eo2j@u%;Y~M{^C)yhrd=&+9vzhgDGf-+7zrv4ITyqn}r-kmoIQjPs_|0S*7J2?V z8_)w}k5Kj6_il!c$8TCQ-GHkk?-So5nWq>)Kj_7-PSPB7jx_Kt=zSqX_D&Ko=6}A0 zsKfweb0kF19p4eg=Bn_Dn6#g1X0kFkN%~nM@8tspe@E6tZ^Zh+~MBA8&qCuuiSIS?}c^?4}uF0QkaA z*D_Kz{aB15(Ft-r-43t9lMUlRr6+D1ePV9wtms}=&FoiO6I zMP)|U?(TNZbNu6;YiIfC*p~obhxVk940VJ5F>N;@@pcAJu>l;#Kn>=?p`2RG#=Xl> z96e$tm6_%hX6G(RQE=v4Z6`;9_w?Vt`%dh|$5c%ivim4-QVl+no0a~UtZQ66xY9su zZ}nS+8t67XnbSNA2myN#2{TOfKDkMX&6EwNwHvdRln(fcr0%w!3tp!gJA2BG^t(^R zT%&j>sWQ`s0qigv%ICwJ4gY|u*ZGfc8P-8*yx_nF?(Za0yKwo(&rue5mzBtSvW@|e zEm^M9)7SU2VABPGqMWqE8o`9Lm%ERtd_`smL!RB=!KzS2BXH5yjB?Fiaxyn{-k6oZ+`mGf(aZ>b@)(z5V*@j#63`k%oEJ z-o)Q9r7$Wb>yZ2&7;>NRm#92olCIPCOh$es9`G%>mIgKL3>ZK&?;a|{WW7O_`A=ka zPfcO{wuoQ+XG-^^7q5SnelSyFM_RtrhRmNPWXxsEfcQRoE(VZ&oy^jhvaA>Xt;?cD z(cwQdDe2z%7`R{o?Fs=IbH@AHS|Ij9>ERojrbv)Jp#hL1*~KqW02FlG9tnDd?->b@ zPRiE!J)z#ajsSx4y6)e_Szv(NH4-uJMRXBdiZ=a2&2gvri&tz`!@uQ*$G%$|w*Qum z*a_QA4i^iXTRByW^BWB0V9>_$gmU}!HD+(iXgnrZ(B?Yr0iMty41xjfvcDEuSYg78 zm3S}K5aP=V_bF2OaG5sS_)ugfq1x8D&sktuU)3E(S-0>zdN8?U)#Tn=DFi<4H1Pio z25^}=e?5sd=ft?;=JaWQkJx^+p7D?aLIg{7{enV%CHv6J`A)GzS0@MCpKqa}&%FGm zMy_>90kTeOPuq6+%q9WuIa;v{2TcUv!3ylE6CEH9M?UW#N8W4WBQwOUy0c{{B|L?H61a@<_-f zXy;aR0HR4-=ytj5Sq~I~1!$`wAO_ zEQZz%j55HjbI1XTBY-V#dF7ML-YTFz!=9y$yvu z>90Wwegi-WK)7X33V5K9DmI0BabG>d8?cuS|LQ%^LdRdbv!m#;^q&D@{I$iHVS7zH z`_Y3LtjSM3ThHXQf58It<}Giime$rC-WS;BVr5Vqb|k08S((x;N$gg85-W!s45_Fy znV28|zOyO`2B!Bdg=p{qC^md3KAo^U!>Ip>{3vo(_rfze=JqTma4r9k*JyGp^&#BJ zcEwAp?P@7PMbsz7rm3(T)89&6CuO>L4YLdOrKTY~^6H1nmT8RS&jT_*lJ%V3*uiT1 z$(|Tso7{1uS|`VtR|2Z9*<#evK0X#Xg7bW|+Zm-&iv&e4Q8pa{nArgEl9eCW3J0YQ z>j^a)AdtTs)zj{tH5flQ757;kT@)^nUZ}pC8G`bXn>d5`vyd33duX(?tgT~LV!Uud zt*cvsrC%?BDD~q+avj~clf0jkE1i~Ek{U`rKFihu9LW<~;uu&8cpcprd0&TW>Mlnn51OnQDcXZLsJd?$gZ{h#gZq9zE z5ubd-_5k4yyr_-x^i#M5DH@?J;M|?YO=7$Dd&N6$nE95%QRO?@d3I-vVFgBEUvY&Q zu!|4uShJ$5(y5mlnyPDHzjAkf<%#!5xe>@`nO*+qOx-6rE|vhe8C5r=07uk;|NK5$?%`>nn@w@KNY8^rN6$D?$O}bH;*m+)u$v%`l#oH)boBWz3V8}h-V+f^#Tv>Jr;ukD(wo)CE|Laz zK?J0RlJ4Usw|Rq=@~u*}80Qv{TY38jEDWXmQj~8#ngrlXoa^h{dwN0DIBK9k%6mq% z==ge!JmDJ2{~(UiuH)H85dX(_WCA*&Qf0c25J>hf6pZ2!mkKPl)%zF&DNJ{#Z9_}6 zVUj4wc@cmjb&b1!%C_&)LuyAk81`Jm8$Nn^m8R!H?^QBZR z4w5A9EuKE;*NbU+rrypn#AjF}xL?!a5JlCXaP0g(>+$^=>1WRu+Ny~oROR;>6S2ty zDLQgPXt((MxUUm#j)Pf!KC$N@r!nc{JSuQ6$wSa}4L<#*4STZMA z+$|i%N;0I43%@n!XWYL?D?*%M)7d*i(-OvUbsYm zS=kpXW=rwQrp)L!2ROI?-ma}95XZqR!Zzw8+PvCTgy615|F8haL9I}3T4f@<#XVCA!fKXI13FiqcVT*REm9?qQz{2v5XE4 z4>steg74|6nqLTEBDLc;4}Yt<^l)i?y{;GjK)ibK1{)AzbeQC)zL*K6uar5fepouV zW-Z_Q>+|f&LAqRkN6C*zlkz-Q16w@K$#`@`n(zFb{_Hx;h{Bw-H>htPFE?bxhAQAA z89|6NU|h(aUSGEKm8X8(^{du!gTJ!ZZVo9wWSG_3O(2F3mP4rWnCI7TO*k8NQsON0 z%pCYklY&ZkiDlaZfi)C95Czc5prq}hVKoEL~sJ-4xkcJI-@P$uzg*=1pxx(iM^7`fv zz1b$>#|%}4@Yck%q{E(!-KGaQa1|< zsl8%hpfeWo#3qiDl?z?_(u(w;9m`a^=~7t7up-8KDjg9Kkpi=()wl+yUF8zOHYiCLs@QP z(}KM~^tu0&OVrBKqXucPb=!sbx_G|A`N=C=A(jN1@{QxIP#rRymovo}j34DQB;Bp2 z=tsBp15m6G3+nx8jXj`wt1^A7<{Y-!aW$`e?{ezLu|P@gpB^YCKMZ0#mA7 z^n2Z8&sSRGhhs&z|D>$NH$0w`9~SvJI`_CDEd5Jj!GSex;~PT<8lgG4!0EFHsRo0` zUP^b>o!%mS=p%Ql5l9iu!e$)Sv4`a`Z@9CDRt+3AT{U0qjY>!lW(%+G9h#OzWm0TC znb{Te5g~msnyTkaYj44co59R}^2(#M)^2lgt02e5Z|*Xy?x)}#WTDAe&An;Eibge( zC!K-WZUq&o_^Twz9v^1UF~)pRWwgsC9@=7_g7zI`a^zd!YKaK-=)P+4vC!yYzZcCWRrpv6lZdm`NI3q$>-G?>Rx z6z}Cipw4Icg5mS)2;`Vk_ID3fO7SJ-Aq^XuthN>|p_`?mE$)^4ETO&qxOzg1$-FI4 znCBab7*CqGxj2v=UzklyETK?SmQ8 z0b=Ys*KGzB9>o`*<)=S3h_YYA`53+NMs*O3#71__Ek?Z(xk-00@^_Wl>K3K{hE5Kh zaqt+C$*=$R{6M5~W{SMm=Y0Fc&SCr)a^Rz`|6vu%P||}jtzqbWb)iMl%$<{E7X~S{ zqEW(ZRKPsnkP-x+??Oljhs%K4tsCLlj%k$i8$;D2h3J{W#qMOnNA}NEIex7~fY{P& z?XyZ>znB^cy$9Z!!jJ#T(aUn&tD9M%yNpG!*I$qhDU>Dol2eO|?A9TW+9l#nx5&aj z`qm}9DbE+WzWp;?k5`CaGZ&1 zPC1v|4f5qAC2QEp=Xe->9_oHETwUq{D-yblE#GvQ@ z16zatcGlD~2VFfU5!>QP@34*SG{0JY>8JPj47wq3S{%t6uWUtnHzc7oXVu zuiu_92E~?A>yO-7J9dq?RmnTW19}Yo&?5Jgikg|Z#)KNW4Fa&tN^}3-lEgW`Cr9ie zHjB{V6I;)*$MpyrSyk~shV^6yV1|779;NgO6JKnAvWf=DFU z02{#hZw*uqtqg85fWE9^hU#wc{#$#6eI5#*7lkABWd0Qs{kM1k3lcAb{I8V%$WaBV zpsWRI^3(t50Lfp&l7r;(5dDt^zl5Qdp)3BQ6V$FGOadkW)&IK?D*caQ|KF?s_4EIw zr%j=h>dJkOl+4kuCZ>f9%?FR!B2gTnl9`t%B zN8R{e3(b}$)7Xc%Q#XG5r|BLxCQ7`?|52U4QPgU)3w-(&B}q@ z65WX^H;+Sq?w_TGH+)*$+@BQ1d^+l_JDyGERNY5Q<1j6Ad;*>iV5+Zs&y5z2d1_rl zI5UiPnm(H=2sLI#&s5_g|2WK+@-%vPeKdN4&dQ<$Bm4K1_IE$si2GQMEeRI{Ej;bs zDSEStJR>tE!@XCzh(_0_2WM1f2+Q|x9i_}DHu%K1QOVinco(pRZ>&|nxcfm40UQay zh;l%dnP9sd=c^y82+UcbIvBD^2g1CQGQp{WrRi(QCGx_I%tV0!Z?#L zB2VP*3)k2SO>Q5WK)?a%`{#Forx^fglq&HL2wJtcDmiTE;@$NMZIzcM8BawaQumuQ zfEsb7IfS^qGTPZT$-(kD-AW8%Tl$0j5=MfCcKf>U6?MAO(&J+5u;>3#4$9?rb|P|l z_ltVsG1s*N3~8_2`07td-5aK;KZ|Lje=++!rhOm}A4QX4G$-9ME^HpVU2KvJdU>7* z>aCo7jsU;Mm_Eqedja>IQwNX0v>ueX5|+IwylPNwV=R!B!#-E}qwbs@4rD++100!i z-#A2PVvP_Wq=CeLGzgGHFiw~dgPg(YFAG0L&(dH}a=_+ybvP)m^sN{mA?MulN6=c7 z1jNeSBZZ+Dk}!BOV8CI@FM$wnrEPKe3C_S#24kZq%M2|o+vsa$TA;lG=!o2(gYi3N z0avkR8kb)Fgn(e>KN42JRWq39#&W6YO_EhwQe;Ut5>jPlUx%Ww;M-)WO>G5qF*8>of20`00Mb*9dSAnjEnA z7w|a3w%{nWzzDh$K%!{FmlWy66F=~=W9aeU^ABXb+kfA^qVBO3{89N*bb9l%tCjVs zJozqm#EN@d8_rFx+q!E?X%WR!MDWi`9eFQ&zzaE$3we}&9kpldKfPo2Ju|m9jR%JR zhKA4qHc*-X%^(h#r2v6&FmMWZ>_GO=CDt~JMkbWZ~Ym8X+yQ?F3f`OBIMbPg)5oMpFV2s&_XhmEn7+@Abv zmTtNQU%u*B)2lIi%GWu!lD{$oMrgX>$Z77V0;!^6F-ejVhO}1U zOVp_pE-?TVks@OS!wFkW=5HB#w?W2xf53a`_Sv+UZZkc!(9EJ&ZD-y#o>CQARWx^R zA46`QNN?NqjPTVV_Q{4QEbe;%DNpTuSEn3Tnjcs}miT`bj3X{6u{mmwZz%QYHc*{M z8F6g)_#9Z*!-3<}zH!`Ax}x|hIBT{bxG}SgB>CFR1dtQT^EL_spRSeh3=zs<8)`or z8pRiJDE`*LC0HMh+j>&wYqC4Qve{S^ss#>=+dsw6Mu1AGXewDNwW!Z{DeOBG;;sqOM3Egc=-RbdcLT1-^i z{>c|c8jvpg9HvD$`FeWPLs;N!wtv)Po|MkjFQYrs49oG(BQj2^QvrZ7jTpb!Jfx5y z^6iMizD{>T?{+EUsLW_WxJ{AuvC`o|A#UC^kCdadQ(Wia=Tu zgA$t8;S&@K5>hP*=*SxvLl0FhwCA2pA}FPY{Ek~zF7@|0a3ZqRCJuL53yf!GnM0&Q-0b%W z2{e{_SvvMi)u=1TIA8!WYPUc=1RWsxEdQ9Dv(YoF-w?rA3IpoEj4y%@E`berlr5mR zFycKmwkY>H2jDS)Wy67fJeE`80t;}R;O-14F`#xQx?u@^K{w*V-FC!DBP}+hY2X~BSMLC(@Tl#3F)^c)&&4rN^bq|vGcPA z1oDgBfE(phUrPO67t3h&VNDvfJY=q*g)j6C14+4~G_MVv56oY`4>w-4M=%<`*qZuz zVAKTp6d|Ae6F85-CQ(`7EhIi*tacMS)qtEhF}OCqOLa97ZQ3}acAqCd&>+ao-4d`VAQ)Bf|1k_e zM6FJjxKNM^;eTEZlhA6GX##*NJi&b~nppS2_TMWYuAe0PU;B&zbr2CKs6X9W)W#2P z^ZwLUXR=q{aVI!v?8>Q+n%^`% zK}hhs_-Uo(Qqo5LqPXU}RPgUW5bwf=7|7F_r z%1mjIU9Xx37l504&3^wLQtL_KU>snyFeji*EUhv@{fJMdkD(YFhhT|;< z{FIO6R*`K{R|^o(x4g7B6w2NUWq9KLE2S?QX-M2y*ad}-)kZ6!+y&$A9qOatthlX9KllPBqyh2Gd1QuZ3(L-}NT z)XFFl*{}FaxF>(}3NzyWXJuULUQ2^LM>w)EVOU;<9SQbb2w;Yf=)dVz@r-e;{$G_{ zcT`hX82>T>Whq0n6mpOPqJk3;8F7@l2;e|Q6&#>P(O8NI2Aik0VnK=pC!$1@f`g%; zKqDk-5fv0P3K3<b0E+q;G_nMFwS(-lUW8YQe!Gn&Qoeb)U$e4x~M}L~U1w@CGxF zTsLKg&JsVN%yYFynqcd%(=x4IE7keYIVKl#-5Cv1o4~gW&L&xunyWZ{;FTnKFj~kD zlLWgbf0ci_o_tKEJuM051tkPI=hdlJF$`fy4^M;yOMBkp} zC@c*+hsypKtHaWw2mtO@D7o6i9;$c@z#JXo7)Wh#;{c`F_=u<%l+=Q!O;*)pv)O?P zUEEu+&km9m$U!|5(-t*PAS?7Vo_Ay-*ug~F2@y!&>$0hExKS=b(pV$~qlV_A@(5&g zB><;(02Vqp$y3=M=jp$BX^Q_hVgFts7ntnAQXRs->NbtIT@-W*Hiu7z2?#FI*CV{r zb0NS@i#^q2dyIy=`c97%mEhR|(l(T09&*Tu zGOc<(OyRY8Lps8*Tzpk=Mxa`ovKJOHZ+pXUJChASe?^k8hPKlsBa8*xZOdeyT<5&q z8a3x0*}(49=el(9)9k7T#|f`-*n48XbSN$)QxzW4r(P+28&r{)X z!z^=uM`w*eXAzYZcOy-|(s4A)L*#O?F1ZXlK36dkoYc1a{p;3|jsejei!*tgt446N zz+Grjx7LE{7F9oHN7Oq+*EQ)9UaHORBO!9NXk?W5vQfAls`H)^8_V>)w6k7kT0+~m z(4zN^TTn8&x{vRcUfs3)1UZSx{=zlzVF>OsV0iq))aVI)o(~{rp>vf2?;H`0Nxal{ zo1;S+TVUm_)b<9n1!11!d@`$$^qGUwA4Ismc1*KC>k!$cX8IgSDe2=0_Unc_Bc|bE zmlEok*b`UUd44u2(I=Mlmn`AP+_MqsS=lAx$L>kH%vpYlyr9L-8XN6lD><_QK>Adr zjCAEK;|`M!lmZ7r_`F@R@w;cA4OAwEO9GLj{+fd8G_Flu;C#ZTCgqZFrHtg!#i;{Z z8e#uhew-sfiyQ)K$fnRpC*hm(9w(Y7LuRV*seDSHLk$oa0S>9pU$Dtd`X1px+bC_4@ik6t8%B%UiFzS@7Y}la%@I!AD%~0vi0=txf`XRKp z4hiS`&7qt@M~?JuuS638>{=*<#yNy`!FOSX8E()$9TOZJs2WlPqb%Ky)tYG%Mzw61 zq~vV$1D2wpxrP*&2bsX#;sW!L064~gUU|}xVm3~j#g-xX7X}XVJWrB%klg$BwvP-~ zlSHc=DG;E2l~UB_seS+F*;L}i12VFEa+6E*=X{*tpWZ+_^$KLFyH^J4%2 literal 0 HcmV?d00001 From 50c1a928fbb7aea45e05baadb879c582052b4cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 8 May 2023 23:07:32 +0200 Subject: [PATCH 0948/1881] =?UTF-8?q?=E2=9C=85=20Refactor=20OpenAPI=20test?= =?UTF-8?q?s,=20prepare=20for=20Pydantic=20v2=20(#9503)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✅ Refactor OpenAPI tests, move inline, prepare for Pydantic v2 tests * ✅ Fix test module loading for conditional OpenAPI * 🐛 Fix missing pytest marker * ✅ Fix test for coverage --- tests/test_additional_properties.py | 169 +- tests/test_additional_response_extra.py | 49 +- ...onal_responses_custom_model_in_callback.py | 212 +- ...tional_responses_custom_validationerror.py | 123 +- ...ional_responses_default_validationerror.py | 135 +- ...est_additional_responses_response_class.py | 147 +- tests/test_additional_responses_router.py | 189 +- tests/test_annotated.py | 333 +- tests/test_application.py | 2255 +-- tests/test_custom_route_class.py | 91 +- tests/test_dependency_duplicates.py | 299 +- tests/test_deprecated_openapi_prefix.py | 50 +- tests/test_duplicate_models_openapi.py | 97 +- tests/test_extra_routes.py | 534 +- tests/test_filter_pydantic_sub_model.py | 185 +- tests/test_get_request_body.py | 163 +- .../test_include_router_defaults_overrides.py | 13030 ++++++++-------- .../test_modules_same_name_body/test_main.py | 248 +- tests/test_multi_body_errors.py | 174 +- tests/test_multi_query_errors.py | 152 +- .../test_openapi_query_parameter_extension.py | 175 +- tests/test_openapi_route_extensions.py | 49 +- tests/test_openapi_servers.py | 65 +- tests/test_param_in_path_and_dependency.py | 144 +- tests/test_put_no_body.py | 145 +- tests/test_repeated_parameter_alias.py | 156 +- tests/test_reponse_set_reponse_code_empty.py | 137 +- tests/test_response_by_alias.py | 379 +- tests/test_response_class_no_mediatype.py | 141 +- tests/test_response_code_no_body.py | 137 +- ...est_response_model_as_return_annotation.py | 1213 +- tests/test_response_model_sub_types.py | 226 +- tests/test_schema_extra_examples.py | 1235 +- tests/test_security_api_key_cookie.py | 63 +- ...est_security_api_key_cookie_description.py | 73 +- .../test_security_api_key_cookie_optional.py | 63 +- tests/test_security_api_key_header.py | 60 +- ...est_security_api_key_header_description.py | 70 +- .../test_security_api_key_header_optional.py | 60 +- tests/test_security_api_key_query.py | 60 +- ...test_security_api_key_query_description.py | 70 +- tests/test_security_api_key_query_optional.py | 60 +- tests/test_security_http_base.py | 56 +- tests/test_security_http_base_description.py | 68 +- tests/test_security_http_base_optional.py | 56 +- tests/test_security_http_basic_optional.py | 56 +- tests/test_security_http_basic_realm.py | 56 +- ...t_security_http_basic_realm_description.py | 68 +- tests/test_security_http_bearer.py | 56 +- .../test_security_http_bearer_description.py | 68 +- tests/test_security_http_bearer_optional.py | 56 +- tests/test_security_http_digest.py | 56 +- .../test_security_http_digest_description.py | 68 +- tests/test_security_http_digest_optional.py | 56 +- tests/test_security_oauth2.py | 236 +- ...curity_oauth2_authorization_code_bearer.py | 78 +- ...2_authorization_code_bearer_description.py | 80 +- tests/test_security_oauth2_optional.py | 236 +- ...st_security_oauth2_optional_description.py | 238 +- ...ecurity_oauth2_password_bearer_optional.py | 66 +- ...h2_password_bearer_optional_description.py | 68 +- tests/test_security_openid_connect.py | 63 +- ...est_security_openid_connect_description.py | 68 +- .../test_security_openid_connect_optional.py | 63 +- tests/test_starlette_exception.py | 252 +- tests/test_sub_callbacks.py | 401 +- tests/test_tuples.py | 366 +- .../test_tutorial001.py | 198 +- .../test_tutorial002.py | 184 +- .../test_tutorial003.py | 203 +- .../test_tutorial004.py | 190 +- .../test_tutorial001.py | 232 +- .../test_behind_a_proxy/test_tutorial001.py | 50 +- .../test_behind_a_proxy/test_tutorial002.py | 50 +- .../test_behind_a_proxy/test_tutorial003.py | 61 +- .../test_behind_a_proxy/test_tutorial004.py | 59 +- .../test_bigger_applications/test_main.py | 663 +- .../test_bigger_applications/test_main_an.py | 663 +- .../test_main_an_py39.py | 665 +- .../test_body/test_tutorial001.py | 166 +- .../test_body/test_tutorial001_py310.py | 168 +- .../test_body_fields/test_tutorial001.py | 217 +- .../test_body_fields/test_tutorial001_an.py | 217 +- .../test_tutorial001_an_py310.py | 218 +- .../test_tutorial001_an_py39.py | 218 +- .../test_tutorial001_py310.py | 218 +- .../test_tutorial001.py | 202 +- .../test_tutorial001_an.py | 202 +- .../test_tutorial001_an_py310.py | 204 +- .../test_tutorial001_an_py39.py | 204 +- .../test_tutorial001_py310.py | 204 +- .../test_tutorial003.py | 224 +- .../test_tutorial003_an.py | 224 +- .../test_tutorial003_an_py310.py | 226 +- .../test_tutorial003_an_py39.py | 226 +- .../test_tutorial003_py310.py | 226 +- .../test_tutorial009.py | 152 +- .../test_tutorial009_py39.py | 154 +- .../test_body_updates/test_tutorial001.py | 264 +- .../test_tutorial001_py310.py | 266 +- .../test_tutorial001_py39.py | 266 +- .../test_tutorial001.py | 57 +- .../test_cookie_params/test_tutorial001.py | 140 +- .../test_cookie_params/test_tutorial001_an.py | 140 +- .../test_tutorial001_an_py310.py | 143 +- .../test_tutorial001_an_py39.py | 143 +- .../test_tutorial001_py310.py | 143 +- .../test_custom_response/test_tutorial001.py | 48 +- .../test_custom_response/test_tutorial001b.py | 48 +- .../test_custom_response/test_tutorial004.py | 47 +- .../test_custom_response/test_tutorial005.py | 48 +- .../test_custom_response/test_tutorial006.py | 47 +- .../test_custom_response/test_tutorial006b.py | 37 +- .../test_custom_response/test_tutorial006c.py | 37 +- .../test_dataclasses/test_tutorial001.py | 166 +- .../test_dataclasses/test_tutorial003.py | 262 +- .../test_dependencies/test_tutorial001.py | 269 +- .../test_dependencies/test_tutorial001_an.py | 269 +- .../test_tutorial001_an_py310.py | 271 +- .../test_tutorial001_an_py39.py | 271 +- .../test_tutorial001_py310.py | 271 +- .../test_dependencies/test_tutorial004.py | 176 +- .../test_dependencies/test_tutorial004_an.py | 176 +- .../test_tutorial004_an_py310.py | 178 +- .../test_tutorial004_an_py39.py | 178 +- .../test_tutorial004_py310.py | 178 +- .../test_dependencies/test_tutorial006.py | 156 +- .../test_dependencies/test_tutorial006_an.py | 156 +- .../test_tutorial006_an_py39.py | 158 +- .../test_dependencies/test_tutorial012.py | 228 +- .../test_dependencies/test_tutorial012_an.py | 228 +- .../test_tutorial012_an_py39.py | 230 +- .../test_events/test_tutorial001.py | 144 +- .../test_events/test_tutorial002.py | 46 +- .../test_events/test_tutorial003.py | 144 +- .../test_tutorial001.py | 68 +- .../test_extra_data_types/test_tutorial001.py | 223 +- .../test_tutorial001_an.py | 223 +- .../test_tutorial001_an_py310.py | 224 +- .../test_tutorial001_an_py39.py | 224 +- .../test_tutorial001_py310.py | 224 +- .../test_extra_models/test_tutorial003.py | 202 +- .../test_tutorial003_py310.py | 204 +- .../test_extra_models/test_tutorial004.py | 90 +- .../test_tutorial004_py39.py | 92 +- .../test_extra_models/test_tutorial005.py | 64 +- .../test_tutorial005_py39.py | 66 +- .../test_first_steps/test_tutorial001.py | 43 +- .../test_generate_clients/test_tutorial003.py | 319 +- .../test_handling_errors/test_tutorial001.py | 144 +- .../test_handling_errors/test_tutorial002.py | 144 +- .../test_handling_errors/test_tutorial003.py | 144 +- .../test_handling_errors/test_tutorial004.py | 144 +- .../test_handling_errors/test_tutorial005.py | 162 +- .../test_handling_errors/test_tutorial006.py | 144 +- .../test_header_params/test_tutorial001.py | 140 +- .../test_header_params/test_tutorial001_an.py | 140 +- .../test_tutorial001_an_py310.py | 140 +- .../test_tutorial001_py310.py | 140 +- .../test_header_params/test_tutorial002.py | 140 +- .../test_header_params/test_tutorial002_an.py | 140 +- .../test_tutorial002_an_py310.py | 140 +- .../test_tutorial002_an_py39.py | 143 +- .../test_tutorial002_py310.py | 143 +- .../test_metadata/test_tutorial001.py | 76 +- .../test_metadata/test_tutorial004.py | 104 +- .../test_tutorial001.py | 311 +- .../test_tutorial001.py | 48 +- .../test_tutorial002.py | 48 +- .../test_tutorial003.py | 22 +- .../test_tutorial004.py | 190 +- .../test_tutorial005.py | 48 +- .../test_tutorial006.py | 80 +- .../test_tutorial007.py | 88 +- .../test_tutorial002b.py | 76 +- .../test_tutorial005.py | 190 +- .../test_tutorial005_py310.py | 192 +- .../test_tutorial005_py39.py | 192 +- .../test_tutorial006.py | 104 +- .../test_path_params/test_tutorial004.py | 144 +- .../test_path_params/test_tutorial005.py | 229 +- .../test_query_params/test_tutorial005.py | 151 +- .../test_query_params/test_tutorial006.py | 179 +- .../test_tutorial006_py310.py | 181 +- .../test_tutorial010.py | 162 +- .../test_tutorial010_an.py | 162 +- .../test_tutorial010_an_py310.py | 164 +- .../test_tutorial010_an_py39.py | 164 +- .../test_tutorial010_py310.py | 164 +- .../test_tutorial011.py | 152 +- .../test_tutorial011_an.py | 152 +- .../test_tutorial011_an_py310.py | 154 +- .../test_tutorial011_an_py39.py | 154 +- .../test_tutorial011_py310.py | 154 +- .../test_tutorial011_py39.py | 154 +- .../test_tutorial012.py | 154 +- .../test_tutorial012_an.py | 154 +- .../test_tutorial012_an_py39.py | 156 +- .../test_tutorial012_py39.py | 156 +- .../test_tutorial013.py | 154 +- .../test_tutorial013_an.py | 154 +- .../test_tutorial013_an_py39.py | 156 +- .../test_tutorial014.py | 129 +- .../test_tutorial014_an.py | 129 +- .../test_tutorial014_an_py310.py | 130 +- .../test_tutorial014_an_py39.py | 130 +- .../test_tutorial014_py310.py | 130 +- .../test_request_files/test_tutorial001.py | 244 +- .../test_request_files/test_tutorial001_02.py | 236 +- .../test_tutorial001_02_an.py | 236 +- .../test_tutorial001_02_an_py310.py | 238 +- .../test_tutorial001_02_an_py39.py | 238 +- .../test_tutorial001_02_py310.py | 238 +- .../test_request_files/test_tutorial001_03.py | 264 +- .../test_tutorial001_03_an.py | 264 +- .../test_tutorial001_03_an_py39.py | 266 +- .../test_request_files/test_tutorial001_an.py | 244 +- .../test_tutorial001_an_py39.py | 246 +- .../test_request_files/test_tutorial002.py | 284 +- .../test_request_files/test_tutorial002_an.py | 284 +- .../test_tutorial002_an_py39.py | 286 +- .../test_tutorial002_py39.py | 286 +- .../test_request_files/test_tutorial003.py | 288 +- .../test_request_files/test_tutorial003_an.py | 288 +- .../test_tutorial003_an_py39.py | 290 +- .../test_tutorial003_py39.py | 290 +- .../test_request_forms/test_tutorial001.py | 166 +- .../test_request_forms/test_tutorial001_an.py | 166 +- .../test_tutorial001_an_py39.py | 168 +- .../test_tutorial001.py | 172 +- .../test_tutorial001_an.py | 172 +- .../test_tutorial001_an_py39.py | 174 +- .../test_response_model/test_tutorial003.py | 202 +- .../test_tutorial003_01.py | 202 +- .../test_tutorial003_01_py310.py | 204 +- .../test_tutorial003_02.py | 152 +- .../test_tutorial003_03.py | 48 +- .../test_tutorial003_05.py | 152 +- .../test_tutorial003_05_py310.py | 154 +- .../test_tutorial003_py310.py | 204 +- .../test_response_model/test_tutorial004.py | 186 +- .../test_tutorial004_py310.py | 188 +- .../test_tutorial004_py39.py | 188 +- .../test_response_model/test_tutorial005.py | 242 +- .../test_tutorial005_py310.py | 244 +- .../test_response_model/test_tutorial006.py | 242 +- .../test_tutorial006_py310.py | 244 +- .../test_tutorial004.py | 230 +- .../test_tutorial004_an.py | 230 +- .../test_tutorial004_an_py310.py | 232 +- .../test_tutorial004_an_py39.py | 232 +- .../test_tutorial004_py310.py | 232 +- .../test_security/test_tutorial001.py | 66 +- .../test_security/test_tutorial001_an.py | 66 +- .../test_security/test_tutorial001_an_py39.py | 68 +- .../test_security/test_tutorial003.py | 220 +- .../test_security/test_tutorial003_an.py | 220 +- .../test_tutorial003_an_py310.py | 222 +- .../test_security/test_tutorial003_an_py39.py | 222 +- .../test_security/test_tutorial003_py310.py | 222 +- .../test_security/test_tutorial005.py | 344 +- .../test_security/test_tutorial005_an.py | 344 +- .../test_tutorial005_an_py310.py | 346 +- .../test_security/test_tutorial005_an_py39.py | 346 +- .../test_security/test_tutorial005_py310.py | 346 +- .../test_security/test_tutorial005_py39.py | 346 +- .../test_security/test_tutorial006.py | 56 +- .../test_security/test_tutorial006_an.py | 56 +- .../test_security/test_tutorial006_an_py39.py | 58 +- .../test_sql_databases/test_sql_databases.py | 582 +- .../test_sql_databases_middleware.py | 582 +- .../test_sql_databases_middleware_py310.py | 584 +- .../test_sql_databases_middleware_py39.py | 584 +- .../test_sql_databases_py310.py | 584 +- .../test_sql_databases_py39.py | 584 +- .../test_sql_databases_peewee.py | 678 +- tests/test_tutorial/test_testing/test_main.py | 44 +- .../test_testing/test_tutorial001.py | 44 +- tests/test_union_body.py | 178 +- tests/test_union_inherited_body.py | 185 +- 280 files changed, 34572 insertions(+), 33644 deletions(-) diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 016c1f734..516a569e4 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -19,92 +19,91 @@ def foo(items: Items): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/foo": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Foo", - "operationId": "foo_foo_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Items"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Items": { - "title": "Items", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "object", - "additionalProperties": {"type": "integer"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_additional_properties_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_additional_properties_post(): response = client.post("/foo", json={"items": {"foo": 1, "bar": 2}}) assert response.status_code == 200, response.text assert response.json() == {"foo": 1, "bar": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/foo": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Foo", + "operationId": "foo_foo_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Items"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Items": { + "title": "Items", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "object", + "additionalProperties": {"type": "integer"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_response_extra.py b/tests/test_additional_response_extra.py index 1df1891e0..d62638c8f 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -17,36 +17,33 @@ router.include_router(sub_router, prefix="/items") app.include_router(router) - -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Item", - "operationId": "read_item_items__get", - } - } - }, -} - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_path_operation(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == {"id": "foo"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Item", + "operationId": "read_item_items__get", + } + } + }, + } diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index a1072cc56..5c08eaa6d 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -25,114 +25,116 @@ def main_route(callback_url: HttpUrl): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "post": { - "summary": "Main Route", - "operationId": "main_route__post", - "parameters": [ - { - "required": True, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, - "name": "callback_url", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "callbacks": { - "callback_route": { - "{$callback_url}/callback/": { - "get": { - "summary": "Callback Route", - "operationId": "callback_route__callback_url__callback__get", - "responses": { - "400": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CustomModel" - } - } - }, - "description": "Bad Request", - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "CustomModel": { - "title": "CustomModel", - "required": ["a"], - "type": "object", - "properties": {"a": {"title": "A", "type": "integer"}}, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Main Route", + "operationId": "main_route__post", + "parameters": [ + { + "required": True, + "schema": { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + }, + "name": "callback_url", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "callback_route": { + "{$callback_url}/callback/": { + "get": { + "summary": "Callback Route", + "operationId": "callback_route__callback_url__callback__get", + "responses": { + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomModel" + } + } + }, + "description": "Bad Request", + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + }, + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "CustomModel": { + "title": "CustomModel", + "required": ["a"], + "type": "object", + "properties": {"a": {"title": "A", "type": "integer"}}, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 811fe6922..052602768 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -30,71 +30,70 @@ async def a(id): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/{id}": { - "get": { - "responses": { - "422": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index cabb536d7..58de46ff6 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -9,77 +9,76 @@ async def a(id): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/{id}": { - "get": { - "responses": { - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a__id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Id"}, - "name": "id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/{id}": { + "get": { + "responses": { + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a__id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Id"}, + "name": "id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index aa549b163..6746760f0 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -35,83 +35,82 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/vnd.api+json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/vnd.api+json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index fe4956f8f..58d54b733 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -53,103 +53,10 @@ async def d(): app.include_router(router) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "501": {"description": "Error 1"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "502": {"description": "Error 2"}, - "4XX": {"description": "Error with range, upper"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - "/c": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": {"description": "Error with range, lower"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": {"description": "A default response"}, - }, - "summary": "C", - "operationId": "c_c_get", - } - }, - "/d": { - "get": { - "responses": { - "400": {"description": "Error with str"}, - "5XX": { - "description": "Server Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ResponseModel"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "default": { - "description": "Default Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ResponseModel"} - } - }, - }, - }, - "summary": "D", - "operationId": "d_d_get", - } - }, - }, - "components": { - "schemas": { - "ResponseModel": { - "title": "ResponseModel", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - } - } - }, -} client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_a(): response = client.get("/a") assert response.status_code == 200, response.text @@ -172,3 +79,99 @@ def test_d(): response = client.get("/d") assert response.status_code == 200, response.text assert response.json() == "d" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "501": {"description": "Error 1"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "502": {"description": "Error 2"}, + "4XX": {"description": "Error with range, upper"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + "/c": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": {"description": "Error with range, lower"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": {"description": "A default response"}, + }, + "summary": "C", + "operationId": "c_c_get", + } + }, + "/d": { + "get": { + "responses": { + "400": {"description": "Error with str"}, + "5XX": { + "description": "Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "default": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseModel" + } + } + }, + }, + }, + "summary": "D", + "operationId": "d_d_get", + } + }, + }, + "components": { + "schemas": { + "ResponseModel": { + "title": "ResponseModel", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + } + } + }, + } diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 30c8efe01..a4f42b038 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -28,161 +28,6 @@ async def unrelated(foo: Annotated[str, object()]): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/default": { - "get": { - "summary": "Default", - "operationId": "default_default_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Foo", "type": "string", "default": "foo"}, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/required": { - "get": { - "summary": "Required", - "operationId": "required_required_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Foo", "minLength": 1, "type": "string"}, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/multiple": { - "get": { - "summary": "Multiple", - "operationId": "multiple_multiple_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Foo", "minLength": 1, "type": "string"}, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/unrelated": { - "get": { - "summary": "Unrelated", - "operationId": "unrelated_unrelated_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Foo", "type": "string"}, - "name": "foo", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} foo_is_missing = { "detail": [ { @@ -217,7 +62,6 @@ foo_is_short = { ("/multiple?foo=", 422, foo_is_short), ("/unrelated?foo=bar", 200, {"foo": "bar"}), ("/unrelated", 422, foo_is_missing), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response): @@ -227,11 +71,14 @@ def test_get(path, expected_status, expected_response): def test_multiple_path(): + app = FastAPI() + @app.get("/test1") @app.get("/test2") async def test(var: Annotated[str, Query()] = "bar"): return {"foo": var} + client = TestClient(app) response = client.get("/test1") assert response.status_code == 200 assert response.json() == {"foo": "bar"} @@ -265,3 +112,177 @@ def test_nested_router(): response = client.get("/nested/test") assert response.status_code == 200 assert response.json() == {"foo": "bar"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/default": { + "get": { + "summary": "Default", + "operationId": "default_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Foo", + "type": "string", + "default": "foo", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/required": { + "get": { + "summary": "Required", + "operationId": "required_required_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/multiple": { + "get": { + "summary": "Multiple", + "operationId": "multiple_multiple_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Foo", + "minLength": 1, + "type": "string", + }, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/unrelated": { + "get": { + "summary": "Unrelated", + "operationId": "unrelated_unrelated_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Foo", "type": "string"}, + "name": "foo", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_application.py b/tests/test_application.py index a4f13e12d..e5f2f4387 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -5,1128 +5,6 @@ from .main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/api_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Operation", - "operationId": "non_operation_api_route_get", - } - }, - "/non_decorated_route": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Non Decorated Route", - "operationId": "non_decorated_route_non_decorated_route_get", - } - }, - "/text": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get Text", - "operationId": "get_text_text_get", - } - }, - "/path/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Id", - "operationId": "get_id_path__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/str/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Str Id", - "operationId": "get_str_id_path_str__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Int Id", - "operationId": "get_int_id_path_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/float/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Float Id", - "operationId": "get_float_id_path_float__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "number"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/bool/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Bool Id", - "operationId": "get_bool_id_path_bool__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "boolean"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Id", - "operationId": "get_path_param_id_path_param__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-minlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Length", - "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Max Length", - "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-min_maxlength/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Min Max Length", - "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maxLength": 3, - "minLength": 2, - "type": "string", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt", - "operationId": "get_path_param_gt_path_param_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt0", - "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge", - "operationId": "get_path_param_ge_path_param_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt", - "operationId": "get_path_param_lt_path_param_lt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt0/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt0", - "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 0.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le", - "operationId": "get_path_param_le_path_param_le__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt", - "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge", - "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "number", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Int", - "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Gt Int", - "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMinimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Int", - "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Ge Int", - "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "minimum": 3.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-lt-gt-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Lt Gt Int", - "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "exclusiveMaximum": 3.0, - "exclusiveMinimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/path/param-le-ge-int/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Path Param Le Ge Int", - "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "maximum": 3.0, - "minimum": 1.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/query": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query", - "operationId": "get_query_query_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Optional", - "operationId": "get_query_optional_query_optional_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type", - "operationId": "get_query_type_query_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int/optional": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Optional", - "operationId": "get_query_type_optional_query_int_optional_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/int/default": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Type Int Default", - "operationId": "get_query_type_int_default_query_int_default_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query", "type": "integer", "default": 10}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param", - "operationId": "get_query_param_query_param_get", - "parameters": [ - { - "required": False, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required", - "operationId": "get_query_param_required_query_param_required_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/query/param-required/int": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Query Param Required Type", - "operationId": "get_query_param_required_type_query_param_required_int_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Query", "type": "integer"}, - "name": "query", - "in": "query", - } - ], - } - }, - "/enum-status-code": { - "get": { - "responses": { - "201": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "Get Enum Status Code", - "operationId": "get_enum_status_code_enum_status_code_get", - } - }, - "/query/frozenset": { - "get": { - "summary": "Get Query Type Frozenset", - "operationId": "get_query_type_frozenset_query_frozenset_get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Query", - "uniqueItems": True, - "type": "array", - "items": {"type": "integer"}, - }, - "name": "query", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -1134,7 +12,6 @@ openapi_schema = { ("/api_route", 200, {"message": "Hello World"}), ("/non_decorated_route", 200, {"message": "Hello World"}), ("/nonexistent", 404, {"detail": "Not Found"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): @@ -1172,3 +49,1135 @@ def test_enum_status_code_response(): response = client.get("/enum-status-code") assert response.status_code == 201, response.text assert response.json() == "foo bar" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/api_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Operation", + "operationId": "non_operation_api_route_get", + } + }, + "/non_decorated_route": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Non Decorated Route", + "operationId": "non_decorated_route_non_decorated_route_get", + } + }, + "/text": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get Text", + "operationId": "get_text_text_get", + } + }, + "/path/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Id", + "operationId": "get_id_path__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/str/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Str Id", + "operationId": "get_str_id_path_str__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Int Id", + "operationId": "get_int_id_path_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/float/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Float Id", + "operationId": "get_float_id_path_float__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "number"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/bool/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Bool Id", + "operationId": "get_bool_id_path_bool__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "boolean"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Id", + "operationId": "get_path_param_id_path_param__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-minlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Length", + "operationId": "get_path_param_min_length_path_param_minlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Max Length", + "operationId": "get_path_param_max_length_path_param_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-min_maxlength/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Min Max Length", + "operationId": "get_path_param_min_max_length_path_param_min_maxlength__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maxLength": 3, + "minLength": 2, + "type": "string", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt", + "operationId": "get_path_param_gt_path_param_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt0", + "operationId": "get_path_param_gt0_path_param_gt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge", + "operationId": "get_path_param_ge_path_param_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt", + "operationId": "get_path_param_lt_path_param_lt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt0/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt0", + "operationId": "get_path_param_lt0_path_param_lt0__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 0.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le", + "operationId": "get_path_param_le_path_param_le__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt", + "operationId": "get_path_param_lt_gt_path_param_lt_gt__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge", + "operationId": "get_path_param_le_ge_path_param_le_ge__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "number", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Int", + "operationId": "get_path_param_lt_int_path_param_lt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Gt Int", + "operationId": "get_path_param_gt_int_path_param_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMinimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Int", + "operationId": "get_path_param_le_int_path_param_le_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Ge Int", + "operationId": "get_path_param_ge_int_path_param_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "minimum": 3.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-lt-gt-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Lt Gt Int", + "operationId": "get_path_param_lt_gt_int_path_param_lt_gt_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "exclusiveMaximum": 3.0, + "exclusiveMinimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/path/param-le-ge-int/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Path Param Le Ge Int", + "operationId": "get_path_param_le_ge_int_path_param_le_ge_int__item_id__get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "maximum": 3.0, + "minimum": 1.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/query": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query", + "operationId": "get_query_query_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Optional", + "operationId": "get_query_optional_query_optional_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type", + "operationId": "get_query_type_query_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int/optional": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Optional", + "operationId": "get_query_type_optional_query_int_optional_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/int/default": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Type Int Default", + "operationId": "get_query_type_int_default_query_int_default_get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Query", + "type": "integer", + "default": 10, + }, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param", + "operationId": "get_query_param_query_param_get", + "parameters": [ + { + "required": False, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required", + "operationId": "get_query_param_required_query_param_required_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/query/param-required/int": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Query Param Required Type", + "operationId": "get_query_param_required_type_query_param_required_int_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Query", "type": "integer"}, + "name": "query", + "in": "query", + } + ], + } + }, + "/enum-status-code": { + "get": { + "responses": { + "201": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Get Enum Status Code", + "operationId": "get_enum_status_code_enum_status_code_get", + } + }, + "/query/frozenset": { + "get": { + "summary": "Get Query Type Frozenset", + "operationId": "get_query_type_frozenset_query_frozenset_get", + "parameters": [ + { + "required": True, + "schema": { + "title": "Query", + "uniqueItems": True, + "type": "array", + "items": {"type": "integer"}, + }, + "name": "query", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index 2e8d9c6de..d1b18ef1d 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -46,49 +46,6 @@ app.include_router(router=router_a, prefix="/a") client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get A", - "operationId": "get_a_a__get", - } - }, - "/a/b/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get B", - "operationId": "get_b_a_b__get", - } - }, - "/a/b/c/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Get C", - "operationId": "get_c_a_b_c__get", - } - }, - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -96,7 +53,6 @@ openapi_schema = { ("/a", 200, {"msg": "A"}), ("/a/b", 200, {"msg": "B"}), ("/a/b/c", 200, {"msg": "C"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): @@ -113,3 +69,50 @@ def test_route_classes(): assert getattr(routes["/a/"], "x_type") == "A" # noqa: B009 assert getattr(routes["/a/b/"], "x_type") == "B" # noqa: B009 assert getattr(routes["/a/b/c/"], "x_type") == "C" # noqa: B009 + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get A", + "operationId": "get_a_a__get", + } + }, + "/a/b/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get B", + "operationId": "get_b_a_b__get", + } + }, + "/a/b/c/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Get C", + "operationId": "get_c_a_b_c__get", + } + }, + }, + } diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 33899134e..285fdf1ab 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -44,156 +44,6 @@ async def no_duplicates_sub( return [item, sub_items] -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/with-duplicates": { - "post": { - "summary": "With Duplicates", - "operationId": "with_duplicates_with_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/no-duplicates": { - "post": { - "summary": "No Duplicates", - "operationId": "no_duplicates_no_duplicates_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/with-duplicates-sub": { - "post": { - "summary": "No Duplicates Sub", - "operationId": "no_duplicates_sub_with_duplicates_sub_post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_no_duplicates_no_duplicates_post": { - "title": "Body_no_duplicates_no_duplicates_post", - "required": ["item", "item2"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "item2": {"$ref": "#/components/schemas/Item"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text @@ -230,3 +80,152 @@ def test_sub_duplicates(): {"data": "myitem"}, [{"data": "myitem"}, {"data": "myitem"}], ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/with-duplicates": { + "post": { + "summary": "With Duplicates", + "operationId": "with_duplicates_with_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/no-duplicates": { + "post": { + "summary": "No Duplicates", + "operationId": "no_duplicates_no_duplicates_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_no_duplicates_no_duplicates_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/with-duplicates-sub": { + "post": { + "summary": "No Duplicates Sub", + "operationId": "no_duplicates_sub_with_duplicates_sub_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_no_duplicates_no_duplicates_post": { + "title": "Body_no_duplicates_no_duplicates_post", + "required": ["item", "item2"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "item2": {"$ref": "#/components/schemas/Item"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py index a3355256f..688b9837f 100644 --- a/tests/test_deprecated_openapi_prefix.py +++ b/tests/test_deprecated_openapi_prefix.py @@ -11,34 +11,32 @@ def read_main(request: Request): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_main(): response = client.get("/app") assert response.status_code == 200 assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/api/v1"}], + } diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index f077dfea0..116b2006a 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -23,60 +23,57 @@ def f(): return {"c": {}, "d": {"a": {}}} -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "F", - "operationId": "f__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model3"} - } - }, - } - }, - } - } - }, - "components": { - "schemas": { - "Model": {"title": "Model", "type": "object", "properties": {}}, - "Model2": { - "title": "Model2", - "required": ["a"], - "type": "object", - "properties": {"a": {"$ref": "#/components/schemas/Model"}}, - }, - "Model3": { - "title": "Model3", - "required": ["c", "d"], - "type": "object", - "properties": { - "c": {"$ref": "#/components/schemas/Model"}, - "d": {"$ref": "#/components/schemas/Model2"}, - }, - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_api_route(): response = client.get("/") assert response.status_code == 200, response.text assert response.json() == {"c": {}, "d": {"a": {}}} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "F", + "operationId": "f__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model3"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Model": {"title": "Model", "type": "object", "properties": {}}, + "Model2": { + "title": "Model2", + "required": ["a"], + "type": "object", + "properties": {"a": {"$ref": "#/components/schemas/Model"}}, + }, + "Model3": { + "title": "Model3", + "required": ["c", "d"], + "type": "object", + "properties": { + "c": {"$ref": "#/components/schemas/Model"}, + "d": {"$ref": "#/components/schemas/Model2"}, + }, + }, + } + }, + } diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index e979628a5..c0db62c19 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -52,273 +52,6 @@ def trace_item(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Items", - "operationId": "get_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "delete": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Delete Item", - "operationId": "delete_item_items__item_id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - "options": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Options Item", - "operationId": "options_item_items__item_id__options", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "head": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Head Item", - "operationId": "head_item_items__item_id__head", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "patch": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Patch Item", - "operationId": "patch_item_items__item_id__patch", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - "trace": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Trace Item", - "operationId": "trace_item_items__item_id__trace", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - }, - "/items-not-decorated/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Not Decorated", - "operationId": "get_not_decorated_items_not_decorated__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_api_route(): response = client.get("/items/foo") @@ -360,3 +93,270 @@ def test_trace(): response = client.request("trace", "/items/foo") assert response.status_code == 200, response.text assert response.headers["content-type"] == "message/http" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Items", + "operationId": "get_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "delete": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Delete Item", + "operationId": "delete_item_items__item_id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + "options": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Options Item", + "operationId": "options_item_items__item_id__options", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "head": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Head Item", + "operationId": "head_item_items__item_id__head", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "patch": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Patch Item", + "operationId": "patch_item_items__item_id__patch", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + "trace": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Trace Item", + "operationId": "trace_item_items__item_id__trace", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + }, + "/items-not-decorated/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Get Not Decorated", + "operationId": "get_not_decorated_items_not_decorated__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model.py index 8814356a1..15b15f862 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model.py @@ -40,99 +40,6 @@ async def get_model_a(name: str, model_c=Depends(get_model_c)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model/{name}": { - "get": { - "summary": "Get Model A", - "operationId": "get_model_a_model__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelA"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelA": { - "title": "ModelA", - "required": ["name", "model_b"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "model_b": {"$ref": "#/components/schemas/ModelB"}, - }, - }, - "ModelB": { - "title": "ModelB", - "required": ["username"], - "type": "object", - "properties": {"username": {"title": "Username", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_filter_sub_model(): response = client.get("/model/modelA") assert response.status_code == 200, response.text @@ -153,3 +60,95 @@ def test_validator_is_cloned(): "type": "value_error", } ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelA"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "model_b"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "model_b": {"$ref": "#/components/schemas/ModelB"}, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": {"username": {"title": "Username", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index 52a052faa..541147fa8 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -19,90 +19,89 @@ async def create_item(product: Product): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/product": { - "get": { - "summary": "Create Item", - "operationId": "create_item_product_get", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Product"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Product": { - "title": "Product", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_get_with_body(): + body = {"name": "Foo", "description": "Some description", "price": 5.5} + response = client.request("GET", "/product", json=body) + assert response.json() == body def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_with_body(): - body = {"name": "Foo", "description": "Some description", "price": 5.5} - response = client.request("GET", "/product", json=body) - assert response.json() == body + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/product": { + "get": { + "summary": "Create Item", + "operationId": "create_item_product_get", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Product"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index ccb6c7229..ced56c84d 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -343,16 +343,6 @@ app.include_router(router2_default) client = TestClient(app) -def test_openapi(): - client = TestClient(app) - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - response = client.get("/openapi.json") - assert issubclass(w[-1].category, UserWarning) - assert "Duplicate Operation ID" in str(w[-1].message) - assert response.json() == openapi_schema - - def test_level1_override(): response = client.get("/override1?level1=foo") assert response.json() == "foo" @@ -445,6179 +435,6863 @@ def test_paths_level5(override1, override2, override3, override4, override5): assert not override5 or "x-level5" in response.headers -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/override1": { - "get": { - "tags": ["path1a", "path1b"], - "summary": "Path1 Override", - "operationId": "path1_override_override1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, +def test_openapi(): + client = TestClient(app) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + response = client.get("/openapi.json") + assert issubclass(w[-1].category, UserWarning) + assert "Duplicate Operation ID" in str(w[-1].message) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/override1": { + "get": { + "tags": ["path1a", "path1b"], + "summary": "Path1 Override", + "operationId": "path1_override_override1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, } } }, }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default1": { - "get": { - "summary": "Path1 Default", - "operationId": "path1_default_default1_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level1", "type": "string"}, - "name": "level1", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level1/level2/override3": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path3a", - "path3b", - ], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level1_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default3": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level1_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level3/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level3a", - "level3b", - ], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level2a", - "level2b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level2/default5": { - "get": { - "tags": ["level1a", "level1b", "level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override3": { - "get": { - "tags": ["level1a", "level1b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_level1_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default3": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_level1_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level3/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/level4/default5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level3/default5": { - "get": { - "tags": ["level1a", "level1b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level1/level4/override5": { - "get": { - "tags": [ - "level1a", - "level1b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level1_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/level4/default5": { - "get": { - "tags": ["level1a", "level1b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level1_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/override5": { - "get": { - "tags": ["level1a", "level1b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level1_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level1/default5": { - "get": { - "tags": ["level1a", "level1b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level1_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-1": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "401": {"description": "Client error level 1"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "501": {"description": "Server error level 1"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback1": { - "/": { - "get": { - "summary": "Callback1", - "operationId": "callback1__get", - "parameters": [ - { - "name": "level1", - "in": "query", - "required": True, - "schema": {"title": "Level1", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level2/override3": { - "get": { - "tags": ["level2a", "level2b", "path3a", "path3b"], - "summary": "Path3 Override Router2 Override", - "operationId": "path3_override_router2_override_level2_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default3": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path3 Default Router2 Override", - "operationId": "path3_default_router2_override_level2_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/level4/default5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "level4a", - "level4b", - ], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level3a", - "level3b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level3/default5": { - "get": { - "tags": ["level2a", "level2b", "level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/override5": { - "get": { - "tags": [ - "level2a", - "level2b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level2_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/level4/default5": { - "get": { - "tags": ["level2a", "level2b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level2_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/override5": { - "get": { - "tags": ["level2a", "level2b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level2_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level2/default5": { - "get": { - "tags": ["level2a", "level2b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level2_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-2": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "402": {"description": "Client error level 2"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "502": {"description": "Server error level 2"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback2": { - "/": { - "get": { - "summary": "Callback2", - "operationId": "callback2__get", - "parameters": [ - { - "name": "level2", - "in": "query", - "required": True, - "schema": {"title": "Level2", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override3": { - "get": { - "tags": ["path3a", "path3b"], - "summary": "Path3 Override Router2 Default", - "operationId": "path3_override_router2_default_override3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default3": { - "get": { - "summary": "Path3 Default Router2 Default", - "operationId": "path3_default_router2_default_default3_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level3", "type": "string"}, - "name": "level3", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - "/level3/level4/override5": { - "get": { - "tags": [ - "level3a", - "level3b", - "level4a", - "level4b", - "path5a", - "path5b", - ], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level3_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/level4/default5": { - "get": { - "tags": ["level3a", "level3b", "level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level3_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/override5": { - "get": { - "tags": ["level3a", "level3b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_level3_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level3/default5": { - "get": { - "tags": ["level3a", "level3b"], - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_level3_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-3": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "403": {"description": "Client error level 3"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "503": {"description": "Server error level 3"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback3": { - "/": { - "get": { - "summary": "Callback3", - "operationId": "callback3__get", - "parameters": [ - { - "name": "level3", - "in": "query", - "required": True, - "schema": {"title": "Level3", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - }, - "/level4/override5": { - "get": { - "tags": ["level4a", "level4b", "path5a", "path5b"], - "summary": "Path5 Override Router4 Override", - "operationId": "path5_override_router4_override_level4_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/level4/default5": { - "get": { - "tags": ["level4a", "level4b"], - "summary": "Path5 Default Router4 Override", - "operationId": "path5_default_router4_override_level4_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-4": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "404": {"description": "Client error level 4"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "504": {"description": "Server error level 4"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback4": { - "/": { - "get": { - "summary": "Callback4", - "operationId": "callback4__get", - "parameters": [ - { - "name": "level4", - "in": "query", - "required": True, - "schema": {"title": "Level4", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/override5": { - "get": { - "tags": ["path5a", "path5b"], - "summary": "Path5 Override Router4 Default", - "operationId": "path5_override_router4_default_override5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-5": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "405": {"description": "Client error level 5"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - "505": {"description": "Server error level 5"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "callback5": { - "/": { - "get": { - "summary": "Callback5", - "operationId": "callback5__get", - "parameters": [ - { - "name": "level5", - "in": "query", - "required": True, - "schema": {"title": "Level5", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - "deprecated": True, - } - }, - "/default5": { - "get": { - "summary": "Path5 Default Router4 Default", - "operationId": "path5_default_router4_default_default5_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Level5", "type": "string"}, - "name": "level5", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/x-level-0": {"schema": {}}}, - }, - "400": {"description": "Client error level 0"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - "500": {"description": "Server error level 0"}, - }, - "callbacks": { - "callback0": { - "/": { - "get": { - "summary": "Callback0", - "operationId": "callback0__get", - "parameters": [ - { - "name": "level0", - "in": "query", - "required": True, - "schema": {"title": "Level0", "type": "string"}, - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, + "deprecated": True, + } }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, + "/default1": { + "get": { + "summary": "Path1 Default", + "operationId": "path1_default_default1_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level1", "type": "string"}, + "name": "level1", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } }, - } - }, -} + "/level1/level2/override3": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path3a", + "path3b", + ], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level1_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/default3": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level1_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level3/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level3a", + "level3b", + ], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level2a", + "level2b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level2/default5": { + "get": { + "tags": ["level1a", "level1b", "level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override3": { + "get": { + "tags": ["level1a", "level1b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_level1_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default3": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_level1_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level3/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/level4/default5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level3/default5": { + "get": { + "tags": ["level1a", "level1b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level1/level4/override5": { + "get": { + "tags": [ + "level1a", + "level1b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level1_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/level4/default5": { + "get": { + "tags": ["level1a", "level1b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level1_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/override5": { + "get": { + "tags": ["level1a", "level1b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level1_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level1/default5": { + "get": { + "tags": ["level1a", "level1b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level1_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-1": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "401": {"description": "Client error level 1"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "501": {"description": "Server error level 1"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback1": { + "/": { + "get": { + "summary": "Callback1", + "operationId": "callback1__get", + "parameters": [ + { + "name": "level1", + "in": "query", + "required": True, + "schema": { + "title": "Level1", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level2/override3": { + "get": { + "tags": ["level2a", "level2b", "path3a", "path3b"], + "summary": "Path3 Override Router2 Override", + "operationId": "path3_override_router2_override_level2_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default3": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path3 Default Router2 Override", + "operationId": "path3_default_router2_override_level2_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/level4/default5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "level4a", + "level4b", + ], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level3a", + "level3b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level3/default5": { + "get": { + "tags": ["level2a", "level2b", "level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/override5": { + "get": { + "tags": [ + "level2a", + "level2b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level2_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/level4/default5": { + "get": { + "tags": ["level2a", "level2b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level2_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/override5": { + "get": { + "tags": ["level2a", "level2b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level2_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level2/default5": { + "get": { + "tags": ["level2a", "level2b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level2_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-2": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "402": {"description": "Client error level 2"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "502": {"description": "Server error level 2"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback2": { + "/": { + "get": { + "summary": "Callback2", + "operationId": "callback2__get", + "parameters": [ + { + "name": "level2", + "in": "query", + "required": True, + "schema": { + "title": "Level2", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override3": { + "get": { + "tags": ["path3a", "path3b"], + "summary": "Path3 Override Router2 Default", + "operationId": "path3_override_router2_default_override3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default3": { + "get": { + "summary": "Path3 Default Router2 Default", + "operationId": "path3_default_router2_default_default3_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level3", "type": "string"}, + "name": "level3", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + "/level3/level4/override5": { + "get": { + "tags": [ + "level3a", + "level3b", + "level4a", + "level4b", + "path5a", + "path5b", + ], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level3_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/level4/default5": { + "get": { + "tags": ["level3a", "level3b", "level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level3_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/override5": { + "get": { + "tags": ["level3a", "level3b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_level3_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level3/default5": { + "get": { + "tags": ["level3a", "level3b"], + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_level3_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-3": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "403": {"description": "Client error level 3"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "503": {"description": "Server error level 3"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback3": { + "/": { + "get": { + "summary": "Callback3", + "operationId": "callback3__get", + "parameters": [ + { + "name": "level3", + "in": "query", + "required": True, + "schema": { + "title": "Level3", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + }, + "/level4/override5": { + "get": { + "tags": ["level4a", "level4b", "path5a", "path5b"], + "summary": "Path5 Override Router4 Override", + "operationId": "path5_override_router4_override_level4_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/level4/default5": { + "get": { + "tags": ["level4a", "level4b"], + "summary": "Path5 Default Router4 Override", + "operationId": "path5_default_router4_override_level4_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-4": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "404": {"description": "Client error level 4"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "504": {"description": "Server error level 4"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback4": { + "/": { + "get": { + "summary": "Callback4", + "operationId": "callback4__get", + "parameters": [ + { + "name": "level4", + "in": "query", + "required": True, + "schema": { + "title": "Level4", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/override5": { + "get": { + "tags": ["path5a", "path5b"], + "summary": "Path5 Override Router4 Default", + "operationId": "path5_override_router4_default_override5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-5": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "405": {"description": "Client error level 5"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + "505": {"description": "Server error level 5"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "callback5": { + "/": { + "get": { + "summary": "Callback5", + "operationId": "callback5__get", + "parameters": [ + { + "name": "level5", + "in": "query", + "required": True, + "schema": { + "title": "Level5", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + "deprecated": True, + } + }, + "/default5": { + "get": { + "summary": "Path5 Default Router4 Default", + "operationId": "path5_default_router4_default_default5_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Level5", "type": "string"}, + "name": "level5", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/x-level-0": {"schema": {}}}, + }, + "400": {"description": "Client error level 0"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + "500": {"description": "Server error level 0"}, + }, + "callbacks": { + "callback0": { + "/": { + "get": { + "summary": "Callback0", + "operationId": "callback0__get", + "parameters": [ + { + "name": "level0", + "in": "query", + "required": True, + "schema": { + "title": "Level0", + "type": "string", + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 8b1aea031..1ebcaee8c 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -4,130 +4,6 @@ from .app.main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a/compute": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Compute", - "operationId": "compute_a_compute_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_a_compute_post" - } - } - }, - "required": True, - }, - } - }, - "/b/compute/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Compute", - "operationId": "compute_b_compute__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_compute_b_compute__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_compute_b_compute__post": { - "title": "Body_compute_b_compute__post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "type": "string"}, - }, - }, - "Body_compute_a_compute_post": { - "title": "Body_compute_a_compute_post", - "required": ["a", "b"], - "type": "object", - "properties": { - "a": {"title": "A", "type": "integer"}, - "b": {"title": "B", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_a(): data = {"a": 2, "b": "foo"} @@ -153,3 +29,127 @@ def test_post_b_invalid(): data = {"a": "bar", "b": "foo"} response = client.post("/b/compute/", json=data) assert response.status_code == 422, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a/compute": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Compute", + "operationId": "compute_a_compute_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_compute_a_compute_post" + } + } + }, + "required": True, + }, + } + }, + "/b/compute/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Compute", + "operationId": "compute_b_compute__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_compute_b_compute__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_compute_b_compute__post": { + "title": "Body_compute_b_compute__post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "type": "string"}, + }, + }, + "Body_compute_a_compute_post": { + "title": "Body_compute_a_compute_post", + "required": ["a", "b"], + "type": "object", + "properties": { + "a": {"title": "A", "type": "integer"}, + "b": {"title": "B", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 31308ea85..358684bc5 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -21,85 +21,6 @@ def save_item_no_body(item: List[Item]): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Item", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "age"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "age": {"title": "Age", "exclusiveMinimum": 0.0, "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - single_error = { "detail": [ { @@ -137,12 +58,6 @@ multiple_errors = { } -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text @@ -159,3 +74,92 @@ def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text assert response.json() == multiple_errors + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Item No Body", + "operationId": "save_item_no_body_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Item", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "age"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "age": { + "title": "Age", + "exclusiveMinimum": 0.0, + "type": "number", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index 3da461af5..e7a833f2b 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -14,76 +14,6 @@ def read_items(q: List[int] = Query(default=None)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "integer"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - multiple_errors = { "detail": [ { @@ -100,12 +30,6 @@ multiple_errors = { } -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_multi_query(): response = client.get("/items/?q=5&q=6") assert response.status_code == 200, response.text @@ -116,3 +40,79 @@ def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text assert response.json() == multiple_errors + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "integer"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index d3996f26e..8a9086ebe 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -32,96 +32,95 @@ def route_with_extra_query_parameters(standard_query_param: Optional[int] = 50): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Route With Extra Query Parameters", - "operationId": "route_with_extra_query_parameters__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - }, - "name": "standard_query_param", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Extra Param 1"}, - "name": "extra_param_1", - "in": "query", - }, - { - "required": True, - "schema": {"title": "Extra Param 2"}, - "name": "extra_param_2", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_route(): - response = client.get("/") - assert response.status_code == 200, response.text - assert response.json() == {} + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Route With Extra Query Parameters", + "operationId": "route_with_extra_query_parameters__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Standard Query Param", + "type": "integer", + "default": 50, + }, + "name": "standard_query_param", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Extra Param 1"}, + "name": "extra_param_1", + "in": "query", + }, + { + "required": True, + "schema": {"title": "Extra Param 2"}, + "name": "extra_param_2", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py index 8a1080d69..943dc43f2 100644 --- a/tests/test_openapi_route_extensions.py +++ b/tests/test_openapi_route_extensions.py @@ -12,34 +12,31 @@ def route_with_extras(): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "Route With Extras", - "operationId": "route_with_extras__get", - "x-custom-extension": "value", - } - }, - }, -} +def test_get_route(): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {} def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get_route(): - response = client.get("/") - assert response.status_code == 200, response.text - assert response.json() == {} + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "Route With Extras", + "operationId": "route_with_extras__get", + "x-custom-extension": "value", + } + }, + }, + } diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index a210154f6..26abeaa12 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -21,40 +21,37 @@ def foo(): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/", "description": "Default, relative server"}, - { - "url": "http://staging.localhost.tiangolo.com:8000", - "description": "Staging but actually localhost still", - }, - {"url": "https://prod.example.com"}, - ], - "paths": { - "/foo": { - "get": { - "summary": "Foo", - "operationId": "foo_foo_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_servers(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_app(): response = client.get("/foo") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/", "description": "Default, relative server"}, + { + "url": "http://staging.localhost.tiangolo.com:8000", + "description": "Staging but actually localhost still", + }, + {"url": "https://prod.example.com"}, + ], + "paths": { + "/foo": { + "get": { + "summary": "Foo", + "operationId": "foo_foo_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 4d85afbce..0aef7ac7b 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -15,79 +15,79 @@ async def read_users(user_id: int): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/{user_id}": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_reused_param(): - response = client.get("/openapi.json") - data = response.json() - assert data == openapi_schema - def test_read_users(): response = client.get("/users/42") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + data = response.json() + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/{user_id}": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "integer"}, + "name": "user_id", + "in": "path", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index 3da294ccf..a02d1152c 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -12,79 +12,6 @@ def save_item_no_body(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Save Item No Body", - "operationId": "save_item_no_body_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_put_no_body(): response = client.put("/items/foo") assert response.status_code == 200, response.text @@ -95,3 +22,75 @@ def test_put_no_body_with_body(): response = client.put("/items/foo", json={"name": "Foo"}) assert response.status_code == 200, response.text assert response.json() == {"item_id": "foo"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Save Item No Body", + "operationId": "save_item_no_body_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index 823f53a95..c656a161d 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -14,87 +14,87 @@ def get_parameters_with_repeated_aliases( client = TestClient(app) -openapi_schema = { - "components": { - "schemas": { - "HTTPValidationError": { - "properties": { - "detail": { - "items": {"$ref": "#/components/schemas/ValidationError"}, - "title": "Detail", - "type": "array", - } - }, - "title": "HTTPValidationError", - "type": "object", - }, - "ValidationError": { - "properties": { - "loc": { - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - "title": "Location", - "type": "array", - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - "required": ["loc", "msg", "type"], - "title": "ValidationError", - "type": "object", - }, - } - }, - "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", - "paths": { - "/{repeated_alias}": { - "get": { - "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", - "parameters": [ - { - "in": "path", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - { - "in": "query", - "name": "repeated_alias", - "required": True, - "schema": {"title": "Repeated Alias", "type": "string"}, - }, - ], - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error", - }, - }, - "summary": "Get Parameters With Repeated Aliases", - } - } - }, -} + +def test_get_parameters(): + response = client.get("/test_path", params={"repeated_alias": "test_query"}) + assert response.status_code == 200, response.text + assert response.json() == {"path": "test_path", "query": "test_query"} def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == status.HTTP_200_OK actual_schema = response.json() - assert actual_schema == openapi_schema - - -def test_get_parameters(): - response = client.get("/test_path", params={"repeated_alias": "test_query"}) - assert response.status_code == 200, response.text - assert response.json() == {"path": "test_path", "query": "test_query"} + assert actual_schema == { + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "title": "Detail", + "type": "array", + } + }, + "title": "HTTPValidationError", + "type": "object", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "title": "Location", + "type": "array", + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + "required": ["loc", "msg", "type"], + "title": "ValidationError", + "type": "object", + }, + } + }, + "info": {"title": "FastAPI", "version": "0.1.0"}, + "openapi": "3.0.2", + "paths": { + "/{repeated_alias}": { + "get": { + "operationId": "get_parameters_with_repeated_aliases__repeated_alias__get", + "parameters": [ + { + "in": "path", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + { + "in": "query", + "name": "repeated_alias", + "required": True, + "schema": {"title": "Repeated Alias", "type": "string"}, + }, + ], + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error", + }, + }, + "summary": "Get Parameters With Repeated Aliases", + } + } + }, + } diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 50ec753a0..14770fed0 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -22,77 +22,76 @@ async def delete_deployment( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/{id}": { - "delete": { - "summary": "Delete Deployment", - "operationId": "delete_deployment__id__delete", - "parameters": [ - { - "required": True, - "schema": {"title": "Id", "type": "integer"}, - "name": "id", - "in": "path", - } - ], - "responses": { - "204": {"description": "Successful Response"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} +def test_dependency_set_status_code(): + response = client.delete("/1") + assert response.status_code == 400 and response.content + assert response.json() == {"msg": "Status overwritten", "id": 1} def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_dependency_set_status_code(): - response = client.delete("/1") - assert response.status_code == 400 and response.content - assert response.json() == {"msg": "Status overwritten", "id": 1} + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/{id}": { + "delete": { + "summary": "Delete Deployment", + "operationId": "delete_deployment__id__delete", + "parameters": [ + { + "required": True, + "schema": {"title": "Id", "type": "integer"}, + "name": "id", + "in": "path", + } + ], + "responses": { + "204": {"description": "Successful Response"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index de45e0880..1861a40fa 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -68,198 +68,9 @@ def no_alias_list(): return [{"name": "Foo"}, {"name": "Bar"}] -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/dict": { - "get": { - "summary": "Read Dict", - "operationId": "read_dict_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/model": { - "get": { - "summary": "Read Model", - "operationId": "read_model_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/list": { - "get": { - "summary": "Read List", - "operationId": "read_list_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read List List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - } - }, - } - }, - "/by-alias/dict": { - "get": { - "summary": "By Alias Dict", - "operationId": "by_alias_dict_by_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/model": { - "get": { - "summary": "By Alias Model", - "operationId": "by_alias_model_by_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - } - }, - } - }, - "/by-alias/list": { - "get": { - "summary": "By Alias List", - "operationId": "by_alias_list_by_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response By Alias List By Alias List Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - } - }, - } - }, - "/no-alias/dict": { - "get": { - "summary": "No Alias Dict", - "operationId": "no_alias_dict_no_alias_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelNoAlias"} - } - }, - } - }, - } - }, - "/no-alias/model": { - "get": { - "summary": "No Alias Model", - "operationId": "no_alias_model_no_alias_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ModelNoAlias"} - } - }, - } - }, - } - }, - "/no-alias/list": { - "get": { - "summary": "No Alias List", - "operationId": "no_alias_list_no_alias_list_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Alias List No Alias List Get", - "type": "array", - "items": { - "$ref": "#/components/schemas/ModelNoAlias" - }, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["alias"], - "type": "object", - "properties": {"alias": {"title": "Alias", "type": "string"}}, - }, - "ModelNoAlias": { - "title": "ModelNoAlias", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_read_dict(): response = client.get("/dict") assert response.status_code == 200, response.text @@ -321,3 +132,193 @@ def test_read_list_no_alias(): {"name": "Foo"}, {"name": "Bar"}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/dict": { + "get": { + "summary": "Read Dict", + "operationId": "read_dict_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/model": { + "get": { + "summary": "Read Model", + "operationId": "read_model_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/list": { + "get": { + "summary": "Read List", + "operationId": "read_list_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read List List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/by-alias/dict": { + "get": { + "summary": "By Alias Dict", + "operationId": "by_alias_dict_by_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/model": { + "get": { + "summary": "By Alias Model", + "operationId": "by_alias_model_by_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + } + }, + } + }, + "/by-alias/list": { + "get": { + "summary": "By Alias List", + "operationId": "by_alias_list_by_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response By Alias List By Alias List Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + } + }, + } + }, + "/no-alias/dict": { + "get": { + "summary": "No Alias Dict", + "operationId": "no_alias_dict_no_alias_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelNoAlias" + } + } + }, + } + }, + } + }, + "/no-alias/model": { + "get": { + "summary": "No Alias Model", + "operationId": "no_alias_model_no_alias_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModelNoAlias" + } + } + }, + } + }, + } + }, + "/no-alias/list": { + "get": { + "summary": "No Alias List", + "operationId": "no_alias_list_no_alias_list_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Alias List No Alias List Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/ModelNoAlias" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["alias"], + "type": "object", + "properties": {"alias": {"title": "Alias", "type": "string"}}, + }, + "ModelNoAlias": { + "title": "ModelNoAlias", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + "description": "response_model_by_alias=False is basically a quick hack, to support proper OpenAPI use another model with the correct field names", + }, + } + }, + } diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index eb8500f3a..2d75c7535 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -35,80 +35,79 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "200": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Error"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "200": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Error"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 6d9b5c333..3851a325d 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -36,80 +36,79 @@ async def b(): pass # pragma: no cover -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/a": { - "get": { - "responses": { - "500": { - "description": "Error", - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/JsonApiError"} - } - }, - }, - "204": {"description": "Successful Response"}, - }, - "summary": "A", - "operationId": "a_a_get", - } - }, - "/b": { - "get": { - "responses": { - "204": {"description": "No Content"}, - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - }, - "summary": "B", - "operationId": "b_b_get", - } - }, - }, - "components": { - "schemas": { - "Error": { - "title": "Error", - "required": ["status", "title"], - "type": "object", - "properties": { - "status": {"title": "Status", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - }, - }, - "JsonApiError": { - "title": "JsonApiError", - "required": ["errors"], - "type": "object", - "properties": { - "errors": { - "title": "Errors", - "type": "array", - "items": {"$ref": "#/components/schemas/Error"}, - } - }, - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_response(): response = client.get("/a") assert response.status_code == 204, response.text assert "content-length" not in response.headers assert response.content == b"" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/a": { + "get": { + "responses": { + "500": { + "description": "Error", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/JsonApiError" + } + } + }, + }, + "204": {"description": "Successful Response"}, + }, + "summary": "A", + "operationId": "a_a_get", + } + }, + "/b": { + "get": { + "responses": { + "204": {"description": "No Content"}, + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + }, + "summary": "B", + "operationId": "b_b_get", + } + }, + }, + "components": { + "schemas": { + "Error": { + "title": "Error", + "required": ["status", "title"], + "type": "object", + "properties": { + "status": {"title": "Status", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + }, + }, + "JsonApiError": { + "title": "JsonApiError", + "required": ["errors"], + "type": "object", + "properties": { + "errors": { + "title": "Errors", + "type": "array", + "items": {"$ref": "#/components/schemas/Error"}, + } + }, + }, + } + }, + } diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index e45364149..7decdff7d 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -249,617 +249,9 @@ def no_response_model_annotation_json_response_class() -> JSONResponse: return JSONResponse(content={"foo": "bar"}) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/no_response_model-no_annotation-return_model": { - "get": { - "summary": "No Response Model No Annotation Return Model", - "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/no_response_model-no_annotation-return_dict": { - "get": { - "summary": "No Response Model No Annotation Return Dict", - "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model-no_annotation-return_same_model": { - "get": { - "summary": "Response Model No Annotation Return Same Model", - "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_exact_dict": { - "get": { - "summary": "Response Model No Annotation Return Exact Dict", - "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_dict": { - "get": { - "summary": "Response Model No Annotation Return Invalid Dict", - "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_invalid_model": { - "get": { - "summary": "Response Model No Annotation Return Invalid Model", - "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Dict With Extra Data", - "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model-no_annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model No Annotation Return Submodel With Extra Data", - "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_same_model": { - "get": { - "summary": "No Response Model Annotation Return Same Model", - "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_exact_dict": { - "get": { - "summary": "No Response Model Annotation Return Exact Dict", - "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_dict": { - "get": { - "summary": "No Response Model Annotation Return Invalid Dict", - "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_invalid_model": { - "get": { - "summary": "No Response Model Annotation Return Invalid Model", - "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_dict_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Dict With Extra Data", - "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/no_response_model-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "No Response Model Annotation Return Submodel With Extra Data", - "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_none-annotation-return_same_model": { - "get": { - "summary": "Response Model None Annotation Return Same Model", - "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_exact_dict": { - "get": { - "summary": "Response Model None Annotation Return Exact Dict", - "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_dict": { - "get": { - "summary": "Response Model None Annotation Return Invalid Dict", - "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_invalid_model": { - "get": { - "summary": "Response Model None Annotation Return Invalid Model", - "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_dict_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Dict With Extra Data", - "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_none-annotation-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model None Annotation Return Submodel With Extra Data", - "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_same_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Same Model", - "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_exact_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", - "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_dict": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", - "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_invalid_model": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", - "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_dict_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { - "get": { - "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", - "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_filtering_model-annotation_submodel-return_submodel": { - "get": { - "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", - "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - } - }, - } - }, - "/response_model_list_of_model-no_annotation": { - "get": { - "summary": "Response Model List Of Model No Annotation", - "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_list_of_model": { - "get": { - "summary": "No Response Model Annotation List Of Model", - "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_forward_ref_list_of_model": { - "get": { - "summary": "No Response Model Annotation Forward Ref List Of Model", - "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model1": { - "get": { - "summary": "Response Model Union No Annotation Return Model1", - "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/response_model_union-no_annotation-return_model2": { - "get": { - "summary": "Response Model Union No Annotation Return Model2", - "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model1": { - "get": { - "summary": "No Response Model Annotation Union Return Model1", - "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_union-return_model2": { - "get": { - "summary": "No Response Model Annotation Union Return Model2", - "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", - "anyOf": [ - {"$ref": "#/components/schemas/User"}, - {"$ref": "#/components/schemas/Item"}, - ], - } - } - }, - } - }, - } - }, - "/no_response_model-annotation_response_class": { - "get": { - "summary": "No Response Model Annotation Response Class", - "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/no_response_model-annotation_json_response_class": { - "get": { - "summary": "No Response Model Annotation Json Response Class", - "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["name", "surname"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "surname": {"title": "Surname", "type": "string"}, - }, - }, - } - }, -} - - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_no_response_model_no_annotation_return_model(): response = client.get("/no_response_model-no_annotation-return_model") assert response.status_code == 200, response.text @@ -1109,3 +501,608 @@ def test_invalid_response_model_field(): assert "valid Pydantic field type" in e.value.args[0] assert "parameter response_model=None" in e.value.args[0] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/no_response_model-no_annotation-return_model": { + "get": { + "summary": "No Response Model No Annotation Return Model", + "operationId": "no_response_model_no_annotation_return_model_no_response_model_no_annotation_return_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-no_annotation-return_dict": { + "get": { + "summary": "No Response Model No Annotation Return Dict", + "operationId": "no_response_model_no_annotation_return_dict_no_response_model_no_annotation_return_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model-no_annotation-return_same_model": { + "get": { + "summary": "Response Model No Annotation Return Same Model", + "operationId": "response_model_no_annotation_return_same_model_response_model_no_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_exact_dict": { + "get": { + "summary": "Response Model No Annotation Return Exact Dict", + "operationId": "response_model_no_annotation_return_exact_dict_response_model_no_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_dict": { + "get": { + "summary": "Response Model No Annotation Return Invalid Dict", + "operationId": "response_model_no_annotation_return_invalid_dict_response_model_no_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_invalid_model": { + "get": { + "summary": "Response Model No Annotation Return Invalid Model", + "operationId": "response_model_no_annotation_return_invalid_model_response_model_no_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Dict With Extra Data", + "operationId": "response_model_no_annotation_return_dict_with_extra_data_response_model_no_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model-no_annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model No Annotation Return Submodel With Extra Data", + "operationId": "response_model_no_annotation_return_submodel_with_extra_data_response_model_no_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_same_model": { + "get": { + "summary": "No Response Model Annotation Return Same Model", + "operationId": "no_response_model_annotation_return_same_model_no_response_model_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_exact_dict": { + "get": { + "summary": "No Response Model Annotation Return Exact Dict", + "operationId": "no_response_model_annotation_return_exact_dict_no_response_model_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_dict": { + "get": { + "summary": "No Response Model Annotation Return Invalid Dict", + "operationId": "no_response_model_annotation_return_invalid_dict_no_response_model_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_invalid_model": { + "get": { + "summary": "No Response Model Annotation Return Invalid Model", + "operationId": "no_response_model_annotation_return_invalid_model_no_response_model_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_dict_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Dict With Extra Data", + "operationId": "no_response_model_annotation_return_dict_with_extra_data_no_response_model_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/no_response_model-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "No Response Model Annotation Return Submodel With Extra Data", + "operationId": "no_response_model_annotation_return_submodel_with_extra_data_no_response_model_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_none-annotation-return_same_model": { + "get": { + "summary": "Response Model None Annotation Return Same Model", + "operationId": "response_model_none_annotation_return_same_model_response_model_none_annotation_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_exact_dict": { + "get": { + "summary": "Response Model None Annotation Return Exact Dict", + "operationId": "response_model_none_annotation_return_exact_dict_response_model_none_annotation_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_dict": { + "get": { + "summary": "Response Model None Annotation Return Invalid Dict", + "operationId": "response_model_none_annotation_return_invalid_dict_response_model_none_annotation_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_invalid_model": { + "get": { + "summary": "Response Model None Annotation Return Invalid Model", + "operationId": "response_model_none_annotation_return_invalid_model_response_model_none_annotation_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_dict_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Dict With Extra Data", + "operationId": "response_model_none_annotation_return_dict_with_extra_data_response_model_none_annotation_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_none-annotation-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model None Annotation Return Submodel With Extra Data", + "operationId": "response_model_none_annotation_return_submodel_with_extra_data_response_model_none_annotation_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_same_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Same Model", + "operationId": "response_model_model1_annotation_model2_return_same_model_response_model_model1_annotation_model2_return_same_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_exact_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Exact Dict", + "operationId": "response_model_model1_annotation_model2_return_exact_dict_response_model_model1_annotation_model2_return_exact_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_dict": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Dict", + "operationId": "response_model_model1_annotation_model2_return_invalid_dict_response_model_model1_annotation_model2_return_invalid_dict_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_invalid_model": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Invalid Model", + "operationId": "response_model_model1_annotation_model2_return_invalid_model_response_model_model1_annotation_model2_return_invalid_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_dict_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Dict With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_dict_with_extra_data_response_model_model1_annotation_model2_return_dict_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_model1-annotation_model2-return_submodel_with_extra_data": { + "get": { + "summary": "Response Model Model1 Annotation Model2 Return Submodel With Extra Data", + "operationId": "response_model_model1_annotation_model2_return_submodel_with_extra_data_response_model_model1_annotation_model2_return_submodel_with_extra_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_filtering_model-annotation_submodel-return_submodel": { + "get": { + "summary": "Response Model Filtering Model Annotation Submodel Return Submodel", + "operationId": "response_model_filtering_model_annotation_submodel_return_submodel_response_model_filtering_model_annotation_submodel_return_submodel_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + } + }, + } + }, + "/response_model_list_of_model-no_annotation": { + "get": { + "summary": "Response Model List Of Model No Annotation", + "operationId": "response_model_list_of_model_no_annotation_response_model_list_of_model_no_annotation_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model List Of Model No Annotation Response Model List Of Model No Annotation Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_list_of_model": { + "get": { + "summary": "No Response Model Annotation List Of Model", + "operationId": "no_response_model_annotation_list_of_model_no_response_model_annotation_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation List Of Model No Response Model Annotation List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_forward_ref_list_of_model": { + "get": { + "summary": "No Response Model Annotation Forward Ref List Of Model", + "operationId": "no_response_model_annotation_forward_ref_list_of_model_no_response_model_annotation_forward_ref_list_of_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Forward Ref List Of Model No Response Model Annotation Forward Ref List Of Model Get", + "type": "array", + "items": {"$ref": "#/components/schemas/User"}, + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model1": { + "get": { + "summary": "Response Model Union No Annotation Return Model1", + "operationId": "response_model_union_no_annotation_return_model1_response_model_union_no_annotation_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model1 Response Model Union No Annotation Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/response_model_union-no_annotation-return_model2": { + "get": { + "summary": "Response Model Union No Annotation Return Model2", + "operationId": "response_model_union_no_annotation_return_model2_response_model_union_no_annotation_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Response Model Union No Annotation Return Model2 Response Model Union No Annotation Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model1": { + "get": { + "summary": "No Response Model Annotation Union Return Model1", + "operationId": "no_response_model_annotation_union_return_model1_no_response_model_annotation_union_return_model1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model1 No Response Model Annotation Union Return Model1 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_union-return_model2": { + "get": { + "summary": "No Response Model Annotation Union Return Model2", + "operationId": "no_response_model_annotation_union_return_model2_no_response_model_annotation_union_return_model2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response No Response Model Annotation Union Return Model2 No Response Model Annotation Union Return Model2 Get", + "anyOf": [ + {"$ref": "#/components/schemas/User"}, + {"$ref": "#/components/schemas/Item"}, + ], + } + } + }, + } + }, + } + }, + "/no_response_model-annotation_response_class": { + "get": { + "summary": "No Response Model Annotation Response Class", + "operationId": "no_response_model_annotation_response_class_no_response_model_annotation_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/no_response_model-annotation_json_response_class": { + "get": { + "summary": "No Response Model Annotation Json Response Class", + "operationId": "no_response_model_annotation_json_response_class_no_response_model_annotation_json_response_class_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["name", "surname"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "surname": {"title": "Surname", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index fd972e6a3..e462006ff 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -32,123 +32,9 @@ def valid4(): pass -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/valid1": { - "get": { - "summary": "Valid1", - "operationId": "valid1_valid1_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid1 Valid1 Get", - "type": "integer", - } - } - }, - }, - }, - } - }, - "/valid2": { - "get": { - "summary": "Valid2", - "operationId": "valid2_valid2_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid2 Valid2 Get", - "type": "array", - "items": {"type": "integer"}, - } - } - }, - }, - }, - } - }, - "/valid3": { - "get": { - "summary": "Valid3", - "operationId": "valid3_valid3_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Model"} - } - }, - }, - }, - } - }, - "/valid4": { - "get": { - "summary": "Valid4", - "operationId": "valid4_valid4_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "500": { - "description": "Internal Server Error", - "content": { - "application/json": { - "schema": { - "title": "Response 500 Valid4 Valid4 Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Model"}, - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Model": { - "title": "Model", - "required": ["name"], - "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, - } - } - }, -} - client = TestClient(app) -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_path_operations(): response = client.get("/valid1") assert response.status_code == 200, response.text @@ -158,3 +44,115 @@ def test_path_operations(): assert response.status_code == 200, response.text response = client.get("/valid4") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/valid1": { + "get": { + "summary": "Valid1", + "operationId": "valid1_valid1_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid1 Valid1 Get", + "type": "integer", + } + } + }, + }, + }, + } + }, + "/valid2": { + "get": { + "summary": "Valid2", + "operationId": "valid2_valid2_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid2 Valid2 Get", + "type": "array", + "items": {"type": "integer"}, + } + } + }, + }, + }, + } + }, + "/valid3": { + "get": { + "summary": "Valid3", + "operationId": "valid3_valid3_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Model"} + } + }, + }, + }, + } + }, + "/valid4": { + "get": { + "summary": "Valid4", + "operationId": "valid4_valid4_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "title": "Response 500 Valid4 Valid4 Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Model"}, + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Model": { + "title": "Model", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + } + } + }, + } diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index f07d2c3b8..74e15d59a 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -234,623 +234,6 @@ def cookie_example_examples( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/schema_extra/": { - "post": { - "summary": "Schema Extra", - "operationId": "schema_extra_schema_extra__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/example/": { - "post": { - "summary": "Example", - "operationId": "example_example__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": {"data": "Data in Body example"}, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/examples/": { - "post": { - "summary": "Examples", - "operationId": "examples_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "summary": "example1 summary", - "value": { - "data": "Data in Body examples, example1" - }, - }, - "example2": { - "value": {"data": "Data in Body examples, example2"} - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/example_examples/": { - "post": { - "summary": "Example Examples", - "operationId": "example_examples_example_examples__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "value": {"data": "examples example_examples 1"} - }, - "example2": { - "value": {"data": "examples example_examples 2"} - }, - }, - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_example/{item_id}": { - "get": { - "summary": "Path Example", - "operationId": "path_example_path_example__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "example": "item_1", - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_examples/{item_id}": { - "get": { - "summary": "Path Examples", - "operationId": "path_examples_path_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, - }, - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/path_example_examples/{item_id}": { - "get": { - "summary": "Path Example Examples", - "operationId": "path_example_examples_path_example_examples__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, - }, - "name": "item_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example/": { - "get": { - "summary": "Query Example", - "operationId": "query_example_query_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "query1", - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_examples/": { - "get": { - "summary": "Query Examples", - "operationId": "query_examples_query_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, - }, - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/query_example_examples/": { - "get": { - "summary": "Query Example Examples", - "operationId": "query_example_examples_query_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, - }, - "name": "data", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example/": { - "get": { - "summary": "Header Example", - "operationId": "header_example_header_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "header1", - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_examples/": { - "get": { - "summary": "Header Examples", - "operationId": "header_examples_header_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "header example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, - }, - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/header_example_examples/": { - "get": { - "summary": "Header Example Examples", - "operationId": "header_example_examples_header_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, - }, - "name": "data", - "in": "header", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example/": { - "get": { - "summary": "Cookie Example", - "operationId": "cookie_example_cookie_example__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "example": "cookie1", - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_examples/": { - "get": { - "summary": "Cookie Examples", - "operationId": "cookie_examples_cookie_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "cookie example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, - }, - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/cookie_example_examples/": { - "get": { - "summary": "Cookie Example Examples", - "operationId": "cookie_example_examples_cookie_example_examples__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, - }, - "name": "data", - "in": "cookie", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["data"], - "type": "object", - "properties": {"data": {"title": "Data", "type": "string"}}, - "example": {"data": "Data in schema_extra"}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - """ - Test that example overrides work: - - * pydantic model schema_extra is included - * Body(example={}) overrides schema_extra in pydantic model - * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model - """ - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_call_api(): response = client.post("/schema_extra/", json={"data": "Foo"}) assert response.status_code == 200, response.text @@ -884,3 +267,621 @@ def test_call_api(): assert response.status_code == 200, response.text response = client.get("/cookie_example_examples/") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + """ + Test that example overrides work: + + * pydantic model schema_extra is included + * Body(example={}) overrides schema_extra in pydantic model + * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model + """ + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/schema_extra/": { + "post": { + "summary": "Schema Extra", + "operationId": "schema_extra_schema_extra__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/example/": { + "post": { + "summary": "Example", + "operationId": "example_example__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": {"data": "Data in Body example"}, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "example1": { + "summary": "example1 summary", + "value": { + "data": "Data in Body examples, example1" + }, + }, + "example2": { + "value": { + "data": "Data in Body examples, example2" + } + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/example_examples/": { + "post": { + "summary": "Example Examples", + "operationId": "example_examples_example_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "examples": { + "example1": { + "value": {"data": "examples example_examples 1"} + }, + "example2": { + "value": {"data": "examples example_examples 2"} + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example/{item_id}": { + "get": { + "summary": "Path Example", + "operationId": "path_example_path_example__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "example": "item_1", + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "examples": { + "example1": { + "summary": "item ID summary", + "value": "item_1", + }, + "example2": {"value": "item_2"}, + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_example_examples/{item_id}": { + "get": { + "summary": "Path Example Examples", + "operationId": "path_example_examples_path_example_examples__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "examples": { + "example1": { + "summary": "item ID summary", + "value": "item_1", + }, + "example2": {"value": "item_2"}, + }, + "name": "item_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example/": { + "get": { + "summary": "Query Example", + "operationId": "query_example_query_example__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "example": "query1", + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "Query example 1", + "value": "query1", + }, + "example2": {"value": "query2"}, + }, + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_example_examples/": { + "get": { + "summary": "Query Example Examples", + "operationId": "query_example_examples_query_example_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "Query example 1", + "value": "query1", + }, + "example2": {"value": "query2"}, + }, + "name": "data", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example/": { + "get": { + "summary": "Header Example", + "operationId": "header_example_header_example__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "example": "header1", + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "header example 1", + "value": "header1", + }, + "example2": {"value": "header2"}, + }, + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_example_examples/": { + "get": { + "summary": "Header Example Examples", + "operationId": "header_example_examples_header_example_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "Query example 1", + "value": "header1", + }, + "example2": {"value": "header2"}, + }, + "name": "data", + "in": "header", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example/": { + "get": { + "summary": "Cookie Example", + "operationId": "cookie_example_cookie_example__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "example": "cookie1", + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "cookie example 1", + "value": "cookie1", + }, + "example2": {"value": "cookie2"}, + }, + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_example_examples/": { + "get": { + "summary": "Cookie Example Examples", + "operationId": "cookie_example_examples_cookie_example_examples__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Data", "type": "string"}, + "examples": { + "example1": { + "summary": "Query example 1", + "value": "cookie1", + }, + "example2": {"value": "cookie2"}, + }, + "name": "data", + "in": "cookie", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["data"], + "type": "object", + "properties": {"data": {"title": "Data", "type": "string"}}, + "example": {"data": "Data in schema_extra"}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 0bf4e9bb3..b1c648c55 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -22,39 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -67,3 +34,33 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index ed4e65239..ac8b4abf8 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -22,44 +22,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": { - "type": "apiKey", - "name": "key", - "in": "cookie", - "description": "An API Cookie Key", - } - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -72,3 +34,38 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": { + "type": "apiKey", + "name": "key", + "in": "cookie", + "description": "An API Cookie Key", + } + } + }, + } diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index 3e7aa81c0..b8c440c9d 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -29,39 +29,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): return current_user -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyCookie": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} - } - }, -} - - -def test_openapi_schema(): - client = TestClient(app) - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_security_api_key(): client = TestClient(app, cookies={"key": "secret"}) response = client.get("/users/me") @@ -74,3 +41,33 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyCookie": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyCookie": {"type": "apiKey", "name": "key", "in": "cookie"} + } + }, + } diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index d53395f99..96ad80b54 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -66,3 +35,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index cc9802708..382f53dd7 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -24,42 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": { - "type": "apiKey", - "name": "key", - "in": "header", - "description": "An API Key Header", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -71,3 +35,37 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": { + "type": "apiKey", + "name": "key", + "in": "header", + "description": "An API Key Header", + } + } + }, + } diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index 4ab599c2d..adfb20ba0 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyHeader": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me", headers={"key": "secret"}) @@ -72,3 +41,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyHeader": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyHeader": {"type": "apiKey", "name": "key", "in": "header"} + } + }, + } diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index 4844c65e2..da98eafd6 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -66,3 +35,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 9b608233a..3c08afc5f 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -24,42 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": { - "type": "apiKey", - "name": "key", - "in": "query", - "description": "API Key Query", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -71,3 +35,37 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": { + "type": "apiKey", + "name": "key", + "in": "query", + "description": "API Key Query", + } + } + }, + } diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 9339b7b3a..99a26cfd0 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"APIKeyQuery": []}], - } - } - }, - "components": { - "securitySchemes": { - "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_api_key(): response = client.get("/users/me?key=secret") @@ -72,3 +41,32 @@ def test_security_api_key_no_key(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"APIKeyQuery": []}], + } + } + }, + "components": { + "securitySchemes": { + "APIKeyQuery": {"type": "apiKey", "name": "key", "in": "query"} + } + }, + } diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 894716279..d3a754203 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -54,3 +25,30 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index 5855e8df4..3d7d15016 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBase": { - "type": "http", - "scheme": "Other", - "description": "Other Security Scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -60,3 +25,36 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBase": { + "type": "http", + "scheme": "Other", + "description": "Other Security Scheme", + } + } + }, + } diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 5a50f9b88..180c9110e 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBase": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_base(): response = client.get("/users/me", headers={"Authorization": "Other foobar"}) @@ -60,3 +31,30 @@ def test_security_http_base_no_credentials(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBase": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBase": {"type": "http", "scheme": "Other"}} + }, + } diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 91824d223..7e7fcac32 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -19,35 +19,6 @@ def read_current_user(credentials: Optional[HTTPBasicCredentials] = Security(sec client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -77,3 +48,30 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == "Basic" assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 6d760c0f9..470afd662 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -16,35 +16,6 @@ def read_current_user(credentials: HTTPBasicCredentials = Security(security)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -75,3 +46,30 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBasic": {"type": "http", "scheme": "basic"}} + }, + } diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 7cc547561..44289007b 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -16,41 +16,6 @@ def read_current_user(credentials: HTTPBasicCredentials = Security(security)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBasic": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBasic": { - "type": "http", - "scheme": "basic", - "description": "HTTPBasic scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_basic(): response = client.get("/users/me", auth=("john", "secret")) @@ -81,3 +46,36 @@ def test_security_http_basic_non_basic_credentials(): assert response.status_code == 401, response.text assert response.headers["WWW-Authenticate"] == 'Basic realm="simple"' assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBasic": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBasic": { + "type": "http", + "scheme": "basic", + "description": "HTTPBasic scheme", + } + } + }, + } diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 39d8c8402..f24869fc3 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -60,3 +31,30 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 132e720fc..6d5ad0b8e 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPBearer": { - "type": "http", - "scheme": "bearer", - "description": "HTTP Bearer token scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -66,3 +31,36 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer", + "description": "HTTP Bearer token scheme", + } + } + }, + } diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index 2e7dfb8a4..b596ac730 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPBearer": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_bearer(): response = client.get("/users/me", headers={"Authorization": "Bearer foobar"}) @@ -66,3 +37,30 @@ def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}} + }, + } diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 8388824ff..2a25efe02 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -14,35 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -62,3 +33,30 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index d00aa1b6e..721f7cfde 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -14,41 +14,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": { - "HTTPDigest": { - "type": "http", - "scheme": "digest", - "description": "HTTPDigest scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -68,3 +33,36 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": { + "HTTPDigest": { + "type": "http", + "scheme": "digest", + "description": "HTTPDigest scheme", + } + } + }, + } diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index 2177b819f..d4c3597bc 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -20,35 +20,6 @@ def read_current_user( client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"HTTPDigest": []}], - } - } - }, - "components": { - "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_http_digest(): response = client.get("/users/me", headers={"Authorization": "Digest foobar"}) @@ -68,3 +39,30 @@ def test_security_http_digest_incorrect_scheme_credentials(): ) assert response.status_code == 403, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"HTTPDigest": []}], + } + } + }, + "components": { + "securitySchemes": {"HTTPDigest": {"type": "http", "scheme": "digest"}} + }, + } diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index b9ac488ee..23dce7cfa 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -40,124 +40,6 @@ def read_current_user(current_user: "User" = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -247,3 +129,121 @@ def test_strict_login(data, expected_status, expected_response): response = client.post("/login", data=data) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index ad9a39ded..6df81528d 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -18,46 +18,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -75,3 +35,41 @@ def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + } + } + }, + } diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index bdaa543fc..c119abde4 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -21,47 +21,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2AuthorizationCodeBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2AuthorizationCodeBearer": { - "type": "oauth2", - "flows": { - "authorizationCode": { - "authorizationUrl": "authorize", - "tokenUrl": "token", - "scopes": {}, - } - }, - "description": "OAuth2 Code Bearer", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -79,3 +38,42 @@ def test_token(): response = client.get("/items", headers={"Authorization": "Bearer testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"token": "testtoken"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2AuthorizationCodeBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2AuthorizationCodeBearer": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "authorize", + "tokenUrl": "token", + "scopes": {}, + } + }, + "description": "OAuth2 Code Bearer", + } + } + }, + } diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index a5fd49b8c..3ef9f4a8d 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -44,124 +44,6 @@ def read_users_me(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -251,3 +133,121 @@ def test_strict_login(data, expected_status, expected_response): response = client.post("/login", data=data) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + } + }, + }, + } diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index 171f96b76..b6425fde4 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -45,125 +45,6 @@ def read_users_me(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/login": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Login", - "operationId": "login_login_post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_login_login_post" - } - } - }, - "required": True, - }, - } - }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Users Me", - "operationId": "read_users_me_users_me_get", - "security": [{"OAuth2": []}], - } - }, - }, - "components": { - "schemas": { - "Body_login_login_post": { - "title": "Body_login_login_post", - "required": ["grant_type", "username", "password"], - "type": "object", - "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, - "username": {"title": "Username", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - }, - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "password": { - "scopes": { - "read:users": "Read the users", - "write:users": "Create users", - }, - "tokenUrl": "token", - } - }, - "description": "OAuth2 security scheme", - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -253,3 +134,122 @@ def test_strict_login(data, expected_status, expected_response): response = client.post("/login", data=data) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/login": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Login", + "operationId": "login_login_post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_login_login_post" + } + } + }, + "required": True, + }, + } + }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Users Me", + "operationId": "read_users_me_users_me_get", + "security": [{"OAuth2": []}], + } + }, + }, + "components": { + "schemas": { + "Body_login_login_post": { + "title": "Body_login_login_post", + "required": ["grant_type", "username", "password"], + "type": "object", + "properties": { + "grant_type": { + "title": "Grant Type", + "pattern": "password", + "type": "string", + }, + "username": {"title": "Username", "type": "string"}, + "password": {"title": "Password", "type": "string"}, + "scope": {"title": "Scope", "type": "string", "default": ""}, + "client_id": {"title": "Client Id", "type": "string"}, + "client_secret": {"title": "Client Secret", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + }, + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "password": { + "scopes": { + "read:users": "Read the users", + "write:users": "Create users", + }, + "tokenUrl": "token", + } + }, + "description": "OAuth2 security scheme", + } + }, + }, + } diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index 3d6637d4a..e5dcbb553 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -18,40 +18,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -69,3 +35,35 @@ def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + } + } + }, + } diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index 9d6a862e3..9ff48e715 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -22,41 +22,6 @@ async def read_items(token: Optional[str] = Security(oauth2_scheme)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "security": [{"OAuth2PasswordBearer": []}], - } - } - }, - "components": { - "securitySchemes": { - "OAuth2PasswordBearer": { - "type": "oauth2", - "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, - "description": "OAuth2PasswordBearer security scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_no_token(): response = client.get("/items") @@ -74,3 +39,36 @@ def test_incorrect_token(): response = client.get("/items", headers={"Authorization": "Notexistent testtoken"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "security": [{"OAuth2PasswordBearer": []}], + } + } + }, + "components": { + "securitySchemes": { + "OAuth2PasswordBearer": { + "type": "oauth2", + "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}}, + "description": "OAuth2PasswordBearer security scheme", + } + } + }, + } diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 8203961be..206de6574 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -24,37 +24,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -72,3 +41,35 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } + } + }, + } diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 218cbfc8f..5884de793 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -26,41 +26,6 @@ def read_current_user(current_user: User = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": { - "type": "openIdConnect", - "openIdConnectUrl": "/openid", - "description": "OpenIdConnect security scheme", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -78,3 +43,36 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 403, response.text assert response.json() == {"detail": "Not authenticated"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + "description": "OpenIdConnect security scheme", + } + } + }, + } diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index 4577dfebb..8ac719118 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -30,37 +30,6 @@ def read_current_user(current_user: Optional[User] = Depends(get_current_user)): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Current User", - "operationId": "read_current_user_users_me_get", - "security": [{"OpenIdConnect": []}], - } - } - }, - "components": { - "securitySchemes": { - "OpenIdConnect": {"type": "openIdConnect", "openIdConnectUrl": "/openid"} - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_security_oauth2(): response = client.get("/users/me", headers={"Authorization": "Bearer footokenbar"}) @@ -78,3 +47,35 @@ def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Current User", + "operationId": "read_current_user_users_me_get", + "security": [{"OpenIdConnect": []}], + } + } + }, + "components": { + "securitySchemes": { + "OpenIdConnect": { + "type": "openIdConnect", + "openIdConnectUrl": "/openid", + } + } + }, + } diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 418ddff7d..96f835b93 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -37,132 +37,6 @@ async def read_starlette_item(item_id: str): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/http-no-body-statuscode-exception": { - "get": { - "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code Exception", - } - }, - "/http-no-body-statuscode-with-detail-exception": { - "get": { - "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", - "responses": { - "200": { - "content": {"application/json": {"schema": {}}}, - "description": "Successful Response", - } - }, - "summary": "No Body Status Code With Detail Exception", - } - }, - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - "/starlette-items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Starlette Item", - "operationId": "read_starlette_item_starlette_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item(): response = client.get("/items/foo") @@ -200,3 +74,129 @@ def test_no_body_status_code_with_detail_exception_handlers(): response = client.get("/http-no-body-statuscode-with-detail-exception") assert response.status_code == 204 assert not response.content + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/http-no-body-statuscode-exception": { + "get": { + "operationId": "no_body_status_code_exception_http_no_body_statuscode_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "No Body Status Code Exception", + } + }, + "/http-no-body-statuscode-with-detail-exception": { + "get": { + "operationId": "no_body_status_code_with_detail_exception_http_no_body_statuscode_with_detail_exception_get", + "responses": { + "200": { + "content": {"application/json": {"schema": {}}}, + "description": "Successful Response", + } + }, + "summary": "No Body Status Code With Detail Exception", + } + }, + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + "/starlette-items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Starlette Item", + "operationId": "read_starlette_item_starlette_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index 7574d6fbc..c5a0237e8 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -74,205 +74,6 @@ app.include_router(subrouter, callbacks=events_callback_router.routes) client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "callbacks": { - "event_callback": { - "{$callback_url}/events/{$request.body.title}": { - "get": { - "summary": "Event Callback", - "operationId": "event_callback__callback_url__events___request_body_title__get", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - }, - } - } - }, - "components": { - "schemas": { - "Event": { - "title": "Event", - "required": ["name", "total"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_get(): response = client.post( @@ -280,3 +81,205 @@ def test_get(): ) assert response.status_code == 200, response.text assert response.json() == {"msg": "Invoice received"} + + +def test_openapi_schema(): + with client: + response = client.get("/openapi.json") + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + }, + "name": "callback_url", + "in": "query", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Invoice"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "event_callback": { + "{$callback_url}/events/{$request.body.title}": { + "get": { + "summary": "Event Callback", + "operationId": "event_callback__callback_url__events___request_body_title__get", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": {"schema": {}} + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + }, + } + } + }, + "components": { + "schemas": { + "Event": { + "title": "Event", + "required": ["name", "total"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 6e2cc0db6..4fa1f7a13 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -33,189 +33,6 @@ def hello(values: Tuple[int, int] = Form()): client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/model-with-tuple/": { - "post": { - "summary": "Post Model With Tuple", - "operationId": "post_model_with_tuple_model_with_tuple__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemGroup"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/tuple-of-models/": { - "post": { - "summary": "Post Tuple Of Models", - "operationId": "post_tuple_of_models_tuple_of_models__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/tuple-form/": { - "post": { - "summary": "Hello", - "operationId": "hello_tuple_form__post", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/Body_hello_tuple_form__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_hello_tuple_form__post": { - "title": "Body_hello_tuple_form__post", - "required": ["values"], - "type": "object", - "properties": { - "values": { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } - }, - }, - "Coordinate": { - "title": "Coordinate", - "required": ["x", "y"], - "type": "object", - "properties": { - "x": {"title": "X", "type": "number"}, - "y": {"title": "Y", "type": "number"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ItemGroup": { - "title": "ItemGroup", - "required": ["items"], - "type": "object", - "properties": { - "items": { - "title": "Items", - "type": "array", - "items": { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - }, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_model_with_tuple_valid(): data = {"items": [["foo", "bar"], ["baz", "whatelse"]]} @@ -263,3 +80,186 @@ def test_tuple_form_invalid(): response = client.post("/tuple-form/", data={"values": ("1")}) assert response.status_code == 422, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model-with-tuple/": { + "post": { + "summary": "Post Model With Tuple", + "operationId": "post_model_with_tuple_model_with_tuple__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemGroup"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-of-models/": { + "post": { + "summary": "Post Tuple Of Models", + "operationId": "post_tuple_of_models_tuple_of_models__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/tuple-form/": { + "post": { + "summary": "Hello", + "operationId": "hello_tuple_form__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Body_hello_tuple_form__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_hello_tuple_form__post": { + "title": "Body_hello_tuple_form__post", + "required": ["values"], + "type": "object", + "properties": { + "values": { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + }, + }, + "Coordinate": { + "title": "Coordinate", + "required": ["x", "y"], + "type": "object", + "properties": { + "x": {"title": "X", "type": "number"}, + "y": {"title": "Y", "type": "number"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemGroup": { + "title": "ItemGroup", + "required": ["items"], + "type": "object", + "properties": { + "items": { + "title": "Items", + "type": "array", + "items": { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 1a8acb523..3d6267023 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -4,105 +4,6 @@ from docs_src.additional_responses.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": { - "description": "Not Found", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Message"} - } - }, - }, - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -114,3 +15,102 @@ def test_path_operation_not_found(): response = client.get("/items/bar") assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Message"} + } + }, + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "Message": { + "title": "Message", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 2adcf15d0..6182ed507 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -7,98 +7,6 @@ from docs_src.additional_responses.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Return the JSON item or an image.", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - }, - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Img", "type": "boolean"}, - "name": "img", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -113,3 +21,95 @@ def test_path_operation_img(): assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Return the JSON item or an image.", + "content": { + "image/png": {}, + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + }, + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Img", "type": "boolean"}, + "name": "img", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 8b2167de0..77568d9d4 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -4,106 +4,6 @@ from docs_src.additional_responses.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": { - "description": "The item was not found", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Message"} - } - }, - }, - "200": { - "description": "Item requested by ID", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "example": {"id": "bar", "value": "The bar tenders"}, - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "Message": { - "title": "Message", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -115,3 +15,106 @@ def test_path_operation_not_found(): response = client.get("/items/bar") assert response.status_code == 404, response.text assert response.json() == {"message": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": { + "description": "The item was not found", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Message"} + } + }, + }, + "200": { + "description": "Item requested by ID", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"}, + "example": { + "id": "bar", + "value": "The bar tenders", + }, + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "Message": { + "title": "Message", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 990d5235a..3fbd91e5c 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -7,101 +7,6 @@ from docs_src.additional_responses.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "404": {"description": "Item not found"}, - "302": {"description": "The item was moved"}, - "403": {"description": "Not enough privileges"}, - "200": { - "description": "Successful Response", - "content": { - "image/png": {}, - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - }, - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Img", "type": "boolean"}, - "name": "img", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["id", "value"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "value": {"title": "Value", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operation(): response = client.get("/items/foo") @@ -116,3 +21,98 @@ def test_path_operation_img(): assert response.headers["Content-Type"] == "image/png" assert len(response.content) os.remove("./image.png") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "404": {"description": "Item not found"}, + "302": {"description": "The item was moved"}, + "403": {"description": "Not enough privileges"}, + "200": { + "description": "Successful Response", + "content": { + "image/png": {}, + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + }, + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Img", "type": "boolean"}, + "name": "img", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["id", "value"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "value": {"title": "Value", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 1ad625db6..3da362a50 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -2,120 +2,6 @@ from fastapi.testclient import TestClient from docs_src.async_sql_databases.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/notes/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Notes Notes Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Note"}, - } - } - }, - } - }, - "summary": "Read Notes", - "operationId": "read_notes_notes__get", - }, - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Note"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Note", - "operationId": "create_note_notes__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/NoteIn"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "NoteIn": { - "title": "NoteIn", - "required": ["text", "completed"], - "type": "object", - "properties": { - "text": {"title": "Text", "type": "string"}, - "completed": {"title": "Completed", "type": "boolean"}, - }, - }, - "Note": { - "title": "Note", - "required": ["id", "text", "completed"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "integer"}, - "text": {"title": "Text", "type": "string"}, - "completed": {"title": "Completed", "type": "boolean"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_create_read(): with TestClient(app) as client: @@ -129,3 +15,121 @@ def test_create_read(): response = client.get("/notes/") assert response.status_code == 200, response.text assert data in response.json() + + +def test_openapi_schema(): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/notes/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Notes Notes Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Note" + }, + } + } + }, + } + }, + "summary": "Read Notes", + "operationId": "read_notes_notes__get", + }, + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Note"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Note", + "operationId": "create_note_notes__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/NoteIn"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "NoteIn": { + "title": "NoteIn", + "required": ["text", "completed"], + "type": "object", + "properties": { + "text": {"title": "Text", "type": "string"}, + "completed": {"title": "Completed", "type": "boolean"}, + }, + }, + "Note": { + "title": "Note", + "required": ["id", "text", "completed"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "integer"}, + "text": {"title": "Text", "type": "string"}, + "completed": {"title": "Completed", "type": "boolean"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py index be9e499bf..7533a1b68 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -4,34 +4,32 @@ from docs_src.behind_a_proxy.tutorial001 import app client = TestClient(app, root_path="/api/v1") -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_main(): response = client.get("/app") assert response.status_code == 200 assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/api/v1"}], + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py index ac192e3db..930ab3bf5 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -4,34 +4,32 @@ from docs_src.behind_a_proxy.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, - "servers": [{"url": "/api/v1"}], -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_main(): response = client.get("/app") assert response.status_code == 200 assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "servers": [{"url": "/api/v1"}], + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 2727525ca..ae8f1a495 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -4,38 +4,39 @@ from docs_src.behind_a_proxy.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "/api/v1"}, - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_main(): response = client.get("/app") assert response.status_code == 200 assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "/api/v1"}, + {"url": "https://stag.example.com", "description": "Staging environment"}, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index 4c4e4b75c..e67ad1cb1 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -4,37 +4,38 @@ from docs_src.behind_a_proxy.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "servers": [ - {"url": "https://stag.example.com", "description": "Staging environment"}, - {"url": "https://prod.example.com", "description": "Production environment"}, - ], - "paths": { - "/app": { - "get": { - "summary": "Read Main", - "operationId": "read_main_app_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_main(): response = client.get("/app") assert response.status_code == 200 assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "servers": [ + {"url": "https://stag.example.com", "description": "Staging environment"}, + { + "url": "https://prod.example.com", + "description": "Production environment", + }, + ], + "paths": { + "/app": { + "get": { + "summary": "Read Main", + "operationId": "read_main_app_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index cd6d7b5c8..a13decd75 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -5,334 +5,6 @@ from docs_src.bigger_applications.app.main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - no_jessica = { "detail": [ @@ -427,7 +99,6 @@ no_jessica = { ), ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), ("/", 422, no_jessica, {}), - ("/openapi.json", 200, openapi_schema, {}), ], ) def test_get_path(path, expected_status, expected_response, headers): @@ -489,3 +160,337 @@ def test_admin_invalid_header(): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py index 4b84a31b5..64e19c3f3 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -5,334 +5,6 @@ from docs_src.bigger_applications.app_an.main import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - no_jessica = { "detail": [ @@ -427,7 +99,6 @@ no_jessica = { ), ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), ("/", 422, no_jessica, {}), - ("/openapi.json", 200, openapi_schema, {}), ], ) def test_get_path(path, expected_status, expected_response, headers): @@ -489,3 +160,337 @@ def test_admin_invalid_header(): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py index 1caf5bd49..70c86b4d7 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -3,335 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/me": { - "get": { - "tags": ["users"], - "summary": "Read User Me", - "operationId": "read_user_me_users_me_get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{username}": { - "get": { - "tags": ["users"], - "summary": "Read User", - "operationId": "read_user_users__username__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Username", "type": "string"}, - "name": "username", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/{item_id}": { - "get": { - "tags": ["items"], - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "put": { - "tags": ["items", "custom"], - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "404": {"description": "Not found"}, - "403": {"description": "Operation forbidden"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/admin/": { - "post": { - "tags": ["admin"], - "summary": "Update Admin", - "operationId": "update_admin_admin__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - }, - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "418": {"description": "I'm a teapot"}, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Token", "type": "string"}, - "name": "token", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - no_jessica = { "detail": [ { @@ -434,7 +105,6 @@ def get_client(): ), ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), ("/", 422, no_jessica, {}), - ("/openapi.json", 200, openapi_schema, {}), ], ) def test_get_path( @@ -504,3 +174,338 @@ def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/me": { + "get": { + "tags": ["users"], + "summary": "Read User Me", + "operationId": "read_user_me_users_me_get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{username}": { + "get": { + "tags": ["users"], + "summary": "Read User", + "operationId": "read_user_users__username__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Username", "type": "string"}, + "name": "username", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "tags": ["items"], + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + "put": { + "tags": ["items", "custom"], + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "404": {"description": "Not found"}, + "403": {"description": "Operation forbidden"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/admin/": { + "post": { + "tags": ["admin"], + "summary": "Update Admin", + "operationId": "update_admin_admin__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + }, + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "418": {"description": "I'm a teapot"}, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Token", "type": "string"}, + "name": "token", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 65cdc758a..cd1209ade 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -7,89 +7,6 @@ from docs_src.body.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - price_missing = { "detail": [ @@ -277,3 +194,86 @@ def test_other_exceptions(): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index 83bcb68f3..5ebcbbf57 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -5,83 +5,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture def client(): @@ -91,13 +14,6 @@ def client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_missing = { "detail": [ { @@ -292,3 +208,87 @@ def test_other_exceptions(client: TestClient): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index fe5a270f3..a7ea0e949 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -6,115 +6,6 @@ from docs_src.body_fields.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_not_greater = { "detail": [ { @@ -167,3 +58,111 @@ def test(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py index 879769147..32f996ecb 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -6,115 +6,6 @@ from docs_src.body_fields.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_not_greater = { "detail": [ { @@ -167,3 +58,111 @@ def test(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py index 0cd57a187..20e032fcd 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -3,108 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -114,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_not_greater = { "detail": [ { @@ -174,3 +65,112 @@ def test(path, body, expected_status, expected_response, client: TestClient): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py index 26ff26f50..e3baf5f2b 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -3,108 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -114,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_not_greater = { "detail": [ { @@ -174,3 +65,112 @@ def test(path, body, expected_status, expected_response, client: TestClient): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 993e2a91d..4c2f48674 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -3,108 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, - "price": { - "title": "Price", - "exclusiveMinimum": 0.0, - "type": "number", - "description": "The price must be greater than zero", - }, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item"], - "type": "object", - "properties": {"item": {"$ref": "#/components/schemas/Item"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -114,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - price_not_greater = { "detail": [ { @@ -174,3 +65,112 @@ def test(path, body, expected_status, expected_response, client: TestClient): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + }, + "price": { + "title": "Price", + "exclusiveMinimum": 0.0, + "type": "number", + "description": "The price must be greater than zero", + }, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item"], + "type": "object", + "properties": {"item": {"$ref": "#/components/schemas/Item"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 8dc710d75..496ab38fb 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -5,107 +5,6 @@ from docs_src.body_multiple_params.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - item_id_not_int = { "detail": [ @@ -145,3 +44,104 @@ def test_post_body(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index 94ba8593a..74a8a9b2e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -5,107 +5,6 @@ from docs_src.body_multiple_params.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - item_id_not_int = { "detail": [ @@ -145,3 +44,104 @@ def test_post_body(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py index cd378ec9c..9c764b6d1 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -3,101 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -107,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - item_id_not_int = { "detail": [ { @@ -153,3 +51,105 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index b8fe1baaf..0cca29433 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -3,101 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -107,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - item_id_not_int = { "detail": [ { @@ -153,3 +51,105 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 5114ccea2..3b61e717e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -3,101 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "The ID of the item to get", - "maximum": 1000.0, - "minimum": 0.0, - "type": "integer", - }, - "name": "item_id", - "in": "path", - }, - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -107,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - item_id_not_int = { "detail": [ { @@ -153,3 +51,105 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "The ID of the item to get", + "maximum": 1000.0, + "minimum": 0.0, + "type": "integer", + }, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index 64aa9c43b..b34377a28 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -5,118 +5,6 @@ from docs_src.body_multiple_params.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - # Test required and embedded body parameters with no bodies sent @pytest.mark.parametrize( @@ -196,3 +84,115 @@ def test_post_body(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py index 788db8b30..9b8d5e15b 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -5,118 +5,6 @@ from docs_src.body_multiple_params.tutorial003_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - # Test required and embedded body parameters with no bodies sent @pytest.mark.parametrize( @@ -196,3 +84,115 @@ def test_post_body(path, body, expected_status, expected_response): response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py index 9003016cd..f8af555fc 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -3,112 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -118,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - # Test required and embedded body parameters with no bodies sent @needs_py310 @pytest.mark.parametrize( @@ -204,3 +91,116 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py index bc014a441..06e2c3146 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -3,112 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -118,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - # Test required and embedded body parameters with no bodies sent @needs_py39 @pytest.mark.parametrize( @@ -204,3 +91,116 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index fc019d8bb..82c5fb101 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -3,112 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_update_item_items__item_id__put" - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "User": { - "title": "User", - "required": ["username"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - }, - }, - "Body_update_item_items__item_id__put": { - "title": "Body_update_item_items__item_id__put", - "required": ["item", "user", "importance"], - "type": "object", - "properties": { - "item": {"$ref": "#/components/schemas/Item"}, - "user": {"$ref": "#/components/schemas/User"}, - "importance": {"title": "Importance", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -118,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - # Test required and embedded body parameters with no bodies sent @needs_py310 @pytest.mark.parametrize( @@ -204,3 +91,116 @@ def test_post_body(path, body, expected_status, expected_response, client: TestC response = client.put(path, json=body) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_update_item_items__item_id__put" + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "User": { + "title": "User", + "required": ["username"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "full_name": {"title": "Full Name", "type": "string"}, + }, + }, + "Body_update_item_items__item_id__put": { + "title": "Body_update_item_items__item_id__put", + "required": ["item", "user", "importance"], + "type": "object", + "properties": { + "item": {"$ref": "#/components/schemas/Item"}, + "user": {"$ref": "#/components/schemas/User"}, + "importance": {"title": "Importance", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index c56d41b5b..378c24197 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -4,82 +4,6 @@ from docs_src.body_nested_models.tutorial009 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/index-weights/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_body(): data = {"2": 2.2, "3": 3.3} @@ -101,3 +25,79 @@ def test_post_invalid_body(): } ] } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 5b8d82861..5ca63a92b 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -3,76 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/index-weights/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Index Weights", - "operationId": "create_index_weights_index_weights__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Weights", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_post_body(client: TestClient): data = {"2": 2.2, "3": 3.3} @@ -111,3 +34,80 @@ def test_post_invalid_body(client: TestClient): } ] } + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/index-weights/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Index Weights", + "operationId": "create_index_weights_index_weights__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Weights", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index efd0e4676..939bf44e0 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -4,138 +4,6 @@ from docs_src.body_updates.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/baz") @@ -160,3 +28,135 @@ def test_put(): "tax": 10.5, "tags": [], } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 49279b320..5f50f2071 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -3,132 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -138,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_get(client: TestClient): response = client.get("/items/baz") @@ -170,3 +37,136 @@ def test_put(client: TestClient): "tax": 10.5, "tags": [], } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 872530bcf..d4fdabce6 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -3,132 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - }, - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Update Item", - "operationId": "update_item_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - }, - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -138,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get(client: TestClient): response = client.get("/items/baz") @@ -170,3 +37,136 @@ def test_put(client: TestClient): "tax": 10.5, "tags": [], } + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index 93c8775ce..c1d8fd805 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -4,35 +4,6 @@ from fastapi.testclient import TestClient from docs_src.conditional_openapi import tutorial001 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Root", - "operationId": "root__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_default_openapi(): - client = TestClient(tutorial001.app) - response = client.get("/openapi.json") - assert response.json() == openapi_schema - response = client.get("/docs") - assert response.status_code == 200, response.text - response = client.get("/redoc") - assert response.status_code == 200, response.text - def test_disable_openapi(monkeypatch): monkeypatch.setenv("OPENAPI_URL", "") @@ -51,3 +22,31 @@ def test_root(): response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} + + +def test_default_openapi(): + importlib.reload(tutorial001) + client = TestClient(tutorial001.app) + response = client.get("/docs") + assert response.status_code == 200, response.text + response = client.get("/redoc") + assert response.status_code == 200, response.text + response = client.get("/openapi.json") + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Root", + "operationId": "root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 38ae211db..c3511d129 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -3,77 +3,10 @@ from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -90,3 +23,76 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index fb60ea993..f4f94c09d 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -3,77 +3,10 @@ from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001_an import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -90,3 +23,76 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py index 308886085..a80f10f81 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -3,78 +3,11 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @needs_py310 @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -93,3 +26,79 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_an_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py index bbfe5ff9a..1be898c09 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -3,78 +3,11 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @needs_py39 @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -93,3 +26,79 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_an_py39 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index 5ad52fb5e..7ba542c90 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -3,78 +3,11 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Ads Id", "type": "string"}, - "name": "ads_id", - "in": "cookie", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @needs_py310 @pytest.mark.parametrize( "path,cookies,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"ads_id": None}), ("/items", {"ads_id": "ads_track"}, 200, {"ads_id": "ads_track"}), ( @@ -93,3 +26,79 @@ def test(path, cookies, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.cookie_params.tutorial001_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Ads Id", "type": "string"}, + "name": "ads_id", + "in": "cookie", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index 430076f88..da2ca8d62 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index 0f15d5f48..f681f5a9d 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial001b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial004.py index 5d75cce96..ef0ba3446 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial004.py @@ -4,24 +4,6 @@ from docs_src.custom_response.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"text/html": {"schema": {"type": "string"}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} html_contents = """ @@ -35,13 +17,30 @@ html_contents = """ """ -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_get_custom_response(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.text == html_contents + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"text/html": {"schema": {"type": "string"}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index ecf6ee2b9..e4b5c1546 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -4,33 +4,31 @@ from docs_src.custom_response.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "summary": "Main", - "operationId": "main__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"text/plain": {"schema": {"type": "string"}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/") assert response.status_code == 200, response.text assert response.text == "Hello World" + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Main", + "operationId": "main__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"text/plain": {"schema": {"type": "string"}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 9b10916e5..9f1b07bee 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -5,33 +5,30 @@ from docs_src.custom_response.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/typer": { - "get": { - "summary": "Redirect Typer", - "operationId": "redirect_typer_typer_get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} +def test_get(): + response = client.get("/typer", follow_redirects=False) + assert response.status_code == 307, response.text + assert response.headers["location"] == "https://typer.tiangolo.com" def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get(): - response = client.get("/typer", follow_redirects=False) - assert response.status_code == 307, response.text - assert response.headers["location"] == "https://typer.tiangolo.com" + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/typer": { + "get": { + "summary": "Redirect Typer", + "operationId": "redirect_typer_typer_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index b3e60e86a..cf204cbc0 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -5,28 +5,25 @@ from docs_src.custom_response.tutorial006b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/fastapi": { - "get": { - "summary": "Redirect Fastapi", - "operationId": "redirect_fastapi_fastapi_get", - "responses": {"307": {"description": "Successful Response"}}, - } - } - }, -} +def test_redirect_response_class(): + response = client.get("/fastapi", follow_redirects=False) + assert response.status_code == 307 + assert response.headers["location"] == "https://fastapi.tiangolo.com" def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_redirect_response_class(): - response = client.get("/fastapi", follow_redirects=False) - assert response.status_code == 307 - assert response.headers["location"] == "https://fastapi.tiangolo.com" + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/fastapi": { + "get": { + "summary": "Redirect Fastapi", + "operationId": "redirect_fastapi_fastapi_get", + "responses": {"307": {"description": "Successful Response"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index 0cb6ddaa3..f196899ac 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -5,28 +5,25 @@ from docs_src.custom_response.tutorial006c import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/pydantic": { - "get": { - "summary": "Redirect Pydantic", - "operationId": "redirect_pydantic_pydantic_get", - "responses": {"302": {"description": "Successful Response"}}, - } - } - }, -} +def test_redirect_status_code(): + response = client.get("/pydantic", follow_redirects=False) + assert response.status_code == 302 + assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/" def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_redirect_status_code(): - response = client.get("/pydantic", follow_redirects=False) - assert response.status_code == 302 - assert response.headers["location"] == "https://pydantic-docs.helpmanual.io/" + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/pydantic": { + "get": { + "summary": "Redirect Pydantic", + "operationId": "redirect_pydantic_pydantic_get", + "responses": {"302": {"description": "Successful Response"}}, + } + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index bf1564194..26ae04c51 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -4,89 +4,6 @@ from docs_src.dataclasses.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_post_item(): response = client.post("/items/", json={"name": "Foo", "price": 3}) @@ -111,3 +28,86 @@ def test_post_invalid_item(): } ] } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 2d86f7b9a..f3378fe62 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -4,136 +4,6 @@ from docs_src.dataclasses.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/authors/{author_id}/items/": { - "post": { - "summary": "Create Author Items", - "operationId": "create_author_items_authors__author_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "Author Id", "type": "string"}, - "name": "author_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Author"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/authors/": { - "get": { - "summary": "Get Authors", - "operationId": "get_authors_authors__get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Get Authors Authors Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Author"}, - } - } - }, - } - }, - } - }, - }, - "components": { - "schemas": { - "Author": { - "title": "Author", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - assert response.json() == openapi_schema - def test_post_authors_item(): response = client.post( @@ -179,3 +49,135 @@ def test_get_authors(): ], }, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/authors/{author_id}/items/": { + "post": { + "summary": "Create Author Items", + "operationId": "create_author_items_authors__author_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Author Id", "type": "string"}, + "name": "author_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Author"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/authors/": { + "get": { + "summary": "Get Authors", + "operationId": "get_authors_authors__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Authors Authors Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Author": { + "title": "Author", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index c3bca5d5b..974b9304f 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -5,132 +5,6 @@ from docs_src.dependencies.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -140,10 +14,151 @@ def test_openapi_schema(): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py index 13960addc..b1ca27ff8 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -5,132 +5,6 @@ from docs_src.dependencies.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -140,10 +14,151 @@ def test_openapi_schema(): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py index 4b093af0d..70bed03f6 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -3,126 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -132,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -148,10 +21,152 @@ def test_openapi_schema(client: TestClient): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py index 6059924cc..9c5723be8 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -3,126 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -132,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -148,10 +21,152 @@ def test_openapi_schema(client: TestClient): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 32a61c821..1bcde4e9f 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -3,126 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - }, - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -132,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -148,10 +21,152 @@ def test_openapi_schema(client: TestClient): ("/items?q=foo&skip=5", 200, {"q": "foo", "skip": 5, "limit": 100}), ("/items?q=foo&skip=5&limit=30", 200, {"q": "foo", "skip": 5, "limit": 30}), ("/users", 200, {"q": None, "skip": 0, "limit": 100}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + }, + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index f2b1878d5..298bc290d 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -5,90 +5,6 @@ from docs_src.dependencies.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -142,3 +58,95 @@ def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py index ef6199b04..f985be8df 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -5,90 +5,6 @@ from docs_src.dependencies.tutorial004_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -142,3 +58,95 @@ def test_get(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index e9736780c..fc0286702 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -3,84 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -90,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -150,3 +65,96 @@ def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 2b346f3b2..1e37673ed 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -3,84 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -90,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -150,3 +65,96 @@ def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index e3ae0c741..ab936ccdc 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -3,84 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Q", "type": "string"}, - "name": "q", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer", "default": 100}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -90,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -150,3 +65,96 @@ def test_get(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Q", "type": "string"}, + "name": "q", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Limit", + "type": "integer", + "default": 100, + }, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 2916577a2..2e9c82d71 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -4,84 +4,6 @@ from docs_src.dependencies.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers(): response = client.get("/items/") @@ -126,3 +48,81 @@ def test_get_valid_headers(): ) assert response.status_code == 200, response.text assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py index f33b67d58..919066dca 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -4,84 +4,6 @@ from docs_src.dependencies.tutorial006_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers(): response = client.get("/items/") @@ -126,3 +48,81 @@ def test_get_valid_headers(): ) assert response.status_code == 200, response.text assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py index 171e39a96..c23718479 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -3,78 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -84,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_no_headers(client: TestClient): response = client.get("/items/") @@ -138,3 +59,82 @@ def test_get_valid_headers(client: TestClient): ) assert response.status_code == 200, response.text assert response.json() == [{"item": "Foo"}, {"item": "Bar"}] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index e4e07395d..b92b96c01 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -4,120 +4,6 @@ from docs_src.dependencies.tutorial012 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers_items(): response = client.get("/items/") @@ -207,3 +93,117 @@ def test_get_valid_headers_users(): ) assert response.status_code == 200, response.text assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py index 0a6908f72..2ddb7bb53 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -4,120 +4,6 @@ from docs_src.dependencies.tutorial012_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_no_headers_items(): response = client.get("/items/") @@ -207,3 +93,117 @@ def test_get_valid_headers_users(): ) assert response.status_code == 200, response.text assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py index 25f54f4c9..595c83a53 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -3,114 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": True, - "schema": {"title": "X-Token", "type": "string"}, - "name": "x-token", - "in": "header", - }, - { - "required": True, - "schema": {"title": "X-Key", "type": "string"}, - "name": "x-key", - "in": "header", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -120,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_no_headers_items(client: TestClient): response = client.get("/items/") @@ -223,3 +108,118 @@ def test_get_valid_headers_users(client: TestClient): ) assert response.status_code == 200, response.text assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "parameters": [ + { + "required": True, + "schema": {"title": "X-Token", "type": "string"}, + "name": "x-token", + "in": "header", + }, + { + "required": True, + "schema": {"title": "X-Key", "type": "string"}, + "name": "x-key", + "in": "header", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index d52dd1a04..52f9beed5 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -2,78 +2,84 @@ from fastapi.testclient import TestClient from docs_src.events.tutorial001 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - def test_events(): with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"name": "Fighters"} + + +def test_openapi_schema(): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index f6ac1e07b..882d41aa5 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -2,33 +2,35 @@ from fastapi.testclient import TestClient from docs_src.events.tutorial002 import app -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - def test_events(): with TestClient(app) as client: - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] with open("log.txt") as log: assert "Application shutdown" in log.read() + + +def test_openapi_schema(): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index 56b493954..b2820b63c 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -6,81 +6,87 @@ from docs_src.events.tutorial003 import ( ml_models, ) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/predict": { - "get": { - "summary": "Predict", - "operationId": "predict_predict_get", - "parameters": [ - { - "required": True, - "schema": {"title": "X", "type": "number"}, - "name": "x", - "in": "query", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - def test_events(): assert not ml_models, "ml_models should be empty" with TestClient(app) as client: assert ml_models["answer_to_everything"] == fake_answer_to_everything_ml_model - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema response = client.get("/predict", params={"x": 2}) assert response.status_code == 200, response.text assert response.json() == {"result": 84.0} assert not ml_models, "ml_models should be empty" + + +def test_openapi_schema(): + with TestClient(app) as client: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/predict": { + "get": { + "summary": "Predict", + "operationId": "predict_predict_get", + "parameters": [ + { + "required": True, + "schema": {"title": "X", "type": "number"}, + "name": "x", + "in": "query", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index ec56e9ca6..6e71bb2de 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -4,41 +4,43 @@ from docs_src.extending_openapi.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": { - "title": "Custom title", - "version": "2.5.0", - "description": "This is a very custom OpenAPI schema", - "x-logo": {"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"}, - }, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": { + "title": "Custom title", + "version": "2.5.0", + "description": "This is a very custom OpenAPI schema", + "x-logo": { + "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" + }, + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + } + openapi_schema = response.json() + # Request again to test the custom cache + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == openapi_schema diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 8522d7b9d..07a834990 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -5,118 +5,6 @@ from docs_src.extra_data_types.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_extra_types(): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" data = { @@ -136,3 +24,114 @@ def test_extra_types(): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py index d5be16dfb..76836d447 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -5,118 +5,6 @@ from docs_src.extra_data_types.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_extra_types(): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" data = { @@ -136,3 +24,114 @@ def test_extra_types(): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py index 80806b694..158ee01b3 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -3,111 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -117,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_extra_types(client: TestClient): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" @@ -144,3 +32,115 @@ def test_extra_types(client: TestClient): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py index 5c7d43394..5be6452ee 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -3,111 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -117,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_extra_types(client: TestClient): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" @@ -144,3 +32,115 @@ def test_extra_types(client: TestClient): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 4efdecc53..5413fe428 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -3,111 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "put": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__item_id__put", - "parameters": [ - { - "required": True, - "schema": { - "title": "Item Id", - "type": "string", - "format": "uuid", - }, - "name": "item_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "Body_read_items_items__item_id__put": { - "title": "Body_read_items_items__item_id__put", - "type": "object", - "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -117,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_extra_types(client: TestClient): item_id = "ff97dd87-a4a5-4a12-b412-cde99f33e00e" @@ -144,3 +32,115 @@ def test_extra_types(client: TestClient): response = client.put(f"/items/{item_id}", json=data) assert response.status_code == 200, response.text assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": { + "title": "Item Id", + "type": "string", + "format": "uuid", + }, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__item_id__put": { + "title": "Body_read_items_items__item_id__put", + "type": "object", + "properties": { + "start_datetime": { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + }, + "end_datetime": { + "title": "End Datetime", + "type": "string", + "format": "date-time", + }, + "repeat_at": { + "title": "Repeat At", + "type": "string", + "format": "time", + }, + "process_after": { + "title": "Process After", + "type": "number", + "format": "time-delta", + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index f1433470c..f08bf4c50 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -4,107 +4,6 @@ from docs_src.extra_models.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": ["description", "size"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "CarItem": { - "title": "CarItem", - "required": ["description"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_car(): response = client.get("/items/item1") @@ -123,3 +22,104 @@ def test_get_plane(): "type": "plane", "size": 5, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index 56fd83ad3..407c71787 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -3,101 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Item Items Item Id Get", - "anyOf": [ - {"$ref": "#/components/schemas/PlaneItem"}, - {"$ref": "#/components/schemas/CarItem"}, - ], - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "PlaneItem": { - "title": "PlaneItem", - "required": ["description", "size"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "plane"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "CarItem": { - "title": "CarItem", - "required": ["description"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "type": {"title": "Type", "type": "string", "default": "car"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -107,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_get_car(client: TestClient): response = client.get("/items/item1") @@ -133,3 +31,105 @@ def test_get_plane(client: TestClient): "type": "plane", "size": 5, } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Item Items Item Id Get", + "anyOf": [ + {"$ref": "#/components/schemas/PlaneItem"}, + {"$ref": "#/components/schemas/CarItem"}, + ], + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "PlaneItem": { + "title": "PlaneItem", + "required": ["description", "size"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "plane"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "CarItem": { + "title": "CarItem", + "required": ["description"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "type": {"title": "Type", "type": "string", "default": "car"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 548fb8834..47790ba8f 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -4,52 +4,6 @@ from docs_src.extra_models.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/items/") @@ -58,3 +12,47 @@ def test_get_items(): {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py index 7f4f5b9be..a98700172 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -3,46 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - } - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -52,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_items(client: TestClient): response = client.get("/items/") @@ -67,3 +20,48 @@ def test_get_items(client: TestClient): {"name": "Foo", "description": "There comes my hero"}, {"name": "Red", "description": "It's my aeroplane"}, ] + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Items Items Get", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index c3dfaa42f..7c094b253 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -4,41 +4,39 @@ from docs_src.extra_models.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/keyword-weights/") assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py index 3bb5a99f1..b40386450 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -3,33 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/keyword-weights/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Keyword Weights Keyword Weights Get", - "type": "object", - "additionalProperties": {"type": "number"}, - } - } - }, - } - }, - "summary": "Read Keyword Weights", - "operationId": "read_keyword_weights_keyword_weights__get", - } - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -39,15 +12,40 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_get_items(client: TestClient): response = client.get("/keyword-weights/") assert response.status_code == 200, response.text assert response.json() == {"foo": 2.3, "bar": 3.4} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/keyword-weights/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Read Keyword Weights Keyword Weights Get", + "type": "object", + "additionalProperties": {"type": "number"}, + } + } + }, + } + }, + "summary": "Read Keyword Weights", + "operationId": "read_keyword_weights_keyword_weights__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001.py index 48d42285c..ea37aec53 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001.py @@ -5,35 +5,38 @@ from docs_src.first_steps.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Root", - "operationId": "root__get", - } - } - }, -} - @pytest.mark.parametrize( "path,expected_status,expected_response", [ ("/", 200, {"message": "Hello World"}), ("/nonexistent", 404, {"detail": "Not Found"}), - ("/openapi.json", 200, openapi_schema), ], ) def test_get_path(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Root", + "operationId": "root__get", + } + } + }, + } diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index 128fcea30..8b22eab9e 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -4,166 +4,6 @@ from docs_src.generate_clients.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "items-get_items", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Items-Get Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - } - }, - }, - "post": { - "tags": ["items"], - "summary": "Create Item", - "operationId": "items-create_item", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/": { - "post": { - "tags": ["users"], - "summary": "Create User", - "operationId": "users-create_user", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ResponseMessage" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - }, - }, - "ResponseMessage": { - "title": "ResponseMessage", - "required": ["message"], - "type": "object", - "properties": {"message": {"title": "Message", "type": "string"}}, - }, - "User": { - "title": "User", - "required": ["username", "email"], - "type": "object", - "properties": { - "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_post_items(): response = client.post("/items/", json={"name": "Foo", "price": 5}) @@ -186,3 +26,162 @@ def test_get_items(): {"name": "Plumbus", "price": 3}, {"name": "Portal Gun", "price": 9001}, ] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "items-get_items", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Items-Get Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + } + }, + } + }, + }, + "post": { + "tags": ["items"], + "summary": "Create Item", + "operationId": "items-create_item", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/users/": { + "post": { + "tags": ["users"], + "summary": "Create User", + "operationId": "users-create_user", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/User"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResponseMessage" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "ResponseMessage": { + "title": "ResponseMessage", + "required": ["message"], + "type": "object", + "properties": {"message": {"title": "Message", "type": "string"}}, + }, + "User": { + "title": "User", + "required": ["username", "email"], + "type": "object", + "properties": { + "username": {"title": "Username", "type": "string"}, + "email": {"title": "Email", "type": "string"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index ffd79ccff..99a1053ca 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item(): response = client.get("/items/foo") @@ -88,3 +16,75 @@ def test_get_item_not_found(): assert response.status_code == 404, response.text assert response.headers.get("x-error") is None assert response.json() == {"detail": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index e678499c6..091c74f4d 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items-header/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item Header", - "operationId": "read_item_header_items_header__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_item_header(): response = client.get("/items-header/foo") @@ -88,3 +16,75 @@ def test_get_item_not_found_header(): assert response.status_code == 404, response.text assert response.headers.get("x-error") == "There goes my error" assert response.json() == {"detail": "Item not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items-header/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item Header", + "operationId": "read_item_header_items_header__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index a01726dc2..1639cb1d8 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/unicorns/{name}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Unicorn", - "operationId": "read_unicorn_unicorns__name__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Name", "type": "string"}, - "name": "name", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/unicorns/shinny") @@ -89,3 +17,75 @@ def test_get_exception(): assert response.json() == { "message": "Oops! yolo did something. There goes a rainbow..." } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/unicorns/{name}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Unicorn", + "operationId": "read_unicorn_unicorns__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 0b5f74798..246f3b94c 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_validation_error(): response = client.get("/items/foo") @@ -98,3 +26,75 @@ def test_get(): response = client.get("/items/2") assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index 253f3d006..af47fd1a4 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -4,87 +4,6 @@ from docs_src.handling_errors.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["title", "size"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "size": {"title": "Size", "type": "integer"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) @@ -106,3 +25,84 @@ def test_post(): response = client.post("/items/", json=data) assert response.status_code == 200, response.text assert response.json() == data + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["title", "size"], + "type": "object", + "properties": { + "title": {"title": "Title", "type": "string"}, + "size": {"title": "Size", "type": "integer"}, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 21233d7bb..4a39bd102 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -4,78 +4,6 @@ from docs_src.handling_errors.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Item", - "operationId": "read_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "integer"}, - "name": "item_id", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_validation_error(): response = client.get("/items/foo") @@ -101,3 +29,75 @@ def test_get(): response = client.get("/items/2") assert response.status_code == 200, response.text assert response.json() == {"item_id": 2} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 273cf3249..80f502d6a 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -6,77 +6,9 @@ from docs_src.header_params.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -86,3 +18,75 @@ def test(path, headers, expected_status, expected_response): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py index 3c155f786..f0ad7b816 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -6,77 +6,9 @@ from docs_src.header_params.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -86,3 +18,75 @@ def test(path, headers, expected_status, expected_response): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py index 1f86f2a5d..d095c7123 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -3,72 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +16,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -92,3 +25,76 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index 77a60eb9d..bf176bba2 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -3,72 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "User-Agent", "type": "string"}, - "name": "user-agent", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +16,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"User-Agent": "testclient"}), ("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}), ("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}), @@ -92,3 +25,76 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "User-Agent", "type": "string"}, + "name": "user-agent", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index b23398287..516abda8b 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -6,77 +6,9 @@ from docs_src.header_params.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Strange Header", "type": "string"}, - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"strange_header": None}), ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), ( @@ -97,3 +29,75 @@ def test(path, headers, expected_status, expected_response): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py index 77d236e09..97493e604 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -6,77 +6,9 @@ from docs_src.header_params.tutorial002_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Strange Header", "type": "string"}, - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"strange_header": None}), ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), ( @@ -97,3 +29,75 @@ def test(path, headers, expected_status, expected_response): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py index 49b0ef462..e0c60342a 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -3,72 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Strange Header", "type": "string"}, - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +16,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"strange_header": None}), ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), ( @@ -103,3 +36,76 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py index 13aaabeb8..c1bc5faf8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -3,72 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Strange Header", "type": "string"}, - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +16,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"strange_header": None}), ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), ( @@ -103,3 +36,79 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(): + from docs_src.header_params.tutorial002_an_py39 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py index 6cae3d338..81871b8c6 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -3,72 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": {"title": "Strange Header", "type": "string"}, - "name": "strange_header", - "in": "header", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,7 +16,6 @@ def get_client(): @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ - ("/openapi.json", None, 200, openapi_schema), ("/items", None, 200, {"strange_header": None}), ("/items", {"X-Header": "notvalid"}, 200, {"strange_header": None}), ( @@ -103,3 +36,79 @@ def test(path, headers, expected_status, expected_response, client: TestClient): response = client.get(path, headers=headers) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(): + from docs_src.header_params.tutorial002_py310 import app + + client = TestClient(app) + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": {"title": "Strange Header", "type": "string"}, + "name": "strange_header", + "in": "header", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index b7281e293..f1ddc3259 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -4,47 +4,45 @@ from docs_src.metadata.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": { - "title": "ChimichangApp", - "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", - "termsOfService": "http://example.com/terms/", - "contact": { - "name": "Deadpoolio the Amazing", - "url": "http://x-force.example.com/contact/", - "email": "dp@x-force.example.com", - }, - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html", - }, - "version": "0.0.1", - }, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_items(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"name": "Katana"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": { + "title": "ChimichangApp", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + }, + "version": "0.0.1", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index 2d255b8b0..f7f47a558 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -4,62 +4,60 @@ from docs_src.metadata.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "tags": ["users"], - "summary": "Get Users", - "operationId": "get_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "get_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, - "tags": [ - { - "name": "users", - "description": "Operations with users. The **login** logic is also here.", - }, - { - "name": "items", - "description": "Manage items. So _fancy_ they have their own docs.", - "externalDocs": { - "description": "Items external docs", - "url": "https://fastapi.tiangolo.com/", - }, - }, - ], -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_path_operations(): response = client.get("/items/") assert response.status_code == 200, response.text response = client.get("/users/") assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "tags": ["users"], + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + "tags": [ + { + "name": "users", + "description": "Operations with users. The **login** logic is also here.", + }, + { + "name": "items", + "description": "Manage items. So _fancy_ they have their own docs.", + "externalDocs": { + "description": "Items external docs", + "url": "https://fastapi.tiangolo.com/", + }, + }, + ], + } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index e773e7f8f..c6cdc6064 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -4,162 +4,6 @@ from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/invoices/": { - "post": { - "summary": "Create Invoice", - "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', - "operationId": "create_invoice_invoices__post", - "parameters": [ - { - "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, - "name": "callback_url", - "in": "query", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Invoice"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "callbacks": { - "invoice_notification": { - "{$callback_url}/invoices/{$request.body.id}": { - "post": { - "summary": "Invoice Notification", - "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", - "requestBody": { - "required": True, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEvent" - } - } - }, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvoiceEventReceived" - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - } - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Invoice": { - "title": "Invoice", - "required": ["id", "customer", "total"], - "type": "object", - "properties": { - "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, - "customer": {"title": "Customer", "type": "string"}, - "total": {"title": "Total", "type": "number"}, - }, - }, - "InvoiceEvent": { - "title": "InvoiceEvent", - "required": ["description", "paid"], - "type": "object", - "properties": { - "description": {"title": "Description", "type": "string"}, - "paid": {"title": "Paid", "type": "boolean"}, - }, - }, - "InvoiceEventReceived": { - "title": "InvoiceEventReceived", - "required": ["ok"], - "type": "object", - "properties": {"ok": {"title": "Ok", "type": "boolean"}}, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - with client: - response = client.get("/openapi.json") - - assert response.json() == openapi_schema - def test_get(): response = client.post( @@ -172,3 +16,158 @@ def test_get(): def test_dummy_callback(): # Just for coverage invoice_notification({}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/invoices/": { + "post": { + "summary": "Create Invoice", + "description": 'Create an invoice.\n\nThis will (let\'s imagine) let the API user (some external developer) create an\ninvoice.\n\nAnd this path operation will:\n\n* Send the invoice to the client.\n* Collect the money from the client.\n* Send a notification back to the API user (the external developer), as a callback.\n * At this point is that the API will somehow send a POST request to the\n external API with the notification of the invoice event\n (e.g. "payment successful").', + "operationId": "create_invoice_invoices__post", + "parameters": [ + { + "required": False, + "schema": { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + }, + "name": "callback_url", + "in": "query", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Invoice"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "callbacks": { + "invoice_notification": { + "{$callback_url}/invoices/{$request.body.id}": { + "post": { + "summary": "Invoice Notification", + "operationId": "invoice_notification__callback_url__invoices___request_body_id__post", + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEvent" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvoiceEventReceived" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + } + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Invoice": { + "title": "Invoice", + "required": ["id", "customer", "total"], + "type": "object", + "properties": { + "id": {"title": "Id", "type": "string"}, + "title": {"title": "Title", "type": "string"}, + "customer": {"title": "Customer", "type": "string"}, + "total": {"title": "Total", "type": "number"}, + }, + }, + "InvoiceEvent": { + "title": "InvoiceEvent", + "required": ["description", "paid"], + "type": "object", + "properties": { + "description": {"title": "Description", "type": "string"}, + "paid": {"title": "Paid", "type": "boolean"}, + }, + }, + "InvoiceEventReceived": { + "title": "InvoiceEventReceived", + "required": ["ok"], + "type": "object", + "properties": {"ok": {"title": "Ok", "type": "boolean"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index 3b5301348..c1cdbee24 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "some_specific_id_you_define", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "some_specific_id_you_define", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index 01acb664c..fdaddd018 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items", - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 4a23db7bc..782c64a84 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -4,20 +4,18 @@ from docs_src.path_operation_advanced_configuration.tutorial003 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": {}, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get(): response = client.get("/items/") assert response.status_code == 200, response.text assert response.json() == [{"item_id": "Foo"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 3de19833b..f5fd868eb 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -4,101 +4,6 @@ from docs_src.path_operation_advanced_configuration.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -110,3 +15,98 @@ def test_query_params_str_validations(): "tax": None, "tags": [], } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py index 5042d1837..52379c01e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -4,33 +4,31 @@ from docs_src.path_operation_advanced_configuration.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "x-aperture-labs-portal": "blue", - } - } - }, -} + +def test_get(): + response = client.get("/items/") + assert response.status_code == 200, response.text def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - -def test_get(): - response = client.get("/items/") - assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "x-aperture-labs-portal": "blue", + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index 330b4e2c7..deb6b0910 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -4,47 +4,6 @@ from docs_src.path_operation_advanced_configuration.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"type": "string"}, - "price": {"type": "number"}, - "description": {"type": "string"}, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post(): response = client.post("/items/", content=b"this is actually not validated") @@ -57,3 +16,42 @@ def test_post(): "description": "Just kiddin', no magic here. ✨", }, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"type": "string"}, + "price": {"type": "number"}, + "description": {"type": "string"}, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 076f60b2f..470956a77 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -4,51 +4,6 @@ from docs_src.path_operation_advanced_configuration.tutorial007 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "summary": "Create Item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/x-yaml": { - "schema": { - "title": "Item", - "required": ["name", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - }, - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post(): yaml_data = """ @@ -95,3 +50,46 @@ def test_post_invalid(): {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} ] } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py index be9f2afec..76e44b5e5 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -4,45 +4,6 @@ from docs_src.path_operation_configuration.tutorial002b import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "tags": ["items"], - "summary": "Get Items", - "operationId": "get_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - "/users/": { - "get": { - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_get_items(): response = client.get("/items/") @@ -54,3 +15,40 @@ def test_get_users(): response = client.get("/users/") assert response.status_code == 200, response.text assert response.json() == ["Rick", "Morty"] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "tags": ["items"], + "summary": "Get Items", + "operationId": "get_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/": { + "get": { + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index e587519a0..cf8e203a0 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -4,101 +4,6 @@ from docs_src.path_operation_configuration.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_query_params_str_validations(): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -110,3 +15,98 @@ def test_query_params_str_validations(): "tax": None, "tags": [], } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index 43a7a610d..497fc9024 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -3,95 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -101,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -119,3 +23,99 @@ def test_query_params_str_validations(client: TestClient): "tax": None, "tags": [], } + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 62aa73ac5..09fac44c4 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -3,95 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "post": { - "responses": { - "200": { - "description": "The created item", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create an item", - "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", - "operationId": "create_item_items__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - "required": True, - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -101,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_query_params_str_validations(client: TestClient): response = client.post("/items/", json={"name": "Foo", "price": 42}) @@ -119,3 +23,99 @@ def test_query_params_str_validations(client: TestClient): "tax": None, "tags": [], } + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index 582caed44..e90771f24 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -5,59 +5,6 @@ from docs_src.path_operation_configuration.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Items", - "operationId": "read_items_items__get", - } - }, - "/users/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["users"], - "summary": "Read Users", - "operationId": "read_users_users__get", - } - }, - "/elements/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "tags": ["items"], - "summary": "Read Elements", - "operationId": "read_elements_elements__get", - "deprecated": True, - } - }, - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - @pytest.mark.parametrize( "path,expected_status,expected_response", @@ -71,3 +18,54 @@ def test_query_params_str_validations(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Items", + "operationId": "read_items_items__get", + } + }, + "/users/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["users"], + "summary": "Read Users", + "operationId": "read_users_users__get", + } + }, + "/elements/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["items"], + "summary": "Read Elements", + "operationId": "read_elements_elements__get", + "deprecated": True, + } + }, + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index 7f0227ecf..ab0455bf5 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -4,78 +4,6 @@ from docs_src.path_params.tutorial004 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/{file_path}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read File", - "operationId": "read_file_files__file_path__get", - "parameters": [ - { - "required": True, - "schema": {"title": "File Path", "type": "string"}, - "name": "file_path", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_file_path(): response = client.get("/files/home/johndoe/myfile.txt") @@ -89,3 +17,75 @@ def test_root_file_path(): print(response.content) assert response.status_code == 200, response.text assert response.json() == {"file_path": "/home/johndoe/myfile.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/{file_path}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read File", + "operationId": "read_file_files__file_path__get", + "parameters": [ + { + "required": True, + "schema": {"title": "File Path", "type": "string"}, + "name": "file_path", + "in": "path", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index eae3637be..3401f2253 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -5,156 +5,6 @@ from docs_src.path_params.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/models/{model_name}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Get Model", - "operationId": "get_model_models__model_name__get", - "parameters": [ - { - "required": True, - "schema": { - "title": "Model Name", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - }, - "name": "model_name", - "in": "path", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -openapi_schema2 = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/models/{model_name}": { - "get": { - "summary": "Get Model", - "operationId": "get_model_models__model_name__get", - "parameters": [ - { - "required": True, - "schema": {"$ref": "#/components/schemas/ModelName"}, - "name": "model_name", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ModelName": { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - data = response.json() - assert data == openapi_schema or data == openapi_schema2 - @pytest.mark.parametrize( "url,status_code,expected", @@ -194,3 +44,82 @@ def test_get_enums(url, status_code, expected): response = client.get(url) assert response.status_code == status_code assert response.json() == expected + + +def test_openapi(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + data = response.json() + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/models/{model_name}": { + "get": { + "summary": "Get Model", + "operationId": "get_model_models__model_name__get", + "parameters": [ + { + "required": True, + "schema": {"$ref": "#/components/schemas/ModelName"}, + "name": "model_name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelName": { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + "description": "An enumeration.", + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 07178f8a6..3c408449b 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -5,78 +5,6 @@ from docs_src.query_params.tutorial005 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - query_required = { "detail": [ @@ -92,7 +20,6 @@ query_required = { @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/openapi.json", 200, openapi_schema), ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}), ("/items/foo", 422, query_required), ("/items/foo", 422, query_required), @@ -102,3 +29,81 @@ def test(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 73c5302e7..7fe58a990 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -5,90 +5,6 @@ from docs_src.query_params.tutorial006 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer"}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - query_required = { "detail": [ @@ -104,7 +20,6 @@ query_required = { @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/openapi.json", 200, openapi_schema), ( "/items/foo?needy=very", 200, @@ -139,3 +54,97 @@ def test(path, expected_status, expected_response): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer"}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index 141525f15..b90c0a6c8 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -3,91 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/{item_id}": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read User Item", - "operationId": "read_user_item_items__item_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "name": "item_id", - "in": "path", - }, - { - "required": True, - "schema": {"title": "Needy", "type": "string"}, - "name": "needy", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Skip", "type": "integer", "default": 0}, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": {"title": "Limit", "type": "integer"}, - "name": "limit", - "in": "query", - }, - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - query_required = { "detail": [ { @@ -111,7 +26,6 @@ def get_client(): @pytest.mark.parametrize( "path,expected_status,expected_response", [ - ("/openapi.json", 200, openapi_schema), ( "/items/foo?needy=very", 200, @@ -146,3 +60,98 @@ def test(path, expected_status, expected_response, client: TestClient): response = client.get(path) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read User Item", + "operationId": "read_user_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "schema": {"title": "Needy", "type": "string"}, + "name": "needy", + "in": "query", + }, + { + "required": False, + "schema": { + "title": "Skip", + "type": "integer", + "default": 0, + }, + "name": "skip", + "in": "query", + }, + { + "required": False, + "schema": {"title": "Limit", "type": "integer"}, + "name": "limit", + "in": "query", + }, + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index f8d7f85c8..c41f554ba 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -5,87 +5,6 @@ from docs_src.query_params_str_validations.tutorial010 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - regex_error = { "detail": [ @@ -120,3 +39,84 @@ def test_query_params_str_validations(q_name, q, expected_status, expected_respo response = client.get(url) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py index b2b9b5018..dc8028f81 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -5,87 +5,6 @@ from docs_src.query_params_str_validations.tutorial010_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - regex_error = { "detail": [ @@ -120,3 +39,84 @@ def test_query_params_str_validations(q_name, q, expected_status, expected_respo response = client.get(url) assert response.status_code == expected_status assert response.json() == expected_response + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py index edbe4d009..496b95b79 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -3,81 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -87,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - regex_error = { "detail": [ { @@ -130,3 +48,85 @@ def test_query_params_str_validations( response = client.get(url) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py index f51e90247..2005e5043 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -3,81 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -87,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - regex_error = { "detail": [ { @@ -130,3 +48,85 @@ def test_query_params_str_validations( response = client.get(url) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py index 298b5d616..8147d768e 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -3,81 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "description": "Query string for the items to search in the database that have a good match", - "required": False, - "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, - "name": "item-query", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -87,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - regex_error = { "detail": [ { @@ -130,3 +48,85 @@ def test_query_params_str_validations( response = client.get(url) assert response.status_code == expected_status assert response.json() == expected_response + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "description": "Query string for the items to search in the database that have a good match", + "required": False, + "deprecated": True, + "schema": { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + }, + "name": "item-query", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index ad3645f31..d6d69c169 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -4,82 +4,6 @@ from docs_src.query_params_str_validations.tutorial011 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -93,3 +17,79 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py index 25a11f2ca..3a53d422e 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -4,82 +4,6 @@ from docs_src.query_params_str_validations.tutorial011_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -93,3 +17,79 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py index 99aaf3948..f00df2879 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -3,76 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +26,80 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py index 902add851..895fb8e9f 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -3,76 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +26,80 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 9330037ed..4f4b1fd55 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -3,76 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +26,80 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index 11f23be27..d85bb6231 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -3,76 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -82,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -103,3 +26,80 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": None} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index d69139dda..7bc020540 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial012 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_default_query_values(): url = "/items/" @@ -94,3 +17,80 @@ def test_multi_query_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py index e57a2178f..be5557f6a 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial012_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_default_query_values(): url = "/items/" @@ -94,3 +17,80 @@ def test_multi_query_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py index 140b74790..d9512e193 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -3,77 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -83,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_default_query_values(client: TestClient): url = "/items/" @@ -104,3 +26,81 @@ def test_multi_query_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py index b25bb2847..b2a2c8d1d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -3,77 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - "default": ["foo", "bar"], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -83,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_default_query_values(client: TestClient): url = "/items/" @@ -104,3 +26,81 @@ def test_multi_query_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": ["baz", "foobar"]} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + "default": ["foo", "bar"], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 1b2e36354..4a0b9e8b5 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial013 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -94,3 +17,80 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": []} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py index fc684b557..71e4638ae 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -4,83 +4,6 @@ from docs_src.query_params_str_validations.tutorial013_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_multi_query_values(): url = "/items/?q=foo&q=bar" @@ -94,3 +17,80 @@ def test_query_no_values(): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": []} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py index 9d3f255e0..4e90db358 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -3,77 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {}, - "default": [], - }, - "name": "q", - "in": "query", - } - ], - } - } - }, - "components": { - "schemas": { - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -83,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_multi_query_values(client: TestClient): url = "/items/?q=foo&q=bar" @@ -104,3 +26,81 @@ def test_query_no_values(client: TestClient): response = client.get(url) assert response.status_code == 200, response.text assert response.json() == {"q": []} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "required": False, + "schema": { + "title": "Q", + "type": "array", + "items": {}, + "default": [], + }, + "name": "q", + "in": "query", + } + ], + } + } + }, + "components": { + "schemas": { + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 57b8b9d94..7686c07b3 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -5,71 +5,6 @@ from docs_src.query_params_str_validations.tutorial014 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_hidden_query(): response = client.get("/items?hidden_query=somevalue") assert response.status_code == 200, response.text @@ -80,3 +15,67 @@ def test_no_hidden_query(): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py index ba5bf7c50..e739044a8 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -5,71 +5,6 @@ from docs_src.query_params_str_validations.tutorial014_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - def test_hidden_query(): response = client.get("/items?hidden_query=somevalue") assert response.status_code == 200, response.text @@ -80,3 +15,67 @@ def test_no_hidden_query(): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py index 69e176bab..73f0ba78b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -3,64 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -70,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_hidden_query(client: TestClient): response = client.get("/items?hidden_query=somevalue") @@ -89,3 +24,68 @@ def test_no_hidden_query(client: TestClient): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py index 2adfddfef..e2c149992 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -3,64 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -70,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_hidden_query(client: TestClient): response = client.get("/items?hidden_query=somevalue") @@ -89,3 +24,68 @@ def test_no_hidden_query(client: TestClient): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py index fe54fc080..07f30b739 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -3,64 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - } - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -70,13 +12,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_hidden_query(client: TestClient): response = client.get("/items?hidden_query=somevalue") @@ -89,3 +24,68 @@ def test_no_hidden_query(client: TestClient): response = client.get("/items") assert response.status_code == 200, response.text assert response.json() == {"hidden_query": "Not found"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 166014c71..3269801ef 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -4,128 +4,6 @@ from docs_src.request_files.tutorial001 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - file_required = { "detail": [ @@ -182,3 +60,125 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index a254bf3e8..4b6edfa06 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -4,124 +4,6 @@ from docs_src.request_files.tutorial001_02 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_form_no_body(): response = client.post("/files/") @@ -155,3 +37,121 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py index 50b05fa4b..0c34620e3 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -4,124 +4,6 @@ from docs_src.request_files.tutorial001_02_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_form_no_body(): response = client.post("/files/") @@ -155,3 +37,121 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py index a5796b74c..04442c76f 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -5,118 +5,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -126,13 +14,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_post_form_no_body(client: TestClient): response = client.post("/files/") @@ -167,3 +48,122 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py index 57175f736..f5249ef5b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -5,118 +5,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -126,13 +14,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") @@ -167,3 +48,122 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index 15b6a8d53..f690d107b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -5,118 +5,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py310 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -126,13 +14,6 @@ def get_client(): return client -@needs_py310 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py310 def test_post_form_no_body(client: TestClient): response = client.post("/files/") @@ -167,3 +48,122 @@ def test_post_upload_file(tmp_path: Path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py310 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index c34165f18..4af659a11 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -4,138 +4,6 @@ from docs_src.request_files.tutorial001_03 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_file(tmp_path): path = tmp_path / "test.txt" @@ -157,3 +25,135 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py index e83fc68bb..91dbc60b9 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -4,138 +4,6 @@ from docs_src.request_files.tutorial001_03_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - def test_post_file(tmp_path): path = tmp_path / "test.txt" @@ -157,3 +25,135 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py index 7808262a7..7c4ad326c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -3,132 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/uploadfile/": { - "post": { - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as bytes", - "format": "binary", - } - }, - }, - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": { - "title": "File", - "type": "string", - "description": "A file read as UploadFile", - "format": "binary", - } - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -138,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - @needs_py39 def test_post_file(tmp_path, client: TestClient): path = tmp_path / "test.txt" @@ -165,3 +32,136 @@ def test_post_upload_file(tmp_path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/uploadfile/": { + "post": { + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as bytes", + "format": "binary", + } + }, + }, + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": { + "title": "File", + "type": "string", + "description": "A file read as UploadFile", + "format": "binary", + } + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py index 739f04b43..80c288ed6 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -4,128 +4,6 @@ from docs_src.request_files.tutorial001_an import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - file_required = { "detail": [ @@ -182,3 +60,125 @@ def test_post_upload_file(tmp_path): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py index 091a9362b..4dc1f752c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -3,122 +3,6 @@ from fastapi.testclient import TestClient from ...utils import needs_py39 -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create File", - "operationId": "create_file_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfile/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload File", - "operationId": "create_upload_file_uploadfile__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } - } - }, - "required": True, - }, - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_file_uploadfile__post": { - "title": "Body_create_upload_file_uploadfile__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "Body_create_file_files__post": { - "title": "Body_create_file_files__post", - "required": ["file"], - "type": "object", - "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - @pytest.fixture(name="client") def get_client(): @@ -128,13 +12,6 @@ def get_client(): return client -@needs_py39 -def test_openapi_schema(client: TestClient): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - - file_required = { "detail": [ { @@ -192,3 +69,126 @@ def test_post_upload_file(tmp_path, client: TestClient): response = client.post("/uploadfile/", files={"file": file}) assert response.status_code == 200, response.text assert response.json() == {"filename": "test.txt"} + + +@needs_py39 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/files/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create File", + "operationId": "create_file_files__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + } + }, + "required": True, + }, + } + }, + "/uploadfile/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create Upload File", + "operationId": "create_upload_file_uploadfile__post", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + } + }, + "required": True, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_upload_file_uploadfile__post": { + "title": "Body_create_upload_file_uploadfile__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "Body_create_file_files__post": { + "title": "Body_create_file_files__post", + "required": ["file"], + "type": "object", + "properties": { + "file": {"title": "File", "type": "string", "format": "binary"} + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 73d1179a1..6868be328 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -4,148 +4,6 @@ from docs_src.request_files.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/files/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Files", - "operationId": "create_files_files__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_files_files__post" - } - } - }, - "required": True, - }, - } - }, - "/uploadfiles/": { - "post": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - "summary": "Create Upload Files", - "operationId": "create_upload_files_uploadfiles__post", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_files_uploadfiles__post" - } - } - }, - "required": True, - }, - } - }, - "/": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Main", - "operationId": "main__get", - } - }, - }, - "components": { - "schemas": { - "Body_create_upload_files_uploadfiles__post": { - "title": "Body_create_upload_files_uploadfiles__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "Body_create_files_files__post": { - "title": "Body_create_files_files__post", - "required": ["files"], - "type": "object", - "properties": { - "files": { - "title": "Files", - "type": "array", - "items": {"type": "string", "format": "binary"}, - } - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == openapi_schema - file_required = { "detail": [ @@ -213,3 +71,145 @@ def test_get_root(): response = client.get("/") assert response.status_code == 200, response.text assert b" Date: Mon, 8 May 2023 21:08:08 +0000 Subject: [PATCH 0949/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f44a2b3b1..07ddb31dc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). * 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). * 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). From f00f0de9ca68d201462affeca1c0e945be243578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 8 May 2023 23:36:13 +0200 Subject: [PATCH 0950/1881] =?UTF-8?q?=E2=9C=85=20Refactor=202=20tests,=20f?= =?UTF-8?q?or=20consistency=20and=20simplification=20(#9504)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Refactor tests, for consistency and simplification --- ...test_request_body_parameters_media_type.py | 164 +++++++++++++++--- .../test_dataclasses/test_tutorial002.py | 110 +++++------- 2 files changed, 185 insertions(+), 89 deletions(-) diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index e9cf4006d..32f6c6a72 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -33,36 +33,146 @@ async def create_shop( pass # pragma: no cover -create_product_request_body = { - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/Body_create_product_products_post"} - } - }, - "required": True, -} - -create_shop_request_body = { - "content": { - "application/vnd.api+json": { - "schema": {"$ref": "#/components/schemas/Body_create_shop_shops_post"} - } - }, - "required": True, -} - client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - openapi_schema = response.json() - assert ( - openapi_schema["paths"]["/products"]["post"]["requestBody"] - == create_product_request_body - ) - assert ( - openapi_schema["paths"]["/shops"]["post"]["requestBody"] - == create_shop_request_body - ) + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/products": { + "post": { + "summary": "Create Product", + "operationId": "create_product_products_post", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/Body_create_product_products_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/shops": { + "post": { + "summary": "Create Shop", + "operationId": "create_shop_shops_post", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/Body_create_shop_shops_post" + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "Body_create_product_products_post": { + "title": "Body_create_product_products_post", + "required": ["data"], + "type": "object", + "properties": {"data": {"$ref": "#/components/schemas/Product"}}, + }, + "Body_create_shop_shops_post": { + "title": "Body_create_shop_shops_post", + "required": ["data"], + "type": "object", + "properties": { + "data": {"$ref": "#/components/schemas/Shop"}, + "included": { + "title": "Included", + "type": "array", + "items": {"$ref": "#/components/schemas/Product"}, + "default": [], + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Product": { + "title": "Product", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + }, + }, + "Shop": { + "title": "Shop", + "required": ["name"], + "type": "object", + "properties": {"name": {"title": "Name", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index f5597e30c..675e826b1 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,71 +1,9 @@ -from copy import deepcopy - from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app client = TestClient(app) -openapi_schema = { - "openapi": "3.0.2", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/items/next": { - "get": { - "summary": "Read Next Item", - "operationId": "read_next_item_items_next_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - } - }, - } - } - }, - "components": { - "schemas": { - "Item": { - "title": "Item", - "required": ["name", "price"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, - }, - } - } - }, -} - - -def test_openapi_schema(): - response = client.get("/openapi.json") - assert response.status_code == 200 - # TODO: remove this once Pydantic 1.9 is released - # Ref: https://github.com/pydantic/pydantic/pull/2557 - data = response.json() - alternative_data1 = deepcopy(data) - alternative_data2 = deepcopy(data) - alternative_data1["components"]["schemas"]["Item"]["required"] = ["name", "price"] - alternative_data2["components"]["schemas"]["Item"]["required"] = [ - "name", - "price", - "tags", - ] - assert alternative_data1 == openapi_schema or alternative_data2 == openapi_schema - def test_get_item(): response = client.get("/items/next") @@ -77,3 +15,51 @@ def test_get_item(): "tags": ["breater"], "tax": None, } + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + data = response.json() + assert data == { + "openapi": "3.0.2", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/next": { + "get": { + "summary": "Read Next Item", + "operationId": "read_next_item_items_next_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "price": {"title": "Price", "type": "number"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + "description": {"title": "Description", "type": "string"}, + "tax": {"title": "Tax", "type": "number"}, + }, + } + } + }, + } From fe55402776192a3cd669bd3e98cbab9a23796736 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 8 May 2023 21:36:55 +0000 Subject: [PATCH 0951/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 07ddb31dc..e8afa23a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). * 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). From 5100a98ccd42bf1201f6cde093222947facbc616 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 9 May 2023 15:32:00 +0100 Subject: [PATCH 0952/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`flask.escape`?= =?UTF-8?q?=20warning=20for=20internal=20tests=20(#9468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix flask.escape warning * 📝 Fix highlight in docs for WSGI --------- Co-authored-by: Sebastián Ramírez --- docs/en/docs/advanced/wsgi.md | 2 +- docs_src/wsgi/tutorial001.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/advanced/wsgi.md b/docs/en/docs/advanced/wsgi.md index df8865961..cfe3c78c1 100644 --- a/docs/en/docs/advanced/wsgi.md +++ b/docs/en/docs/advanced/wsgi.md @@ -12,7 +12,7 @@ Then wrap the WSGI (e.g. Flask) app with the middleware. And then mount that under a path. -```Python hl_lines="2-3 22" +```Python hl_lines="2-3 23" {!../../../docs_src/wsgi/tutorial001.py!} ``` diff --git a/docs_src/wsgi/tutorial001.py b/docs_src/wsgi/tutorial001.py index 500ecf883..7f27a85a1 100644 --- a/docs_src/wsgi/tutorial001.py +++ b/docs_src/wsgi/tutorial001.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.wsgi import WSGIMiddleware -from flask import Flask, escape, request +from flask import Flask, request +from markupsafe import escape flask_app = Flask(__name__) From d59c27d017a805fcf847aa624b5edd7e4659bbe0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 9 May 2023 14:32:48 +0000 Subject: [PATCH 0953/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e8afa23a9..358f8bfc3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `flask.escape` warning for internal tests. PR [#9468](https://github.com/tiangolo/fastapi/pull/9468) by [@samuelcolvin](https://github.com/samuelcolvin). * ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). From b4535abe8f112a63292b6f23c31df04c951ada9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 May 2023 15:29:40 +0200 Subject: [PATCH 0954/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20version=20to=20`>=3D0.27.0`=20for=20a=20security=20releas?= =?UTF-8?q?e=20(#9541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6aa095a64..bee5723e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.26.1,<0.27.0", + "starlette>=0.27.0,<0.28.0", "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From 66259ddbb557fe9fa82aab9ddd9159fd436906c4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 16 May 2023 13:30:24 +0000 Subject: [PATCH 0955/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 358f8bfc3..163cca139 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Starlette version to `>=0.27.0` for a security release. PR [#9541](https://github.com/tiangolo/fastapi/pull/9541) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix `flask.escape` warning for internal tests. PR [#9468](https://github.com/tiangolo/fastapi/pull/9468) by [@samuelcolvin](https://github.com/samuelcolvin). * ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). * ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). From 6d235d1fe1cf5828fb2070381d7da7c5f0f8e60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 May 2023 15:38:23 +0200 Subject: [PATCH 0956/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 163cca139..f5a5b4a13 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,10 +2,10 @@ ## Latest Changes -* ⬆️ Upgrade Starlette version to `>=0.27.0` for a security release. PR [#9541](https://github.com/tiangolo/fastapi/pull/9541) by [@tiangolo](https://github.com/tiangolo). -* 🐛 Fix `flask.escape` warning for internal tests. PR [#9468](https://github.com/tiangolo/fastapi/pull/9468) by [@samuelcolvin](https://github.com/samuelcolvin). -* ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). -* ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). +* ⬆️ Upgrade Starlette version to `>=0.27.0` for a security release. PR [#9541](https://github.com/tiangolo/fastapi/pull/9541) by [@tiangolo](https://github.com/tiangolo). Details on [Starlette's security advisory](https://github.com/encode/starlette/security/advisories/GHSA-v5gw-mw7f-84px). + +### Translations + * 🌐 Add Portuguese translation for `docs/pt/docs/advanced/events.md`. PR [#9326](https://github.com/tiangolo/fastapi/pull/9326) by [@oandersonmagalhaes](https://github.com/oandersonmagalhaes). * 🌐 Add Russian translation for `docs/ru/docs/deployment/manually.md`. PR [#9417](https://github.com/tiangolo/fastapi/pull/9417) by [@Xewus](https://github.com/Xewus). * 🌐 Add setup for translations to Lao. PR [#9396](https://github.com/tiangolo/fastapi/pull/9396) by [@TheBrown](https://github.com/TheBrown). @@ -16,6 +16,12 @@ * 🌐 Initiate Czech translation setup. PR [#9288](https://github.com/tiangolo/fastapi/pull/9288) by [@3p1463k](https://github.com/3p1463k). * ✏ Fix typo in Portuguese docs for `docs/pt/docs/index.md`. PR [#9337](https://github.com/tiangolo/fastapi/pull/9337) by [@lucasbalieiro](https://github.com/lucasbalieiro). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-status-code.md`. PR [#9370](https://github.com/tiangolo/fastapi/pull/9370) by [@nadia3373](https://github.com/nadia3373). + +### Internal + +* 🐛 Fix `flask.escape` warning for internal tests. PR [#9468](https://github.com/tiangolo/fastapi/pull/9468) by [@samuelcolvin](https://github.com/samuelcolvin). +* ✅ Refactor 2 tests, for consistency and simplification. PR [#9504](https://github.com/tiangolo/fastapi/pull/9504) by [@tiangolo](https://github.com/tiangolo). +* ✅ Refactor OpenAPI tests, prepare for Pydantic v2. PR [#9503](https://github.com/tiangolo/fastapi/pull/9503) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump dawidd6/action-download-artifact from 2.26.0 to 2.27.0. PR [#9394](https://github.com/tiangolo/fastapi/pull/9394) by [@dependabot[bot]](https://github.com/apps/dependabot). * 💚 Disable setup-python pip cache in CI. PR [#9438](https://github.com/tiangolo/fastapi/pull/9438) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump pypa/gh-action-pypi-publish from 1.6.4 to 1.8.5. PR [#9346](https://github.com/tiangolo/fastapi/pull/9346) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8cc967a7605d3883bd04ceb5d25cc94ae079612f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 16 May 2023 15:39:43 +0200 Subject: [PATCH 0957/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?95.2?= 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 f5a5b4a13..b7bbe99ee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.95.2 + * ⬆️ Upgrade Starlette version to `>=0.27.0` for a security release. PR [#9541](https://github.com/tiangolo/fastapi/pull/9541) by [@tiangolo](https://github.com/tiangolo). Details on [Starlette's security advisory](https://github.com/encode/starlette/security/advisories/GHSA-v5gw-mw7f-84px). ### Translations diff --git a/fastapi/__init__.py b/fastapi/__init__.py index e1c2be990..a9ad62958 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.95.1" +__version__ = "0.95.2" from starlette import status as status From e0961cbd1c73582343e350b43ae6bf24bf067c31 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 3 Jun 2023 14:09:57 +0200 Subject: [PATCH 0958/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#9602)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 261 ++++++++++++---------------- docs/en/data/people.yml | 282 +++++++++++++++---------------- 2 files changed, 242 insertions(+), 301 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 2a8573f19..71afb66b1 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,10 +1,4 @@ sponsors: -- - login: jina-ai - avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 - url: https://github.com/jina-ai -- - login: armand-sauzay - avatarUrl: https://avatars.githubusercontent.com/u/35524799?u=56e3e944bfe62770d1709c09552d2efc6d285ca6&v=4 - url: https://github.com/armand-sauzay - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi @@ -14,9 +8,6 @@ sponsors: - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI - - login: chaserowbotham - avatarUrl: https://avatars.githubusercontent.com/u/97751084?v=4 - url: https://github.com/chaserowbotham - - login: mikeckennedy avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4 url: https://github.com/mikeckennedy @@ -26,48 +17,42 @@ sponsors: - login: deepset-ai avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 url: https://github.com/deepset-ai - - login: investsuite - avatarUrl: https://avatars.githubusercontent.com/u/73833632?v=4 - url: https://github.com/investsuite - login: svix avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 url: https://github.com/svix + - login: databento-bot + avatarUrl: https://avatars.githubusercontent.com/u/98378480?u=494f679996e39427f7ddb1a7de8441b7c96fb670&v=4 + url: https://github.com/databento-bot - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry -- - login: InesIvanova - avatarUrl: https://avatars.githubusercontent.com/u/22920417?u=409882ec1df6dbd77455788bb383a8de223dbf6f&v=4 - url: https://github.com/InesIvanova -- - login: vyos - avatarUrl: https://avatars.githubusercontent.com/u/5647000?v=4 - url: https://github.com/vyos - - login: takashi-yoneya +- - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya + - login: mercedes-benz + avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 + url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare + - login: marvin-robot + avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4 + url: https://github.com/marvin-robot - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP -- - login: johnadjei - avatarUrl: https://avatars.githubusercontent.com/u/767860?v=4 - url: https://github.com/johnadjei - - login: HiredScore +- - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore - - login: ianshan0915 - avatarUrl: https://avatars.githubusercontent.com/u/5893101?u=a178d247d882578b1d1ef214b2494e52eb28634c&v=4 - url: https://github.com/ianshan0915 - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: Lovage-Labs - avatarUrl: https://avatars.githubusercontent.com/u/71685552?v=4 - url: https://github.com/Lovage-Labs +- - login: JonasKs + avatarUrl: https://avatars.githubusercontent.com/u/5310116?u=98a049f3e1491bffb91e1feb7e93def6881a9389&v=4 + url: https://github.com/JonasKs - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck @@ -83,12 +68,9 @@ sponsors: - login: tizz98 avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 url: https://github.com/tizz98 - - login: dorianturba - avatarUrl: https://avatars.githubusercontent.com/u/9381120?u=4bfc7032a824d1ed1994aa8256dfa597c8f187ad&v=4 - url: https://github.com/dorianturba - - login: jmaralc - avatarUrl: https://avatars.githubusercontent.com/u/21101214?u=b15a9f07b7cbf6c9dcdbcb6550bbd2c52f55aa50&v=4 - url: https://github.com/jmaralc + - login: americanair + avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 + url: https://github.com/americanair - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries @@ -132,7 +114,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 url: https://github.com/koxudaxi - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 @@ -146,24 +128,15 @@ sponsors: - login: mrkmcknz avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 url: https://github.com/mrkmcknz - - login: coffeewasmyidea - avatarUrl: https://avatars.githubusercontent.com/u/1636488?u=8e32a4f200eff54dd79cd79d55d254bfce5e946d&v=4 - url: https://github.com/coffeewasmyidea + - login: mickaelandrieu + avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 + url: https://github.com/mickaelandrieu - login: jonakoudijs avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4 url: https://github.com/jonakoudijs - - login: corleyma - avatarUrl: https://avatars.githubusercontent.com/u/2080732?u=c61f9a4bbc45a45f5d855f93e5f6e0fc8b32c468&v=4 - url: https://github.com/corleyma - - login: andre1sk - avatarUrl: https://avatars.githubusercontent.com/u/3148093?v=4 - url: https://github.com/andre1sk - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 - - login: ColliotL - avatarUrl: https://avatars.githubusercontent.com/u/3412402?u=ca64b07ecbef2f9da1cc2cac3f37522aa4814902&v=4 - url: https://github.com/ColliotL - login: dblackrun avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 url: https://github.com/dblackrun @@ -203,69 +176,48 @@ sponsors: - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 url: https://github.com/simw - - login: pkucmus - avatarUrl: https://avatars.githubusercontent.com/u/6347418?u=98f5918b32e214a168a2f5d59b0b8ebdf57dca0d&v=4 - url: https://github.com/pkucmus - - login: s3ich4n - avatarUrl: https://avatars.githubusercontent.com/u/6926298?u=6690c5403bc1d9a1837886defdc5256e9a43b1db&v=4 - url: https://github.com/s3ich4n - login: Rehket avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 url: https://github.com/Rehket - - login: ValentinCalomme - avatarUrl: https://avatars.githubusercontent.com/u/7288672?u=e09758c7a36c49f0fb3574abe919cbd344fdc2d6&v=4 - url: https://github.com/ValentinCalomme - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm - login: Shackelford-Arden avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 url: https://github.com/Shackelford-Arden + - login: savannahostrowski + avatarUrl: https://avatars.githubusercontent.com/u/8949415?u=c3177aa099fb2b8c36aeba349278b77f9a8df211&v=4 + url: https://github.com/savannahostrowski - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow - - login: svats2k - avatarUrl: https://avatars.githubusercontent.com/u/12378398?u=ecf28c19f61052e664bdfeb2391f8107d137915c&v=4 - url: https://github.com/svats2k - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade - login: khadrawy avatarUrl: https://avatars.githubusercontent.com/u/13686061?u=59f25ef42ecf04c22657aac4238ce0e2d3d30304&v=4 url: https://github.com/khadrawy - - login: pablonnaoji - avatarUrl: https://avatars.githubusercontent.com/u/15187159?u=7480e0eaf959e9c5dfe3a05286f2ea4588c0a3c6&v=4 - url: https://github.com/pablonnaoji - login: mjohnsey avatarUrl: https://avatars.githubusercontent.com/u/16784016?u=38fad2e6b411244560b3af99c5f5a4751bc81865&v=4 url: https://github.com/mjohnsey - - login: abdalla19977 - avatarUrl: https://avatars.githubusercontent.com/u/17257234?v=4 - url: https://github.com/abdalla19977 - login: wedwardbeck avatarUrl: https://avatars.githubusercontent.com/u/19333237?u=1de4ae2bf8d59eb4c013f21d863cbe0f2010575f&v=4 url: https://github.com/wedwardbeck + - login: RaamEEIL + avatarUrl: https://avatars.githubusercontent.com/u/20320552?v=4 + url: https://github.com/RaamEEIL - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu - - login: Pablongo24 - avatarUrl: https://avatars.githubusercontent.com/u/24843427?u=78a6798469889d7a0690449fc667c39e13d5c6a9&v=4 - url: https://github.com/Pablongo24 - - login: Joeriksson - avatarUrl: https://avatars.githubusercontent.com/u/25037079?v=4 - url: https://github.com/Joeriksson - - login: cometa-haley - avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 - url: https://github.com/cometa-haley + - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota - login: LarryGF avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4 url: https://github.com/LarryGF - - login: veprimk - avatarUrl: https://avatars.githubusercontent.com/u/29689749?u=f8cb5a15a286e522e5b189bc572d5a1a90217fb2&v=4 - url: https://github.com/veprimk - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy @@ -290,27 +242,21 @@ sponsors: - login: arleybri18 avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 url: https://github.com/arleybri18 + - login: thenickben + avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 + url: https://github.com/thenickben - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler - login: ddilidili avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 url: https://github.com/ddilidili - - login: VictorCalderon - avatarUrl: https://avatars.githubusercontent.com/u/44529243?u=cea69884f826a29aff1415493405209e0706d07a&v=4 - url: https://github.com/VictorCalderon - - login: rafsaf - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 - url: https://github.com/rafsaf - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - login: thisistheplace avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 url: https://github.com/thisistheplace - - login: kyjoconn - avatarUrl: https://avatars.githubusercontent.com/u/58443406?u=a3e9c2acfb7ba62edda9334aba61cf027f41f789&v=4 - url: https://github.com/kyjoconn - login: A-Edge avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 url: https://github.com/A-Edge @@ -320,9 +266,6 @@ sponsors: - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia - - login: predictionmachine - avatarUrl: https://avatars.githubusercontent.com/u/63719559?v=4 - url: https://github.com/predictionmachine - login: daverin avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 url: https://github.com/daverin @@ -341,24 +284,21 @@ sponsors: - login: Dagmaara avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 url: https://github.com/Dagmaara +- - login: Yarden-zamir + avatarUrl: https://avatars.githubusercontent.com/u/8178413?u=ee177a8b0f87ea56747f4d96f34cd4e9604a8217&v=4 + url: https://github.com/Yarden-zamir - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy - - login: linux-china - avatarUrl: https://avatars.githubusercontent.com/u/46711?u=cd77c65338b158750eb84dc7ff1acf3209ccfc4f&v=4 - url: https://github.com/linux-china - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier - - login: jhb - avatarUrl: https://avatars.githubusercontent.com/u/142217?v=4 - url: https://github.com/jhb - - login: justinrmiller - avatarUrl: https://avatars.githubusercontent.com/u/143998?u=b507a940394d4fc2bc1c27cea2ca9c22538874bd&v=4 - url: https://github.com/justinrmiller - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson + - login: slafs + avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 + url: https://github.com/slafs - login: adamghill avatarUrl: https://avatars.githubusercontent.com/u/317045?u=f1349d5ffe84a19f324e204777859fbf69ddf633&v=4 url: https://github.com/adamghill @@ -378,11 +318,8 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - login: janfilips - avatarUrl: https://avatars.githubusercontent.com/u/870699?u=50de77b93d3a0b06887e672d4e8c7b9d643085aa&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/870699?u=96df18ad355e58b9397accc55f4eeb7a86e959b0&v=4 url: https://github.com/janfilips - - login: allen0125 - avatarUrl: https://avatars.githubusercontent.com/u/1448456?u=dc2ad819497eef494b88688a1796e0adb87e7cae&v=4 - url: https://github.com/allen0125 - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan @@ -392,17 +329,20 @@ sponsors: - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: paul121 - avatarUrl: https://avatars.githubusercontent.com/u/3116995?u=6e2d8691cc345e63ee02e4eb4d7cef82b1fcbedc&v=4 - url: https://github.com/paul121 + - login: Patechoc + avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 + url: https://github.com/Patechoc - login: larsvik avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4 url: https://github.com/larsvik - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti + - login: jonathanhle + avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 + url: https://github.com/jonathanhle - login: nikeee - avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=63f8eee593f25138e0f1032ef442e9ad24907d4c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=bbe73151f2b409c120160d032dc9aa6875ef0c4b&v=4 url: https://github.com/nikeee - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 @@ -410,6 +350,12 @@ sponsors: - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood + - login: yuawn + avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4 + url: https://github.com/yuawn + - login: sdevkota + avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 + url: https://github.com/sdevkota - login: unredundant avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4 url: https://github.com/unredundant @@ -419,11 +365,11 @@ sponsors: - login: KentShikama avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 url: https://github.com/KentShikama - - login: holec - avatarUrl: https://avatars.githubusercontent.com/u/6438041?u=f5af71ec85b3a9d7b8139cb5af0512b02fa9ab1e&v=4 - url: https://github.com/holec + - login: katnoria + avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 + url: https://github.com/katnoria - login: mattwelke - avatarUrl: https://avatars.githubusercontent.com/u/7719209?v=4 + avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=80f02a799323b1472b389b836d95957c93a6d856&v=4 url: https://github.com/mattwelke - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 @@ -431,6 +377,9 @@ sponsors: - login: moonape1226 avatarUrl: https://avatars.githubusercontent.com/u/8532038?u=d9f8b855a429fff9397c3833c2ff83849ebf989d&v=4 url: https://github.com/moonape1226 + - login: albertkun + avatarUrl: https://avatars.githubusercontent.com/u/8574425?u=aad2a9674273c9275fe414d99269b7418d144089&v=4 + url: https://github.com/albertkun - login: xncbf avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 url: https://github.com/xncbf @@ -440,6 +389,9 @@ sponsors: - login: hard-coders avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders + - login: supdann + avatarUrl: https://avatars.githubusercontent.com/u/9986994?u=9671810f4ae9504c063227fee34fd47567ff6954&v=4 + url: https://github.com/supdann - login: satwikkansal avatarUrl: https://avatars.githubusercontent.com/u/10217535?u=b12d6ef74ea297de9e46da6933b1a5b7ba9e6a61&v=4 url: https://github.com/satwikkansal @@ -456,38 +408,32 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 url: https://github.com/giuliano-oliveira - login: TheR1D - avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b2923ac17fe6e2a7c9ea14800351ddb92f79b100&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 url: https://github.com/TheR1D - - login: cdsre - avatarUrl: https://avatars.githubusercontent.com/u/16945936?v=4 - url: https://github.com/cdsre - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia - - login: paulowiz - avatarUrl: https://avatars.githubusercontent.com/u/18649504?u=d8a6ac40321f2bded0eba78b637751c7f86c6823&v=4 - url: https://github.com/paulowiz - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota + - login: kadekillary + avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 + url: https://github.com/kadekillary - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams - login: joerambo avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 url: https://github.com/joerambo + - login: rlnchow + avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 + url: https://github.com/rlnchow - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli - - login: ruizdiazever - avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 - url: https://github.com/ruizdiazever - login: HosamAlmoghraby avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 url: https://github.com/HosamAlmoghraby @@ -495,53 +441,56 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=76cdc0a8b4e88c7d3e58dccb4b2670839e1247b4&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=9fbf76b9bf7786275e2900efa51d1394bcf1f06a&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon - - login: alvarobartt - avatarUrl: https://avatars.githubusercontent.com/u/36760800?u=9b38695807eb981d452989699ff72ec2d8f6508e&v=4 - url: https://github.com/alvarobartt - - login: d-e-h-i-o - avatarUrl: https://avatars.githubusercontent.com/u/36816716?v=4 - url: https://github.com/d-e-h-i-o - - login: ww-daniel-mora - avatarUrl: https://avatars.githubusercontent.com/u/38921751?u=ae14bc1e40f2dd5a9c5741fc0b0dffbd416a5fa9&v=4 - url: https://github.com/ww-daniel-mora - - login: rwxd - avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 - url: https://github.com/rwxd + - login: miraedbswo + avatarUrl: https://avatars.githubusercontent.com/u/36796047?u=9e7a5b3e558edc61d35d0f9dfac37541bae7f56d&v=4 + url: https://github.com/miraedbswo + - login: kristiangronberg + avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 + url: https://github.com/kristiangronberg - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=5265858add14a6822bd145f7547323cf078563e6&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4 url: https://github.com/arrrrrmin + - login: ArtyomVancyan + avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 + url: https://github.com/ArtyomVancyan - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby - - login: data-djinn - avatarUrl: https://avatars.githubusercontent.com/u/56449985?u=42146e140806908d49bd59ccc96f222abf587886&v=4 - url: https://github.com/data-djinn + - login: eladgunders + avatarUrl: https://avatars.githubusercontent.com/u/52347338?u=83d454817cf991a035c8827d46ade050c813e2d6&v=4 + url: https://github.com/eladgunders + - login: conservative-dude + avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 + url: https://github.com/conservative-dude - login: leo-jp-edwards avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 url: https://github.com/leo-jp-edwards - - login: apar-tiwari - avatarUrl: https://avatars.githubusercontent.com/u/61064197?v=4 - url: https://github.com/apar-tiwari - - login: Vyvy-vi - avatarUrl: https://avatars.githubusercontent.com/u/62864373?u=1a9b0b28779abc2bc9b62cb4d2e44d453973c9c3&v=4 - url: https://github.com/Vyvy-vi + - login: tamtam-fitness + avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 + url: https://github.com/tamtam-fitness - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - - login: realabja - avatarUrl: https://avatars.githubusercontent.com/u/66185192?u=001e2dd9297784f4218997981b4e6fa8357bb70b&v=4 - url: https://github.com/realabja - - login: garydsong - avatarUrl: https://avatars.githubusercontent.com/u/105745865?u=03cc1aa9c978be0020e5a1ce1ecca323dd6c8d65&v=4 - url: https://github.com/garydsong -- - login: Leon0824 - avatarUrl: https://avatars.githubusercontent.com/u/1922026?v=4 - url: https://github.com/Leon0824 +- - login: ssbarnea + avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 + url: https://github.com/ssbarnea + - login: sadikkuzu + avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 + url: https://github.com/sadikkuzu + - login: ruizdiazever + avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 + url: https://github.com/ruizdiazever - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline + - login: rwxd + avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 + url: https://github.com/rwxd + - login: xNykram + avatarUrl: https://avatars.githubusercontent.com/u/55030025?u=2c1ba313fd79d29273b5ff7c9c5cf4edfb271b29&v=4 + url: https://github.com/xNykram diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 412f4517a..2da1c968b 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1827 - prs: 384 + answers: 1839 + prs: 398 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 376 + count: 410 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -22,69 +22,73 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: JarroVGIT - count: 192 + count: 193 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: euri10 - count: 151 + count: 152 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 - login: phy25 count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 -- login: iudeen - count: 116 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: jgould22 - count: 101 + count: 124 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: iudeen + count: 118 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: raphaelauv count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: ArcLightSlavik - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik - login: ghandic count: 71 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic +- login: ArcLightSlavik + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 + url: https://github.com/ArcLightSlavik - login: falkben count: 57 - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=0c8d8f33d87f1aa1a6488d3f02105e9abc838105&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - login: sm-Fifteen count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: Dustyposa +- login: yinziyan1206 count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 - url: https://github.com/Dustyposa + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: insomnes count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 url: https://github.com/insomnes +- login: acidjunk + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk +- login: Dustyposa + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 + url: https://github.com/Dustyposa +- login: adriangb + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 + url: https://github.com/adriangb - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 -- login: acidjunk - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: odiseo0 count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 url: https://github.com/odiseo0 -- login: adriangb - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 - url: https://github.com/adriangb - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -97,12 +101,8 @@ experts: count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: yinziyan1206 - count: 34 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 - login: chbndrhnns - count: 34 + count: 35 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns - login: panla @@ -125,10 +125,10 @@ experts: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: caeser1996 - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 - url: https://github.com/caeser1996 +- login: acnebs + count: 22 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 + url: https://github.com/acnebs - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 @@ -137,34 +137,38 @@ experts: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: acnebs - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 - url: https://github.com/acnebs - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: retnikt - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 - url: https://github.com/retnikt - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet -- login: nkhitrov - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 - url: https://github.com/nkhitrov -- login: harunyasar - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 - url: https://github.com/harunyasar +- login: retnikt + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 + url: https://github.com/retnikt - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner +- login: n8sty + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty +- login: harunyasar + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 + url: https://github.com/harunyasar +- login: nkhitrov + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/28262306?u=66ee21316275ef356081c2efc4ed7a4572e690dc&v=4 + url: https://github.com/nkhitrov +- login: caeser1996 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 + url: https://github.com/caeser1996 - login: jonatasoli count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 @@ -173,10 +177,6 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny -- login: jorgerpo - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 - url: https://github.com/jorgerpo - login: ghost count: 15 avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 @@ -185,55 +185,43 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4 url: https://github.com/simondale00 +- login: jorgerpo + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 + url: https://github.com/jorgerpo +- login: ebottos94 + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: hellocoldworld count: 14 avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 url: https://github.com/hellocoldworld -- login: waynerv - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 - url: https://github.com/waynerv -- login: mbroton - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/50829834?u=a48610bf1bffaa9c75d03228926e2eb08a2e24ee&v=4 - url: https://github.com/mbroton last_month_active: -- login: mr-st0rm - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/48455163?u=6b83550e4e70bea57cd2fdb41e717aeab7f64a91&v=4 - url: https://github.com/mr-st0rm -- login: caeser1996 - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 - url: https://github.com/caeser1996 -- login: ebottos94 - count: 6 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 - login: jgould22 - count: 6 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: Kludex - count: 5 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: clemens-tolboom +- login: abhint + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=5b9f9f6192c83ca86a411eafd4be46d9e5828585&v=4 + url: https://github.com/abhint +- login: chrisK824 count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/371014?v=4 - url: https://github.com/clemens-tolboom -- login: williamjamir + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 +- login: djimontyp count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/5083518?u=b76ca8e08b906a86fa195fb817dd94e8d9d3d8f6&v=4 - url: https://github.com/williamjamir -- login: nymous + avatarUrl: https://avatars.githubusercontent.com/u/53098395?u=583bade70950b277c322d35f1be2b75c7b0f189c&v=4 + url: https://github.com/djimontyp +- login: JavierSanchezCastro count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: frankie567 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro top_contributors: - login: waynerv count: 25 @@ -263,6 +251,10 @@ top_contributors: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl +- login: Xewus + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: Smlep count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -271,6 +263,10 @@ top_contributors: count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 url: https://github.com/Serrones +- login: rjNemo + count: 8 + avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 + url: https://github.com/rjNemo - login: RunningIkkyu count: 7 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 @@ -279,10 +275,6 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders -- login: rjNemo - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 - url: https://github.com/rjNemo - login: batlopes count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 @@ -291,6 +283,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes +- login: samuelcolvin + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 + url: https://github.com/samuelcolvin - login: SwftAlpc count: 5 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 @@ -307,18 +303,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 url: https://github.com/NinaHwang -- login: Xewus - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 url: https://github.com/jekirl -- login: samuelcolvin - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 - url: https://github.com/samuelcolvin - login: jfunez count: 4 avatarUrl: https://avatars.githubusercontent.com/u/805749?v=4 @@ -339,9 +327,13 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: axel584 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 + url: https://github.com/axel584 top_reviewers: - login: Kludex - count: 111 + count: 117 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan @@ -349,8 +341,8 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=636b4f79645176df4527dd45c12d5dbb5a4193cf&v=4 + count: 74 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 - login: tokusumi count: 51 @@ -384,6 +376,10 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda +- login: Xewus + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 @@ -400,30 +396,34 @@ top_reviewers: count: 26 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: Ryandaydev + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 + url: https://github.com/Ryandaydev - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 url: https://github.com/dmontagu - login: LorhanSohaky - count: 22 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 url: https://github.com/LorhanSohaky - login: rjNemo - count: 20 + count: 21 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo - login: hard-coders - count: 20 + count: 21 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: odiseo0 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 + url: https://github.com/odiseo0 - login: 0417taehyun count: 19 avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun -- login: odiseo0 - count: 19 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=16f9255804161c6ff3c8b7ef69848f0126bcd405&v=4 - url: https://github.com/odiseo0 - login: Smlep count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -452,34 +452,38 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: Ryandaydev - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 - url: https://github.com/Ryandaydev -- login: Xewus - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk - login: peidrao count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5401640e0b961cc199dee39ec79e162c7833cd6b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5b94b548ef0002ef3219d7c07ac0fac17c6201a2&v=4 url: https://github.com/peidrao +- login: r0b2g1t + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 + url: https://github.com/r0b2g1t - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu +- login: axel584 + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 + url: https://github.com/axel584 - login: solomein-sv count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=46acfb4aeefb1d7b9fdc5a8cbd9eb8744683c47a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 url: https://github.com/solomein-sv - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl +- login: raphaelauv + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 + url: https://github.com/raphaelauv - login: Attsun1031 count: 10 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 @@ -492,10 +496,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: r0b2g1t +- login: Alexandrhub count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 - url: https://github.com/r0b2g1t + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -516,23 +520,11 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca -- login: dimaqq +- login: oandersonmagalhaes count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/662249?v=4 - url: https://github.com/dimaqq -- login: raphaelauv - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 - url: https://github.com/raphaelauv -- login: axel584 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?v=4 - url: https://github.com/axel584 -- login: blt232018 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/43393471?u=172b0e0391db1aa6c1706498d6dfcb003c8a4857&v=4 - url: https://github.com/blt232018 -- login: rogerbrinkmann - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/5690226?v=4 - url: https://github.com/rogerbrinkmann + avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 + url: https://github.com/oandersonmagalhaes +- login: NinaHwang + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 + url: https://github.com/NinaHwang From 72c72774c5043ee3f8b4c09d8dcd4aedd0d0aa5e Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sat, 3 Jun 2023 15:13:10 +0300 Subject: [PATCH 0959/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/body-multiple-params.md`?= =?UTF-8?q?=20(#9586)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌐 Add Russian translation for docs/tutorial/body-multiple-params.md Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/body-multiple-params.md | 309 ++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 310 insertions(+) create mode 100644 docs/ru/docs/tutorial/body-multiple-params.md diff --git a/docs/ru/docs/tutorial/body-multiple-params.md b/docs/ru/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..a20457092 --- /dev/null +++ b/docs/ru/docs/tutorial/body-multiple-params.md @@ -0,0 +1,309 @@ +# Body - Множество параметров + +Теперь, когда мы увидели, как использовать `Path` и `Query` параметры, давайте рассмотрим более продвинутые примеры обьявления тела запроса. + +## Обьединение `Path`, `Query` и параметров тела запроса + +Во-первых, конечно, вы можете объединять параметры `Path`, `Query` и объявления тела запроса в своих функциях обработки, **FastAPI** автоматически определит, что с ними нужно делать. + +Вы также можете объявить параметры тела запроса как необязательные, установив значение по умолчанию, равное `None`: + +=== "Python 3.10+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +!!! Заметка + Заметьте, что в данном случае параметр `item`, который будет взят из тела запроса, необязателен. Так как было установлено значение `None` по умолчанию. + +## Несколько параметров тела запроса + +В предыдущем примере, *операции пути* ожидали тело запроса в формате JSON-тело с параметрами, соответствующими атрибутам `Item`, например: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Но вы также можете объявить множество параметров тела запроса, например `item` и `user`: + +=== "Python 3.10+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +В этом случае **FastAPI** заметит, что в функции есть более одного параметра тела (два параметра, которые являются моделями Pydantic). + +Таким образом, имена параметров будут использоваться в качестве ключей (имён полей) в теле запроса, и будет ожидаться запрос следующего формата: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! Внимание + Обратите внимание, что хотя параметр `item` был объявлен таким же способом, как и раньше, теперь предпологается, что он находится внутри тела с ключом `item`. + + +**FastAPI** сделает автоматические преобразование из запроса, так что параметр `item` получит своё конкретное содержимое, и то же самое происходит с пользователем `user`. + +Произойдёт проверка составных данных, и создание документации в схеме OpenAPI и автоматических документах. + +## Отдельные значения в теле запроса + +Точно так же, как `Query` и `Path` используются для определения дополнительных данных для query и path параметров, **FastAPI** предоставляет аналогичный инструмент - `Body`. + +Например, расширяя предыдущую модель, вы можете решить, что вам нужен еще один ключ `importance` в том же теле запроса, помимо параметров `item` и `user`. + +Если вы объявите его без указания, какой именно объект (Path, Query, Body и .т.п.) ожидаете, то, поскольку это является простым типом данных, **FastAPI** будет считать, что это query-параметр. + +Но вы можете указать **FastAPI** обрабатывать его, как ещё один ключ тела запроса, используя `Body`: + +=== "Python 3.10+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +В этом случае, **FastAPI** будет ожидать тело запроса в формате: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +И всё будет работать так же - преобразование типов данных, валидация, документирование и т.д. + +## Множество body и query параметров + +Конечно, вы также можете объявлять query-параметры в любое время, дополнительно к любым body-параметрам. + +Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так: + +```Python +q: Union[str, None] = None +``` + +Или в Python 3.10 и выше: + +```Python +q: str | None = None +``` + +Например: + +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +!!! Информация + `Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже. + +## Добавление одного body-параметра + +Предположим, у вас есть только один body-параметр `item` из Pydantic модели `Item`. + +По умолчанию, **FastAPI** ожидает получить тело запроса напрямую. + +Но если вы хотите чтобы он ожидал JSON с ключом `item` с содержимым модели внутри, также как это происходит при объявлении дополнительных body-параметров, вы можете использовать специальный параметр `embed` у типа `Body`: + +```Python +item: Item = Body(embed=True) +``` + +так же, как в этом примере: + +=== "Python 3.10+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! Заметка + Рекомендуется использовать `Annotated` версию, если это возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +В этом случае **FastAPI** будет ожидать тело запроса в формате: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +вместо этого: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Резюме + +Вы можете добавлять несколько body-параметров вашей *функции операции пути*, несмотря даже на то, что запрос может содержать только одно тело. + +Но **FastAPI** справится с этим, предоставит правильные данные в вашей функции, а также сделает валидацию и документацию правильной схемы *операции пути*. + +Вы также можете объявить отдельные значения для получения в рамках тела запроса. + +И вы можете настроить **FastAPI** таким образом, чтобы включить тело запроса в ключ, даже если объявлен только один параметр. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 93fae36ce..260d56258 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -74,6 +74,7 @@ nav: - tutorial/cookie-params.md - tutorial/testing.md - tutorial/response-status-code.md + - tutorial/body-multiple-params.md - async.md - Развёртывание: - deployment/index.md From 061e912ccf99788d6ddfc80d5989e73445351afd Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Sat, 3 Jun 2023 15:21:05 +0300 Subject: [PATCH 0960/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/deployment/concepts.md`=20(#9577)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ru/docs/deployment/concepts.md | 311 ++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 312 insertions(+) create mode 100644 docs/ru/docs/deployment/concepts.md diff --git a/docs/ru/docs/deployment/concepts.md b/docs/ru/docs/deployment/concepts.md new file mode 100644 index 000000000..681acf15e --- /dev/null +++ b/docs/ru/docs/deployment/concepts.md @@ -0,0 +1,311 @@ +# Концепции развёртывания + +Существует несколько концепций, применяемых для развёртывания приложений **FastAPI**, равно как и для любых других типов веб-приложений, среди которых Вы можете выбрать **наиболее подходящий** способ. + +Самые важные из них: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Рассмотрим ниже влияние каждого из них на процесс **развёртывания**. + +Наша конечная цель - **обслуживать клиентов Вашего API безопасно** и **бесперебойно**, с максимально эффективным использованием **вычислительных ресурсов** (например, удалённых серверов/виртуальных машин). 🚀 + +Здесь я немного расскажу Вам об этих **концепциях** и надеюсь, что у Вас сложится **интуитивное понимание**, какой способ выбрать при развертывании Вашего API в различных окружениях, возможно, даже **ещё не существующих**. + +Ознакомившись с этими концепциями, Вы сможете **оценить и выбрать** лучший способ развёртывании **Вашего API**. + +В последующих главах я предоставлю Вам **конкретные рецепты** развёртывания приложения FastAPI. + +А сейчас давайте остановимся на важных **идеях этих концепций**. Эти идеи можно также применить и к другим типам веб-приложений. 💡 + +## Использование более безопасного протокола HTTPS + +В [предыдущей главе об HTTPS](./https.md){.internal-link target=_blank} мы рассмотрели, как HTTPS обеспечивает шифрование для Вашего API. + +Также мы заметили, что обычно для работы с HTTPS Вашему приложению нужен **дополнительный** компонент - **прокси-сервер завершения работы TLS**. + +И если прокси-сервер не умеет сам **обновлять сертификаты HTTPS**, то нужен ещё один компонент для этого действия. + +### Примеры инструментов для работы с HTTPS + +Вот некоторые инструменты, которые Вы можете применять как прокси-серверы: + +* Traefik + * С автоматическим обновлением сертификатов ✨ +* Caddy + * С автоматическим обновлением сертификатов ✨ +* Nginx + * С дополнительным компонентом типа Certbot для обновления сертификатов +* HAProxy + * С дополнительным компонентом типа Certbot для обновления сертификатов +* Kubernetes с Ingress Controller похожим на Nginx + * С дополнительным компонентом типа cert-manager для обновления сертификатов +* Использование услуг облачного провайдера (читайте ниже 👇) + +В последнем варианте Вы можете воспользоваться услугами **облачного сервиса**, который сделает большую часть работы, включая настройку HTTPS. Это может наложить дополнительные ограничения или потребовать дополнительную плату и т.п. Зато Вам не понадобится самостоятельно заниматься настройками прокси-сервера. + +В дальнейшем я покажу Вам некоторые конкретные примеры их применения. + +--- + +Следующие концепции рассматривают применение программы, запускающей Ваш API (такой как Uvicorn). + +## Программа и процесс + +Мы часто будем встречать слова **процесс** и **программа**, потому следует уяснить отличия между ними. + +### Что такое программа + +Термином **программа** обычно описывают множество вещей: + +* **Код**, который Вы написали, в нашем случае **Python-файлы**. +* **Файл**, который может быть **исполнен** операционной системой, например `python`, `python.exe` или `uvicorn`. +* Конкретная программа, **запущенная** операционной системой и использующая центральный процессор и память. В таком случае это также называется **процесс**. + +### Что такое процесс + +Термин **процесс** имеет более узкое толкование, подразумевая что-то, запущенное операционной системой (как в последнем пункте из вышестоящего абзаца): + +* Конкретная программа, **запущенная** операционной системой. + * Это не имеет отношения к какому-либо файлу или коду, но нечто **определённое**, управляемое и **выполняемое** операционной системой. +* Любая программа, любой код, **могут делать что-то** только когда они **выполняются**. То есть, когда являются **работающим процессом**. +* Процесс может быть **прерван** (или "убит") Вами или Вашей операционной системой. В результате чего он перестанет исполняться и **не будет продолжать делать что-либо**. +* Каждое приложение, которое Вы запустили на своём компьютере, каждая программа, каждое "окно" запускает какой-то процесс. И обычно на включенном компьютере **одновременно** запущено множество процессов. +* И **одна программа** может запустить **несколько параллельных процессов**. + +Если Вы заглянете в "диспетчер задач" или "системный монитор" (или аналогичные инструменты) Вашей операционной системы, то увидите множество работающих процессов. + +Вполне вероятно, что Вы увидите несколько процессов с одним и тем же названием браузерной программы (Firefox, Chrome, Edge и т. Д.). Обычно браузеры запускают один процесс на вкладку и вдобавок некоторые дополнительные процессы. + + + +--- + +Теперь, когда нам известна разница между **процессом** и **программой**, давайте продолжим обсуждение развёртывания. + +## Настройки запуска приложения + +В большинстве случаев когда Вы создаёте веб-приложение, то желаете, чтоб оно **работало постоянно** и непрерывно, предоставляя клиентам доступ в любое время. Хотя иногда у Вас могут быть причины, чтоб оно запускалось только при определённых условиях. + +### Удалённый сервер + +Когда Вы настраиваете удалённый сервер (облачный сервер, виртуальную машину и т.п.), самое простое, что можно сделать, запустить Uvicorn (или его аналог) вручную, как Вы делаете при локальной разработке. + +Это рабочий способ и он полезен **во время разработки**. + +Но если Вы потеряете соединение с сервером, то не сможете отслеживать - работает ли всё ещё **запущенный Вами процесс**. + +И если сервер перезагрузится (например, после обновления или каких-то действий облачного провайдера), Вы скорее всего **этого не заметите**, чтобы снова запустить процесс вручную. Вследствие этого Ваш API останется мёртвым. 😱 + +### Автоматический запуск программ + +Вероятно Вы пожелаете, чтоб Ваша серверная программа (такая как Uvicorn) стартовала автоматически при включении сервера, без **человеческого вмешательства** и всегда могла управлять Вашим API (так как Uvicorn запускает приложение FastAPI). + +### Отдельная программа + +Для этого у обычно используют отдельную программу, которая следит за тем, чтобы Ваши приложения запускались при включении сервера. Такой подход гарантирует, что другие компоненты или приложения также будут запущены, например, база данных + +### Примеры инструментов, управляющих запуском программ + +Вот несколько примеров, которые могут справиться с такой задачей: + +* Docker +* Kubernetes +* Docker Compose +* Docker в режиме Swarm +* Systemd +* Supervisor +* Использование услуг облачного провайдера +* Прочие... + +Я покажу Вам некоторые примеры их использования в следующих главах. + +## Перезапуск + +Вы, вероятно, также пожелаете, чтоб Ваше приложение **перезапускалось**, если в нём произошёл сбой. + +### Мы ошибаемся + +Все люди совершают **ошибки**. Программное обеспечение почти *всегда* содержит **баги** спрятавшиеся в разных местах. 🐛 + +И мы, будучи разработчиками, продолжаем улучшать код, когда обнаруживаем в нём баги или добавляем новый функционал (возможно, добавляя при этом баги 😅). + +### Небольшие ошибки обрабатываются автоматически + +Когда Вы создаёте свои API на основе FastAPI и допускаете в коде ошибку, то FastAPI обычно остановит её распространение внутри одного запроса, при обработке которого она возникла. 🛡 + +Клиент получит ошибку **500 Internal Server Error** в ответ на свой запрос, но приложение не сломается и будет продолжать работать с последующими запросами. + +### Большие ошибки - Падение приложений + +Тем не менее, может случиться так, что ошибка вызовет **сбой всего приложения** или даже сбой в Uvicorn, а то и в самом Python. 💥 + +Но мы всё ещё хотим, чтобы приложение **продолжало работать** несмотря на эту единственную ошибку, обрабатывая, как минимум, запросы к *операциям пути* не имеющим ошибок. + +### Перезапуск после падения + +Для случаев, когда ошибки приводят к сбою в запущенном **процессе**, Вам понадобится добавить компонент, который **перезапустит** процесс хотя бы пару раз... + +!!! tip "Заметка" + ... Если приложение падает сразу же после запуска, вероятно бесполезно его бесконечно перезапускать. Но полагаю, Вы заметите такое поведение во время разработки или, по крайней мере, сразу после развёртывания. + + Так что давайте сосредоточимся на конкретных случаях, когда приложение может полностью выйти из строя, но всё ещё есть смысл его запустить заново. + +Возможно Вы захотите, чтоб был некий **внешний компонент**, ответственный за перезапуск Вашего приложения даже если уже не работает Uvicorn или Python. То есть ничего из того, что написано в Вашем коде внутри приложения, не может быть выполнено в принципе. + +### Примеры инструментов для автоматического перезапуска + +В большинстве случаев инструменты **запускающие программы при старте сервера** умеют **перезапускать** эти программы. + +В качестве примера можно взять те же: + +* Docker +* Kubernetes +* Docker Compose +* Docker в режиме Swarm +* Systemd +* Supervisor +* Использование услуг облачного провайдера +* Прочие... + +## Запуск нескольких экземпляров приложения (Репликация) - Процессы и память + +Приложение FastAPI, управляемое серверной программой (такой как Uvicorn), запускается как **один процесс** и может обслуживать множество клиентов одновременно. + +Но часто Вам может понадобиться несколько одновременно работающих одинаковых процессов. + +### Множество процессов - Воркеры (Workers) + +Если количество Ваших клиентов больше, чем может обслужить один процесс (допустим, что виртуальная машина не слишком мощная), но при этом Вам доступно **несколько ядер процессора**, то Вы можете запустить **несколько процессов** одного и того же приложения параллельно и распределить запросы между этими процессами. + +**Несколько запущенных процессов** одной и той же API-программы часто называют **воркерами**. + +### Процессы и порты́ + +Помните ли Вы, как на странице [Об HTTPS](./https.md){.internal-link target=_blank} мы обсуждали, что на сервере только один процесс может слушать одну комбинацию IP-адреса и порта? + +С тех пор ничего не изменилось. + +Соответственно, чтобы иметь возможность работать с **несколькими процессами** одновременно, должен быть **один процесс, прослушивающий порт** и затем каким-либо образом передающий данные каждому рабочему процессу. + +### У каждого процесса своя память + +Работающая программа загружает в память данные, необходимые для её работы, например, переменные содержащие модели машинного обучения или большие файлы. Каждая переменная **потребляет некоторое количество оперативной памяти (RAM)** сервера. + +Обычно процессы **не делятся памятью друг с другом**. Сие означает, что каждый работающий процесс имеет свои данные, переменные и свой кусок памяти. И если для выполнения Вашего кода процессу нужно много памяти, то **каждый такой же процесс** запущенный дополнительно, потребует такого же количества памяти. + +### Память сервера + +Допустим, что Ваш код загружает модель машинного обучения **размером 1 ГБ**. Когда Вы запустите своё API как один процесс, он займёт в оперативной памяти не менее 1 ГБ. А если Вы запустите **4 таких же процесса** (4 воркера), то каждый из них займёт 1 ГБ оперативной памяти. В результате Вашему API потребуется **4 ГБ оперативной памяти (RAM)**. + +И если Ваш удалённый сервер или виртуальная машина располагает только 3 ГБ памяти, то попытка загрузить в неё 4 ГБ данных вызовет проблемы. 🚨 + +### Множество процессов - Пример + +В этом примере **менеджер процессов** запустит и будет управлять двумя **воркерами**. + +Менеджер процессов будет слушать определённый **сокет** (IP:порт) и передавать данные работающим процессам. + +Каждый из этих процессов будет запускать Ваше приложение для обработки полученного **запроса** и возвращения вычисленного **ответа** и они будут использовать оперативную память. + + + +Безусловно, на этом же сервере будут работать и **другие процессы**, которые не относятся к Вашему приложению. + +Интересная деталь - обычно в течение времени процент **использования центрального процессора (CPU)** каждым процессом может очень сильно **изменяться**, но объём занимаемой **оперативной памяти (RAM)** остаётся относительно **стабильным**. + +Если у Вас есть API, который каждый раз выполняет сопоставимый объем вычислений, и у Вас много клиентов, то **загрузка процессора**, вероятно, *также будет стабильной* (вместо того, чтобы постоянно быстро увеличиваться и уменьшаться). + +### Примеры стратегий и инструментов для запуска нескольких экземпляров приложения + +Существует несколько подходов для достижения целей репликации и я расскажу Вам больше о конкретных стратегиях в следующих главах, например, когда речь пойдет о Docker и контейнерах. + +Основное ограничение при этом - только **один** компонент может работать с определённым **портом публичного IP**. И должен быть способ **передачи** данных между этим компонентом и копиями **процессов/воркеров**. + +Вот некоторые возможные комбинации и стратегии: + +* **Gunicorn** управляющий **воркерами Uvicorn** + * Gunicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **множества работающих процессов Uvicorn**. +* **Uvicorn** управляющий **воркерами Uvicorn** + * Один процесс Uvicorn будет выступать как **менеджер процессов**, прослушивая **IP:port**. Он будет запускать **множество работающих процессов Uvicorn**. +* **Kubernetes** и аналогичные **контейнерные системы** + * Какой-то компонент в **Kubernetes** будет слушать **IP:port**. Необходимое количество запущенных экземпляров приложения будет осуществляться посредством запуска **нескольких контейнеров**, в каждом из которых работает **один процесс Uvicorn**. +* **Облачные сервисы**, которые позаботятся обо всём за Вас + * Возможно, что облачный сервис умеет **управлять запуском дополнительных экземпляров приложения**. Вероятно, он потребует, чтоб Вы указали - какой **процесс** или **образ** следует клонировать. Скорее всего, Вы укажете **один процесс Uvicorn** и облачный сервис будет запускать его копии при необходимости. + +!!! tip "Заметка" + Если Вы не знаете, что такое **контейнеры**, Docker или Kubernetes, не переживайте. + + Я поведаю Вам о контейнерах, образах, Docker, Kubernetes и т.п. в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}. + +## Шаги, предшествующие запуску + +Часто бывает, что Вам необходимо произвести какие-то подготовительные шаги **перед запуском** своего приложения. + +Например, запустить **миграции базы данных**. + +Но в большинстве случаев такие действия достаточно произвести **однократно**. + +Поэтому Вам нужен будет **один процесс**, выполняющий эти **подготовительные шаги** до запуска приложения. + +Также Вам нужно будет убедиться, что этот процесс выполнил подготовительные шаги *даже* если впоследствии Вы запустите **несколько процессов** (несколько воркеров) самого приложения. Если бы эти шаги выполнялись в каждом **клонированном процессе**, они бы **дублировали** работу, пытаясь выполнить её **параллельно**. И если бы эта работа была бы чем-то деликатным, вроде миграции базы данных, то это может вызвать конфликты между ними. + +Безусловно, возможны случаи, когда нет проблем при выполнении предварительной подготовки параллельно или несколько раз. Тогда Вам повезло, работать с ними намного проще. + +!!! tip "Заметка" + Имейте в виду, что в некоторых случаях запуск Вашего приложения **может не требовать каких-либо предварительных шагов вовсе**. + + Что ж, тогда Вам не нужно беспокоиться об этом. 🤷 + +### Примеры стратегий запуска предварительных шагов + +Существует **сильная зависимость** от того, как Вы **развёртываете свою систему**, запускаете программы, обрабатываете перезапуски и т.д. + +Вот некоторые возможные идеи: + +* При использовании Kubernetes нужно предусмотреть "инициализирующий контейнер", запускаемый до контейнера с приложением. +* Bash-скрипт, выполняющий предварительные шаги, а затем запускающий приложение. + * При этом Вам всё ещё нужно найти способ - как запускать/перезапускать *такой* bash-скрипт, обнаруживать ошибки и т.п. + +!!! tip "Заметка" + Я приведу Вам больше конкретных примеров работы с контейнерами в главе: [FastAPI внутри контейнеров - Docker](./docker.md){.internal-link target=_blank}. + +## Утилизация ресурсов + +Ваш сервер располагает ресурсами, которые Ваши программы могут потреблять или **утилизировать**, а именно - время работы центрального процессора и объём оперативной памяти. + +Как много системных ресурсов Вы предполагаете потребить/утилизировать? Если не задумываться, то можно ответить - "немного", но на самом деле Вы, вероятно, пожелаете использовать **максимально возможное количество**. + +Если Вы платите за содержание трёх серверов, но используете лишь малую часть системных ресурсов каждого из них, то Вы **выбрасываете деньги на ветер**, а также **впустую тратите электроэнергию** и т.п. + +В таком случае было бы лучше обойтись двумя серверами, но более полно утилизировать их ресурсы (центральный процессор, оперативную память, жёсткий диск, сети передачи данных и т.д). + +С другой стороны, если Вы располагаете только двумя серверами и используете **на 100% их процессоры и память**, но какой-либо процесс запросит дополнительную память, то операционная система сервера будет использовать жёсткий диск для расширения оперативной памяти (а диск работает в тысячи раз медленнее), а то вовсе **упадёт**. Или если какому-то процессу понадобится произвести вычисления, то ему придётся подождать, пока процессор освободится. + +В такой ситуации лучше подключить **ещё один сервер** и перераспределить процессы между серверами, чтоб всем **хватало памяти и процессорного времени**. + +Также есть вероятность, что по какой-то причине возник **всплеск** запросов к Вашему API. Возможно, это был вирус, боты или другие сервисы начали пользоваться им. И для таких происшествий Вы можете захотеть иметь дополнительные ресурсы. + +При настройке логики развёртываний, Вы можете указать **целевое значение** утилизации ресурсов, допустим, **от 50% до 90%**. Обычно эти метрики и используют. + +Вы можете использовать простые инструменты, такие как `htop`, для отслеживания загрузки центрального процессора и оперативной памяти сервера, в том числе каждым процессом. Или более сложные системы мониторинга нескольких серверов. + +## Резюме + +Вы прочитали некоторые из основных концепций, которые необходимо иметь в виду при принятии решения о развертывании приложений: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения. + +Осознание этих идей и того, как их применять, должно дать Вам интуитивное понимание, необходимое для принятия решений при настройке развертываний. 🤓 + +В следующих разделах я приведу более конкретные примеры возможных стратегий, которым Вы можете следовать. 🚀 diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 260d56258..05866b174 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -79,6 +79,7 @@ nav: - Развёртывание: - deployment/index.md - deployment/versions.md + - deployment/concepts.md - deployment/https.md - deployment/manually.md - project-generation.md From ad77d7f926c90ef2b2f07d3c9d705a5373c03920 Mon Sep 17 00:00:00 2001 From: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Date: Sat, 3 Jun 2023 18:22:10 +0600 Subject: [PATCH 0961/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/path-params-numeric-valida?= =?UTF-8?q?tions.md`=20(#9563)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- .../path-params-numeric-validations.md | 292 ++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 293 insertions(+) create mode 100644 docs/ru/docs/tutorial/path-params-numeric-validations.md diff --git a/docs/ru/docs/tutorial/path-params-numeric-validations.md b/docs/ru/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..0d034ef34 --- /dev/null +++ b/docs/ru/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,292 @@ +# Path-параметры и валидация числовых данных + +Так же, как с помощью `Query` вы можете добавлять валидацию и метаданные для query-параметров, так и с помощью `Path` вы можете добавлять такую же валидацию и метаданные для path-параметров. + +## Импорт Path + +Сначала импортируйте `Path` из `fastapi`, а также импортируйте `Annotated`: + +=== "Python 3.10+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +!!! info "Информация" + Поддержка `Annotated` была добавлена в FastAPI начиная с версии 0.95.0 (и с этой версии рекомендуется использовать этот подход). + + Если вы используете более старую версию, вы столкнётесь с ошибками при попытке использовать `Annotated`. + + Убедитесь, что вы [обновили версию FastAPI](../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank} как минимум до 0.95.1 перед тем, как использовать `Annotated`. + +## Определите метаданные + +Вы можете указать все те же параметры, что и для `Query`. + +Например, чтобы указать значение метаданных `title` для path-параметра `item_id`, вы можете написать: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` + +!!! note "Примечание" + Path-параметр всегда является обязательным, поскольку он составляет часть пути. + + Поэтому следует объявить его с помощью `...`, чтобы обозначить, что этот параметр обязательный. + + Тем не менее, даже если вы объявите его как `None` или установите для него значение по умолчанию, это ни на что не повлияет и параметр останется обязательным. + +## Задайте нужный вам порядок параметров + +!!! tip "Подсказка" + Это не имеет большого значения, если вы используете `Annotated`. + +Допустим, вы хотите объявить query-параметр `q` как обязательный параметр типа `str`. + +И если вам больше ничего не нужно указывать для этого параметра, то нет необходимости использовать `Query`. + +Но вам по-прежнему нужно использовать `Path` для path-параметра `item_id`. И если по какой-либо причине вы не хотите использовать `Annotated`, то могут возникнуть небольшие сложности. + +Если вы поместите параметр со значением по умолчанию перед другим параметром, у которого нет значения по умолчанию, то Python укажет на ошибку. + +Но вы можете изменить порядок параметров, чтобы параметр без значения по умолчанию (query-параметр `q`) шёл первым. + +Это не имеет значения для **FastAPI**. Он распознает параметры по их названиям, типам и значениям по умолчанию (`Query`, `Path`, и т.д.), ему не важен их порядок. + +Поэтому вы можете определить функцию так: + +=== "Python 3.6 без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` + +Но имейте в виду, что если вы используете `Annotated`, вы не столкнётесь с этой проблемой, так как вы не используете `Query()` или `Path()` в качестве значения по умолчанию для параметра функции. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} + ``` + +## Задайте нужный вам порядок параметров, полезные приёмы + +!!! tip "Подсказка" + Это не имеет большого значения, если вы используете `Annotated`. + +Здесь описан **небольшой приём**, который может оказаться удобным, хотя часто он вам не понадобится. + +Если вы хотите: + +* объявить query-параметр `q` без `Query` и без значения по умолчанию +* объявить path-параметр `item_id` с помощью `Path` +* указать их в другом порядке +* не использовать `Annotated` + +...то вы можете использовать специальную возможность синтаксиса Python. + +Передайте `*` в качестве первого параметра функции. + +Python не будет ничего делать с `*`, но он будет знать, что все следующие параметры являются именованными аргументами (парами ключ-значение), также известными как kwargs, даже если у них нет значений по умолчанию. + +```Python hl_lines="7" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +### Лучше с `Annotated` + +Имейте в виду, что если вы используете `Annotated`, то, поскольку вы не используете значений по умолчанию для параметров функции, то у вас не возникнет подобной проблемы и вам не придётся использовать `*`. + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} + ``` + +## Валидация числовых данных: больше или равно + +С помощью `Query` и `Path` (и других классов, которые мы разберём позже) вы можете добавлять ограничения для числовых данных. + +В этом примере при указании `ge=1`, параметр `item_id` должен быть больше или равен `1` ("`g`reater than or `e`qual"). + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial004.py!} + ``` + +## Валидация числовых данных: больше и меньше или равно + +То же самое применимо к: + +* `gt`: больше (`g`reater `t`han) +* `le`: меньше или равно (`l`ess than or `e`qual) + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/path_params_numeric_validations/tutorial005.py!} + ``` + +## Валидация числовых данных: числа с плавающей точкой, больше и меньше + +Валидация также применима к значениям типа `float`. + +В этом случае становится важной возможность добавить ограничение gt, вместо ge, поскольку в таком случае вы можете, например, создать ограничение, чтобы значение было больше `0`, даже если оно меньше `1`. + +Таким образом, `0.5` будет корректным значением. А `0.0` или `0` — нет. + +То же самое справедливо и для lt. + +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial006.py!} + ``` + +## Резюме + +С помощью `Query`, `Path` (и других классов, которые мы пока не затронули) вы можете добавлять метаданные и строковую валидацию тем же способом, как и в главе [Query-параметры и валидация строк](query-params-str-validations.md){.internal-link target=_blank}. + +А также вы можете добавить валидацию числовых данных: + +* `gt`: больше (`g`reater `t`han) +* `ge`: больше или равно (`g`reater than or `e`qual) +* `lt`: меньше (`l`ess `t`han) +* `le`: меньше или равно (`l`ess than or `e`qual) + +!!! info "Информация" + `Query`, `Path` и другие классы, которые мы разберём позже, являются наследниками общего класса `Param`. + + Все они используют те же параметры для дополнительной валидации и метаданных, которые вы видели ранее. + +!!! note "Технические детали" + `Query`, `Path` и другие "классы", которые вы импортируете из `fastapi`, на самом деле являются функциями, которые при вызове возвращают экземпляры одноимённых классов. + + Объект `Query`, который вы импортируете, является функцией. И при вызове она возвращает экземпляр одноимённого класса `Query`. + + Использование функций (вместо использования классов напрямую) нужно для того, чтобы ваш редактор не подсвечивал ошибки, связанные с их типами. + + Таким образом вы можете использовать привычный вам редактор и инструменты разработки, не добавляя дополнительных конфигураций для игнорирования подобных ошибок. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 05866b174..30cf3bd47 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -68,6 +68,7 @@ nav: - python-types.md - Учебник - руководство пользователя: - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md - tutorial/body-fields.md - tutorial/background-tasks.md - tutorial/extra-data-types.md From 5017949010c8c077eccb028e532e91090ec794d8 Mon Sep 17 00:00:00 2001 From: Artem Golicyn <86262613+AGolicyn@users.noreply.github.com> Date: Sat, 3 Jun 2023 15:31:44 +0300 Subject: [PATCH 0962/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/path-params.md`=20(#9519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/path-params.md | 251 +++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 252 insertions(+) create mode 100644 docs/ru/docs/tutorial/path-params.md diff --git a/docs/ru/docs/tutorial/path-params.md b/docs/ru/docs/tutorial/path-params.md new file mode 100644 index 000000000..55b498ef0 --- /dev/null +++ b/docs/ru/docs/tutorial/path-params.md @@ -0,0 +1,251 @@ +# Path-параметры + +Вы можете определить "параметры" или "переменные" пути, используя синтаксис форматированных строк Python: + +```Python hl_lines="6-7" +{!../../../docs_src/path_params/tutorial001.py!} +``` + +Значение параметра пути `item_id` будет передано в функцию в качестве аргумента `item_id`. + +Если запустите этот пример и перейдёте по адресу: http://127.0.0.1:8000/items/foo, то увидите ответ: + +```JSON +{"item_id":"foo"} +``` + +## Параметры пути с типами + +Вы можете объявить тип параметра пути в функции, используя стандартные аннотации типов Python. + +```Python hl_lines="7" +{!../../../docs_src/path_params/tutorial002.py!} +``` + +Здесь, `item_id` объявлен типом `int`. + +!!! check "Заметка" + Это обеспечит поддержку редактора внутри функции (проверка ошибок, автодополнение и т.п.). + +## Преобразование данных + +Если запустите этот пример и перейдёте по адресу: http://127.0.0.1:8000/items/3, то увидите ответ: + +```JSON +{"item_id":3} +``` + +!!! check "Заметка" + Обратите внимание на значение `3`, которое получила (и вернула) функция. Это целочисленный Python `int`, а не строка `"3"`. + + Используя определения типов, **FastAPI** выполняет автоматический "парсинг" запросов. + +## Проверка данных + +Если откроете браузер по адресу http://127.0.0.1:8000/items/foo, то увидите интересную HTTP-ошибку: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +из-за того, что параметр пути `item_id` имеет значение `"foo"`, которое не является типом `int`. + +Та же ошибка возникнет, если вместо `int` передать `float` , например: http://127.0.0.1:8000/items/4.2 + +!!! check "Заметка" + **FastAPI** обеспечивает проверку типов, используя всё те же определения типов. + + Обратите внимание, что в тексте ошибки явно указано место не прошедшее проверку. + + Это очень полезно при разработке и отладке кода, который взаимодействует с API. + +## Документация + +И теперь, когда откроете браузер по адресу: http://127.0.0.1:8000/docs, то увидите вот такую автоматически сгенерированную документацию API: + + + +!!! check "Заметка" + Ещё раз, просто используя определения типов, **FastAPI** обеспечивает автоматическую интерактивную документацию (с интеграцией Swagger UI). + + Обратите внимание, что параметр пути объявлен целочисленным. + +## Преимущества стандартизации, альтернативная документация + +Поскольку сгенерированная схема соответствует стандарту OpenAPI, её можно использовать со множеством совместимых инструментов. + +Именно поэтому, FastAPI сам предоставляет альтернативную документацию API (используя ReDoc), которую можно получить по адресу: http://127.0.0.1:8000/redoc. + + + +По той же причине, есть множество совместимых инструментов, включая инструменты генерации кода для многих языков. + +## Pydantic + +Вся проверка данных выполняется под капотом с помощью Pydantic. Поэтому вы можете быть уверены в качестве обработки данных. + +Вы можете использовать в аннотациях как простые типы данных, вроде `str`, `float`, `bool`, так и более сложные типы. + +Некоторые из них рассматриваются в следующих главах данного руководства. + +## Порядок имеет значение + +При создании *операций пути* можно столкнуться с ситуацией, когда путь является фиксированным. + +Например, `/users/me`. Предположим, что это путь для получения данных о текущем пользователе. + +У вас также может быть путь `/users/{user_id}`, чтобы получить данные о конкретном пользователе по его ID. + +Поскольку *операции пути* выполняются в порядке их объявления, необходимо, чтобы путь для `/users/me` был объявлен раньше, чем путь для `/users/{user_id}`: + + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003.py!} +``` + +Иначе путь для `/users/{user_id}` также будет соответствовать `/users/me`, "подразумевая", что он получает параметр `user_id` со значением `"me"`. + +Аналогично, вы не можете переопределить операцию с путем: + +```Python hl_lines="6 11" +{!../../../docs_src/path_params/tutorial003b.py!} +``` + +Первый будет выполняться всегда, так как путь совпадает первым. + +## Предопределенные значения + +Что если нам нужно заранее определить допустимые *параметры пути*, которые *операция пути* может принимать? В таком случае можно использовать стандартное перечисление `Enum` Python. + +### Создание класса `Enum` + +Импортируйте `Enum` и создайте подкласс, который наследуется от `str` и `Enum`. + +Мы наследуемся от `str`, чтобы документация API могла понять, что значения должны быть типа `string` и отображалась правильно. + +Затем создайте атрибуты класса с фиксированными допустимыми значениями: + +```Python hl_lines="1 6-9" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! info "Дополнительная информация" + Перечисления (enum) доступны в Python начиная с версии 3.4. + +!!! tip "Подсказка" + Если интересно, то "AlexNet", "ResNet" и "LeNet" - это названия моделей машинного обучения. + +### Определение *параметра пути* + +Определите *параметр пути*, используя в аннотации типа класс перечисления (`ModelName`), созданный ранее: + +```Python hl_lines="16" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +### Проверьте документацию + +Поскольку доступные значения *параметра пути* определены заранее, интерактивная документация может наглядно их отображать: + + + +### Работа с *перечислениями* в Python + +Значение *параметра пути* будет *элементом перечисления*. + +#### Сравнение *элементов перечисления* + +Вы можете сравнить это значение с *элементом перечисления* класса `ModelName`: + +```Python hl_lines="17" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +#### Получение *значения перечисления* + +Можно получить фактическое значение (в данном случае - `str`) с помощью `model_name.value` или в общем случае `your_enum_member.value`: + +```Python hl_lines="20" +{!../../../docs_src/path_params/tutorial005.py!} +``` + +!!! tip "Подсказка" + Значение `"lenet"` также можно получить с помощью `ModelName.lenet.value`. + +#### Возврат *элементов перечисления* + +Из *операции пути* можно вернуть *элементы перечисления*, даже вложенные в тело JSON (например в `dict`). + +Они будут преобразованы в соответствующие значения (в данном случае - строки) перед их возвратом клиенту: + +```Python hl_lines="18 21 23" +{!../../../docs_src/path_params/tutorial005.py!} +``` +Вы отправите клиенту такой JSON-ответ: + +```JSON +{ + "model_name": "alexnet", + "message": "Deep Learning FTW!" +} +``` + +## Path-параметры, содержащие пути + +Предположим, что есть *операция пути* с путем `/files/{file_path}`. + +Но вам нужно, чтобы `file_path` сам содержал *путь*, например, `home/johndoe/myfile.txt`. + +Тогда URL для этого файла будет такой: `/files/home/johndoe/myfile.txt`. + +### Поддержка OpenAPI + +OpenAPI не поддерживает способов объявления *параметра пути*, содержащего внутри *путь*, так как это может привести к сценариям, которые сложно определять и тестировать. + +Тем не менее это можно сделать в **FastAPI**, используя один из внутренних инструментов Starlette. + +Документация по-прежнему будет работать, хотя и не добавит никакой информации о том, что параметр должен содержать путь. + +### Конвертер пути + +Благодаря одной из опций Starlette, можете объявить *параметр пути*, содержащий *путь*, используя URL вроде: + +``` +/files/{file_path:path} +``` + +В этом случае `file_path` - это имя параметра, а часть `:path`, указывает, что параметр должен соответствовать любому *пути*. + +Можете использовать так: + +```Python hl_lines="6" +{!../../../docs_src/path_params/tutorial004.py!} +``` + +!!! tip "Подсказка" + Возможно, вам понадобится, чтобы параметр содержал `/home/johndoe/myfile.txt` с ведущим слэшем (`/`). + + В этом случае URL будет таким: `/files//home/johndoe/myfile.txt`, с двойным слэшем (`//`) между `files` и `home`. + +## Резюме +Используя **FastAPI** вместе со стандартными объявлениями типов Python (короткими и интуитивно понятными), вы получаете: + +* Поддержку редактора (проверку ошибок, автозаполнение и т.п.) +* "Парсинг" данных +* Валидацию данных +* Автоматическую документацию API с указанием типов параметров. + +И объявлять типы достаточно один раз. + +Это, вероятно, является главным заметным преимуществом **FastAPI** по сравнению с альтернативными фреймворками (кроме сырой производительности). diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 30cf3bd47..a5db79f47 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -67,6 +67,7 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/path-params.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md - tutorial/body-fields.md From ffb818970f5c09a799ec5e75a9202e22fe67bcc2 Mon Sep 17 00:00:00 2001 From: Lemonyte <49930425+lemonyte@users.noreply.github.com> Date: Sat, 3 Jun 2023 08:32:40 -0400 Subject: [PATCH 0963/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20Deta=20deployment=20tutorial=20(#9501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix typo (Data -> Deta) --- docs/en/docs/deployment/deta.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/deployment/deta.md b/docs/en/docs/deployment/deta.md index 04fc04c0c..229d7fd5d 100644 --- a/docs/en/docs/deployment/deta.md +++ b/docs/en/docs/deployment/deta.md @@ -276,7 +276,7 @@ Also, notice that Deta Space correctly handles HTTPS for you, so you don't have ## Create a release -Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Data Space cloud. +Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Deta Space cloud. To do so, run `space release` in the Space CLI to create an **unlisted release**: From 7cdee0eb6326bad027eb2aefaf6258ec03bde8f8 Mon Sep 17 00:00:00 2001 From: Andres Bermeo Date: Sat, 3 Jun 2023 07:37:15 -0500 Subject: [PATCH 0964/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Spanish=20tra?= =?UTF-8?q?nslation=20including=20new=20illustrations=20in=20`docs/es/docs?= =?UTF-8?q?/async.md`=20(#9483)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/async.md | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/es/docs/async.md b/docs/es/docs/async.md index 90fd7b3d8..83dd532ee 100644 --- a/docs/es/docs/async.md +++ b/docs/es/docs/async.md @@ -104,24 +104,40 @@ Para entender las diferencias, imagina la siguiente historia sobre hamburguesas: Vas con la persona que te gusta 😍 a pedir comida rápida 🍔, haces cola mientras el cajero 💁 recoge los pedidos de las personas de delante tuyo. +illustration + Llega tu turno, haces tu pedido de 2 hamburguesas impresionantes para esa persona 😍 y para ti. -Pagas 💸. +illustration El cajero 💁 le dice algo al chico de la cocina 👨‍🍳 para que sepa que tiene que preparar tus hamburguesas 🍔 (a pesar de que actualmente está preparando las de los clientes anteriores). +illustration + +Pagas 💸. El cajero 💁 te da el número de tu turno. +illustration + Mientras esperas, vas con esa persona 😍 y eliges una mesa, se sientan y hablan durante un rato largo (ya que las hamburguesas son muy impresionantes y necesitan un rato para prepararse ✨🍔✨). Mientras te sientas en la mesa con esa persona 😍, esperando las hamburguesas 🍔, puedes disfrutar ese tiempo admirando lo increíble, inteligente, y bien que se ve ✨😍✨. +illustration + Mientras esperas y hablas con esa persona 😍, de vez en cuando, verificas el número del mostrador para ver si ya es tu turno. Al final, en algún momento, llega tu turno. Vas al mostrador, coges tus hamburguesas 🍔 y vuelves a la mesa. +illustration + Tú y esa persona 😍 se comen las hamburguesas 🍔 y la pasan genial ✨. +illustration + +!!! info + Las ilustraciones fueron creados por Ketrina Thompson. 🎨 + --- Imagina que eres el sistema / programa 🤖 en esa historia. @@ -150,26 +166,41 @@ Haces la cola mientras varios cajeros (digamos 8) que a la vez son cocineros Todos los que están antes de ti están esperando 🕙 que sus hamburguesas 🍔 estén listas antes de dejar el mostrador porque cada uno de los 8 cajeros prepara la hamburguesa de inmediato antes de recibir el siguiente pedido. +illustration + Entonces finalmente es tu turno, haces tu pedido de 2 hamburguesas 🍔 impresionantes para esa persona 😍 y para ti. Pagas 💸. +illustration + El cajero va a la cocina 👨‍🍳. Esperas, de pie frente al mostrador 🕙, para que nadie más recoja tus hamburguesas 🍔, ya que no hay números para los turnos. +illustration + Como tu y esa persona 😍 están ocupados en impedir que alguien se ponga delante y recoja tus hamburguesas apenas llegan 🕙, tampoco puedes prestarle atención a esa persona 😞. Este es un trabajo "síncrono", estás "sincronizado" con el cajero / cocinero 👨‍🍳. Tienes que esperar y estar allí en el momento exacto en que el cajero / cocinero 👨‍🍳 termina las hamburguesas 🍔 y te las da, o de lo contrario, alguien más podría cogerlas. +illustration + Luego, el cajero / cocinero 👨‍🍳 finalmente regresa con tus hamburguesas 🍔, después de mucho tiempo esperando 🕙 frente al mostrador. +illustration + Cojes tus hamburguesas 🍔 y vas a la mesa con esa persona 😍. Sólo las comes y listo 🍔 ⏹. +illustration + No has hablado ni coqueteado mucho, ya que has pasado la mayor parte del tiempo esperando 🕙 frente al mostrador 😞. +!!! info + Las ilustraciones fueron creados por Ketrina Thompson. 🎨 + --- En este escenario de las hamburguesas paralelas, tú eres un sistema / programa 🤖 con dos procesadores (tú y la persona que te gusta 😍), ambos esperando 🕙 y dedicando su atención ⏯ a estar "esperando en el mostrador" 🕙 durante mucho tiempo. @@ -240,7 +271,7 @@ Pero en este caso, si pudieras traer a los 8 ex cajeros / cocineros / ahora limp En este escenario, cada uno de los limpiadores (incluido tú) sería un procesador, haciendo su parte del trabajo. -Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una CPU , a estos problemas se les llama "CPU bond". +Y como la mayor parte del tiempo de ejecución lo coge el trabajo real (en lugar de esperar), y el trabajo en un sistema lo realiza una CPU , a estos problemas se les llama "CPU bound". --- @@ -257,7 +288,7 @@ Por ejemplo: Con **FastAPI** puedes aprovechar la concurrencia que es muy común para el desarrollo web (atractivo principal de NodeJS). -Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bond** como las de los sistemas de Machine Learning. +Pero también puedes aprovechar los beneficios del paralelismo y el multiprocesamiento (tener múltiples procesos ejecutándose en paralelo) para cargas de trabajo **CPU bound** como las de los sistemas de Machine Learning. Eso, más el simple hecho de que Python es el lenguaje principal para **Data Science**, Machine Learning y especialmente Deep Learning, hacen de FastAPI una muy buena combinación para las API y aplicaciones web de Data Science / Machine Learning (entre muchas otras). From d057294de1cccb7cd4ad24ae9b676190544431d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=AE=9A=E7=84=95?= <108172295+wdh99@users.noreply.github.com> Date: Sat, 3 Jun 2023 20:49:32 +0800 Subject: [PATCH 0965/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/static-files.md`=20(#9436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Chinese translation for docs/zh/docs/tutorial/static-files.md --- docs/zh/docs/tutorial/static-files.md | 39 +++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 40 insertions(+) create mode 100644 docs/zh/docs/tutorial/static-files.md diff --git a/docs/zh/docs/tutorial/static-files.md b/docs/zh/docs/tutorial/static-files.md new file mode 100644 index 000000000..e7c5c3f0a --- /dev/null +++ b/docs/zh/docs/tutorial/static-files.md @@ -0,0 +1,39 @@ +# 静态文件 + +您可以使用 `StaticFiles`从目录中自动提供静态文件。 + +## 使用`StaticFiles` + +* 导入`StaticFiles`。 +* "挂载"(Mount) 一个 `StaticFiles()` 实例到一个指定路径。 + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "技术细节" + 你也可以用 `from starlette.staticfiles import StaticFiles`。 + + **FastAPI** 提供了和 `starlette.staticfiles` 相同的 `fastapi.staticfiles` ,只是为了方便你,开发者。但它确实来自Starlette。 + +### 什么是"挂载"(Mounting) + +"挂载" 表示在特定路径添加一个完全"独立的"应用,然后负责处理所有子路径。 + +这与使用`APIRouter`不同,因为安装的应用程序是完全独立的。OpenAPI和来自你主应用的文档不会包含已挂载应用的任何东西等等。 + +你可以在**高级用户指南**中了解更多。 + +## 细节 + +这个 "子应用" 会被 "挂载" 到第一个 `"/static"` 指向的子路径。因此,任何以`"/static"`开头的路径都会被它处理。 + + `directory="static"` 指向包含你的静态文件的目录名字。 + +`name="static"` 提供了一个能被**FastAPI**内部使用的名字。 + +所有这些参数可以不同于"`static`",根据你应用的需要和具体细节调整它们。 + +## 更多信息 + +更多细节和选择查阅 Starlette's docs about Static Files. diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index ef7ab1fd9..75bd2ccab 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -108,6 +108,7 @@ nav: - tutorial/sql-databases.md - tutorial/bigger-applications.md - tutorial/metadata.md + - tutorial/static-files.md - tutorial/debugging.md - 高级用户指南: - advanced/index.md From 27618aa2e8b4052e8a742f01e763267d27f0e8d6 Mon Sep 17 00:00:00 2001 From: Zanie Adkins Date: Sat, 3 Jun 2023 08:37:41 -0500 Subject: [PATCH 0966/1881] =?UTF-8?q?=E2=9A=A1=20Update=20`create=5Fcloned?= =?UTF-8?q?=5Ffield`=20to=20use=20a=20global=20cache=20and=20improve=20sta?= =?UTF-8?q?rtup=20performance=20(#4645)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: Huon Wilson --- fastapi/utils.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index d8be53c57..9b9ebcb85 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -2,7 +2,18 @@ import re import warnings from dataclasses import is_dataclass from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Dict, + MutableMapping, + Optional, + Set, + Type, + Union, + cast, +) +from weakref import WeakKeyDictionary import fastapi from fastapi.datastructures import DefaultPlaceholder, DefaultType @@ -16,6 +27,11 @@ from pydantic.utils import lenient_issubclass if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute +# Cache for `create_cloned_field` +_CLONED_TYPES_CACHE: MutableMapping[ + Type[BaseModel], Type[BaseModel] +] = WeakKeyDictionary() + def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: if status_code is None: @@ -98,11 +114,13 @@ def create_response_field( def create_cloned_field( field: ModelField, *, - cloned_types: Optional[Dict[Type[BaseModel], Type[BaseModel]]] = None, + cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: - # _cloned_types has already cloned types, to support recursive models + # cloned_types caches already cloned types to support recursive models and improve + # performance by avoiding unecessary cloning if cloned_types is None: - cloned_types = {} + cloned_types = _CLONED_TYPES_CACHE + original_type = field.type_ if is_dataclass(original_type) and hasattr(original_type, "__pydantic_model__"): original_type = original_type.__pydantic_model__ From 3c7a4b568c34924ff683692f83d580817ae6bb1a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:38:23 +0000 Subject: [PATCH 0967/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b7bbe99ee..05de1aaea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz). ## 0.95.2 From 68809d6f9783d12ffff3da54a889f63bf038a257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Jun 2023 15:51:39 +0200 Subject: [PATCH 0968/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20InvestSuite=20(#9612)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 8baee7825..e45e7f56c 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 6e81e4890..ad31dc0bc 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -6,9 +6,6 @@ silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas img: https://fastapi.tiangolo.com/img/sponsors/deta.svg - - url: https://www.investsuite.com/jobs - title: Wealthtech jobs with FastAPI - img: https://fastapi.tiangolo.com/img/sponsors/investsuite.svg - url: https://training.talkpython.fm/fastapi-courses title: FastAPI video courses on demand from people you trust img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png From 2c091aa0a43ab71de4b778232cbf6f442f51a239 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:52:14 +0000 Subject: [PATCH 0969/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 05de1aaea..358797912 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). * ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz). ## 0.95.2 From 795419ceeeac127ccdd819e292861affc1f0840d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:54:48 +0000 Subject: [PATCH 0970/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 358797912..eb1bcd9ff 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). * 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). * ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz). From f2b0670f04891cef04b06fc3658f499e6209bf87 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:55:50 +0000 Subject: [PATCH 0971/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 eb1bcd9ff..cb263f1c5 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 [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). * 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). * ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz). From beedcd90c7246bc3f9a7c90909167dd51d8bb29e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:56:02 +0000 Subject: [PATCH 0972/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cb263f1c5..7654ebfe7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). * 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). * 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). From f0b4d590af1a931cd181ab7e7cb6b9ed9264a488 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:56:16 +0000 Subject: [PATCH 0973/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7654ebfe7..913527c49 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). * 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). * 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). From 1ecc9a1810aad7605e6d935c95c136874d95dd5a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:56:26 +0000 Subject: [PATCH 0974/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 913527c49..03238cf74 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). * 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). From d5b588f2460a092a64f75b4e4c5625b8c70b4f45 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:57:42 +0000 Subject: [PATCH 0975/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 03238cf74..414a2e513 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). From ee017fdffa7b4619a84d20d8f56b2d64d26b8010 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:58:09 +0000 Subject: [PATCH 0976/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 414a2e513..41290c0ee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). * ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). From 5d2942f8fd77a0c657cddfc22e4c41e0d7c9ebd3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 13:58:16 +0000 Subject: [PATCH 0977/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 41290c0ee..a6ed593b5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). * 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). * ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). From 8e1280bf877a6698377f639778dd134c6c74f867 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:01:51 +0000 Subject: [PATCH 0978/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a6ed593b5..45373362a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). * 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). * ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). From 1f92ad349c5f8cecffe4069ad9f4231092b6e014 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sat, 3 Jun 2023 17:05:22 +0300 Subject: [PATCH 0979/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/debugging.md`=20(#9579)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/debugging.md | 112 +++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 113 insertions(+) create mode 100644 docs/ru/docs/tutorial/debugging.md diff --git a/docs/ru/docs/tutorial/debugging.md b/docs/ru/docs/tutorial/debugging.md new file mode 100644 index 000000000..755d98cf2 --- /dev/null +++ b/docs/ru/docs/tutorial/debugging.md @@ -0,0 +1,112 @@ +# Отладка + +Вы можете подключить отладчик в своем редакторе, например, в Visual Studio Code или PyCharm. + +## Вызов `uvicorn` + +В вашем FastAPI приложении, импортируйте и вызовите `uvicorn` напрямую: + +```Python hl_lines="1 15" +{!../../../docs_src/debugging/tutorial001.py!} +``` + +### Описание `__name__ == "__main__"` + +Главная цель использования `__name__ == "__main__"` в том, чтобы код выполнялся при запуске файла с помощью: + +

+ +```console +$ python myapp.py +``` + +
+ +но не вызывался, когда другой файл импортирует это, например:: + +```Python +from myapp import app +``` + +#### Больше деталей + +Давайте назовём ваш файл `myapp.py`. + +Если вы запустите его с помощью: + +
+ +```console +$ python myapp.py +``` + +
+ +то встроенная переменная `__name__`, автоматически создаваемая Python в вашем файле, будет иметь значение строкового типа `"__main__"`. + +Тогда выполнится условие и эта часть кода: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +будет запущена. + +--- + +Но этого не произойдет, если вы импортируете этот модуль (файл). + +Таким образом, если у вас есть файл `importer.py` с таким импортом: + +```Python +from myapp import app + +# Some more code +``` + +то автоматическая создаваемая внутри файла `myapp.py` переменная `__name__` будет иметь значение отличающееся от `"__main__"`. + +Следовательно, строка: + +```Python + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +не будет выполнена. + +!!! Информация + Для получения дополнительной информации, ознакомьтесь с официальной документацией Python. + +## Запуск вашего кода с помощью отладчика + +Так как вы запускаете сервер Uvicorn непосредственно из вашего кода, вы можете вызвать Python программу (ваше FastAPI приложение) напрямую из отладчика. + +--- + +Например, в Visual Studio Code вы можете выполнить следующие шаги: + +* Перейдите на панель "Debug". +* Выберите "Add configuration...". +* Выберите "Python" +* Запустите отладчик "`Python: Current File (Integrated Terminal)`". + +Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д. + +Вот как это может выглядеть: + + + +--- + +Если используете Pycharm, вы можете выполнить следующие шаги: + +* Открыть "Run" меню. +* Выбрать опцию "Debug...". +* Затем в появившемся контекстном меню. +* Выбрать файл для отладки (в данном случае, `main.py`). + +Это запустит сервер с вашим **FastAPI** кодом, остановится на точках останова, и т.д. + +Вот как это может выглядеть: + + diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index a5db79f47..c32c59b98 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -77,6 +77,7 @@ nav: - tutorial/testing.md - tutorial/response-status-code.md - tutorial/body-multiple-params.md + - tutorial/debugging.md - async.md - Развёртывание: - deployment/index.md From 4c9ac665540c116d87f8d946625328e06b9b0ca2 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sat, 3 Jun 2023 17:05:30 +0300 Subject: [PATCH 0980/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/query-params.md`=20(#9584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/query-params.md | 225 ++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 226 insertions(+) create mode 100644 docs/ru/docs/tutorial/query-params.md diff --git a/docs/ru/docs/tutorial/query-params.md b/docs/ru/docs/tutorial/query-params.md new file mode 100644 index 000000000..68333ec56 --- /dev/null +++ b/docs/ru/docs/tutorial/query-params.md @@ -0,0 +1,225 @@ +# Query-параметры + +Когда вы объявляете параметры функции, которые не являются параметрами пути, они автоматически интерпретируются как "query"-параметры. + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +Query-параметры представляют из себя набор пар ключ-значение, которые идут после знака `?` в URL-адресе, разделенные символами `&`. + +Например, в этом URL-адресе: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +...параметры запроса такие: + +* `skip`: со значением `0` +* `limit`: со значением `10` + +Будучи частью URL-адреса, они "по умолчанию" являются строками. + +Но когда вы объявляете их с использованием аннотаций (в примере выше, как `int`), они конвертируются в указанный тип данных и проходят проверку на соответствие ему. + +Все те же правила, которые применяются к path-параметрам, также применяются и query-параметрам: + +* Поддержка от редактора кода (очевидно) +* "Парсинг" данных +* Проверка на соответствие данных (Валидация) +* Автоматическая документация + +## Значения по умолчанию + +Поскольку query-параметры не являются фиксированной частью пути, они могут быть не обязательными и иметь значения по умолчанию. + +В примере выше значения по умолчанию равны `skip=0` и `limit=10`. + +Таким образом, результат перехода по URL-адресу: + +``` +http://127.0.0.1:8000/items/ +``` + +будет таким же, как если перейти используя параметры по умолчанию: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +Но если вы введёте, например: + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +Значения параметров в вашей функции будут: + +* `skip=20`: потому что вы установили это в URL-адресе +* `limit=10`: т.к это было значение по умолчанию + +## Необязательные параметры + +Аналогично, вы можете объявлять необязательные query-параметры, установив их значение по умолчанию, равное `None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +В этом случае, параметр `q` будет не обязательным и будет иметь значение `None` по умолчанию. + +!!! Важно + Также обратите внимание, что **FastAPI** достаточно умён чтобы заметить, что параметр `item_id` является path-параметром, а `q` нет, поэтому, это параметр запроса. + +## Преобразование типа параметра запроса + +Вы также можете объявлять параметры с типом `bool`, которые будут преобразованы соответственно: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +В этом случае, если вы сделаете запрос: + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +или + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +или + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +или + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +или + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +или в любом другом варианте написания (в верхнем регистре, с заглавной буквой, и т.п), внутри вашей функции параметр `short` будет иметь значение `True` типа данных `bool` . В противном случае - `False`. + + +## Смешивание query-параметров и path-параметров + +Вы можете объявлять несколько query-параметров и path-параметров одновременно,**FastAPI** сам разберётся, что чем является. + +И вы не обязаны объявлять их в каком-либо определенном порядке. + +Они будут обнаружены по именам: + +=== "Python 3.10+" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +## Обязательные query-параметры + +Когда вы объявляете значение по умолчанию для параметра, который не является path-параметром (в этом разделе, мы пока что познакомились только с path-параметрами), то это значение не является обязательным. + +Если вы не хотите задавать конкретное значение, но хотите сделать параметр необязательным, вы можете установить значение по умолчанию равным `None`. + +Но если вы хотите сделать query-параметр обязательным, вы можете просто не указывать значение по умолчанию: + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +Здесь параметр запроса `needy` является обязательным параметром с типом данных `str`. + +Если вы откроете в браузере URL-адрес, например: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +...без добавления обязательного параметра `needy`, вы увидите подобного рода ошибку: + +```JSON +{ + "detail": [ + { + "loc": [ + "query", + "needy" + ], + "msg": "field required", + "type": "value_error.missing" + } + ] +} +``` + +Поскольку `needy` является обязательным параметром, вам необходимо указать его в URL-адресе: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +...это будет работать: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +Конечно, вы можете определить некоторые параметры как обязательные, некоторые - со значением по умполчанию, а некоторые - полностью необязательные: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +В этом примере, у нас есть 3 параметра запроса: + +* `needy`, обязательный `str`. +* `skip`, типа `int` и со значением по умолчанию `0`. +* `limit`, необязательный `int`. + +!!! подсказка + Вы можете использовать класс `Enum` также, как ранее применяли его с [Path-параметрами](path-params.md#predefined-values){.internal-link target=_blank}. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index c32c59b98..8c2806a82 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -76,6 +76,7 @@ nav: - tutorial/cookie-params.md - tutorial/testing.md - tutorial/response-status-code.md + - tutorial/query-params.md - tutorial/body-multiple-params.md - tutorial/debugging.md - async.md From 918d96f6ad41d87e74168bc2d7caeee2af9466b0 Mon Sep 17 00:00:00 2001 From: Artem Golicyn <86262613+AGolicyn@users.noreply.github.com> Date: Sat, 3 Jun 2023 17:05:53 +0300 Subject: [PATCH 0981/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/first-steps.md`=20(#9471)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/first-steps.md | 333 +++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 334 insertions(+) create mode 100644 docs/ru/docs/tutorial/first-steps.md diff --git a/docs/ru/docs/tutorial/first-steps.md b/docs/ru/docs/tutorial/first-steps.md new file mode 100644 index 000000000..b46f235bc --- /dev/null +++ b/docs/ru/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Первые шаги + +Самый простой FastAPI файл может выглядеть так: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Скопируйте в файл `main.py`. + +Запустите сервер в режиме реального времени: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note "Технические детали" + Команда `uvicorn main:app` обращается к: + + * `main`: файл `main.py` (модуль Python). + * `app`: объект, созданный внутри файла `main.py` в строке `app = FastAPI()`. + * `--reload`: перезапускает сервер после изменения кода. Используйте только для разработки. + +В окне вывода появится следующая строка: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Эта строка показывает URL-адрес, по которому приложение доступно на локальной машине. + +### Проверьте + +Откройте браузер по адресу: http://127.0.0.1:8000. + +Вы увидите JSON-ответ следующего вида: + +```JSON +{"message": "Hello World"} +``` + +### Интерактивная документация API + +Перейдите по адресу: http://127.0.0.1:8000/docs. + +Вы увидите автоматически сгенерированную, интерактивную документацию по API (предоставленную Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Альтернативная документация API + +Теперь перейдите по адресу http://127.0.0.1:8000/redoc. + +Вы увидите альтернативную автоматически сгенерированную документацию (предоставленную ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** генерирует "схему" всего API, используя стандарт **OpenAPI**. + +#### "Схема" + +"Схема" - это определение или описание чего-либо. Не код, реализующий это, а только абстрактное описание. + +#### API "схема" + +OpenAPI - это спецификация, которая определяет, как описывать схему API. + +Определение схемы содержит пути (paths) API, их параметры и т.п. + +#### "Схема" данных + +Термин "схема" также может относиться к формату или структуре некоторых данных, например, JSON. + +Тогда, подразумеваются атрибуты JSON, их типы данных и т.п. + +#### OpenAPI и JSON Schema + +OpenAPI описывает схему API. Эта схема содержит определения (или "схемы") данных, отправляемых и получаемых API. Для описания структуры данных в JSON используется стандарт **JSON Schema**. + +#### Рассмотрим `openapi.json` + +Если Вас интересует, как выглядит исходная схема OpenAPI, то FastAPI автоматически генерирует JSON-схему со всеми описаниями API. + +Можете посмотреть здесь: http://127.0.0.1:8000/openapi.json. + +Вы увидите примерно такой JSON: + +```JSON +{ + "openapi": "3.0.2", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### Для чего нужен OpenAPI + +Схема OpenAPI является основой для обеих систем интерактивной документации. + +Существуют десятки альтернативных инструментов, основанных на OpenAPI. Вы можете легко добавить любой из них к **FastAPI** приложению. + +Вы также можете использовать OpenAPI для автоматической генерации кода для клиентов, которые взаимодействуют с API. Например, для фронтенд-, мобильных или IoT-приложений. + +## Рассмотрим поэтапно + +### Шаг 1: импортируйте `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` это класс в Python, который предоставляет всю функциональность для API. + +!!! note "Технические детали" + `FastAPI` это класс, который наследуется непосредственно от `Starlette`. + + Вы можете использовать всю функциональность Starlette в `FastAPI`. + +### Шаг 2: создайте экземпляр `FastAPI` + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Переменная `app` является экземпляром класса `FastAPI`. + +Это единая точка входа для создания и взаимодействия с API. + +Именно к этой переменной `app` обращается `uvicorn` в команде: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Если создать такое приложение: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +И поместить его в `main.py`, тогда вызов `uvicorn` будет таким: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Шаг 3: определите *операцию пути (path operation)* + +#### Путь (path) + +"Путь" это часть URL, после первого символа `/`, следующего за именем домена. + +Для URL: + +``` +https://example.com/items/foo +``` + +...путь выглядит так: + +``` +/items/foo +``` + +!!! info "Дополнительная иформация" + Термин "path" также часто называется "endpoint" или "route". + +При создании API, "путь" является основным способом разделения "задач" и "ресурсов". + +#### Операция (operation) + +"Операция" это один из "методов" HTTP. + +Таких, как: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...и более экзотических: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +По протоколу HTTP можно обращаться к каждому пути, используя один (или несколько) из этих "методов". + +--- + +При создании API принято использовать конкретные HTTP-методы для выполнения определенных действий. + +Обычно используют: + +* `POST`: создать данные. +* `GET`: прочитать. +* `PUT`: изменить (обновить). +* `DELETE`: удалить. + +В OpenAPI каждый HTTP метод называется "**операция**". + +Мы также будем придерживаться этого термина. + +#### Определите *декоратор операции пути (path operation decorator)* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Декоратор `@app.get("/")` указывает **FastAPI**, что функция, прямо под ним, отвечает за обработку запросов, поступающих по адресу: + +* путь `/` +* использующих get операцию + +!!! info "`@decorator` Дополнительная информация" + Синтаксис `@something` в Python называется "декоратор". + + Вы помещаете его над функцией. Как красивую декоративную шляпу (думаю, что оттуда и происходит этот термин). + + "Декоратор" принимает функцию ниже и выполняет с ней какое-то действие. + + В нашем случае, этот декоратор сообщает **FastAPI**, что функция ниже соответствует **пути** `/` и **операции** `get`. + + Это и есть "**декоратор операции пути**". + +Можно также использовать операции: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +И более экзотические: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip "Подсказка" + Вы можете использовать каждую операцию (HTTP-метод) по своему усмотрению. + + **FastAPI** не навязывает определенного значения для каждого метода. + + Информация здесь представлена как рекомендация, а не требование. + + Например, при использовании GraphQL обычно все действия выполняются только с помощью POST операций. + +### Шаг 4: определите **функцию операции пути** + +Вот "**функция операции пути**": + +* **путь**: `/`. +* **операция**: `get`. +* **функция**: функция ниже "декоратора" (ниже `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Это обычная Python функция. + +**FastAPI** будет вызывать её каждый раз при получении `GET` запроса к URL "`/`". + +В данном случае это асинхронная функция. + +--- + +Вы также можете определить ее как обычную функцию вместо `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note "Технические детали" + Если не знаете в чём разница, посмотрите [Конкурентность: *"Нет времени?"*](../async.md#in-a-hurry){.internal-link target=_blank}. + +### Шаг 5: верните результат + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Вы можете вернуть `dict`, `list`, отдельные значения `str`, `int` и т.д. + +Также можно вернуть модели Pydantic (рассмотрим это позже). + +Многие объекты и модели будут автоматически преобразованы в JSON (включая ORM). Пробуйте использовать другие объекты, которые предпочтительней для Вас, вероятно, они уже поддерживаются. + +## Резюме + +* Импортируем `FastAPI`. +* Создаём экземпляр `app`. +* Пишем **декоратор операции пути** (такой как `@app.get("/")`). +* Пишем **функцию операции пути** (`def root(): ...`). +* Запускаем сервер в режиме разработки (`uvicorn main:app --reload`). diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 8c2806a82..3e341bc46 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -67,6 +67,7 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/first-steps.md - tutorial/path-params.md - tutorial/query-params-str-validations.md - tutorial/path-params-numeric-validations.md From ede2b53a0f0b3d3c8c77ab958fde48eeea4bed14 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:06:02 +0000 Subject: [PATCH 0982/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 45373362a..37af6fb78 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). * 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). From 47c13874a0bc96655d971f8578d1f11ed2cd2830 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:06:48 +0000 Subject: [PATCH 0983/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 37af6fb78..4a19aef68 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). From b086b6580de481d85a5211ec672a4021f373a767 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:15:41 +0000 Subject: [PATCH 0984/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4a19aef68..e5424a296 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). From 1309f67f6431b499737b56cbad52f5419c33eed4 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sat, 3 Jun 2023 17:18:30 +0300 Subject: [PATCH 0985/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/static-files.md`=20(#9580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/static-files.md | 40 +++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 41 insertions(+) create mode 100644 docs/ru/docs/tutorial/static-files.md diff --git a/docs/ru/docs/tutorial/static-files.md b/docs/ru/docs/tutorial/static-files.md new file mode 100644 index 000000000..ec09eb5a3 --- /dev/null +++ b/docs/ru/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# Статические Файлы + +Вы можете предоставлять статические файлы автоматически из директории, используя `StaticFiles`. + +## Использование `StaticFiles` + +* Импортируйте `StaticFiles`. +* "Примонтируйте" экземпляр `StaticFiles()` с указанием определенной директории. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! заметка "Технические детали" + Вы также можете использовать `from starlette.staticfiles import StaticFiles`. + + **FastAPI** предоставляет `starlette.staticfiles` под псевдонимом `fastapi.staticfiles`, просто для вашего удобства, как разработчика. Но на самом деле это берётся напрямую из библиотеки Starlette. + +### Что такое "Монтирование" + +"Монтирование" означает добавление полноценного "независимого" приложения в определенную директорию, которое затем обрабатывает все подпути. + +Это отличается от использования `APIRouter`, так как примонтированное приложение является полностью независимым. +OpenAPI и документация из вашего главного приложения не будет содержать ничего из примонтированного приложения, и т.д. + +Вы можете прочитать больше об этом в **Расширенном руководстве пользователя**. + +## Детали + +Первый параметр `"/static"` относится к подпути, по которому это "подприложение" будет "примонтировано". Таким образом, любой путь начинающийся со `"/static"` будет обработан этим приложением. + +Параметр `directory="static"` относится к имени директории, которая содержит ваши статические файлы. + +`name="static"` даёт имя маршруту, которое может быть использовано внутри **FastAPI**. + +Все эти параметры могут отличаться от "`static`", настройте их в соответствии с вашими нуждами и конкретными деталями вашего собственного приложения. + +## Больше информации + +Для получения дополнительной информации о деталях и настройках ознакомьтесь с Документацией Starlette о статических файлах. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 3e341bc46..e41333894 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -79,6 +79,7 @@ nav: - tutorial/response-status-code.md - tutorial/query-params.md - tutorial/body-multiple-params.md + - tutorial/static-files.md - tutorial/debugging.md - async.md - Развёртывание: From 4d5e40190b6fd529cb6c0b37e491ce97364be9d5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:19:04 +0000 Subject: [PATCH 0986/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e5424a296..49ff87865 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/static-files.md`. PR [#9580](https://github.com/tiangolo/fastapi/pull/9580) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/debugging.md`. PR [#9579](https://github.com/tiangolo/fastapi/pull/9579) by [@Alexandrhub](https://github.com/Alexandrhub). From 8474bae7442ceca2a1fb3378da499b6c0c0e6e4c Mon Sep 17 00:00:00 2001 From: Sergei Solomein <46193920+solomein-sv@users.noreply.github.com> Date: Sat, 3 Jun 2023 19:19:58 +0500 Subject: [PATCH 0987/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/body.md`=20(#3885)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Teregov_Ruslan <48125303+RuslanTer@users.noreply.github.com> Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: FedorGN <66411909+FedorGN@users.noreply.github.com> --- docs/ru/docs/tutorial/body.md | 165 ++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 docs/ru/docs/tutorial/body.md diff --git a/docs/ru/docs/tutorial/body.md b/docs/ru/docs/tutorial/body.md new file mode 100644 index 000000000..c03d40c3f --- /dev/null +++ b/docs/ru/docs/tutorial/body.md @@ -0,0 +1,165 @@ +# Тело запроса + +Когда вам необходимо отправить данные из клиента (допустим, браузера) в ваш API, вы отправляете их как **тело запроса**. + +Тело **запроса** --- это данные, отправляемые клиентом в ваш API. Тело **ответа** --- это данные, которые ваш API отправляет клиенту. + +Ваш API почти всегда отправляет тело **ответа**. Но клиентам не обязательно всегда отправлять тело **запроса**. + +Чтобы объявить тело **запроса**, необходимо использовать модели Pydantic, со всей их мощью и преимуществами. + +!!! info "Информация" + Чтобы отправить данные, необходимо использовать один из методов: `POST` (обычно), `PUT`, `DELETE` или `PATCH`. + + Отправка тела с запросом `GET` имеет неопределенное поведение в спецификациях, тем не менее, оно поддерживается FastAPI только для очень сложных/экстремальных случаев использования. + + Поскольку это не рекомендуется, интерактивная документация со Swagger UI не будет отображать информацию для тела при использовании метода GET, а промежуточные прокси-серверы могут не поддерживать такой вариант запроса. + +## Импортирование `BaseModel` из Pydantic + +Первое, что вам необходимо сделать, это импортировать `BaseModel` из пакета `pydantic`: + +```Python hl_lines="4" +{!../../../docs_src/body/tutorial001.py!} +``` + +## Создание вашей собственной модели + +После этого вы описываете вашу модель данных как класс, наследующий от `BaseModel`. + +Используйте аннотации типов Python для всех атрибутов: + +```Python hl_lines="7-11" +{!../../../docs_src/body/tutorial001.py!} +``` + +Также как и при описании параметров запроса, когда атрибут модели имеет значение по умолчанию, он является необязательным. Иначе он обязателен. Используйте `None`, чтобы сделать его необязательным без использования конкретных значений по умолчанию. + +Например, модель выше описывает вот такой JSON "объект" (или словарь Python): + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...поскольку `description` и `tax` являются необязательными (с `None` в качестве значения по умолчанию), вот такой JSON "объект" также подходит: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Объявление как параметра функции + +Чтобы добавить параметр к вашему *обработчику*, объявите его также, как вы объявляли параметры пути или параметры запроса: + +```Python hl_lines="18" +{!../../../docs_src/body/tutorial001.py!} +``` + +...и укажите созданную модель в качестве типа параметра, `Item`. + +## Результаты + +Всего лишь с помощью аннотации типов Python, **FastAPI**: + +* Читает тело запроса как JSON. +* Приводит к соответствующим типам (если есть необходимость). +* Проверяет корректность данных. + * Если данные некорректны, будет возращена читаемая и понятная ошибка, показывающая что именно и в каком месте некорректно в данных. +* Складывает полученные данные в параметр `item`. + * Поскольку внутри функции вы объявили его с типом `Item`, то теперь у вас есть поддержка со стороны редактора (автодополнение и т.п.) для всех атрибутов и их типов. +* Генерирует декларативное описание модели в виде JSON Schema, так что вы можете его использовать где угодно, если это имеет значение для вашего проекта. +* Эти схемы являются частью сгенерированной схемы OpenAPI и используются для автоматического документирования UI. + +## Автоматическое документирование + +Схема JSON ваших моделей будет частью сгенерированной схемы OpenAPI и будет отображена в интерактивной документации API: + + + +Также она будет указана в документации по API внутри каждой *операции пути*, в которой используются: + + + +## Поддержка редактора + +В вашем редакторе внутри вашей функции у вас будут подсказки по типам и автодополнение (это не будет работать, если вы получаете словарь вместо модели Pydantic): + + + +Также вы будете получать ошибки в случае несоответствия типов: + + + +Это не случайно, весь фреймворк построен вокруг такого дизайна. + +И это все тщательно протестировано еще на этапе разработки дизайна, до реализации, чтобы это работало со всеми редакторами. + +Для поддержки этого даже были внесены некоторые изменения в сам Pydantic. + +На всех предыдущих скриншотах используется Visual Studio Code. + +Но у вас будет такая же поддержка и с PyCharm, и вообще с любым редактором Python: + + + +!!! tip "Подсказка" + Если вы используете PyCharm в качестве редактора, то вам стоит попробовать плагин Pydantic PyCharm Plugin. + + Он улучшает поддержку редактором моделей Pydantic в части: + + * автодополнения, + * проверки типов, + * рефакторинга, + * поиска, + * инспектирования. + +## Использование модели + +Внутри функции вам доступны все атрибуты объекта модели напрямую: + +```Python hl_lines="21" +{!../../../docs_src/body/tutorial002.py!} +``` + +## Тело запроса + параметры пути + +Вы можете одновременно объявлять параметры пути и тело запроса. + +**FastAPI** распознает, какие параметры функции соответствуют параметрам пути и должны быть **получены из пути**, а какие параметры функции, объявленные как модели Pydantic, должны быть **получены из тела запроса**. + +```Python hl_lines="17-18" +{!../../../docs_src/body/tutorial003.py!} +``` + +## Тело запроса + параметры пути + параметры запроса + +Вы также можете одновременно объявить параметры для **пути**, **запроса** и **тела запроса**. + +**FastAPI** распознает каждый из них и возьмет данные из правильного источника. + +```Python hl_lines="18" +{!../../../docs_src/body/tutorial004.py!} +``` + +Параметры функции распознаются следующим образом: + +* Если параметр также указан в **пути**, то он будет использоваться как параметр пути. +* Если аннотация типа параметра содержит **примитивный тип** (`int`, `float`, `str`, `bool` и т.п.), он будет интерпретирован как параметр **запроса**. +* Если аннотация типа параметра представляет собой **модель Pydantic**, он будет интерпретирован как параметр **тела запроса**. + +!!! note "Заметка" + FastAPI понимает, что значение параметра `q` не является обязательным, потому что имеет значение по умолчанию `= None`. + + Аннотация `Optional` в `Optional[str]` не используется FastAPI, но помогает вашему редактору лучше понимать ваш код и обнаруживать ошибки. + +## Без Pydantic + +Если вы не хотите использовать модели Pydantic, вы все еще можете использовать параметры **тела запроса**. Читайте в документации раздел [Тело - Несколько параметров: Единичные значения в теле](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. From 6b72d541360f7932640241202e1e122890e0a941 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 3 Jun 2023 14:20:32 +0000 Subject: [PATCH 0988/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 49ff87865..65ae67a17 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/tutorial/body.md`. PR [#3885](https://github.com/tiangolo/fastapi/pull/3885) by [@solomein-sv](https://github.com/solomein-sv). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/static-files.md`. PR [#9580](https://github.com/tiangolo/fastapi/pull/9580) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/first-steps.md`. PR [#9471](https://github.com/tiangolo/fastapi/pull/9471) by [@AGolicyn](https://github.com/AGolicyn). From 99ed2a227fe3484a3323a634524cf50f931e1585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Jun 2023 16:28:37 +0200 Subject: [PATCH 0989/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 65ae67a17..109ca08cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,17 @@ ## Latest Changes +### Features + +* ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz) and previous original PR by [@huonw](https://github.com/huonw). + +### Docs + +* 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). +* ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). + +### Translations + * 🌐 Add Russian translation for `docs/tutorial/body.md`. PR [#3885](https://github.com/tiangolo/fastapi/pull/3885) by [@solomein-sv](https://github.com/solomein-sv). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/static-files.md`. PR [#9580](https://github.com/tiangolo/fastapi/pull/9580) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/query-params.md`. PR [#9584](https://github.com/tiangolo/fastapi/pull/9584) by [@Alexandrhub](https://github.com/Alexandrhub). @@ -10,14 +21,14 @@ * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params.md`. PR [#9519](https://github.com/tiangolo/fastapi/pull/9519) by [@AGolicyn](https://github.com/AGolicyn). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/static-files.md`. PR [#9436](https://github.com/tiangolo/fastapi/pull/9436) by [@wdh99](https://github.com/wdh99). * 🌐 Update Spanish translation including new illustrations in `docs/es/docs/async.md`. PR [#9483](https://github.com/tiangolo/fastapi/pull/9483) by [@andresbermeoq](https://github.com/andresbermeoq). -* ✏️ Fix typo in Deta deployment tutorial. PR [#9501](https://github.com/tiangolo/fastapi/pull/9501) by [@lemonyte](https://github.com/lemonyte). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/path-params-numeric-validations.md`. PR [#9563](https://github.com/tiangolo/fastapi/pull/9563) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/ru/docs/deployment/concepts.md`. PR [#9577](https://github.com/tiangolo/fastapi/pull/9577) by [@Xewus](https://github.com/Xewus). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-multiple-params.md`. PR [#9586](https://github.com/tiangolo/fastapi/pull/9586) by [@Alexandrhub](https://github.com/Alexandrhub). + +### Internal + * 👥 Update FastAPI People. PR [#9602](https://github.com/tiangolo/fastapi/pull/9602) by [@github-actions[bot]](https://github.com/apps/github-actions). -* 📝 Update Deta deployment tutorial for compatibility with Deta Space. PR [#6004](https://github.com/tiangolo/fastapi/pull/6004) by [@mikBighne98](https://github.com/mikBighne98). * 🔧 Update sponsors, remove InvestSuite. PR [#9612](https://github.com/tiangolo/fastapi/pull/9612) by [@tiangolo](https://github.com/tiangolo). -* ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz). ## 0.95.2 From 1574c9623103d03d27117f64b937d754cba12584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 3 Jun 2023 16:29:23 +0200 Subject: [PATCH 0990/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?96.0?= 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 109ca08cb..cb209fde0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.96.0 + ### Features * ⚡ Update `create_cloned_field` to use a global cache and improve startup performance. PR [#4645](https://github.com/tiangolo/fastapi/pull/4645) by [@madkinsz](https://github.com/madkinsz) and previous original PR by [@huonw](https://github.com/huonw). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index a9ad62958..d564d5fa3 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.95.2" +__version__ = "0.96.0" from starlette import status as status From 2d35651a5a21db07d2164258cedf35e718662540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 7 Jun 2023 22:44:12 +0200 Subject: [PATCH 0991/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20OpenAPI=20model?= =?UTF-8?q?=20fields=20int=20validations,=20change=20`gte`=20to=20`ge`=20(?= =?UTF-8?q?#9635)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 Fix OpenAPI model fields int validations, change `gte` to `ge` --- fastapi/openapi/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 35aa1672b..11edfe38a 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -108,14 +108,14 @@ class Schema(BaseModel): exclusiveMaximum: Optional[float] = None minimum: Optional[float] = None exclusiveMinimum: Optional[float] = None - maxLength: Optional[int] = Field(default=None, gte=0) - minLength: Optional[int] = Field(default=None, gte=0) + maxLength: Optional[int] = Field(default=None, ge=0) + minLength: Optional[int] = Field(default=None, ge=0) pattern: Optional[str] = None - maxItems: Optional[int] = Field(default=None, gte=0) - minItems: Optional[int] = Field(default=None, gte=0) + maxItems: Optional[int] = Field(default=None, ge=0) + minItems: Optional[int] = Field(default=None, ge=0) uniqueItems: Optional[bool] = None - maxProperties: Optional[int] = Field(default=None, gte=0) - minProperties: Optional[int] = Field(default=None, gte=0) + maxProperties: Optional[int] = Field(default=None, ge=0) + minProperties: Optional[int] = Field(default=None, ge=0) required: Optional[List[str]] = None enum: Optional[List[Any]] = None type: Optional[str] = None From 155fc5e24e4bfde690dd2314c02d93b92ca5d78b Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 7 Jun 2023 20:44:47 +0000 Subject: [PATCH 0992/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cb209fde0..f6739a714 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). ## 0.96.0 From 61a8d6720cfcaad8aa4bccfbf4359d3f2199d460 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 8 Jun 2023 20:30:49 +0200 Subject: [PATCH 0993/1881] =?UTF-8?q?=F0=9F=93=8C=20Update=20minimum=20ver?= =?UTF-8?q?sion=20of=20Pydantic=20to=20>=3D1.7.4=20(#9567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bee5723e1..3bae6a3ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ ] dependencies = [ "starlette>=0.27.0,<0.28.0", - "pydantic >=1.6.2,!=1.7,!=1.7.1,!=1.7.2,!=1.7.3,!=1.8,!=1.8.1,<2.0.0", + "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", ] dynamic = ["version"] From 4b31beef358e4108666013e1b7697baabdc467c0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 8 Jun 2023 18:31:33 +0000 Subject: [PATCH 0994/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f6739a714..977c56a1e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). * 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). ## 0.96.0 From 010d44ee1bbe82b431225d57d77455643a24a2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Sat, 10 Jun 2023 14:05:35 +0200 Subject: [PATCH 0995/1881] =?UTF-8?q?=E2=99=BB=20Instantiate=20`HTTPExcept?= =?UTF-8?q?ion`=20only=20when=20needed,=20optimization=20refactor=20(#5356?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/security/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 8b677299d..8fc0aafd9 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -73,11 +73,6 @@ class HTTPBasic(HTTPBase): unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} else: unauthorized_headers = {"WWW-Authenticate": "Basic"} - invalid_user_credentials_exc = HTTPException( - status_code=HTTP_401_UNAUTHORIZED, - detail="Invalid authentication credentials", - headers=unauthorized_headers, - ) if not authorization or scheme.lower() != "basic": if self.auto_error: raise HTTPException( @@ -87,6 +82,11 @@ class HTTPBasic(HTTPBase): ) else: return None + invalid_user_credentials_exc = HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials", + headers=unauthorized_headers, + ) try: data = b64decode(param).decode("ascii") except (ValueError, UnicodeDecodeError, binascii.Error): From d189c38aaf1e88c9bb8f15f5f0d57e64ecc6b1da Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 12:06:21 +0000 Subject: [PATCH 0996/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 977c56a1e..4ab82fd58 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). * 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). * 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). From 4c23c0644b517d3d89b581389596c7131d77e0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 14:39:34 +0200 Subject: [PATCH 0997/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20custom=20token?= =?UTF-8?q?=20to=20Smokeshow=20and=20Preview=20Docs=20for=20download-artif?= =?UTF-8?q?act,=20to=20prevent=20API=20rate=20limits=20(#9646)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/preview-docs.yml | 2 +- .github/workflows/smokeshow.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 91b0cba02..8730185bd 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -18,7 +18,7 @@ jobs: - name: Download Artifact Docs uses: dawidd6/action-download-artifact@v2.27.0 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} name: docs-zip diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index f135fb3e4..65a174329 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -22,6 +22,7 @@ jobs: - uses: dawidd6/action-download-artifact@v2.27.0 with: + github_token: ${{ secrets.FASTAPI_SMOKESHOW_DOWNLOAD_ARTIFACTS }} workflow: test.yml commit: ${{ github.event.workflow_run.head_sha }} From 8d29e494e068d7bbc39be9d377244eeb0ffafcde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 16:25:54 +0200 Subject: [PATCH 0998/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20custom=20tokens?= =?UTF-8?q?=20for=20GitHub=20Actions=20to=20avoid=20rate=20limits=20(#9647?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/issue-manager.yml | 2 +- .github/workflows/label-approved.yml | 2 +- .github/workflows/latest-changes.yml | 4 +--- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 4 +--- .github/workflows/preview-docs.yml | 4 ++-- .github/workflows/publish.yml | 6 ------ .github/workflows/smokeshow.yml | 2 +- 9 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 68a180e38..95cb8578b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -42,7 +42,7 @@ jobs: with: publish-dir: './site' production-branch: master - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.FASTAPI_BUILD_DOCS_NETLIFY }} enable-commit-comment: false env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index e2fb4f7a4..617105b6e 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -20,7 +20,7 @@ jobs: steps: - uses: tiangolo/issue-manager@0.4.0 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_ISSUE_MANAGER }} config: > { "answered": { diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index b2646dd16..4a73b02aa 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -10,4 +10,4 @@ jobs: steps: - uses: docker://tiangolo/label-approved:0.0.2 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_LABEL_APPROVED }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 4aa8475b6..f11a63848 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -30,11 +30,9 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} with: limit-access-to-actor: true - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} - uses: docker://tiangolo/latest-changes:0.0.3 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_LATEST_CHANGES }} latest_changes_file: docs/en/docs/release-notes.md latest_changes_header: '## Latest Changes\n\n' debug_logs: true diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index fdd24414c..0926486e9 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -19,4 +19,4 @@ jobs: limit-access-to-actor: true - uses: ./.github/actions/notify-translations with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_NOTIFY_TRANSLATIONS }} diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index cca1329e7..b167c268f 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -24,9 +24,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} with: limit-access-to-actor: true - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} - uses: ./.github/actions/people with: token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.GITHUB_TOKEN }} + standard_token: ${{ secrets.FASTAPI_PEOPLE }} diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 8730185bd..298f75b02 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -34,7 +34,7 @@ jobs: with: publish-dir: './site' production-deploy: false - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.FASTAPI_PREVIEW_DOCS_NETLIFY }} enable-commit-comment: false env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} @@ -42,5 +42,5 @@ jobs: - name: Comment Deploy uses: ./.github/actions/comment-docs-preview-in-pr with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }} deploy_url: "${{ steps.netlify.outputs.deploy-url }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ad11f8d2..bdadcc6d3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,9 +39,3 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - # - name: Notify - # env: - # GITTER_TOKEN: ${{ secrets.GITTER_TOKEN }} - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # TAG: ${{ github.event.release.name }} - # run: bash scripts/notify.sh diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 65a174329..c6d894d9f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -31,6 +31,6 @@ jobs: SMOKESHOW_GITHUB_STATUS_DESCRIPTION: Coverage {coverage-percentage} SMOKESHOW_GITHUB_COVERAGE_THRESHOLD: 100 SMOKESHOW_GITHUB_CONTEXT: coverage - SMOKESHOW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKESHOW_GITHUB_TOKEN: ${{ secrets.FASTAPI_SMOKESHOW_UPLOAD }} SMOKESHOW_GITHUB_PR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} SMOKESHOW_AUTH_KEY: ${{ secrets.SMOKESHOW_AUTH_KEY }} From 2c7a0aca95ff86881740ba30c5c30b6a2e55ec45 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 14:26:29 +0000 Subject: [PATCH 0999/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4ab82fd58..8f7ecd07e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). * ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). * 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). * 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). From 503cec56494585201a43c644d48f7e659cd3d413 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 16:03:40 +0000 Subject: [PATCH 1000/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8f7ecd07e..9012b491e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). * ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). * 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). From 52fd0afc945e503a56725e79f5d44fb9a7c75f09 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Jun 2023 19:04:29 +0200 Subject: [PATCH 1001/1881] =?UTF-8?q?=E2=99=BB=20Remove=20`media=5Ftype`?= =?UTF-8?q?=20from=20`ORJSONResponse`=20as=20it's=20inherited=20from=20the?= =?UTF-8?q?=20parent=20class=20(#5805)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/responses.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fastapi/responses.py b/fastapi/responses.py index 88dba96e8..c0a13b755 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -27,8 +27,6 @@ class UJSONResponse(JSONResponse): class ORJSONResponse(JSONResponse): - media_type = "application/json" - def render(self, content: Any) -> bytes: assert orjson is not None, "orjson must be installed to use ORJSONResponse" return orjson.dumps( From e645a2db1b78da1918d6013eab1f8bf969178dc0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 17:05:03 +0000 Subject: [PATCH 1002/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9012b491e..410039fd1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). * 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). * ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). From 19757d1859914652a9d245891bb3e29c675e1c7b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Sat, 10 Jun 2023 19:05:42 +0200 Subject: [PATCH 1003/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20link=20to=20P?= =?UTF-8?q?ydantic's=20benchmark,=20as=20it=20was=20removed=20there=20(#58?= =?UTF-8?q?11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/features.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index 387ff86c9..98f37b534 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -189,8 +189,6 @@ With **FastAPI** you get all of **Pydantic**'s features (as FastAPI is based on * If you know Python types you know how to use Pydantic. * Plays nicely with your **IDE/linter/brain**: * Because pydantic data structures are just instances of classes you define; auto-completion, linting, mypy and your intuition should all work properly with your validated data. -* **Fast**: - * in benchmarks Pydantic is faster than all other tested libraries. * Validate **complex structures**: * Use of hierarchical Pydantic models, Python `typing`’s `List` and `Dict`, etc. * And validators allow complex data schemas to be clearly and easily defined, checked and documented as JSON Schema. From ae5c51afa67258b9b801ccae2b26919f4331d98f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 17:06:14 +0000 Subject: [PATCH 1004/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 410039fd1..f456c2930 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). * ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). * 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). From 6dd8e567cc2de5993bdb69f57d1cbc2554e6b09e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 19:23:12 +0200 Subject: [PATCH 1005/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`HTTPException`?= =?UTF-8?q?=20header=20type=20annotations=20(#9648)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index ca097b1ce..cac5330a2 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -11,7 +11,7 @@ class HTTPException(StarletteHTTPException): self, status_code: int, detail: Any = None, - headers: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, ) -> None: super().__init__(status_code=status_code, detail=detail, headers=headers) From ca8ddb28937a7f252e2f3fd9d63e63d8ac6dbcf2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 17:23:47 +0000 Subject: [PATCH 1006/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f456c2930..ffa68e2c3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). * ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). * 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). From 510fa5b7fe56320a4ee8b836996c5ea3e8e64fe4 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Sat, 10 Jun 2023 23:29:08 +0300 Subject: [PATCH 1007/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/schema-extra-example.md`?= =?UTF-8?q?=20(#9621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> --- docs/ru/docs/tutorial/schema-extra-example.md | 189 ++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 190 insertions(+) create mode 100644 docs/ru/docs/tutorial/schema-extra-example.md diff --git a/docs/ru/docs/tutorial/schema-extra-example.md b/docs/ru/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..a0363b9ba --- /dev/null +++ b/docs/ru/docs/tutorial/schema-extra-example.md @@ -0,0 +1,189 @@ +# Объявление примера запроса данных + +Вы можете объявлять примеры данных, которые ваше приложение может получать. + +Вот несколько способов, как это можно сделать. + +## Pydantic `schema_extra` + +Вы можете объявить ключ `example` для модели Pydantic, используя класс `Config` и переменную `schema_extra`, как описано в Pydantic документации: Настройка схемы: + +=== "Python 3.10+" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` + +Эта дополнительная информация будет включена в **JSON Schema** выходных данных для этой модели, и она будет использоваться в документации к API. + +!!! tip Подсказка + Вы можете использовать тот же метод для расширения JSON-схемы и добавления своей собственной дополнительной информации. + + Например, вы можете использовать это для добавления дополнительной информации для пользовательского интерфейса в вашем веб-приложении и т.д. + +## Дополнительные аргументы поля `Field` + +При использовании `Field()` с моделями Pydantic, вы также можете объявлять дополнительную информацию для **JSON Schema**, передавая любые другие произвольные аргументы в функцию. + +Вы можете использовать это, чтобы добавить аргумент `example` для каждого поля: + +=== "Python 3.10+" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` + +!!! warning Внимание + Имейте в виду, что эти дополнительные переданные аргументы не добавляют никакой валидации, только дополнительную информацию для документации. + +## Использование `example` и `examples` в OpenAPI + +При использовании любой из этих функций: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +вы также можете добавить аргумент, содержащий `example` или группу `examples` с дополнительной информацией, которая будет добавлена в **OpenAPI**. + +### Параметр `Body` с аргументом `example` + +Здесь мы передаём аргумент `example`, как пример данных ожидаемых в параметре `Body()`: + +=== "Python 3.10+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` + +### Аргумент "example" в UI документации + +С любым из вышеуказанных методов это будет выглядеть так в `/docs`: + + + +### `Body` с аргументом `examples` + +В качестве альтернативы одному аргументу `example`, вы можете передавать `examples` используя тип данных `dict` с **несколькими примерами**, каждый из которых содержит дополнительную информацию, которая также будет добавлена в **OpenAPI**. + +Ключи `dict` указывают на каждый пример, а значения для каждого из них - на еще один тип `dict` с дополнительной информацией. + +Каждый конкретный пример типа `dict` в аргументе `examples` может содержать: + +* `summary`: Краткое описание для примера. +* `description`: Полное описание, которое может содержать текст в формате Markdown. +* `value`: Это конкретный пример, который отображается, например, в виде типа `dict`. +* `externalValue`: альтернатива параметру `value`, URL-адрес, указывающий на пример. Хотя это может не поддерживаться таким же количеством инструментов разработки и тестирования API, как параметр `value`. + +=== "Python 3.10+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip Заметка + Рекомендуется использовать версию с `Annotated`, если это возможно. + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial004.py!} + ``` + +### Аргумент "examples" в UI документации + +С аргументом `examples`, добавленным в `Body()`, страница документации `/docs` будет выглядеть так: + + + +## Технические Детали + +!!! warning Внимание + Эти технические детали относятся к стандартам **JSON Schema** и **OpenAPI**. + + Если предложенные выше идеи уже работают для вас, возможно этого будет достаточно и эти детали вам не потребуются, можете спокойно их пропустить. + +Когда вы добавляете пример внутрь модели Pydantic, используя `schema_extra` или `Field(example="something")`, этот пример добавляется в **JSON Schema** для данной модели Pydantic. + +И эта **JSON Schema** модели Pydantic включается в **OpenAPI** вашего API, а затем используется в UI документации. + +Поля `example` как такового не существует в стандартах **JSON Schema**. В последних версиях JSON-схемы определено поле `examples`, но OpenAPI 3.0.3 основан на более старой версии JSON-схемы, которая не имела поля `examples`. + +Таким образом, OpenAPI 3.0.3 определяет своё собственное поле `example` для модифицированной версии **JSON Schema**, которую он использует чтобы достичь той же цели (однако это именно поле `example`, а не `examples`), и именно это используется API в UI документации (с интеграцией Swagger UI). + +Итак, хотя поле `example` не является частью JSON-схемы, оно является частью настраиваемой версии JSON-схемы в OpenAPI, и именно это поле будет использоваться в UI документации. + +Однако, когда вы используете поле `example` или `examples` с любой другой функцией (`Query()`, `Body()`, и т.д.), эти примеры не добавляются в JSON-схему, которая описывает эти данные (даже в собственную версию JSON-схемы OpenAPI), они добавляются непосредственно в объявление *операции пути* в OpenAPI (вне частей OpenAPI, которые используют JSON-схему). + +Для функций `Path()`, `Query()`, `Header()`, и `Cookie()`, аргументы `example` или `examples` добавляются в определение OpenAPI, к объекту `Parameter Object` (в спецификации). + +И для функций `Body()`, `File()` и `Form()` аргументы `example` или `examples` аналогично добавляются в определение OpenAPI, к объекту `Request Body Object`, в поле `content` в объекте `Media Type Object` (в спецификации). + +С другой стороны, существует более новая версия OpenAPI: **3.1.0**, недавно выпущенная. Она основана на последней версии JSON-схемы и большинство модификаций из OpenAPI JSON-схемы удалены в обмен на новые возможности из последней версии JSON-схемы, так что все эти мелкие отличия устранены. Тем не менее, Swagger UI в настоящее время не поддерживает OpenAPI 3.1.0, поэтому пока лучше продолжать использовать вышеупомянутые методы. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index e41333894..5fb453dd2 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -81,6 +81,7 @@ nav: - tutorial/body-multiple-params.md - tutorial/static-files.md - tutorial/debugging.md + - tutorial/schema-extra-example.md - async.md - Развёртывание: - deployment/index.md From 6fe26b5689ebc150c360f45570765f1f5cb5fa36 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 20:29:47 +0000 Subject: [PATCH 1008/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ffa68e2c3..6df84bbe3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). * 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). * ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). From 57679e8370ab0f792b6201c2f189adb8147e2ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E8=BF=87=E5=88=9D=E6=99=B4?= <129537877+ChoyeonChern@users.noreply.github.com> Date: Sun, 11 Jun 2023 04:30:28 +0800 Subject: [PATCH 1009/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ations=20for=20`docs/zh/docs/advanced/response-change-status-co?= =?UTF-8?q?de.md`=20and=20`docs/zh/docs/advanced/response-headers.md`=20(#?= =?UTF-8?q?9544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/response-change-status-code.md | 31 +++++++++++++++ docs/zh/docs/advanced/response-headers.md | 39 +++++++++++++++++++ docs/zh/mkdocs.yml | 2 + 3 files changed, 72 insertions(+) create mode 100644 docs/zh/docs/advanced/response-change-status-code.md create mode 100644 docs/zh/docs/advanced/response-headers.md diff --git a/docs/zh/docs/advanced/response-change-status-code.md b/docs/zh/docs/advanced/response-change-status-code.md new file mode 100644 index 000000000..a289cf201 --- /dev/null +++ b/docs/zh/docs/advanced/response-change-status-code.md @@ -0,0 +1,31 @@ +# 响应 - 更改状态码 + +你可能之前已经了解到,你可以设置默认的[响应状态码](../tutorial/response-status-code.md){.internal-link target=_blank}。 + +但在某些情况下,你需要返回一个不同于默认值的状态码。 + +## 使用场景 + +例如,假设你想默认返回一个HTTP状态码为“OK”`200`。 + +但如果数据不存在,你想创建它,并返回一个HTTP状态码为“CREATED”`201`。 + +但你仍然希望能够使用`response_model`过滤和转换你返回的数据。 + +对于这些情况,你可以使用一个`Response`参数。 + +## 使用 `Response` 参数 + +你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies和头部做的那样)。 + +然后你可以在这个*临时*响应对象中设置`status_code`。 + +```Python hl_lines="1 9 12" +{!../../../docs_src/response_change_status_code/tutorial001.py!} +``` + +然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 + +**FastAPI**将使用这个临时响应来提取状态码(也包括cookies和头部),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 + +你也可以在依赖项中声明`Response`参数,并在其中设置状态码。但请注意,最后设置的状态码将会生效。 diff --git a/docs/zh/docs/advanced/response-headers.md b/docs/zh/docs/advanced/response-headers.md new file mode 100644 index 000000000..85dab15ac --- /dev/null +++ b/docs/zh/docs/advanced/response-headers.md @@ -0,0 +1,39 @@ +# 响应头 + +## 使用 `Response` 参数 + +你可以在你的*路径操作函数*中声明一个`Response`类型的参数(就像你可以为cookies做的那样)。 + +然后你可以在这个*临时*响应对象中设置头部。 +```Python hl_lines="1 7-8" +{!../../../docs_src/response_headers/tutorial002.py!} +``` + +然后你可以像平常一样返回任何你需要的对象(例如一个`dict`或者一个数据库模型)。如果你声明了一个`response_model`,它仍然会被用来过滤和转换你返回的对象。 + +**FastAPI**将使用这个临时响应来提取头部(也包括cookies和状态码),并将它们放入包含你返回的值的最终响应中,该响应由任何`response_model`过滤。 + +你也可以在依赖项中声明`Response`参数,并在其中设置头部(和cookies)。 + +## 直接返回 `Response` + +你也可以在直接返回`Response`时添加头部。 + +按照[直接返回响应](response-directly.md){.internal-link target=_blank}中所述创建响应,并将头部作为附加参数传递: +```Python hl_lines="10-12" +{!../../../docs_src/response_headers/tutorial001.py!} +``` + + +!!! 注意 "技术细节" + 你也可以使用`from starlette.responses import Response`或`from starlette.responses import JSONResponse`。 + + **FastAPI**提供了与`fastapi.responses`相同的`starlette.responses`,只是为了方便开发者。但是,大多数可用的响应都直接来自Starlette。 + + 由于`Response`经常用于设置头部和cookies,因此**FastAPI**还在`fastapi.Response`中提供了它。 + +## 自定义头部 + +请注意,可以使用'X-'前缀添加自定义专有头部。 + +但是,如果你有自定义头部,你希望浏览器中的客户端能够看到它们,你需要将它们添加到你的CORS配置中(在[CORS(跨源资源共享)](../tutorial/cors.md){.internal-link target=_blank}中阅读更多),使用在Starlette的CORS文档中记录的`expose_headers`参数。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 75bd2ccab..522c83766 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -117,6 +117,8 @@ nav: - advanced/response-directly.md - advanced/custom-response.md - advanced/response-cookies.md + - advanced/response-change-status-code.md + - advanced/response-headers.md - advanced/wsgi.md - contributing.md - help-fastapi.md From 9b141076950e16fac842701488c9441e867d15f8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 20:31:03 +0000 Subject: [PATCH 1010/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6df84bbe3..d66fa73d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). * 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). From e3d67a150c8370a4e2fd26fb77b82689859bc62f Mon Sep 17 00:00:00 2001 From: Ildar Ramazanov Date: Sun, 11 Jun 2023 00:36:25 +0400 Subject: [PATCH 1011/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/index.md`=20(#5896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 81 insertions(+) create mode 100644 docs/ru/docs/tutorial/index.md diff --git a/docs/ru/docs/tutorial/index.md b/docs/ru/docs/tutorial/index.md new file mode 100644 index 000000000..4277a6c4f --- /dev/null +++ b/docs/ru/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Учебник - Руководство пользователя - Введение + +В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций. + +Каждый раздел постепенно основывается на предыдущих, но он структурирован по отдельным темам, так что вы можете перейти непосредственно к конкретной теме для решения ваших конкретных потребностей в API. + +Он также создан для использования в качестве будущего справочника. + +Так что вы можете вернуться и посмотреть именно то, что вам нужно. + +## Запустите код + +Все блоки кода можно копировать и использовать напрямую (на самом деле это проверенные файлы Python). + +Чтобы запустить любой из примеров, скопируйте код в файл `main.py` и запустите `uvicorn` с параметрами: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +**НАСТОЯТЕЛЬНО рекомендуется**, чтобы вы написали или скопировали код, отредактировали его и запустили локально. + +Использование кода в вашем редакторе — это то, что действительно показывает вам преимущества FastAPI, видя, как мало кода вам нужно написать, все проверки типов, автодополнение и т.д. + +--- + +## Установка FastAPI + +Первый шаг — установить FastAPI. + +Для руководства вы, возможно, захотите установить его со всеми дополнительными зависимостями и функциями: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...это также включает `uvicorn`, который вы можете использовать в качестве сервера, который запускает ваш код. + +!!! note "Технические детали" + Вы также можете установить его по частям. + + Это то, что вы, вероятно, сделаете, когда захотите развернуть свое приложение в рабочей среде: + + ``` + pip install fastapi + ``` + + Также установите `uvicorn` для работы в качестве сервера: + + ``` + pip install "uvicorn[standard]" + ``` + + И то же самое для каждой из необязательных зависимостей, которые вы хотите использовать. + +## Продвинутое руководство пользователя + +Существует также **Продвинутое руководство пользователя**, которое вы сможете прочитать после руководства **Учебник - Руководство пользователя**. + +**Продвинутое руководство пользователя** основано на этом, использует те же концепции и учит вас некоторым дополнительным функциям. + +Но вы должны сначала прочитать **Учебник - Руководство пользователя** (то, что вы читаете прямо сейчас). + +Он разработан таким образом, что вы можете создать полноценное приложение, используя только **Учебник - Руководство пользователя**, а затем расширить его различными способами, в зависимости от ваших потребностей, используя некоторые дополнительные идеи из **Продвинутого руководства пользователя**. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 5fb453dd2..9fb56ce1b 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -67,6 +67,7 @@ nav: - fastapi-people.md - python-types.md - Учебник - руководство пользователя: + - tutorial/index.md - tutorial/first-steps.md - tutorial/path-params.md - tutorial/query-params-str-validations.md From 4c64c15ead4d59d667d2956d8087a916f5de71b4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 20:37:02 +0000 Subject: [PATCH 1012/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d66fa73d8..5a3161734 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). * 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). From edc939eb3abdb2a2d557e0f32c2b40d944cd2528 Mon Sep 17 00:00:00 2001 From: Purwo Widodo Date: Sun, 11 Jun 2023 03:48:51 +0700 Subject: [PATCH 1013/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20spelling=20in=20?= =?UTF-8?q?Indonesian=20translation=20of=20`docs/id/docs/tutorial/index.md?= =?UTF-8?q?`=20(#5635)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Purwo Widodo Co-authored-by: Sebastián Ramírez --- docs/id/docs/tutorial/index.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/id/docs/tutorial/index.md b/docs/id/docs/tutorial/index.md index 8fec3c087..b8ed96ae1 100644 --- a/docs/id/docs/tutorial/index.md +++ b/docs/id/docs/tutorial/index.md @@ -10,9 +10,9 @@ Sehingga kamu dapat kembali lagi dan mencari apa yang kamu butuhkan dengan tepat ## Jalankan kode -Semua blok-blok kode dapat dicopy dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji). +Semua blok-blok kode dapat disalin dan digunakan langsung (Mereka semua sebenarnya adalah file python yang sudah teruji). -Untuk menjalankan setiap contoh, copy kode ke file `main.py`, dan jalankan `uvicorn` dengan: +Untuk menjalankan setiap contoh, salin kode ke file `main.py`, dan jalankan `uvicorn` dengan:
@@ -28,7 +28,7 @@ $ uvicorn main:app --reload
-**SANGAT disarankan** agar kamu menulis atau meng-copy kode, meng-editnya dan menjalankannya secara lokal. +**SANGAT disarankan** agar kamu menulis atau menyalin kode, mengubahnya dan menjalankannya secara lokal. Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari FastAPI, melihat bagaimana sedikitnya kode yang harus kamu tulis, semua pengecekan tipe, pelengkapan otomatis, dll. @@ -38,7 +38,7 @@ Dengan menggunakannya di dalam editor, benar-benar memperlihatkan manfaat dari F Langkah pertama adalah dengan meng-install FastAPI. -Untuk tutorial, kamu mungkin hendak meng-instalnya dengan semua pilihan fitur dan dependensinya: +Untuk tutorial, kamu mungkin hendak meng-installnya dengan semua pilihan fitur dan dependensinya:
@@ -53,15 +53,15 @@ $ pip install "fastapi[all]" ...yang juga termasuk `uvicorn`, yang dapat kamu gunakan sebagai server yang menjalankan kodemu. !!! catatan - Kamu juga dapat meng-instalnya bagian demi bagian. + Kamu juga dapat meng-installnya bagian demi bagian. - Hal ini mungkin yang akan kamu lakukan ketika kamu hendak men-deploy aplikasimu ke tahap produksi: + Hal ini mungkin yang akan kamu lakukan ketika kamu hendak menyebarkan (men-deploy) aplikasimu ke tahap produksi: ``` pip install fastapi ``` - Juga install `uvicorn` untk menjalankan server" + Juga install `uvicorn` untuk menjalankan server" ``` pip install "uvicorn[standard]" @@ -77,4 +77,4 @@ Tersedia juga **Pedoman Pengguna Lanjutan** yang dapat kamu baca nanti setelah * Tetapi kamu harus membaca terlebih dahulu **Tutorial - Pedoman Pengguna** (apa yang sedang kamu baca sekarang). -Hal ini didesain sehingga kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**. +Hal ini dirancang supaya kamu dapat membangun aplikasi lengkap dengan hanya **Tutorial - Pedoman Pengguna**, dan kemudian mengembangkannya ke banyak cara yang berbeda, tergantung dari kebutuhanmu, menggunakan beberapa ide-ide tambahan dari **Pedoman Pengguna Lanjutan**. From 3d162455a7186f065a31c92a4defe21a19bf7287 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 20:49:25 +0000 Subject: [PATCH 1014/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5a3161734..4c7b3694a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). From 4ac8b8e4432db5214e0a6081ccb6ef68a30f62d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 22:58:15 +0200 Subject: [PATCH 1015/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor=20Platfo?= =?UTF-8?q?rm.sh=20(#9650)?= 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 + docs/en/docs/img/sponsors/platform-sh-banner.png | Bin 0 -> 6313 bytes docs/en/docs/img/sponsors/platform-sh.png | Bin 0 -> 5779 bytes docs/en/overrides/main.html | 6 ++++++ 6 files changed, 11 insertions(+) create mode 100644 docs/en/docs/img/sponsors/platform-sh-banner.png create mode 100644 docs/en/docs/img/sponsors/platform-sh.png diff --git a/README.md b/README.md index e45e7f56c..ee25f1803 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index ad31dc0bc..9913c5df5 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -2,6 +2,9 @@ gold: - url: https://cryptapi.io/ title: "CryptAPI: Your easy to use, secure and privacy oriented payment gateway." img: https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg + - url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023 + title: "Build, run and scale your apps on a modern, reliable, and secure PaaS." + img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index a95af177c..014744a10 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -15,3 +15,4 @@ logins: - svix - armand-sauzay - databento-bot + - nanram22 diff --git a/docs/en/docs/img/sponsors/platform-sh-banner.png b/docs/en/docs/img/sponsors/platform-sh-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..f9f4580fac519541b46776d40071002d18804508 GIT binary patch literal 6313 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/platform-sh.png b/docs/en/docs/img/sponsors/platform-sh.png new file mode 100644 index 0000000000000000000000000000000000000000..fb4e07becdb896502137ac3a4f673ed368875a86 GIT binary patch literal 5779 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 nDxm6LO1s
+
{% endblock %} From 58e50622dee3dfad35fb342c677407bd0a0f3f8a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 10 Jun 2023 20:58:55 +0000 Subject: [PATCH 1016/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4c7b3694a..b00a75a21 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo). * 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). From 20d93fad94699eef779d668860772687b4f66270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 23:50:09 +0200 Subject: [PATCH 1017/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b00a75a21..eb0a08fdf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,19 +2,33 @@ ## Latest Changes -* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo). +### Fixes + +* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). +* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* 📌 Update minimum version of Pydantic to >=1.7.4. This fixes an issue when trying to use an old version of Pydantic. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). + +### Docs + +* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). + +### Translations + * 🌐 Fix spelling in Indonesian translation of `docs/id/docs/tutorial/index.md`. PR [#5635](https://github.com/tiangolo/fastapi/pull/5635) by [@purwowd](https://github.com/purwowd). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/index.md`. PR [#5896](https://github.com/tiangolo/fastapi/pull/5896) by [@Wilidon](https://github.com/Wilidon). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/response-change-status-code.md` and `docs/zh/docs/advanced/response-headers.md`. PR [#9544](https://github.com/tiangolo/fastapi/pull/9544) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/schema-extra-example.md`. PR [#9621](https://github.com/tiangolo/fastapi/pull/9621) by [@Alexandrhub](https://github.com/Alexandrhub). -* 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). -* 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). + +### Internal + +* 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo). * ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). * 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). * ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). -* 📌 Update minimum version of Pydantic to >=1.7.4. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). -* 🐛 Fix OpenAPI model fields int validations, `gte` to `ge`. PR [#9635](https://github.com/tiangolo/fastapi/pull/9635) by [@tiangolo](https://github.com/tiangolo). ## 0.96.0 From 19347bfc3cd1d3dcf3d8216c642033dcb9a3d6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 10 Jun 2023 23:51:40 +0200 Subject: [PATCH 1018/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?96.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 eb0a08fdf..0bf888183 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.96.1 + ### Fixes * 🐛 Fix `HTTPException` header type annotations. PR [#9648](https://github.com/tiangolo/fastapi/pull/9648) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index d564d5fa3..2bc795b4b 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.96.0" +__version__ = "0.96.1" from starlette import status as status From f5e2dd8025e2164af6779bdd883d432a47d2bd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Jun 2023 00:03:27 +0200 Subject: [PATCH 1019/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0bf888183..765be57a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -14,6 +14,11 @@ * 📌 Update minimum version of Pydantic to >=1.7.4. This fixes an issue when trying to use an old version of Pydantic. PR [#9567](https://github.com/tiangolo/fastapi/pull/9567) by [@Kludex](https://github.com/Kludex). +### Refactors + +* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). +* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). + ### Docs * 🔥 Remove link to Pydantic's benchmark, as it was removed there. PR [#5811](https://github.com/tiangolo/fastapi/pull/5811) by [@Kludex](https://github.com/Kludex). @@ -28,10 +33,8 @@ ### Internal * 🔧 Add sponsor Platform.sh. PR [#9650](https://github.com/tiangolo/fastapi/pull/9650) by [@tiangolo](https://github.com/tiangolo). -* ♻ Remove `media_type` from `ORJSONResponse` as it's inherited from the parent class. PR [#5805](https://github.com/tiangolo/fastapi/pull/5805) by [@Kludex](https://github.com/Kludex). * 👷 Add custom token to Smokeshow and Preview Docs for download-artifact, to prevent API rate limits. PR [#9646](https://github.com/tiangolo/fastapi/pull/9646) by [@tiangolo](https://github.com/tiangolo). * 👷 Add custom tokens for GitHub Actions to avoid rate limits. PR [#9647](https://github.com/tiangolo/fastapi/pull/9647) by [@tiangolo](https://github.com/tiangolo). -* ♻ Instantiate `HTTPException` only when needed, optimization refactor. PR [#5356](https://github.com/tiangolo/fastapi/pull/5356) by [@pawamoy](https://github.com/pawamoy). ## 0.96.0 From ab03f226353394da467b77ff08cad4cbf94463e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Valur=20J=C3=B3nsson?= Date: Sun, 11 Jun 2023 19:08:14 +0000 Subject: [PATCH 1020/1881] =?UTF-8?q?=E2=9C=A8=20Add=20exception=20handler?= =?UTF-8?q?=20for=20`WebSocketRequestValidationError`=20(which=20also=20al?= =?UTF-8?q?lows=20to=20override=20it)=20(#6030)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 8 +- fastapi/exception_handlers.py | 13 ++- fastapi/routing.py | 2 - tests/test_ws_router.py | 152 +++++++++++++++++++++++++++++++++- 4 files changed, 166 insertions(+), 9 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 8b3a74d3c..d5ea1d72a 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -19,8 +19,9 @@ from fastapi.encoders import DictIntStrAny, SetIntStr from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, + websocket_request_validation_exception_handler, ) -from fastapi.exceptions import RequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( @@ -145,6 +146,11 @@ class FastAPI(Starlette): self.exception_handlers.setdefault( RequestValidationError, request_validation_exception_handler ) + self.exception_handlers.setdefault( + WebSocketRequestValidationError, + # Starlette still has incorrect type specification for the handlers + websocket_request_validation_exception_handler, # type: ignore + ) self.user_middleware: List[Middleware] = ( [] if middleware is None else list(middleware) diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 4d7ea5ec2..6c2ba7fed 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,10 +1,11 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError +from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.utils import is_body_allowed_for_status_code +from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response -from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION async def http_exception_handler(request: Request, exc: HTTPException) -> Response: @@ -23,3 +24,11 @@ async def request_validation_exception_handler( status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": jsonable_encoder(exc.errors())}, ) + + +async def websocket_request_validation_exception_handler( + websocket: WebSocket, exc: WebSocketRequestValidationError +) -> None: + await websocket.close( + code=WS_1008_POLICY_VIOLATION, reason=jsonable_encoder(exc.errors()) + ) diff --git a/fastapi/routing.py b/fastapi/routing.py index 06c71bffa..7f1936f7f 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -56,7 +56,6 @@ from starlette.routing import ( request_response, websocket_session, ) -from starlette.status import WS_1008_POLICY_VIOLATION from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket @@ -283,7 +282,6 @@ def get_websocket_app( ) values, errors, _, _2, _3 = solved_result if errors: - await websocket.close(code=WS_1008_POLICY_VIOLATION) raise WebSocketRequestValidationError(errors) assert dependant.call is not None, "dependant.call must be a function" await dependant.call(**values) diff --git a/tests/test_ws_router.py b/tests/test_ws_router.py index c312821e9..240a42bb0 100644 --- a/tests/test_ws_router.py +++ b/tests/test_ws_router.py @@ -1,4 +1,16 @@ -from fastapi import APIRouter, Depends, FastAPI, WebSocket +import functools + +import pytest +from fastapi import ( + APIRouter, + Depends, + FastAPI, + Header, + WebSocket, + WebSocketDisconnect, + status, +) +from fastapi.middleware import Middleware from fastapi.testclient import TestClient router = APIRouter() @@ -63,9 +75,44 @@ async def router_native_prefix_ws(websocket: WebSocket): await websocket.close() -app.include_router(router) -app.include_router(prefix_router, prefix="/prefix") -app.include_router(native_prefix_route) +async def ws_dependency_err(): + raise NotImplementedError() + + +@router.websocket("/depends-err/") +async def router_ws_depends_err(websocket: WebSocket, data=Depends(ws_dependency_err)): + pass # pragma: no cover + + +async def ws_dependency_validate(x_missing: str = Header()): + pass # pragma: no cover + + +@router.websocket("/depends-validate/") +async def router_ws_depends_validate( + websocket: WebSocket, data=Depends(ws_dependency_validate) +): + pass # pragma: no cover + + +class CustomError(Exception): + pass + + +@router.websocket("/custom_error/") +async def router_ws_custom_error(websocket: WebSocket): + raise CustomError() + + +def make_app(app=None, **kwargs): + app = app or FastAPI(**kwargs) + app.include_router(router) + app.include_router(prefix_router, prefix="/prefix") + app.include_router(native_prefix_route) + return app + + +app = make_app(app) def test_app(): @@ -125,3 +172,100 @@ def test_router_with_params(): assert data == "path/to/file" data = websocket.receive_text() assert data == "a_query_param" + + +def test_wrong_uri(): + """ + Verify that a websocket connection to a non-existent endpoing returns in a shutdown + """ + client = TestClient(app) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/no-router/"): + pass # pragma: no cover + assert e.value.code == status.WS_1000_NORMAL_CLOSURE + + +def websocket_middleware(middleware_func): + """ + Helper to create a Starlette pure websocket middleware + """ + + def middleware_constructor(app): + @functools.wraps(app) + async def wrapped_app(scope, receive, send): + if scope["type"] != "websocket": + return await app(scope, receive, send) # pragma: no cover + + async def call_next(): + return await app(scope, receive, send) + + websocket = WebSocket(scope, receive=receive, send=send) + return await middleware_func(websocket, call_next) + + return wrapped_app + + return middleware_constructor + + +def test_depend_validation(): + """ + Verify that a validation in a dependency invokes the correct exception handler + """ + caught = [] + + @websocket_middleware + async def catcher(websocket, call_next): + try: + return await call_next() + except Exception as e: # pragma: no cover + caught.append(e) + raise + + myapp = make_app(middleware=[Middleware(catcher)]) + + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/depends-validate/"): + pass # pragma: no cover + # the validation error does produce a close message + assert e.value.code == status.WS_1008_POLICY_VIOLATION + # and no error is leaked + assert caught == [] + + +def test_depend_err_middleware(): + """ + Verify that it is possible to write custom WebSocket middleware to catch errors + """ + + @websocket_middleware + async def errorhandler(websocket: WebSocket, call_next): + try: + return await call_next() + except Exception as e: + await websocket.close(code=status.WS_1006_ABNORMAL_CLOSURE, reason=repr(e)) + + myapp = make_app(middleware=[Middleware(errorhandler)]) + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/depends-err/"): + pass # pragma: no cover + assert e.value.code == status.WS_1006_ABNORMAL_CLOSURE + assert "NotImplementedError" in e.value.reason + + +def test_depend_err_handler(): + """ + Verify that it is possible to write custom WebSocket middleware to catch errors + """ + + async def custom_handler(websocket: WebSocket, exc: CustomError) -> None: + await websocket.close(1002, "foo") + + myapp = make_app(exception_handlers={CustomError: custom_handler}) + client = TestClient(myapp) + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/custom_error/"): + pass # pragma: no cover + assert e.value.code == 1002 + assert "foo" in e.value.reason From ee96a099d8acc7ede6c66aaef987b6412e0fcc54 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 19:08:50 +0000 Subject: [PATCH 1021/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 765be57a9..8d51bb26e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). ## 0.96.1 From d8b8f211e813ba4d53987a2bae16587eeaff4ad2 Mon Sep 17 00:00:00 2001 From: Paulo Costa Date: Sun, 11 Jun 2023 17:35:39 -0300 Subject: [PATCH 1022/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`de?= =?UTF-8?q?pendencies`=20in=20WebSocket=20routes=20(#4534)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 27 +++++++++++-- fastapi/routing.py | 47 +++++++++++++++++----- tests/test_ws_dependencies.py | 73 +++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 tests/test_ws_dependencies.py diff --git a/fastapi/applications.py b/fastapi/applications.py index d5ea1d72a..298aca921 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -401,15 +401,34 @@ class FastAPI(Starlette): return decorator def add_api_websocket_route( - self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None + self, + path: str, + endpoint: Callable[..., Any], + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[Depends]] = None, ) -> None: - self.router.add_api_websocket_route(path, endpoint, name=name) + self.router.add_api_websocket_route( + path, + endpoint, + name=name, + dependencies=dependencies, + ) def websocket( - self, path: str, name: Optional[str] = None + self, + path: str, + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[Depends]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: - self.add_api_websocket_route(path, func, name=name) + self.add_api_websocket_route( + path, + func, + name=name, + dependencies=dependencies, + ) return func return decorator diff --git a/fastapi/routing.py b/fastapi/routing.py index 7f1936f7f..af628f32d 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -296,13 +296,21 @@ class APIWebSocketRoute(routing.WebSocketRoute): endpoint: Callable[..., Any], *, name: Optional[str] = None, + dependencies: Optional[Sequence[params.Depends]] = None, dependency_overrides_provider: Optional[Any] = None, ) -> None: self.path = path self.endpoint = endpoint self.name = get_name(endpoint) if name is None else name + self.dependencies = list(dependencies or []) self.path_regex, self.path_format, self.param_convertors = compile_path(path) self.dependant = get_dependant(path=self.path_format, call=self.endpoint) + for depends in self.dependencies[::-1]: + self.dependant.dependencies.insert( + 0, + get_parameterless_sub_dependant(depends=depends, path=self.path_format), + ) + self.app = websocket_session( get_websocket_app( dependant=self.dependant, @@ -416,10 +424,7 @@ class APIRoute(routing.Route): else: self.response_field = None # type: ignore self.secure_cloned_response_field = None - if dependencies: - self.dependencies = list(dependencies) - else: - self.dependencies = [] + self.dependencies = list(dependencies or []) self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "") # if a "form feed" character (page break) is found in the description text, # truncate description text to the content preceding the first "form feed" @@ -514,7 +519,7 @@ class APIRouter(routing.Router): ), "A path prefix must not end with '/', as the routes will start with '/'" self.prefix = prefix self.tags: List[Union[str, Enum]] = tags or [] - self.dependencies = list(dependencies or []) or [] + self.dependencies = list(dependencies or []) self.deprecated = deprecated self.include_in_schema = include_in_schema self.responses = responses or {} @@ -688,21 +693,37 @@ class APIRouter(routing.Router): return decorator def add_api_websocket_route( - self, path: str, endpoint: Callable[..., Any], name: Optional[str] = None + self, + path: str, + endpoint: Callable[..., Any], + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[params.Depends]] = None, ) -> None: + current_dependencies = self.dependencies.copy() + if dependencies: + current_dependencies.extend(dependencies) + route = APIWebSocketRoute( self.prefix + path, endpoint=endpoint, name=name, + dependencies=current_dependencies, dependency_overrides_provider=self.dependency_overrides_provider, ) self.routes.append(route) def websocket( - self, path: str, name: Optional[str] = None + self, + path: str, + name: Optional[str] = None, + *, + dependencies: Optional[Sequence[params.Depends]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: - self.add_api_websocket_route(path, func, name=name) + self.add_api_websocket_route( + path, func, name=name, dependencies=dependencies + ) return func return decorator @@ -817,8 +838,16 @@ class APIRouter(routing.Router): name=route.name, ) elif isinstance(route, APIWebSocketRoute): + current_dependencies = [] + if dependencies: + current_dependencies.extend(dependencies) + if route.dependencies: + current_dependencies.extend(route.dependencies) self.add_api_websocket_route( - prefix + route.path, route.endpoint, name=route.name + prefix + route.path, + route.endpoint, + dependencies=current_dependencies, + name=route.name, ) elif isinstance(route, routing.WebSocketRoute): self.add_websocket_route( diff --git a/tests/test_ws_dependencies.py b/tests/test_ws_dependencies.py new file mode 100644 index 000000000..ccb1c4b7d --- /dev/null +++ b/tests/test_ws_dependencies.py @@ -0,0 +1,73 @@ +import json +from typing import List + +from fastapi import APIRouter, Depends, FastAPI, WebSocket +from fastapi.testclient import TestClient +from typing_extensions import Annotated + + +def dependency_list() -> List[str]: + return [] + + +DepList = Annotated[List[str], Depends(dependency_list)] + + +def create_dependency(name: str): + def fun(deps: DepList): + deps.append(name) + + return Depends(fun) + + +router = APIRouter(dependencies=[create_dependency("router")]) +prefix_router = APIRouter(dependencies=[create_dependency("prefix_router")]) +app = FastAPI(dependencies=[create_dependency("app")]) + + +@app.websocket("/", dependencies=[create_dependency("index")]) +async def index(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +@router.websocket("/router", dependencies=[create_dependency("routerindex")]) +async def routerindex(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +@prefix_router.websocket("/", dependencies=[create_dependency("routerprefixindex")]) +async def routerprefixindex(websocket: WebSocket, deps: DepList): + await websocket.accept() + await websocket.send_text(json.dumps(deps)) + await websocket.close() + + +app.include_router(router, dependencies=[create_dependency("router2")]) +app.include_router( + prefix_router, prefix="/prefix", dependencies=[create_dependency("prefix_router2")] +) + + +def test_index(): + client = TestClient(app) + with client.websocket_connect("/") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "index"] + + +def test_routerindex(): + client = TestClient(app) + with client.websocket_connect("/router") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "router2", "router", "routerindex"] + + +def test_routerprefixindex(): + client = TestClient(app) + with client.websocket_connect("/prefix/") as websocket: + data = json.loads(websocket.receive_text()) + assert data == ["app", "prefix_router2", "prefix_router", "routerprefixindex"] From c8b729aea72aaae22384461ead80ef39bf8588b0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 20:36:12 +0000 Subject: [PATCH 1023/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8d51bb26e..e3b7c32cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). * ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). ## 0.96.1 From 6595658324237b2905f16d3857bd524e58180f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Jun 2023 23:38:15 +0200 Subject: [PATCH 1024/1881] =?UTF-8?q?=E2=AC=87=EF=B8=8F=20Separate=20requi?= =?UTF-8?q?rements=20for=20development=20into=20their=20own=20requirements?= =?UTF-8?q?.txt=20files,=20they=20shouldn't=20be=20extras=20(#9655)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 2 +- docs/em/docs/contributing.md | 2 +- docs/en/docs/contributing.md | 9 +++++-- docs/ja/docs/contributing.md | 2 +- docs/pt/docs/contributing.md | 2 +- docs/ru/docs/contributing.md | 2 +- docs/zh/docs/contributing.md | 2 +- pyproject.toml | 41 -------------------------------- requirements-docs.txt | 8 +++++++ requirements-tests.txt | 26 ++++++++++++++++++++ requirements.txt | 6 +++++ scripts/build-docs.sh | 2 ++ scripts/test.sh | 2 -- 14 files changed, 56 insertions(+), 52 deletions(-) create mode 100644 requirements-docs.txt create mode 100644 requirements-tests.txt create mode 100644 requirements.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 95cb8578b..41eb55b85 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -25,7 +25,7 @@ jobs: key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' - run: pip install .[doc] + run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65b29be20..e3abe4b21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' - run: pip install -e .[all,dev,doc,test] + run: pip install -r requirements-tests.txt - name: Lint run: bash scripts/lint.sh - run: mkdir coverage diff --git a/docs/em/docs/contributing.md b/docs/em/docs/contributing.md index 7749d27a1..748928f88 100644 --- a/docs/em/docs/contributing.md +++ b/docs/em/docs/contributing.md @@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index a1a32a1fe..660914a08 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -108,7 +108,7 @@ After activating the environment as described above:
```console -$ pip install -e ".[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` @@ -121,10 +121,15 @@ It will install all the dependencies and your local FastAPI in your local enviro If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code. -And if you update that local FastAPI source code, as it is installed with `-e`, when you run that Python file again, it will use the fresh version of FastAPI you just edited. +And if you update that local FastAPI source code when you run that Python file again, it will use the fresh version of FastAPI you just edited. That way, you don't have to "install" your local version to be able to test every change. +!!! note "Technical Details" + This only happens when you install using this included `requiements.txt` instead of installing `pip install fastapi` directly. + + That is because inside of the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. + ### Format There is a script that you can run that will format and clean all your code: diff --git a/docs/ja/docs/contributing.md b/docs/ja/docs/contributing.md index 9affea443..31db51c52 100644 --- a/docs/ja/docs/contributing.md +++ b/docs/ja/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/pt/docs/contributing.md b/docs/pt/docs/contributing.md index f95b6f4ec..02895fcfc 100644 --- a/docs/pt/docs/contributing.md +++ b/docs/pt/docs/contributing.md @@ -98,7 +98,7 @@ Após ativar o ambiente como descrito acima:
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/ru/docs/contributing.md b/docs/ru/docs/contributing.md index f61ef1cb6..f9b8912e5 100644 --- a/docs/ru/docs/contributing.md +++ b/docs/ru/docs/contributing.md @@ -108,7 +108,7 @@ $ python -m pip install --upgrade pip
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/docs/zh/docs/contributing.md b/docs/zh/docs/contributing.md index 36c3631c4..4ebd67315 100644 --- a/docs/zh/docs/contributing.md +++ b/docs/zh/docs/contributing.md @@ -97,7 +97,7 @@ $ python -m venv env
```console -$ pip install -e ."[dev,doc,test]" +$ pip install -r requirements.txt ---> 100% ``` diff --git a/pyproject.toml b/pyproject.toml index 3bae6a3ef..69c42b254 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,47 +51,6 @@ Homepage = "https://github.com/tiangolo/fastapi" Documentation = "https://fastapi.tiangolo.com/" [project.optional-dependencies] -test = [ - "pytest >=7.1.3,<8.0.0", - "coverage[toml] >= 6.5.0,< 8.0", - "mypy ==0.982", - "ruff ==0.0.138", - "black == 23.1.0", - "isort >=5.0.6,<6.0.0", - "httpx >=0.23.0,<0.24.0", - "email_validator >=1.1.1,<2.0.0", - # TODO: once removing databases from tutorial, upgrade SQLAlchemy - # probably when including SQLModel - "sqlalchemy >=1.3.18,<1.4.43", - "peewee >=3.13.3,<4.0.0", - "databases[sqlite] >=0.3.2,<0.7.0", - "orjson >=3.2.1,<4.0.0", - "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0", - "python-multipart >=0.0.5,<0.0.7", - "flask >=1.1.2,<3.0.0", - "anyio[trio] >=3.2.1,<4.0.0", - "python-jose[cryptography] >=3.3.0,<4.0.0", - "pyyaml >=5.3.1,<7.0.0", - "passlib[bcrypt] >=1.7.2,<2.0.0", - - # types - "types-ujson ==5.7.0.1", - "types-orjson ==3.6.2", -] -doc = [ - "mkdocs >=1.1.2,<2.0.0", - "mkdocs-material >=8.1.4,<9.0.0", - "mdx-include >=1.4.1,<2.0.0", - "mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0", - "typer-cli >=0.0.13,<0.0.14", - "typer[all] >=0.6.1,<0.8.0", - "pyyaml >=5.3.1,<7.0.0", -] -dev = [ - "ruff ==0.0.138", - "uvicorn[standard] >=0.12.0,<0.21.0", - "pre-commit >=2.17.0,<3.0.0", -] all = [ "httpx >=0.23.0", "jinja2 >=2.11.2", diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..e9d0567ed --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,8 @@ +-e . +mkdocs >=1.1.2,<2.0.0 +mkdocs-material >=8.1.4,<9.0.0 +mdx-include >=1.4.1,<2.0.0 +mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 +typer-cli >=0.0.13,<0.0.14 +typer[all] >=0.6.1,<0.8.0 +pyyaml >=5.3.1,<7.0.0 diff --git a/requirements-tests.txt b/requirements-tests.txt new file mode 100644 index 000000000..52a44cec5 --- /dev/null +++ b/requirements-tests.txt @@ -0,0 +1,26 @@ +-e . +pytest >=7.1.3,<8.0.0 +coverage[toml] >= 6.5.0,< 8.0 +mypy ==0.982 +ruff ==0.0.138 +black == 23.1.0 +isort >=5.0.6,<6.0.0 +httpx >=0.23.0,<0.24.0 +email_validator >=1.1.1,<2.0.0 +# TODO: once removing databases from tutorial, upgrade SQLAlchemy +# probably when including SQLModel +sqlalchemy >=1.3.18,<1.4.43 +peewee >=3.13.3,<4.0.0 +databases[sqlite] >=0.3.2,<0.7.0 +orjson >=3.2.1,<4.0.0 +ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0 +python-multipart >=0.0.5,<0.0.7 +flask >=1.1.2,<3.0.0 +anyio[trio] >=3.2.1,<4.0.0 +python-jose[cryptography] >=3.3.0,<4.0.0 +pyyaml >=5.3.1,<7.0.0 +passlib[bcrypt] >=1.7.2,<2.0.0 + +# types +types-ujson ==5.7.0.1 +types-orjson ==3.6.2 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..9d51e1cb3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +-e .[all] +-r requirements-tests.txt +-r requirements-docs.txt +ruff ==0.0.138 +uvicorn[standard] >=0.12.0,<0.21.0 +pre-commit >=2.17.0,<3.0.0 diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 383ad3f44..ebf864afa 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -3,4 +3,6 @@ set -e set -x +# Check README.md is up to date +python ./scripts/docs.py verify-readme python ./scripts/docs.py build-all diff --git a/scripts/test.sh b/scripts/test.sh index 62449ea41..7d17add8f 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,7 +3,5 @@ set -e set -x -# Check README.md is up to date -python ./scripts/docs.py verify-readme export PYTHONPATH=./docs_src coverage run -m pytest tests ${@} From df58ecdee2eda8bbac7fb8b8bcb00c4479e5d6db Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 21:38:54 +0000 Subject: [PATCH 1025/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e3b7c32cc..160ec2fe8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). * ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). From 17e49bc9f75d9f596eb3fea42a3f51f3a716475c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 11 Jun 2023 23:49:18 +0200 Subject: [PATCH 1026/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20`Asyn?= =?UTF-8?q?cExitStackMiddleware`=20as=20without=20Python=203.6=20`AsyncExi?= =?UTF-8?q?tStack`=20is=20always=20available=20(#9657)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ♻️ Simplify AsyncExitStackMiddleware as without Python 3.6 AsyncExitStack is always available --- fastapi/middleware/asyncexitstack.py | 29 +++++++++++++--------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/fastapi/middleware/asyncexitstack.py b/fastapi/middleware/asyncexitstack.py index 503a68ac7..30a0ae626 100644 --- a/fastapi/middleware/asyncexitstack.py +++ b/fastapi/middleware/asyncexitstack.py @@ -10,19 +10,16 @@ class AsyncExitStackMiddleware: self.context_name = context_name async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if AsyncExitStack: - dependency_exception: Optional[Exception] = None - async with AsyncExitStack() as stack: - scope[self.context_name] = stack - try: - await self.app(scope, receive, send) - except Exception as e: - dependency_exception = e - raise e - if dependency_exception: - # This exception was possibly handled by the dependency but it should - # still bubble up so that the ServerErrorMiddleware can return a 500 - # or the ExceptionMiddleware can catch and handle any other exceptions - raise dependency_exception - else: - await self.app(scope, receive, send) # pragma: no cover + dependency_exception: Optional[Exception] = None + async with AsyncExitStack() as stack: + scope[self.context_name] = stack + try: + await self.app(scope, receive, send) + except Exception as e: + dependency_exception = e + raise e + if dependency_exception: + # This exception was possibly handled by the dependency but it should + # still bubble up so that the ServerErrorMiddleware can return a 500 + # or the ExceptionMiddleware can catch and handle any other exceptions + raise dependency_exception From 32cefb9bff624825d3086bed84bd380d8cc01f15 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 21:49:52 +0000 Subject: [PATCH 1027/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 160ec2fe8..2ea4ef8e1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). * ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). * ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). From f5844e76b5e710ae8d42654927c1202f00f79526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:08:56 +0200 Subject: [PATCH 1028/1881] =?UTF-8?q?=F0=9F=92=9A=20Update=20CI=20cache=20?= =?UTF-8?q?to=20fix=20installs=20when=20dependencies=20change=20(#9659)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 2 +- .github/workflows/test.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 41eb55b85..a0e83e5c8 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -22,7 +22,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e3abe4b21..b17d2e9d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,12 +23,12 @@ jobs: python-version: ${{ matrix.python-version }} # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" - cache-dependency-path: pyproject.toml + # cache-dependency-path: pyproject.toml - uses: actions/cache@v3 id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -57,7 +57,7 @@ jobs: python-version: '3.8' # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" - cache-dependency-path: pyproject.toml + # cache-dependency-path: pyproject.toml - name: Get coverage files uses: actions/download-artifact@v3 From 3390a82832df2e5b6f0348ba71c570bd6f3a7f82 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 22:09:33 +0000 Subject: [PATCH 1029/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2ea4ef8e1..c01399327 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). * ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). From 4ac55af283457d7279711224c5f9a3810d4d6534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:16:01 +0200 Subject: [PATCH 1030/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20interna?= =?UTF-8?q?l=20type=20annotations=20and=20upgrade=20mypy=20(#9658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/openapi/models.py | 13 ++++++++----- fastapi/security/api_key.py | 12 +++++++++--- fastapi/security/oauth2.py | 25 ++++++++++++++++--------- requirements-tests.txt | 2 +- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 11edfe38a..81a24f389 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -3,6 +3,7 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Union from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field +from typing_extensions import Literal try: import email_validator # type: ignore @@ -298,18 +299,18 @@ class APIKeyIn(Enum): class APIKey(SecurityBase): - type_ = Field(SecuritySchemeType.apiKey, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.apiKey, alias="type") in_: APIKeyIn = Field(alias="in") name: str class HTTPBase(SecurityBase): - type_ = Field(SecuritySchemeType.http, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.http, alias="type") scheme: str class HTTPBearer(HTTPBase): - scheme = "bearer" + scheme: Literal["bearer"] = "bearer" bearerFormat: Optional[str] = None @@ -349,12 +350,14 @@ class OAuthFlows(BaseModel): class OAuth2(SecurityBase): - type_ = Field(SecuritySchemeType.oauth2, alias="type") + type_: SecuritySchemeType = Field(default=SecuritySchemeType.oauth2, alias="type") flows: OAuthFlows class OpenIdConnect(SecurityBase): - type_ = Field(SecuritySchemeType.openIdConnect, alias="type") + type_: SecuritySchemeType = Field( + default=SecuritySchemeType.openIdConnect, alias="type" + ) openIdConnectUrl: str diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 61730187a..8b2c5c080 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -21,7 +21,9 @@ class APIKeyQuery(APIKeyBase): auto_error: bool = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, name=name, description=description + **{"in": APIKeyIn.query}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -48,7 +50,9 @@ class APIKeyHeader(APIKeyBase): auto_error: bool = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, name=name, description=description + **{"in": APIKeyIn.header}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -75,7 +79,9 @@ class APIKeyCookie(APIKeyBase): auto_error: bool = True, ): self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, name=name, description=description + **{"in": APIKeyIn.cookie}, # type: ignore[arg-type] + name=name, + description=description, ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index dc75dc9fe..938dec37c 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Union, cast from fastapi.exceptions import HTTPException from fastapi.openapi.models import OAuth2 as OAuth2Model @@ -121,7 +121,9 @@ class OAuth2(SecurityBase): description: Optional[str] = None, auto_error: bool = True, ): - self.model = OAuth2Model(flows=flows, description=description) + self.model = OAuth2Model( + flows=cast(OAuthFlowsModel, flows), description=description + ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error @@ -148,7 +150,9 @@ class OAuth2PasswordBearer(OAuth2): ): if not scopes: scopes = {} - flows = OAuthFlowsModel(password={"tokenUrl": tokenUrl, "scopes": scopes}) + flows = OAuthFlowsModel( + password=cast(Any, {"tokenUrl": tokenUrl, "scopes": scopes}) + ) super().__init__( flows=flows, scheme_name=scheme_name, @@ -185,12 +189,15 @@ class OAuth2AuthorizationCodeBearer(OAuth2): if not scopes: scopes = {} flows = OAuthFlowsModel( - authorizationCode={ - "authorizationUrl": authorizationUrl, - "tokenUrl": tokenUrl, - "refreshUrl": refreshUrl, - "scopes": scopes, - } + authorizationCode=cast( + Any, + { + "authorizationUrl": authorizationUrl, + "tokenUrl": tokenUrl, + "refreshUrl": refreshUrl, + "scopes": scopes, + }, + ) ) super().__init__( flows=flows, diff --git a/requirements-tests.txt b/requirements-tests.txt index 52a44cec5..5105071be 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,7 +1,7 @@ -e . pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 -mypy ==0.982 +mypy ==1.3.0 ruff ==0.0.138 black == 23.1.0 isort >=5.0.6,<6.0.0 From ba882c10feb34f8056d6d6819261d94d8820b3a5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 22:16:38 +0000 Subject: [PATCH 1031/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c01399327..91e1c7aba 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). * 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). * ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). From 7167c77a18627c69fae2063cb987048ffc0a5633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:37:34 +0200 Subject: [PATCH 1032/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20and=20?= =?UTF-8?q?fully=20migrate=20to=20Ruff,=20remove=20isort,=20includes=20a?= =?UTF-8?q?=20couple=20of=20tweaks=20suggested=20by=20the=20new=20version?= =?UTF-8?q?=20of=20Ruff=20(#9660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 13 +------------ fastapi/openapi/utils.py | 8 +++----- fastapi/routing.py | 13 +++++++++---- pyproject.toml | 6 +----- requirements-tests.txt | 3 +-- requirements.txt | 1 - scripts/format.sh | 1 - scripts/lint.sh | 1 - tests/test_empty_router.py | 3 ++- 9 files changed, 17 insertions(+), 32 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 25e797d24..7050aa31c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,22 +21,11 @@ repos: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.254 + rev: v0.0.272 hooks: - id: ruff args: - --fix -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) - - id: isort - name: isort (cython) - types: [cython] - - id: isort - name: isort (pyi) - types: [pyi] - repo: https://github.com/psf/black rev: 23.1.0 hooks: diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 86e15b46d..6d736647b 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -181,7 +181,7 @@ def get_openapi_operation_metadata( file_name = getattr(route.endpoint, "__globals__", {}).get("__file__") if file_name: message += f" at {file_name}" - warnings.warn(message) + warnings.warn(message, stacklevel=1) operation_ids.add(operation_id) operation["operationId"] = operation_id if route.deprecated: @@ -332,10 +332,8 @@ def get_openapi_path( openapi_response["description"] = description http422 = str(HTTP_422_UNPROCESSABLE_ENTITY) if (all_route_params or route.body_field) and not any( - [ - status in operation["responses"] - for status in [http422, "4XX", "default"] - ] + status in operation["responses"] + for status in [http422, "4XX", "default"] ): operation["responses"][http422] = { "description": "Validation Error", diff --git a/fastapi/routing.py b/fastapi/routing.py index af628f32d..ec8af99b3 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -30,7 +30,11 @@ from fastapi.dependencies.utils import ( solve_dependencies, ) from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import ( + FastAPIError, + RequestValidationError, + WebSocketRequestValidationError, +) from fastapi.types import DecoratedCallable from fastapi.utils import ( create_cloned_field, @@ -48,14 +52,15 @@ from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response -from starlette.routing import BaseRoute, Match -from starlette.routing import Mount as Mount # noqa from starlette.routing import ( + BaseRoute, + Match, compile_path, get_name, request_response, websocket_session, ) +from starlette.routing import Mount as Mount # noqa from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket @@ -763,7 +768,7 @@ class APIRouter(routing.Router): path = getattr(r, "path") # noqa: B009 name = getattr(r, "name", "unknown") if path is not None and not path: - raise Exception( + raise FastAPIError( f"Prefix and path cannot be both empty (path operation: {name})" ) if responses is None: diff --git a/pyproject.toml b/pyproject.toml index 69c42b254..547137144 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,10 +66,6 @@ all = [ [tool.hatch.version] path = "fastapi/__init__.py" -[tool.isort] -profile = "black" -known_third_party = ["fastapi", "pydantic", "starlette"] - [tool.mypy] strict = true @@ -125,7 +121,7 @@ select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes - # "I", # isort + "I", # isort "C", # flake8-comprehensions "B", # flake8-bugbear ] diff --git a/requirements-tests.txt b/requirements-tests.txt index 5105071be..a98280677 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,9 +2,8 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.3.0 -ruff ==0.0.138 +ruff ==0.0.272 black == 23.1.0 -isort >=5.0.6,<6.0.0 httpx >=0.23.0,<0.24.0 email_validator >=1.1.1,<2.0.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy diff --git a/requirements.txt b/requirements.txt index 9d51e1cb3..cb9abb44a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ -e .[all] -r requirements-tests.txt -r requirements-docs.txt -ruff ==0.0.138 uvicorn[standard] >=0.12.0,<0.21.0 pre-commit >=2.17.0,<3.0.0 diff --git a/scripts/format.sh b/scripts/format.sh index 3ac1fead8..3fb3eb4f1 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -3,4 +3,3 @@ set -x ruff fastapi tests docs_src scripts --fix black fastapi tests docs_src scripts -isort fastapi tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 0feb973a8..4db5caa96 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -6,4 +6,3 @@ set -x mypy fastapi ruff fastapi tests docs_src scripts black fastapi tests --check -isort fastapi tests docs_src scripts --check-only diff --git a/tests/test_empty_router.py b/tests/test_empty_router.py index 186ceb347..1a40cbe30 100644 --- a/tests/test_empty_router.py +++ b/tests/test_empty_router.py @@ -1,5 +1,6 @@ import pytest from fastapi import APIRouter, FastAPI +from fastapi.exceptions import FastAPIError from fastapi.testclient import TestClient app = FastAPI() @@ -31,5 +32,5 @@ def test_use_empty(): def test_include_empty(): # if both include and router.path are empty - it should raise exception - with pytest.raises(Exception): + with pytest.raises(FastAPIError): app.include_router(router) From 32897962860ad4c5045748c7955562922f659d08 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 22:38:17 +0000 Subject: [PATCH 1033/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 91e1c7aba..14b1d5588 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). * 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). * ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). From 34fca99b284665c60d49ebac925ddeecf58eaca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:46:44 +0200 Subject: [PATCH 1034/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Black?= =?UTF-8?q?=20(#9661)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- requirements-tests.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7050aa31c..2a8a03136 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: args: - --fix - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black ci: diff --git a/requirements-tests.txt b/requirements-tests.txt index a98280677..3ef3c4fd9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -3,7 +3,7 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.3.0 ruff ==0.0.272 -black == 23.1.0 +black == 23.3.0 httpx >=0.23.0,<0.24.0 email_validator >=1.1.1,<2.0.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy From e958d30d1ddd82d5deadd613f3b9887865427522 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 11 Jun 2023 22:47:16 +0000 Subject: [PATCH 1035/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 14b1d5588..6a09d5416 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). * 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). From 395ece75aad0ee46eb39b9786bb52ceb89627837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:49:35 +0200 Subject: [PATCH 1036/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6a09d5416..63d7d3e5e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,15 +2,27 @@ ## Latest Changes -* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo). -* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo). -* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). -* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). -* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). -* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). + +### Features + * ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). * ✨ Add exception handler for `WebSocketRequestValidationError` (which also allows to override it). PR [#6030](https://github.com/tiangolo/fastapi/pull/6030) by [@kristjanvalur](https://github.com/kristjanvalur). +### Refactors + +* ⬆️ Upgrade and fully migrate to Ruff, remove isort, includes a couple of tweaks suggested by the new version of Ruff. PR [#9660](https://github.com/tiangolo/fastapi/pull/9660) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Update internal type annotations and upgrade mypy. PR [#9658](https://github.com/tiangolo/fastapi/pull/9658) by [@tiangolo](https://github.com/tiangolo). +* ♻️ Simplify `AsyncExitStackMiddleware` as without Python 3.6 `AsyncExitStack` is always available. PR [#9657](https://github.com/tiangolo/fastapi/pull/9657) by [@tiangolo](https://github.com/tiangolo). + +### Upgrades + +* ⬆️ Upgrade Black. PR [#9661](https://github.com/tiangolo/fastapi/pull/9661) by [@tiangolo](https://github.com/tiangolo). + +### Internal + +* 💚 Update CI cache to fix installs when dependencies change. PR [#9659](https://github.com/tiangolo/fastapi/pull/9659) by [@tiangolo](https://github.com/tiangolo). +* ⬇️ Separate requirements for development into their own requirements.txt files, they shouldn't be extras. PR [#9655](https://github.com/tiangolo/fastapi/pull/9655) by [@tiangolo](https://github.com/tiangolo). + ## 0.96.1 ### Fixes From 32935103b12b1548117abef0b4af9dd883898308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 12 Jun 2023 00:50:06 +0200 Subject: [PATCH 1037/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?97.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 63d7d3e5e..917090784 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3,6 +3,8 @@ ## Latest Changes +## 0.97.0 + ### Features * ✨ Add support for `dependencies` in WebSocket routes. PR [#4534](https://github.com/tiangolo/fastapi/pull/4534) by [@paulo-raca](https://github.com/paulo-raca). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2bc795b4b..46a056363 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.96.1" +__version__ = "0.97.0" from starlette import status as status From 8767634932293d94209f4be575e2dfe4b2761c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 16 Jun 2023 16:49:01 +0200 Subject: [PATCH 1038/1881] =?UTF-8?q?=F0=9F=91=B7=20Lint=20in=20CI=20only?= =?UTF-8?q?=20once,=20only=20with=20one=20version=20of=20Python,=20run=20t?= =?UTF-8?q?ests=20with=20all=20of=20them=20(#9686)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 40 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b17d2e9d5..84f101424 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,16 +5,39 @@ on: branches: - master pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + # Issue ref: https://github.com/actions/setup-python/issues/436 + # cache: "pip" + # cache-dependency-path: pyproject.toml + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + - name: Install Dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-tests.txt + - name: Lint + run: bash scripts/lint.sh + test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] fail-fast: false - steps: - uses: actions/checkout@v3 - name: Set up Python @@ -32,8 +55,6 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt - - name: Lint - run: bash scripts/lint.sh - run: mkdir coverage - name: Test run: bash scripts/test.sh @@ -45,33 +66,28 @@ jobs: with: name: coverage path: coverage + coverage-combine: needs: [test] runs-on: ubuntu-latest - steps: - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 with: python-version: '3.8' # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" # cache-dependency-path: pyproject.toml - - name: Get coverage files uses: actions/download-artifact@v3 with: name: coverage path: coverage - - run: pip install coverage[toml] - - run: ls -la coverage - run: coverage combine coverage - run: coverage report - run: coverage html --show-contexts --title "Coverage for ${{ github.sha }}" - - name: Store coverage HTML uses: actions/upload-artifact@v3 with: @@ -80,14 +96,10 @@ jobs: # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection - if: always() - needs: - coverage-combine - runs-on: ubuntu-latest - steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 From 49bc3e0873c8e89edbffdd3a41b1c2c56f15c823 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 16 Jun 2023 14:49:35 +0000 Subject: [PATCH 1039/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 917090784..15e951035 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). ## 0.97.0 From 87d58703146ff37dca04487fb960901786148602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 19 Jun 2023 14:33:32 +0200 Subject: [PATCH 1040/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Flint=20(#9699)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Set up sponsor Flint * 🔧 Add configs for Flint sponsor --- docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/flint.png | Bin 0 -> 10409 bytes 3 files changed, 4 insertions(+) create mode 100644 docs/en/docs/img/sponsors/flint.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 9913c5df5..1b5240b5e 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -31,3 +31,6 @@ bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png + - url: https://www.flint.sh + title: IT expertise, consulting and development by passionate people + img: https://fastapi.tiangolo.com/img/sponsors/flint.png diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 014744a10..b3cb06327 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -16,3 +16,4 @@ logins: - armand-sauzay - databento-bot - nanram22 + - Flint-company diff --git a/docs/en/docs/img/sponsors/flint.png b/docs/en/docs/img/sponsors/flint.png new file mode 100644 index 0000000000000000000000000000000000000000..761cc334c241f3c52a574c04a880640065376a3b GIT binary patch literal 10409 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 Kh?R@z`~5e=$(Wh| literal 0 HcmV?d00001 From b7ce10079eb23873d5c54e264cd3618ac890d7b3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 19 Jun 2023 12:34:13 +0000 Subject: [PATCH 1041/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 15e951035..cb75ddc98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). * 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). ## 0.97.0 From e94c13ce74990eb682aead3fd976df45beee93d6 Mon Sep 17 00:00:00 2001 From: cyberlis Date: Thu, 22 Jun 2023 13:37:50 +0300 Subject: [PATCH 1042/1881] =?UTF-8?q?=E2=9C=A8=20Add=20allow=20disabling?= =?UTF-8?q?=20`redirect=5Fslashes`=20at=20the=20FastAPI=20app=20level=20(#?= =?UTF-8?q?3432)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Denis Lisovik Co-authored-by: Sebastián Ramírez --- fastapi/applications.py | 2 ++ tests/test_router_redirect_slashes.py | 40 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/test_router_redirect_slashes.py diff --git a/fastapi/applications.py b/fastapi/applications.py index 298aca921..9b161c5ec 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -62,6 +62,7 @@ class FastAPI(Starlette): servers: Optional[List[Dict[str, Union[str, Any]]]] = None, dependencies: Optional[Sequence[Depends]] = None, default_response_class: Type[Response] = Default(JSONResponse), + redirect_slashes: bool = True, docs_url: Optional[str] = "/docs", redoc_url: Optional[str] = "/redoc", swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect", @@ -127,6 +128,7 @@ class FastAPI(Starlette): self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} self.router: routing.APIRouter = routing.APIRouter( routes=routes, + redirect_slashes=redirect_slashes, dependency_overrides_provider=self, on_startup=on_startup, on_shutdown=on_shutdown, diff --git a/tests/test_router_redirect_slashes.py b/tests/test_router_redirect_slashes.py new file mode 100644 index 000000000..086665c04 --- /dev/null +++ b/tests/test_router_redirect_slashes.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient + + +def test_redirect_slashes_enabled(): + app = FastAPI() + router = APIRouter() + + @router.get("/hello/") + def hello_page() -> str: + return "Hello, World!" + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/hello/", follow_redirects=False) + assert response.status_code == 200 + + response = client.get("/hello", follow_redirects=False) + assert response.status_code == 307 + + +def test_redirect_slashes_disabled(): + app = FastAPI(redirect_slashes=False) + router = APIRouter() + + @router.get("/hello/") + def hello_page() -> str: + return "Hello, World!" + + app.include_router(router) + + client = TestClient(app) + + response = client.get("/hello/", follow_redirects=False) + assert response.status_code == 200 + + response = client.get("/hello", follow_redirects=False) + assert response.status_code == 404 From dd1c2018dc885e2e4e3cfd6fc56bd9c9f4d98a07 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 10:38:27 +0000 Subject: [PATCH 1043/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cb75ddc98..a7c487d2d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). * 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). * 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). From 2cef119cd75dc60f644fe537885747bd82a6f745 Mon Sep 17 00:00:00 2001 From: Harsha Laxman Date: Thu, 22 Jun 2023 04:20:12 -0700 Subject: [PATCH 1044/1881] =?UTF-8?q?=F0=9F=93=9D=20Use=20in=20memory=20da?= =?UTF-8?q?tabase=20for=20testing=20SQL=20in=20docs=20(#1223)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Harsha Laxman Co-authored-by: Marcelo Trylesinski Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/en/docs/advanced/testing-database.md | 2 +- docs_src/sql_databases/sql_app/tests/test_sql_app.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/testing-database.md b/docs/en/docs/advanced/testing-database.md index 16484b09a..13a6959b6 100644 --- a/docs/en/docs/advanced/testing-database.md +++ b/docs/en/docs/advanced/testing-database.md @@ -44,7 +44,7 @@ So the new file structure looks like: First, we create a new database session with the new database. -For the tests we'll use a file `test.db` instead of `sql_app.db`. +We'll use an in-memory database that persists during the tests instead of the local file `sql_app.db`. But the rest of the session code is more or less the same, we just copy it. diff --git a/docs_src/sql_databases/sql_app/tests/test_sql_app.py b/docs_src/sql_databases/sql_app/tests/test_sql_app.py index c60c3356f..5f55add0a 100644 --- a/docs_src/sql_databases/sql_app/tests/test_sql_app.py +++ b/docs_src/sql_databases/sql_app/tests/test_sql_app.py @@ -1,14 +1,17 @@ from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool from ..database import Base from ..main import app, get_db -SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db" +SQLALCHEMY_DATABASE_URL = "sqlite://" engine = create_engine( - SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, ) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) From 2f048f7199b6a3ec0b0ef8694ffac563563a1d13 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:20:49 +0000 Subject: [PATCH 1045/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a7c487d2d..1e96ff52c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). * ✨ Add allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). * 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). * 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). From b4b39d335940487649b03bf929c016b6f84b1128 Mon Sep 17 00:00:00 2001 From: Ryan Russell Date: Thu, 22 Jun 2023 07:26:11 -0400 Subject: [PATCH 1046/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20data=20for=20tests=20(#4958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- tests/test_param_include_in_schema.py | 4 ++-- tests/test_schema_extra_examples.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index cb182a1cd..d0c29f7b2 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -33,7 +33,7 @@ async def hidden_query( return {"hidden_query": hidden_query} -openapi_shema = { +openapi_schema = { "openapi": "3.0.2", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -162,7 +162,7 @@ def test_openapi_schema(): client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200 - assert response.json() == openapi_shema + assert response.json() == openapi_schema @pytest.mark.parametrize( diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 74e15d59a..41021a983 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -42,7 +42,7 @@ def examples( @app.post("/example_examples/") def example_examples( item: Item = Body( - example={"data": "Overriden example"}, + example={"data": "Overridden example"}, examples={ "example1": {"value": {"data": "examples example_examples 1"}}, "example2": {"value": {"data": "examples example_examples 2"}}, @@ -76,7 +76,7 @@ def example_examples( # def form_example_examples( # lastname: str = Form( # ..., -# example="Doe overriden", +# example="Doe overridden", # examples={ # "example1": {"summary": "last name summary", "value": "Doe"}, # "example2": {"value": "Doesn't"}, @@ -110,7 +110,7 @@ def path_examples( @app.get("/path_example_examples/{item_id}") def path_example_examples( item_id: str = Path( - example="item_overriden", + example="item_overridden", examples={ "example1": {"summary": "item ID summary", "value": "item_1"}, "example2": {"value": "item_2"}, @@ -147,7 +147,7 @@ def query_examples( def query_example_examples( data: Union[str, None] = Query( default=None, - example="query_overriden", + example="query_overridden", examples={ "example1": {"summary": "Query example 1", "value": "query1"}, "example2": {"value": "query2"}, @@ -184,7 +184,7 @@ def header_examples( def header_example_examples( data: Union[str, None] = Header( default=None, - example="header_overriden", + example="header_overridden", examples={ "example1": {"summary": "Query example 1", "value": "header1"}, "example2": {"value": "header2"}, @@ -221,7 +221,7 @@ def cookie_examples( def cookie_example_examples( data: Union[str, None] = Cookie( default=None, - example="cookie_overriden", + example="cookie_overridden", examples={ "example1": {"summary": "Query example 1", "value": "cookie1"}, "example2": {"value": "cookie2"}, From 05c5ce3689af541ce586df72a1cd3bdde99bccc8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:26:45 +0000 Subject: [PATCH 1047/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1e96ff52c..d1bc66f57 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). * 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). * ✨ Add allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). * 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). From 428376d285150b1ea602a49ef1ed639c00d17df2 Mon Sep 17 00:00:00 2001 From: Jacob Coffee Date: Thu, 22 Jun 2023 06:32:09 -0500 Subject: [PATCH 1048/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20repo=20link=20to?= =?UTF-8?q?=20PyPI=20(#9559)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 547137144..2f68a7efa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ dynamic = ["version"] [project.urls] Homepage = "https://github.com/tiangolo/fastapi" Documentation = "https://fastapi.tiangolo.com/" +Repository = "https://github.com/tiangolo/fastapi" [project.optional-dependencies] all = [ From 3279f0ba63ab0a99d6d5bebd972dcbfd82d2ae26 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:32:46 +0000 Subject: [PATCH 1049/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d1bc66f57..0c1a13264 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). * ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). * 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). * ✨ Add allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). From 74de9a7b1575d0570f4fe01ea8d1227abbfe9121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 22 Jun 2023 13:35:12 +0200 Subject: [PATCH 1050/1881] =?UTF-8?q?=F0=9F=94=A7=20Set=20minimal=20hatchl?= =?UTF-8?q?ing=20version=20needed=20to=20build=20the=20package=20(#9240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set minimal hatchling version needed to build the package Set the minimal hatchling version that is needed to build fastapi to 1.13.0. Older versions fail to build because they do not recognize the trove classifiers used, e.g. 1.12.2 yields: ValueError: Unknown classifier in field `project.classifiers`: Framework :: Pydantic 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 2f68a7efa..5c0d3c48e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["hatchling"] +requires = ["hatchling >= 1.13.0"] build-backend = "hatchling.build" [project] From d47eea9bb61f62685a5329c7921e8b830e223f54 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:35:49 +0000 Subject: [PATCH 1051/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0c1a13264..206c4f525 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). * 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). * ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). * 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). From 7c66ec8a8b47d16fefe9544c8d32b2de1ce7e314 Mon Sep 17 00:00:00 2001 From: Ricardo Castro Date: Thu, 22 Jun 2023 11:42:48 +0000 Subject: [PATCH 1052/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20`An?= =?UTF-8?q?notation`=20->=20`Annotated`=20in=20`docs/en/docs/tutorial/quer?= =?UTF-8?q?y-params-str-validations.md`=20(#9625)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index c4b221cb1..549e6c75b 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -44,7 +44,7 @@ To achieve that, first import: === "Python 3.6+" - In versions of Python below Python 3.9 you import `Annotation` from `typing_extensions`. + In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`. It will already be installed with FastAPI. From a2aede32b477a603ba11f5de497761938bec38f1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:43:21 +0000 Subject: [PATCH 1053/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 206c4f525..f582e3754 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). * 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). * 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). * ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). From 57727fa4e07c3ff6b57a1029838f14cf7ef51a04 Mon Sep 17 00:00:00 2001 From: Alexandr Date: Thu, 22 Jun 2023 14:46:36 +0300 Subject: [PATCH 1054/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/body-nested-models.md`=20(?= =?UTF-8?q?#9605)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/body-nested-models.md | 382 ++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 383 insertions(+) create mode 100644 docs/ru/docs/tutorial/body-nested-models.md diff --git a/docs/ru/docs/tutorial/body-nested-models.md b/docs/ru/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..6435e316f --- /dev/null +++ b/docs/ru/docs/tutorial/body-nested-models.md @@ -0,0 +1,382 @@ +# Body - Вложенные модели + +С помощью **FastAPI**, вы можете определять, валидировать, документировать и использовать модели произвольной вложенности (благодаря библиотеке Pydantic). + +## Определение полей содержащих списки + +Вы можете определять атрибут как подтип. Например, тип `list` в Python: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +Это приведёт к тому, что обьект `tags` преобразуется в список, несмотря на то что тип его элементов не объявлен. + +## Определение полей содержащих список с определением типов его элементов + +Однако в Python есть способ объявления списков с указанием типов для вложенных элементов: + +### Импортируйте `List` из модуля typing + +В Python 3.9 и выше вы можете использовать стандартный тип `list` для объявления аннотаций типов, как мы увидим ниже. 💡 + +Но в версиях Python до 3.9 (начиная с 3.6) сначала вам необходимо импортировать `List` из стандартного модуля `typing` в Python: + +```Python hl_lines="1" +{!> ../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### Объявление `list` с указанием типов для вложенных элементов + +Объявление типов для элементов (внутренних типов) вложенных в такие типы как `list`, `dict`, `tuple`: + +* Если у вас Python версии ниже чем 3.9, импортируйте их аналог из модуля `typing` +* Передайте внутренний(ие) тип(ы) как "параметры типа", используя квадратные скобки: `[` и `]` + +В Python версии 3.9 это будет выглядеть так: + +```Python +my_list: list[str] +``` + +В версиях Python до 3.9 это будет выглядеть так: + +```Python +from typing import List + +my_list: List[str] +``` + +Это всё стандартный синтаксис Python для объявления типов. + +Используйте этот же стандартный синтаксис для атрибутов модели с внутренними типами. + +Таким образом, в нашем примере мы можем явно указать тип данных для поля `tags` как "список строк": + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +## Типы множеств + +Но затем мы подумали и поняли, что теги не должны повторяться и, вероятно, они должны быть уникальными строками. + +И в Python есть специальный тип данных для множеств уникальных элементов - `set`. + +Тогда мы может обьявить поле `tags` как множество строк: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +С помощью этого, даже если вы получите запрос с повторяющимися данными, они будут преобразованы в множество уникальных элементов. + +И когда вы выводите эти данные, даже если исходный набор содержал дубликаты, они будут выведены в виде множества уникальных элементов. + +И они также будут соответствующим образом аннотированы / задокументированы. + +## Вложенные Модели + +У каждого атрибута Pydantic-модели есть тип. + +Но этот тип может сам быть другой моделью Pydantic. + +Таким образом вы можете объявлять глубоко вложенные JSON "объекты" с определёнными именами атрибутов, типами и валидацией. + +Всё это может быть произвольно вложенным. + +### Определение подмодели + +Например, мы можем определить модель `Image`: + +=== "Python 3.10+" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +### Использование вложенной модели в качестве типа + +Также мы можем использовать эту модель как тип атрибута: + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +Это означает, что **FastAPI** будет ожидать тело запроса, аналогичное этому: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Ещё раз: сделав такое объявление, с помощью **FastAPI** вы получите: + +* Поддержку редакторов IDE (автодополнение и т.д), даже для вложенных моделей +* Преобразование данных +* Валидацию данных +* Автоматическую документацию + +## Особые типы и валидация + +Помимо обычных простых типов, таких как `str`, `int`, `float`, и т.д. Вы можете использовать более сложные базовые типы, которые наследуются от типа `str`. + +Чтобы увидеть все варианты, которые у вас есть, ознакомьтесь с документацией по необычным типам Pydantic. Вы увидите некоторые примеры в следующей главе. + +Например, так как в модели `Image` у нас есть поле `url`, то мы можем объявить его как тип `HttpUrl` из модуля Pydantic вместо типа `str`: + +=== "Python 3.10+" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +Строка будет проверена на соответствие допустимому URL-адресу и задокументирована в JSON схему / OpenAPI. + +## Атрибуты, содержащие списки подмоделей + +Вы также можете использовать модели Pydantic в качестве типов вложенных в `list`, `set` и т.д: + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +Такая реализация будет ожидать (конвертировать, валидировать, документировать и т.д) JSON-содержимое в следующем формате: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info "Информация" + Заметьте, что теперь у ключа `images` есть список объектов изображений. + +## Глубоко вложенные модели + +Вы можете определять модели с произвольным уровнем вложенности: + +=== "Python 3.10+" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +!!! info "Информация" + Заметьте, что у объекта `Offer` есть список объектов `Item`, которые, в свою очередь, могут содержать необязательный список объектов `Image` + +## Тела с чистыми списками элементов + +Если верхний уровень значения тела JSON-объекта представляет собой JSON `array` (в Python - `list`), вы можете объявить тип в параметре функции, так же, как в моделях Pydantic: + +```Python +images: List[Image] +``` + +в Python 3.9 и выше: + +```Python +images: list[Image] +``` + +например так: + +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +## Универсальная поддержка редактора + +И вы получаете поддержку редактора везде. + +Даже для элементов внутри списков: + + + +Вы не могли бы получить такую поддержку редактора, если бы работали напрямую с `dict`, а не с моделями Pydantic. + +Но вы также не должны беспокоиться об этом, входящие словари автоматически конвертируются, а ваш вывод также автоматически преобразуется в формат JSON. + +## Тела запросов с произвольными словарями (`dict` ) + +Вы также можете объявить тело запроса как `dict` с ключами определенного типа и значениями другого типа данных. + +Без необходимости знать заранее, какие значения являются допустимыми для имён полей/атрибутов (как это было бы в случае с моделями Pydantic). + +Это было бы полезно, если вы хотите получить ключи, которые вы еще не знаете. + +--- + +Другой полезный случай - когда вы хотите чтобы ключи были другого типа данных, например, `int`. + +Именно это мы сейчас и увидим здесь. + +В этом случае вы принимаете `dict`, пока у него есть ключи типа `int` со значениями типа `float`: + +=== "Python 3.9+" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +!!! tip "Совет" + Имейте в виду, что JSON поддерживает только ключи типа `str`. + + Но Pydantic обеспечивает автоматическое преобразование данных. + + Это значит, что даже если пользователи вашего API могут отправлять только строки в качестве ключей, при условии, что эти строки содержат целые числа, Pydantic автоматический преобразует и валидирует эти данные. + + А `dict`, с именем `weights`, который вы получите в качестве ответа Pydantic, действительно будет иметь ключи типа `int` и значения типа `float`. + +## Резюме + +С помощью **FastAPI** вы получаете максимальную гибкость, предоставляемую моделями Pydantic, сохраняя при этом простоту, краткость и элегантность вашего кода. + +И дополнительно вы получаете: + +* Поддержку редактора (автодополнение доступно везде!) +* Преобразование данных (также известно как парсинг / сериализация) +* Валидацию данных +* Документацию схемы данных +* Автоматическую генерацию документации diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 9fb56ce1b..ecd3aead1 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -83,6 +83,7 @@ nav: - tutorial/static-files.md - tutorial/debugging.md - tutorial/schema-extra-example.md + - tutorial/body-nested-models.md - async.md - Развёртывание: - deployment/index.md From 7505f24f2eebbc760e199c9763b5cc803ad25d2c Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 11:47:12 +0000 Subject: [PATCH 1055/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f582e3754..bc3f29534 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). * ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). * 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). * 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). From a92e9c957a0f4bedc61f0fc083dd3be7534989a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Broto=C5=84?= <50829834+mbroton@users.noreply.github.com> Date: Thu, 22 Jun 2023 16:29:05 +0200 Subject: [PATCH 1056/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Polish=20transla?= =?UTF-8?q?tion=20for=20`docs/pl/docs/features.md`=20(#5348)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/pl/docs/features.md | 200 +++++++++++++++++++++++++++++++++++++++ docs/pl/mkdocs.yml | 1 + 2 files changed, 201 insertions(+) create mode 100644 docs/pl/docs/features.md diff --git a/docs/pl/docs/features.md b/docs/pl/docs/features.md new file mode 100644 index 000000000..49d362dd9 --- /dev/null +++ b/docs/pl/docs/features.md @@ -0,0 +1,200 @@ +# Cechy + +## Cechy FastAPI + +**FastAPI** zapewnia Ci następujące korzyści: + +### Oparcie o standardy open + +* OpenAPI do tworzenia API, w tym deklaracji ścieżek operacji, parametrów, ciał zapytań, bezpieczeństwa, itp. +* Automatyczna dokumentacja modelu danych za pomocą JSON Schema (ponieważ OpenAPI bazuje na JSON Schema). +* Zaprojektowane z myślą o zgodności z powyższymi standardami zamiast dodawania ich obsługi po fakcie. +* Możliwość automatycznego **generowania kodu klienta** w wielu językach. + +### Automatyczna dokumentacja + +Interaktywna dokumentacja i webowe interfejsy do eksploracji API. Z racji tego, że framework bazuje na OpenAPI, istnieje wiele opcji, z czego 2 są domyślnie dołączone. + +* Swagger UI, z interaktywnym interfejsem - odpytuj i testuj swoje API bezpośrednio z przeglądarki. + +![Swagger UI interakcja](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Alternatywna dokumentacja API z ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Nowoczesny Python + +Wszystko opiera się na standardowych deklaracjach typu **Python 3.6** (dzięki Pydantic). Brak nowej składni do uczenia. Po prostu standardowy, współczesny Python. + +Jeśli potrzebujesz szybkiego przypomnienia jak używać deklaracji typów w Pythonie (nawet jeśli nie używasz FastAPI), sprawdź krótki samouczek: [Python Types](python-types.md){.internal-link target=_blank}. + +Wystarczy, że napiszesz standardowe deklaracje typów Pythona: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Zadeklaruj parametr jako str +# i uzyskaj wsparcie edytora wewnątrz funkcji +def main(user_id: str): + return user_id + + +# Model Pydantic +class User(BaseModel): + id: int + name: str + joined: date +``` + +A one będą mogły zostać później użyte w następujący sposób: + +```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` oznacza: + + Przekaż klucze i wartości słownika `second_user_data` bezpośrednio jako argumenty klucz-wartość, co jest równoznaczne z: `User(id=4, name="Mary", joined="2018-11-30")` + +### Wsparcie edytora + +Cały framework został zaprojektowany tak, aby był łatwy i intuicyjny w użyciu. Wszystkie pomysły zostały przetestowane na wielu edytorach jeszcze przed rozpoczęciem procesu tworzenia, aby zapewnić najlepsze wrażenia programistyczne. + +Ostatnia ankieta Python developer survey jasno wskazuje, że najczęściej używaną funkcjonalnością jest autouzupełnianie w edytorze. + +Cała struktura frameworku **FastAPI** jest na tym oparta. Autouzupełnianie działa wszędzie. + +Rzadko będziesz musiał wracać do dokumentacji. + +Oto, jak twój edytor może Ci pomóc: + +* Visual Studio Code: + +![wsparcie edytora](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* PyCharm: + +![wsparcie edytora](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Otrzymasz uzupełnienie nawet w miejscach, w których normalnie uzupełnienia nie ma. Na przykład klucz "price" w treści JSON (który mógł być zagnieżdżony), który pochodzi z zapytania. + +Koniec z wpisywaniem błędnych nazw kluczy, przechodzeniem tam i z powrotem w dokumentacji lub przewijaniem w górę i w dół, aby sprawdzić, czy w końcu użyłeś nazwy `username` czy `user_name`. + +### Zwięzłość + +Wszystko posiada sensowne **domyślne wartości**. Wszędzie znajdziesz opcjonalne konfiguracje. Wszystkie parametry możesz dostroić, aby zrobić to co potrzebujesz do zdefiniowania API. + +Ale domyślnie wszystko **"po prostu działa"**. + +### Walidacja + +* Walidacja większości (lub wszystkich?) **typów danych** Pythona, w tym: + * Obiektów JSON (`dict`). + * Tablic JSON (`list`) ze zdefiniowanym typem elementów. + * Pól tekstowych (`str`) z określeniem minimalnej i maksymalnej długości. + * Liczb (`int`, `float`) z wartościami minimalnymi, maksymalnymi, itp. + +* Walidacja bardziej egzotycznych typów danych, takich jak: + * URL. + * Email. + * UUID. + * ...i inne. + +Cała walidacja jest obsługiwana przez ugruntowaną i solidną bibliotekę **Pydantic**. + +### Bezpieczeństwo i uwierzytelnianie + +Bezpieczeństwo i uwierzytelnianie jest zintegrowane. Bez żadnych kompromisów z bazami czy modelami danych. + +Wszystkie schematy bezpieczeństwa zdefiniowane w OpenAPI, w tym: + +* Podstawowy protokół HTTP. +* **OAuth2** (również z **tokenami JWT**). Sprawdź samouczek [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* Klucze API w: + * Nagłówkach. + * Parametrach zapytań. + * Ciasteczkach, itp. + +Plus wszystkie funkcje bezpieczeństwa Starlette (włączając w to **ciasteczka sesyjne**). + +Wszystko zbudowane jako narzędzia i komponenty wielokrotnego użytku, które można łatwo zintegrować z systemami, magazynami oraz bazami danych - relacyjnymi, NoSQL, itp. + +### Wstrzykiwanie Zależności + +FastAPI zawiera niezwykle łatwy w użyciu, ale niezwykle potężny system Wstrzykiwania Zależności. + +* Nawet zależności mogą mieć zależności, tworząc hierarchię lub **"graf" zależności**. +* Wszystko jest **obsługiwane automatycznie** przez framework. +* Wszystkie zależności mogą wymagać danych w żądaniach oraz rozszerzać ograniczenia i automatyczną dokumentację **operacji na ścieżce**. +* **Automatyczna walidacja** parametrów *operacji na ścieżce* zdefiniowanych w zależnościach. +* Obsługa złożonych systemów uwierzytelniania użytkowników, **połączeń z bazami danych**, itp. +* Bazy danych, front end, itp. **bez kompromisów**, ale wciąż łatwe do integracji. + +### Nieograniczone "wtyczki" + +Lub ujmując to inaczej - brak potrzeby wtyczek. Importuj i używaj kod, który potrzebujesz. + +Każda integracja została zaprojektowana tak, aby była tak prosta w użyciu (z zależnościami), że możesz utworzyć "wtyczkę" dla swojej aplikacji w 2 liniach kodu, używając tej samej struktury i składni, które są używane w *operacjach na ścieżce*. + +### Testy + +* 100% pokrycia kodu testami. +* 100% adnotacji typów. +* Używany w aplikacjach produkcyjnych. + +## Cechy Starlette + +**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) Starlette. Tak więc każdy dodatkowy kod Starlette, który posiadasz, również będzie działał. + +`FastAPI` jest w rzeczywistości podklasą `Starlette`, więc jeśli już znasz lub używasz Starlette, większość funkcji będzie działać w ten sam sposób. + +Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Starlette** (ponieważ FastAPI to po prostu Starlette na sterydach): + +* Bardzo imponująca wydajność. Jest to jeden z najszybszych dostępnych frameworków Pythona, na równi z **NodeJS** i **Go**. +* Wsparcie dla **WebSocket**. +* Zadania w tle. +* Eventy startup i shutdown. +* Klient testowy zbudowany na bazie biblioteki `requests`. +* **CORS**, GZip, pliki statyczne, streamy. +* Obsługa **sesji i ciasteczek**. +* 100% pokrycie testami. +* 100% adnotacji typów. + +## Cechy Pydantic + +**FastAPI** jest w pełni kompatybilny z (oraz bazuje na) Pydantic. Tak więc każdy dodatkowy kod Pydantic, który posiadasz, również będzie działał. + +Wliczając w to zewnętrzne biblioteki, również oparte o Pydantic, takie jak ORM, ODM dla baz danych. + +Oznacza to, że w wielu przypadkach możesz przekazać ten sam obiekt, który otrzymasz z żądania **bezpośrednio do bazy danych**, ponieważ wszystko jest walidowane automatycznie. + +Działa to również w drugą stronę, w wielu przypadkach możesz po prostu przekazać obiekt otrzymany z bazy danych **bezpośrednio do klienta**. + +Dzięki **FastAPI** otrzymujesz wszystkie funkcje **Pydantic** (ponieważ FastAPI bazuje na Pydantic do obsługi wszystkich danych): + +* **Bez prania mózgu**: + * Brak nowego mikrojęzyka do definiowania schematu, którego trzeba się nauczyć. + * Jeśli znasz adnotacje typów Pythona to wiesz jak używać Pydantic. +* Dobrze współpracuje z Twoim **IDE/linterem/mózgiem**: + * Ponieważ struktury danych Pydantic to po prostu instancje klas, które definiujesz; autouzupełnianie, linting, mypy i twoja intuicja powinny działać poprawnie z Twoimi zwalidowanymi danymi. +* **Szybkość**: + * w benchmarkach Pydantic jest szybszy niż wszystkie inne testowane biblioteki. +* Walidacja **złożonych struktur**: + * Wykorzystanie hierarchicznych modeli Pydantic, Pythonowego modułu `typing` zawierającego `List`, `Dict`, itp. + * Walidatory umożliwiają jasne i łatwe definiowanie, sprawdzanie złożonych struktur danych oraz dokumentowanie ich jako JSON Schema. + * Możesz mieć głęboko **zagnieżdżone obiekty JSON** i wszystkie je poddać walidacji i adnotować. +* **Rozszerzalność**: + * Pydantic umożliwia zdefiniowanie niestandardowych typów danych lub rozszerzenie walidacji o metody na modelu, na których użyty jest dekorator walidatora. +* 100% pokrycie testami. diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 0d7a783fc..5ca1bbfef 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -63,6 +63,7 @@ nav: - tr: /tr/ - uk: /uk/ - zh: /zh/ +- features.md - Samouczek: - tutorial/index.md - tutorial/first-steps.md From 09319d62712865ecdc7e8b7aa8a7afea8660d2ec Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 14:29:41 +0000 Subject: [PATCH 1057/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bc3f29534..6287b8c98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). * ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). * 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). From a2a0119c14e18df2d92fbefdb19dff3c8f40b076 Mon Sep 17 00:00:00 2001 From: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Date: Thu, 22 Jun 2023 20:29:56 +0600 Subject: [PATCH 1058/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/cors.md`=20(#9608)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexandr Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/cors.md | 84 +++++++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 85 insertions(+) create mode 100644 docs/ru/docs/tutorial/cors.md diff --git a/docs/ru/docs/tutorial/cors.md b/docs/ru/docs/tutorial/cors.md new file mode 100644 index 000000000..8c7fbc046 --- /dev/null +++ b/docs/ru/docs/tutorial/cors.md @@ -0,0 +1,84 @@ +# CORS (Cross-Origin Resource Sharing) + +Понятие CORS или "Cross-Origin Resource Sharing" относится к ситуациям, при которых запущенный в браузере фронтенд содержит JavaScript-код, который взаимодействует с бэкендом, находящимся на другом "источнике" ("origin"). + +## Источник + +Источник - это совокупность протокола (`http`, `https`), домена (`myapp.com`, `localhost`, `localhost.tiangolo.com`) и порта (`80`, `443`, `8080`). + +Поэтому это три разных источника: + +* `http://localhost` +* `https://localhost` +* `http://localhost:8080` + +Даже если они все расположены в `localhost`, они используют разные протоколы и порты, а значит, являются разными источниками. + +## Шаги + +Допустим, у вас есть фронтенд, запущенный в браузере по адресу `http://localhost:8080`, и его JavaScript-код пытается взаимодействовать с бэкендом, запущенным по адресу `http://localhost` (поскольку мы не указали порт, браузер по умолчанию будет использовать порт `80`). + +Затем браузер отправит бэкенду HTTP-запрос `OPTIONS`, и если бэкенд вернёт соответствующие заголовки для авторизации взаимодействия с другим источником (`http://localhost:8080`), то браузер разрешит JavaScript-коду на фронтенде отправить запрос на этот бэкенд. + +Чтобы это работало, у бэкенда должен быть список "разрешённых источников" ("allowed origins"). + +В таком случае этот список должен содержать `http://localhost:8080`, чтобы фронтенд работал корректно. + +## Подстановочный символ `"*"` + +В качестве списка источников можно указать подстановочный символ `"*"` ("wildcard"), чтобы разрешить любые источники. + +Но тогда не будут разрешены некоторые виды взаимодействия, включая всё связанное с учётными данными: куки, заголовки Authorization с Bearer-токенами наподобие тех, которые мы использовали ранее и т.п. + +Поэтому, чтобы всё работало корректно, лучше явно указывать список разрешённых источников. + +## Использование `CORSMiddleware` + +Вы можете настроить этот механизм в вашем **FastAPI** приложении, используя `CORSMiddleware`. + +* Импортируйте `CORSMiddleware`. +* Создайте список разрешённых источников (в виде строк). +* Добавьте его как "middleware" к вашему **FastAPI** приложению. + +Вы также можете указать, разрешает ли ваш бэкенд использование: + +* Учётных данных (включая заголовки Authorization, куки и т.п.). +* Отдельных HTTP-методов (`POST`, `PUT`) или всех вместе, используя `"*"`. +* Отдельных HTTP-заголовков или всех вместе, используя `"*"`. + +```Python hl_lines="2 6-11 13-19" +{!../../../docs_src/cors/tutorial001.py!} +``` + +`CORSMiddleware` использует для параметров "запрещающие" значения по умолчанию, поэтому вам нужно явным образом разрешить использование отдельных источников, методов или заголовков, чтобы браузеры могли использовать их в кросс-доменном контексте. + +Поддерживаются следующие аргументы: + +* `allow_origins` - Список источников, на которые разрешено выполнять кросс-доменные запросы. Например, `['https://example.org', 'https://www.example.org']`. Можно использовать `['*']`, чтобы разрешить любые источники. +* `allow_origin_regex` - Регулярное выражение для определения источников, на которые разрешено выполнять кросс-доменные запросы. Например, `'https://.*\.example\.org'`. +* `allow_methods` - Список HTTP-методов, которые разрешены для кросс-доменных запросов. По умолчанию равно `['GET']`. Можно использовать `['*']`, чтобы разрешить все стандартные методы. +* `allow_headers` - Список HTTP-заголовков, которые должны поддерживаться при кросс-доменных запросах. По умолчанию равно `[]`. Можно использовать `['*']`, чтобы разрешить все заголовки. Заголовки `Accept`, `Accept-Language`, `Content-Language` и `Content-Type` всегда разрешены для простых CORS-запросов. +* `allow_credentials` - указывает, что куки разрешены в кросс-доменных запросах. По умолчанию равно `False`. Также, `allow_origins` нельзя присвоить `['*']`, если разрешено использование учётных данных. В таком случае должен быть указан список источников. +* `expose_headers` - Указывает любые заголовки ответа, которые должны быть доступны браузеру. По умолчанию равно `[]`. +* `max_age` - Устанавливает максимальное время в секундах, в течение которого браузер кэширует CORS-ответы. По умолчанию равно `600`. + +`CORSMiddleware` отвечает на два типа HTTP-запросов... + +### CORS-запросы с предварительной проверкой + +Это любые `OPTIONS` запросы с заголовками `Origin` и `Access-Control-Request-Method`. + +В этом случае middleware перехватит входящий запрос и отправит соответствующие CORS-заголовки в ответе, а также ответ `200` или `400` в информационных целях. + +### Простые запросы + +Любые запросы с заголовком `Origin`. В этом случае middleware передаст запрос дальше как обычно, но добавит соответствующие CORS-заголовки к ответу. + +## Больше информации + +Для получения более подробной информации о CORS, обратитесь к Документации CORS от Mozilla. + +!!! note "Технические детали" + Вы также можете использовать `from starlette.middleware.cors import CORSMiddleware`. + + **FastAPI** предоставляет несколько middleware в `fastapi.middleware` только для вашего удобства как разработчика. Но большинство доступных middleware взяты напрямую из Starlette. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index ecd3aead1..7b8e351f8 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -80,6 +80,7 @@ nav: - tutorial/response-status-code.md - tutorial/query-params.md - tutorial/body-multiple-params.md + - tutorial/cors.md - tutorial/static-files.md - tutorial/debugging.md - tutorial/schema-extra-example.md From 223ed676821f729cc31018c377279258087acecc Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 14:30:35 +0000 Subject: [PATCH 1059/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6287b8c98..f52d3d3a2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). * ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). From 612cbee165d05dcdbcdc56ec03c68b76ce9c6861 Mon Sep 17 00:00:00 2001 From: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:14:16 +0600 Subject: [PATCH 1060/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/extra-models.md`=20(#9619)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexandr --- docs/ru/docs/tutorial/extra-models.md | 252 ++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 253 insertions(+) create mode 100644 docs/ru/docs/tutorial/extra-models.md diff --git a/docs/ru/docs/tutorial/extra-models.md b/docs/ru/docs/tutorial/extra-models.md new file mode 100644 index 000000000..a346f7432 --- /dev/null +++ b/docs/ru/docs/tutorial/extra-models.md @@ -0,0 +1,252 @@ +# Дополнительные модели + +В продолжение прошлого примера будет уже обычным делом иметь несколько связанных между собой моделей. + +Это особенно применимо в случае моделей пользователя, потому что: + +* **Модель для ввода** должна иметь возможность содержать пароль. +* **Модель для вывода** не должна содержать пароль. +* **Модель для базы данных**, возможно, должна содержать хэшированный пароль. + +!!! danger "Внимание" + Никогда не храните пароли пользователей в чистом виде. Всегда храните "безопасный хэш", который вы затем сможете проверить. + + Если вам это не знакомо, вы можете узнать про "хэш пароля" в [главах о безопасности](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}. + +## Множественные модели + +Ниже изложена основная идея того, как могут выглядеть эти модели с полями для паролей, а также описаны места, где они используются: + +=== "Python 3.10+" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` + +### Про `**user_in.dict()` + +#### `.dict()` из Pydantic + +`user_in` - это Pydantic-модель класса `UserIn`. + +У Pydantic-моделей есть метод `.dict()`, который возвращает `dict` с данными модели. + +Поэтому, если мы создадим Pydantic-объект `user_in` таким способом: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +и затем вызовем: + +```Python +user_dict = user_in.dict() +``` + +то теперь у нас есть `dict` с данными модели в переменной `user_dict` (это `dict` вместо объекта Pydantic-модели). + +И если мы вызовем: + +```Python +print(user_dict) +``` + +мы можем получить `dict` с такими данными: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### Распаковка `dict` + +Если мы возьмём `dict` наподобие `user_dict` и передадим его в функцию (или класс), используя `**user_dict`, Python распакует его. Он передаст ключи и значения `user_dict` напрямую как аргументы типа ключ-значение. + +Поэтому, продолжая описанный выше пример с `user_dict`, написание такого кода: + +```Python +UserInDB(**user_dict) +``` + +Будет работать так же, как примерно такой код: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +Или, если для большей точности мы напрямую используем `user_dict` с любым потенциальным содержимым, то этот пример будет выглядеть так: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### Pydantic-модель из содержимого другой модели + +Как в примере выше мы получили `user_dict` из `user_in.dict()`, этот код: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +будет равнозначен такому: + +```Python +UserInDB(**user_in.dict()) +``` + +...потому что `user_in.dict()` - это `dict`, и затем мы указываем, чтобы Python его "распаковал", когда передаём его в `UserInDB` и ставим перед ним `**`. + +Таким образом мы получаем Pydantic-модель на основе данных из другой Pydantic-модели. + +#### Распаковка `dict` и дополнительные именованные аргументы + +И затем, если мы добавим дополнительный именованный аргумент `hashed_password=hashed_password` как здесь: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +... то мы получим что-то подобное: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning "Предупреждение" + Цель использованных в примере вспомогательных функций - не более чем демонстрация возможных операций с данными, но, конечно, они не обеспечивают настоящую безопасность. + +## Сократите дублирование + +Сокращение дублирования кода - это одна из главных идей **FastAPI**. + +Поскольку дублирование кода повышает риск появления багов, проблем с безопасностью, проблем десинхронизации кода (когда вы обновляете код в одном месте, но не обновляете в другом), и т.д. + +А все описанные выше модели используют много общих данных и дублируют названия атрибутов и типов. + +Мы можем это улучшить. + +Мы можем определить модель `UserBase`, которая будет базовой для остальных моделей. И затем мы можем создать подклассы этой модели, которые будут наследовать её атрибуты (объявления типов, валидацию, и т.п.). + +Все операции конвертации, валидации, документации, и т.п. будут по-прежнему работать нормально. + +В этом случае мы можем определить только различия между моделями (с `password` в чистом виде, с `hashed_password` и без пароля): + +=== "Python 3.10+" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` + +## `Union` или `anyOf` + +Вы можете определить ответ как `Union` из двух типов. Это означает, что ответ должен соответствовать одному из них. + +Он будет определён в OpenAPI как `anyOf`. + +Для этого используйте стандартные аннотации типов в Python `typing.Union`: + +!!! note "Примечание" + При объявлении `Union`, сначала указывайте наиболее детальные типы, затем менее детальные. В примере ниже более детальный `PlaneItem` стоит перед `CarItem` в `Union[PlaneItem, CarItem]`. + +=== "Python 3.10+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` + +### `Union` в Python 3.10 + +В этом примере мы передаём `Union[PlaneItem, CarItem]` в качестве значения аргумента `response_model`. + +Поскольку мы передаём его как **значение аргумента** вместо того, чтобы поместить его в **аннотацию типа**, нам придётся использовать `Union` даже в Python 3.10. + +Если оно было бы указано в аннотации типа, то мы могли бы использовать вертикальную черту как в примере: + +```Python +some_variable: PlaneItem | CarItem +``` + +Но если мы помещаем его в `response_model=PlaneItem | CarItem` мы получим ошибку, потому что Python попытается произвести **некорректную операцию** между `PlaneItem` и `CarItem` вместо того, чтобы интерпретировать это как аннотацию типа. + +## Список моделей + +Таким же образом вы можете определять ответы как списки объектов. + +Для этого используйте `typing.List` из стандартной библиотеки Python (или просто `list` в Python 3.9 и выше): + +=== "Python 3.9+" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` + +## Ответ с произвольным `dict` + +Вы также можете определить ответ, используя произвольный одноуровневый `dict` и определяя только типы ключей и значений без использования Pydantic-моделей. + +Это полезно, если вы заранее не знаете корректных названий полей/атрибутов (которые будут нужны при использовании Pydantic-модели). + +В этом случае вы можете использовать `typing.Dict` (или просто `dict` в Python 3.9 и выше): + +=== "Python 3.9+" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` + +## Резюме + +Используйте несколько Pydantic-моделей и свободно применяйте наследование для каждой из них. + +Вам не обязательно иметь единственную модель данных для каждой сущности, если эта сущность должна иметь возможность быть в разных "состояниях". Как в случае с "сущностью" пользователя, у которого есть состояния с полями `password`, `password_hash` и без пароля. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 7b8e351f8..24ab15726 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -77,6 +77,7 @@ nav: - tutorial/extra-data-types.md - tutorial/cookie-params.md - tutorial/testing.md + - tutorial/extra-models.md - tutorial/response-status-code.md - tutorial/query-params.md - tutorial/body-multiple-params.md From 234cecb5bf51ad0cdd9c748f2958744f4ac03404 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:14:54 +0000 Subject: [PATCH 1061/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f52d3d3a2..4981b848d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). From e17cacfee4fc039ad6f4524ac141fa55556f994d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=AE=9A=E7=84=95?= <108172295+wdh99@users.noreply.github.com> Date: Fri, 23 Jun 2023 00:16:06 +0800 Subject: [PATCH 1062/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/testing.md`=20(#9641)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/zh/docs/tutorial/testing.md | 212 +++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 213 insertions(+) create mode 100644 docs/zh/docs/tutorial/testing.md diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md new file mode 100644 index 000000000..41f01f8d8 --- /dev/null +++ b/docs/zh/docs/tutorial/testing.md @@ -0,0 +1,212 @@ +# 测试 + +感谢 Starlette,测试**FastAPI** 应用轻松又愉快。 + +它基于 HTTPX, 而HTTPX又是基于Requests设计的,所以很相似且易懂。 + +有了它,你可以直接与**FastAPI**一起使用 pytest。 + +## 使用 `TestClient` + +!!! 信息 + 要使用 `TestClient`,先要安装 `httpx`. + + 例:`pip install httpx`. + +导入 `TestClient`. + +通过传入你的**FastAPI**应用创建一个 `TestClient` 。 + +创建名字以 `test_` 开头的函数(这是标准的 `pytest` 约定)。 + +像使用 `httpx` 那样使用 `TestClient` 对象。 + +为你需要检查的地方用标准的Python表达式写个简单的 `assert` 语句(重申,标准的`pytest`)。 + +```Python hl_lines="2 12 15-18" +{!../../../docs_src/app_testing/tutorial001.py!} +``` + +!!! 提示 + 注意测试函数是普通的 `def`,不是 `async def`。 + + 还有client的调用也是普通的调用,不是用 `await`。 + + 这让你可以直接使用 `pytest` 而不会遇到麻烦。 + +!!! note "技术细节" + 你也可以用 `from starlette.testclient import TestClient`。 + + **FastAPI** 提供了和 `starlette.testclient` 一样的 `fastapi.testclient`,只是为了方便开发者。但它直接来自Starlette。 + +!!! 提示 + 除了发送请求之外,如果你还想测试时在FastAPI应用中调用 `async` 函数(例如异步数据库函数), 可以在高级教程中看下 [Async Tests](../advanced/async-tests.md){.internal-link target=_blank} 。 + +## 分离测试 + +在实际应用中,你可能会把你的测试放在另一个文件里。 + +您的**FastAPI**应用程序也可能由一些文件/模块组成等等。 + +### **FastAPI** app 文件 + +假设你有一个像 [更大的应用](./bigger-applications.md){.internal-link target=_blank} 中所描述的文件结构: + +``` +. +├── app +│   ├── __init__.py +│   └── main.py +``` + +在 `main.py` 文件中你有一个 **FastAPI** app: + + +```Python +{!../../../docs_src/app_testing/main.py!} +``` + +### 测试文件 + +然后你会有一个包含测试的文件 `test_main.py` 。app可以像Python包那样存在(一样是目录,但有个 `__init__.py` 文件): + +``` hl_lines="5" +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +因为这文件在同一个包中,所以你可以通过相对导入从 `main` 模块(`main.py`)导入`app`对象: + +```Python hl_lines="3" +{!../../../docs_src/app_testing/test_main.py!} +``` + +...然后测试代码和之前一样的。 + +## 测试:扩展示例 + +现在让我们扩展这个例子,并添加更多细节,看下如何测试不同部分。 + +### 扩展后的 **FastAPI** app 文件 + +让我们继续之前的文件结构: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── test_main.py +``` + +假设现在包含**FastAPI** app的文件 `main.py` 有些其他**路径操作**。 + +有个 `GET` 操作会返回错误。 + +有个 `POST` 操作会返回一些错误。 + +所有*路径操作* 都需要一个`X-Token` 头。 + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py310/main.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/app_testing/app_b_an/main.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/app_testing/app_b_py310/main.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python + {!> ../../../docs_src/app_testing/app_b/main.py!} + ``` + +### 扩展后的测试文件 + +然后您可以使用扩展后的测试更新`test_main.py`: + +```Python +{!> ../../../docs_src/app_testing/app_b/test_main.py!} +``` + +每当你需要客户端在请求中传递信息,但你不知道如何传递时,你可以通过搜索(谷歌)如何用 `httpx`做,或者是用 `requests` 做,毕竟HTTPX的设计是基于Requests的设计的。 + +接着只需在测试中同样操作。 + +示例: + +* 传一个*路径* 或*查询* 参数,添加到URL上。 +* 传一个JSON体,传一个Python对象(例如一个`dict`)到参数 `json`。 +* 如果你需要发送 *Form Data* 而不是 JSON,使用 `data` 参数。 +* 要发送 *headers*,传 `dict` 给 `headers` 参数。 +* 对于 *cookies*,传 `dict` 给 `cookies` 参数。 + +关于如何传数据给后端的更多信息 (使用`httpx` 或 `TestClient`),请查阅 HTTPX 文档. + +!!! 信息 + 注意 `TestClient` 接收可以被转化为JSON的数据,而不是Pydantic模型。 + + 如果你在测试中有一个Pydantic模型,并且你想在测试时发送它的数据给应用,你可以使用在[JSON Compatible Encoder](encoder.md){.internal-link target=_blank}介绍的`jsonable_encoder` 。 + +## 运行起来 + +之后,你只需要安装 `pytest`: + +
+ +```console +$ pip install pytest + +---> 100% +``` + +
+ +他会自动检测文件和测试,执行测试,然后向你报告结果。 + +执行测试: + +
+ +```console +$ pytest + +================ test session starts ================ +platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 +rootdir: /home/user/code/superawesome-cli/app +plugins: forked-1.1.3, xdist-1.31.0, cov-2.8.1 +collected 6 items + +---> 100% + +test_main.py ...... [100%] + +================= 1 passed in 0.03s ================= +``` + +
diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 522c83766..d71c8bf00 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -109,6 +109,7 @@ nav: - tutorial/bigger-applications.md - tutorial/metadata.md - tutorial/static-files.md + - tutorial/testing.md - tutorial/debugging.md - 高级用户指南: - advanced/index.md From 1182b363625fe845303b6fe774250c0169066293 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:16:43 +0000 Subject: [PATCH 1063/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4981b848d..0714848dd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99). * 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). From 4a7b21483b8f146869b48a6ea490bdbb9a9f2be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E8=BF=87=E5=88=9D=E6=99=B4?= <129537877+ChoyeonChern@users.noreply.github.com> Date: Fri, 23 Jun 2023 00:17:12 +0800 Subject: [PATCH 1064/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ations=20for=20`docs/zh/docs/advanced/websockets.md`=20(#9651)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/websockets.md | 214 ++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 215 insertions(+) create mode 100644 docs/zh/docs/advanced/websockets.md diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md new file mode 100644 index 000000000..a723487fd --- /dev/null +++ b/docs/zh/docs/advanced/websockets.md @@ -0,0 +1,214 @@ +# WebSockets + +您可以在 **FastAPI** 中使用 [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)。 + +## 安装 `WebSockets` + +首先,您需要安装 `WebSockets`: + +```console +$ pip install websockets + +---> 100% +``` + +## WebSockets 客户端 + +### 在生产环境中 + +在您的生产系统中,您可能使用现代框架(如React、Vue.js或Angular)创建了一个前端。 + +要使用 WebSockets 与后端进行通信,您可能会使用前端的工具。 + +或者,您可能有一个原生移动应用程序,直接使用原生代码与 WebSocket 后端通信。 + +或者,您可能有其他与 WebSocket 终端通信的方式。 + +--- + +但是,在本示例中,我们将使用一个非常简单的HTML文档,其中包含一些JavaScript,全部放在一个长字符串中。 + +当然,这并不是最优的做法,您不应该在生产环境中使用它。 + +在生产环境中,您应该选择上述任一选项。 + +但这是一种专注于 WebSockets 的服务器端并提供一个工作示例的最简单方式: + +```Python hl_lines="2 6-38 41-43" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +## 创建 `websocket` + +在您的 **FastAPI** 应用程序中,创建一个 `websocket`: + +```Python hl_lines="1 46-47" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +!!! note "技术细节" + 您也可以使用 `from starlette.websockets import WebSocket`。 + + **FastAPI** 直接提供了相同的 `WebSocket`,只是为了方便开发人员。但它直接来自 Starlette。 + +## 等待消息并发送消息 + +在您的 WebSocket 路由中,您可以使用 `await` 等待消息并发送消息。 + +```Python hl_lines="48-52" +{!../../../docs_src/websockets/tutorial001.py!} +``` + +您可以接收和发送二进制、文本和 JSON 数据。 + +## 尝试一下 + +如果您的文件名为 `main.py`,请使用以下命令运行应用程序: + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +在浏览器中打开 http://127.0.0.1:8000。 + +您将看到一个简单的页面,如下所示: + + + +您可以在输入框中输入消息并发送: + + + +您的 **FastAPI** 应用程序将回复: + + + +您可以发送(和接收)多条消息: + + + +所有这些消息都将使用同一个 WebSocket 连 + +接。 + +## 使用 `Depends` 和其他依赖项 + +在 WebSocket 端点中,您可以从 `fastapi` 导入并使用以下内容: + +* `Depends` +* `Security` +* `Cookie` +* `Header` +* `Path` +* `Query` + +它们的工作方式与其他 FastAPI 端点/ *路径操作* 相同: + +=== "Python 3.10+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="68-69 82" + {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="69-70 83" + {!> ../../../docs_src/websockets/tutorial002_an.py!} + ``` + +=== "Python 3.10+ 非带注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="66-67 79" + {!> ../../../docs_src/websockets/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ 非带注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="68-69 81" + {!> ../../../docs_src/websockets/tutorial002.py!} + ``` + +!!! info + 由于这是一个 WebSocket,抛出 `HTTPException` 并不是很合理,而是抛出 `WebSocketException`。 + + 您可以使用规范中定义的有效代码。 + +### 尝试带有依赖项的 WebSockets + +如果您的文件名为 `main.py`,请使用以下命令运行应用程序: + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +在浏览器中打开 http://127.0.0.1:8000。 + +在页面中,您可以设置: + +* "Item ID",用于路径。 +* "Token",作为查询参数。 + +!!! tip + 注意,查询参数 `token` 将由依赖项处理。 + +通过这样,您可以连接 WebSocket,然后发送和接收消息: + + + +## 处理断开连接和多个客户端 + +当 WebSocket 连接关闭时,`await websocket.receive_text()` 将引发 `WebSocketDisconnect` 异常,您可以捕获并处理该异常,就像本示例中的示例一样。 + +=== "Python 3.9+" + + ```Python hl_lines="79-81" + {!> ../../../docs_src/websockets/tutorial003_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="81-83" + {!> ../../../docs_src/websockets/tutorial003.py!} + ``` + +尝试以下操作: + +* 使用多个浏览器选项卡打开应用程序。 +* 从这些选项卡中发送消息。 +* 然后关闭其中一个选项卡。 + +这将引发 `WebSocketDisconnect` 异常,并且所有其他客户端都会收到类似以下的消息: + +``` +Client #1596980209979 left the chat +``` + +!!! tip + 上面的应用程序是一个最小和简单的示例,用于演示如何处理和向多个 WebSocket 连接广播消息。 + + 但请记住,由于所有内容都在内存中以单个列表的形式处理,因此它只能在进程运行时工作,并且只能使用单个进程。 + + 如果您需要与 FastAPI 集成更简单但更强大的功能,支持 Redis、PostgreSQL 或其他功能,请查看 [encode/broadcaster](https://github.com/encode/broadcaster)。 + +## 更多信息 + +要了解更多选项,请查看 Starlette 的文档: + +* [WebSocket 类](https://www.starlette.io/websockets/) +* [基于类的 WebSocket 处理](https://www.starlette.io/endpoints/#websocketendpoint)。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index d71c8bf00..6c5001e2a 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -120,6 +120,7 @@ nav: - advanced/response-cookies.md - advanced/response-change-status-code.md - advanced/response-headers.md + - advanced/websockets.md - advanced/wsgi.md - contributing.md - help-fastapi.md From 847befdc1df7c9f591568d982eafa622076d892a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:17:50 +0000 Subject: [PATCH 1065/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0714848dd..9a724feef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99). * 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). From 804a0a90cf6e7ed03a171db759c8b7cb632b5316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E8=BF=87=E5=88=9D=E6=99=B4?= <129537877+ChoyeonChern@users.noreply.github.com> Date: Fri, 23 Jun 2023 00:18:04 +0800 Subject: [PATCH 1066/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ations=20for=20`docs/zh/docs/advanced/settings.md`=20(#9652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/settings.md | 433 ++++++++++++++++++++++++++++++ docs/zh/mkdocs.yml | 1 + 2 files changed, 434 insertions(+) create mode 100644 docs/zh/docs/advanced/settings.md diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md new file mode 100644 index 000000000..597e99a77 --- /dev/null +++ b/docs/zh/docs/advanced/settings.md @@ -0,0 +1,433 @@ +# 设置和环境变量 + +在许多情况下,您的应用程序可能需要一些外部设置或配置,例如密钥、数据库凭据、电子邮件服务的凭据等等。 + +这些设置中的大多数是可变的(可以更改的),比如数据库的 URL。而且许多设置可能是敏感的,比如密钥。 + +因此,通常会将它们提供为由应用程序读取的环境变量。 + +## 环境变量 + +!!! tip + 如果您已经知道什么是"环境变量"以及如何使用它们,请随意跳到下面的下一节。 + +环境变量(也称为"env var")是一种存在于 Python 代码之外、存在于操作系统中的变量,可以被您的 Python 代码(或其他程序)读取。 + +您可以在 shell 中创建和使用环境变量,而无需使用 Python: + +=== "Linux、macOS、Windows Bash" + +
+ + ```console + // 您可以创建一个名为 MY_NAME 的环境变量 + $ export MY_NAME="Wade Wilson" + + // 然后您可以与其他程序一起使用它,例如 + $ echo "Hello $MY_NAME" + + Hello Wade Wilson + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + // 创建一个名为 MY_NAME 的环境变量 + $ $Env:MY_NAME = "Wade Wilson" + + // 与其他程序一起使用它,例如 + $ echo "Hello $Env:MY_NAME" + + Hello Wade Wilson + ``` + +
+ +### 在 Python 中读取环境变量 + +您还可以在 Python 之外的地方(例如终端中或使用任何其他方法)创建环境变量,然后在 Python 中读取它们。 + +例如,您可以有一个名为 `main.py` 的文件,其中包含以下内容: + +```Python hl_lines="3" +import os + +name = os.getenv("MY_NAME", "World") +print(f"Hello {name} from Python") +``` + +!!! tip + `os.getenv()` 的第二个参数是要返回的默认值。 + + 如果没有提供默认值,默认为 `None`,此处我们提供了 `"World"` 作为要使用的默认值。 + +然后,您可以调用该 Python 程序: + +
+ +```console +// 这里我们还没有设置环境变量 +$ python main.py + +// 因为我们没有设置环境变量,所以我们得到默认值 + +Hello World from Python + +// 但是如果我们先创建一个环境变量 +$ export MY_NAME="Wade Wilson" + +// 然后再次调用程序 +$ python main.py + +// 现在它可以读取环境变量 + +Hello Wade Wilson from Python +``` + +
+ +由于环境变量可以在代码之外设置,但可以由代码读取,并且不需要与其他文件一起存储(提交到 `git`),因此通常将它们用于配置或设置。 + + + +您还可以仅为特定程序调用创建一个环境变量,该环境变量仅对该程序可用,并且仅在其运行期间有效。 + +要做到这一点,在程序本身之前的同一行创建它: + +
+ +```console +// 在此程序调用行中创建一个名为 MY_NAME 的环境变量 +$ MY_NAME="Wade Wilson" python main.py + +// 现在它可以读取环境变量 + +Hello Wade Wilson from Python + +// 之后环境变量不再存在 +$ python main.py + +Hello World from Python +``` + +
+ +!!! tip + 您可以在 Twelve-Factor App: Config 中阅读更多相关信息。 + +### 类型和验证 + +这些环境变量只能处理文本字符串,因为它们是外部于 Python 的,并且必须与其他程序和整个系统兼容(甚至与不同的操作系统,如 Linux、Windows、macOS)。 + +这意味着从环境变量中在 Python 中读取的任何值都将是 `str` 类型,任何类型的转换或验证都必须在代码中完成。 + +## Pydantic 的 `Settings` + +幸运的是,Pydantic 提供了一个很好的工具来处理来自环境变量的设置,即Pydantic: Settings management。 + +### 创建 `Settings` 对象 + +从 Pydantic 导入 `BaseSettings` 并创建一个子类,与 Pydantic 模型非常相似。 + +与 Pydantic 模型一样,您使用类型注释声明类属性,还可以指定默认值。 + +您可以使用与 Pydantic 模型相同的验证功能和工具,比如不同的数据类型和使用 `Field()` 进行附加验证。 + +```Python hl_lines="2 5-8 11" +{!../../../docs_src/settings/tutorial001.py!} +``` + +!!! tip + 如果您需要一个快速的复制粘贴示例,请不要使用此示例,而应使用下面的最后一个示例。 + +然后,当您创建该 `Settings` 类的实例(在此示例中是 `settings` 对象)时,Pydantic 将以不区分大小写的方式读取环境变量,因此,大写的变量 `APP_NAME` 仍将为属性 `app_name` 读取。 + +然后,它将转换和验证数据。因此,当您使用该 `settings` 对象时,您将获得您声明的类型的数据(例如 `items_per_user` 将为 `int` 类型)。 + +### 使用 `settings` + +然后,您可以在应用程序中使用新的 `settings` 对象: + +```Python hl_lines="18-20" +{!../../../docs_src/settings/tutorial001.py!} +``` + +### 运行服务器 + +接下来,您将运行服务器,并将配置作为环境变量传递。例如,您可以设置一个 `ADMIN_EMAIL` 和 `APP_NAME`,如下所示: + +
+ +```console +$ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +!!! tip + 要为单个命令设置多个环境变量,只需用空格分隔它们,并将它们全部放在命令之前。 + +然后,`admin_email` 设置将为 `"deadpool@example.com"`。 + +`app_name` 将为 `"ChimichangApp"`。 + +而 `items_per_user` 将保持其默认值为 `50`。 + +## 在另一个模块中设置 + +您可以将这些设置放在另一个模块文件中,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中所见的那样。 + +例如,您可以创建一个名为 `config.py` 的文件,其中包含以下内容: + +```Python +{!../../../docs_src/settings/app01/config.py!} +``` + +然后在一个名为 `main.py` 的文件中使用它: + +```Python hl_lines="3 11-13" +{!../../../docs_src/settings/app01/main.py!} +``` +!!! tip + 您还需要一个名为 `__init__.py` 的文件,就像您在[Bigger Applications - Multiple Files](../tutorial/bigger-applications.md){.internal-link target=_blank}中看到的那样。 + +## 在依赖项中使用设置 + +在某些情况下,从依赖项中提供设置可能比在所有地方都使用全局对象 `settings` 更有用。 + +这在测试期间尤其有用,因为很容易用自定义设置覆盖依赖项。 + +### 配置文件 + +根据前面的示例,您的 `config.py` 文件可能如下所示: + +```Python hl_lines="10" +{!../../../docs_src/settings/app02/config.py!} +``` + +请注意,现在我们不创建默认实例 `settings = Settings()`。 + +### 主应用程序文件 + +现在我们创建一个依赖项,返回一个新的 `config.Settings()`。 + +=== "Python 3.9+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="6 12-13" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="5 11-12" + {!> ../../../docs_src/settings/app02/main.py!} + ``` + +!!! tip + 我们稍后会讨论 `@lru_cache()`。 + + 目前,您可以将 `get_settings()` 视为普通函数。 + +然后,我们可以将其作为依赖项从“路径操作函数”中引入,并在需要时使用它。 + +=== "Python 3.9+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 19-21" + {!> ../../../docs_src/settings/app02_an/main.py!} + ``` + +=== "Python 3.6+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="16 18-20" + {!> ../../../docs_src/settings/app02/main.py!} + ``` + +### 设置和测试 + +然后,在测试期间,通过创建 `get_settings` 的依赖项覆盖,很容易提供一个不同的设置对象: + +```Python hl_lines="9-10 13 21" +{!../../../docs_src/settings/app02/test_main.py!} +``` + +在依赖项覆盖中,我们在创建新的 `Settings` 对象时为 `admin_email` 设置了一个新值,然后返回该新对象。 + +然后,我们可以测试它是否被使用。 + +## 从 `.env` 文件中读取设置 + +如果您有许多可能经常更改的设置,可能在不同的环境中,将它们放在一个文件中,然后从该文件中读取它们,就像它们是环境变量一样,可能非常有用。 + +这种做法相当常见,有一个名称,这些环境变量通常放在一个名为 `.env` 的文件中,该文件被称为“dotenv”。 + +!!! tip + 以点 (`.`) 开头的文件是 Unix-like 系统(如 Linux 和 macOS)中的隐藏文件。 + + 但是,dotenv 文件实际上不一定要具有确切的文件名。 + +Pydantic 支持使用外部库从这些类型的文件中读取。您可以在Pydantic 设置: Dotenv (.env) 支持中阅读更多相关信息。 + +!!! tip + 要使其工作,您需要执行 `pip install python-dotenv`。 + +### `.env` 文件 + +您可以使用以下内容创建一个名为 `.env` 的文件: + +```bash +ADMIN_EMAIL="deadpool@example.com" +APP_NAME="ChimichangApp" +``` + +### 从 `.env` 文件中读取设置 + +然后,您可以使用以下方式更新您的 `config.py`: + +```Python hl_lines="9-10" +{!../../../docs_src/settings/app03/config.py!} +``` + +在这里,我们在 Pydantic 的 `Settings` 类中创建了一个名为 `Config` 的类,并将 `env_file` 设置为我们想要使用的 dotenv 文件的文件名。 + +!!! tip + `Config` 类仅用于 Pydantic 配置。您可以在Pydantic Model Config中阅读更多相关信息。 + +### 使用 `lru_cache` 仅创建一次 `Settings` + +从磁盘中读取文件通常是一项耗时的(慢)操作,因此您可能希望仅在首次读取后并重复使用相同的设置对象,而不是为每个请求都读取它。 + +但是,每次执行以下操作: + +```Python +Settings() +``` + +都会创建一个新的 `Settings` 对象,并且在创建时会再次读取 `.env` 文件。 + +如果依赖项函数只是这样的: + +```Python +def get_settings(): + return Settings() +``` + +我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️ + +但是,由于我们在顶部使用了 `@lru_cache()` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ + +=== "Python 3.9+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an_py39/main.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 11" + {!> ../../../docs_src/settings/app03_an/main.py!} + ``` + +=== "Python 3.6+ 非注解版本" + + !!! tip + 如果可能,请尽量使用 `Annotated` 版本。 + + ```Python hl_lines="1 10" + {!> ../../../docs_src/settings/app03/main.py!} + ``` + +然后,在下一次请求的依赖项中对 `get_settings()` 进行任何后续调用时,它不会执行 `get_settings()` 的内部代码并创建新的 `Settings` 对象,而是返回在第一次调用时返回的相同对象,一次又一次。 + +#### `lru_cache` 技术细节 + +`@lru_cache()` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 + +因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。 + +例如,如果您有一个函数: +```Python +@lru_cache() +def say_hi(name: str, salutation: str = "Ms."): + return f"Hello {salutation} {name}" +``` + +您的程序可以像这样执行: + +```mermaid +sequenceDiagram + +participant code as Code +participant function as say_hi() +participant execute as Execute function + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Camila") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: 返回存储的结果 + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 0, .1) + code ->> function: say_hi(name="Rick", salutation="Mr.") + function ->> execute: 执行函数代码 + execute ->> code: 返回结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Rick") + function ->> code: 返回存储的结果 + end + + rect rgba(0, 255, 255, .1) + code ->> function: say_hi(name="Camila") + function ->> code: 返回存储的结果 + end +``` + +对于我们的依赖项 `get_settings()`,该函数甚至不接受任何参数,因此它始终返回相同的值。 + +这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。 + +`@lru_cache()` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在Python 文档中了解有关 `@lru_cache()` 的更多信息。 + +## 小结 + +您可以使用 Pydantic 设置处理应用程序的设置或配置,利用 Pydantic 模型的所有功能。 + +* 通过使用依赖项,您可以简化测试。 +* 您可以使用 `.env` 文件。 +* 使用 `@lru_cache()` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 6c5001e2a..0e09101eb 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -119,6 +119,7 @@ nav: - advanced/custom-response.md - advanced/response-cookies.md - advanced/response-change-status-code.md + - advanced/settings.md - advanced/response-headers.md - advanced/websockets.md - advanced/wsgi.md From 2f0541f17a2e9a4baa52325dc83b3907941897f7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:18:54 +0000 Subject: [PATCH 1067/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9a724feef..30a0137c3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99). * 🌐 Add Russian translation for `docs/tutorial/extra-models.md`. PR [#9619](https://github.com/tiangolo/fastapi/pull/9619) by [@ivan-abc](https://github.com/ivan-abc). From fa7474b2e849f96687ba883e10d99f06ddafb51e Mon Sep 17 00:00:00 2001 From: lordqyxz <31722468+lordqyxz@users.noreply.github.com> Date: Fri, 23 Jun 2023 00:19:49 +0800 Subject: [PATCH 1068/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/security/index.md`=20(#966?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: shiyz --- docs/zh/docs/advanced/security/index.md | 16 ++++++++++++++++ docs/zh/mkdocs.yml | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 docs/zh/docs/advanced/security/index.md diff --git a/docs/zh/docs/advanced/security/index.md b/docs/zh/docs/advanced/security/index.md new file mode 100644 index 000000000..962523c09 --- /dev/null +++ b/docs/zh/docs/advanced/security/index.md @@ -0,0 +1,16 @@ +# 高级安全 - 介绍 + +## 附加特性 + +除 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank} 中涵盖的功能之外,还有一些额外的功能来处理安全性. + +!!! tip "小贴士" + 接下来的章节 **并不一定是 "高级的"**. + + 而且对于你的使用场景来说,解决方案很可能就在其中。 + +## 先阅读教程 + +接下来的部分假设你已经阅读了主要的 [教程 - 用户指南: 安全性](../../tutorial/security/){.internal-link target=_blank}. + +它们都基于相同的概念,但支持一些额外的功能. diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 0e09101eb..a6afb3039 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -123,6 +123,8 @@ nav: - advanced/response-headers.md - advanced/websockets.md - advanced/wsgi.md + - 高级安全: + - advanced/security/index.md - contributing.md - help-fastapi.md - benchmarks.md From fd6a78cbfe67a28478ceb6aa4fc7ceea59fc6aac Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:20:40 +0000 Subject: [PATCH 1069/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 30a0137c3..02c7f16ec 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/testing.md`. PR [#9641](https://github.com/tiangolo/fastapi/pull/9641) by [@wdh99](https://github.com/wdh99). From 0ef164e1eefdb09f8b7f5e67ccbe171b60a5f3fa Mon Sep 17 00:00:00 2001 From: Alexandr Date: Thu, 22 Jun 2023 19:32:53 +0300 Subject: [PATCH 1070/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20`Annotated`?= =?UTF-8?q?=20notes=20in=20`docs/en/docs/tutorial/schema-extra-example.md`?= =?UTF-8?q?=20(#9620)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update for docs/tutorial/schema-extra-example.md When working on the translation, I noticed that this page is missing the annotated tips that can be found in the rest of the documentation (I checked, and it's the only page where they're missing). --- docs/en/docs/tutorial/schema-extra-example.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 5312254d9..e0f7ed256 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -86,6 +86,9 @@ Here we pass an `example` of the data expected in `Body()`: === "Python 3.10+ non-Annotated" + !!! tip + Prefer to use the `Annotated` version if possible. + ```Python hl_lines="18-23" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` @@ -138,6 +141,9 @@ Each specific example `dict` in the `examples` can contain: === "Python 3.10+ non-Annotated" + !!! tip + Prefer to use the `Annotated` version if possible. + ```Python hl_lines="19-45" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` From c7dad1bb59b5543f89d1470001c71dde3bb76d77 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:33:28 +0000 Subject: [PATCH 1071/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 02c7f16ec..9d0b94a57 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). From 4c401aef0f1b169927249e4fc8218ad709002482 Mon Sep 17 00:00:00 2001 From: TabarakoAkula <113298631+TabarakoAkula@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:33:47 +0300 Subject: [PATCH 1072/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/path-operation-configuration.md`?= =?UTF-8?q?=20(#9696)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Alexandr Co-authored-by: Sebastián Ramírez --- .../tutorial/path-operation-configuration.md | 179 ++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 180 insertions(+) create mode 100644 docs/ru/docs/tutorial/path-operation-configuration.md diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..013903add --- /dev/null +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,179 @@ +# Конфигурация операций пути + +Существует несколько параметров, которые вы можете передать вашему *декоратору операций пути* для его настройки. + +!!! warning "Внимание" + Помните, что эти параметры передаются непосредственно *декоратору операций пути*, а не вашей *функции-обработчику операций пути*. + +## Коды состояния + +Вы можете определить (HTTP) `status_code`, который будет использован в ответах вашей *операции пути*. + +Вы можете передать только `int`-значение кода, например `404`. + +Но если вы не помните, для чего нужен каждый числовой код, вы можете использовать сокращенные константы в параметре `status`: + +=== "Python 3.10+" + + ```Python hl_lines="1 15" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3 17" + {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} + ``` + +Этот код состояния будет использован в ответе и будет добавлен в схему OpenAPI. + +!!! note "Технические детали" + Вы также можете использовать `from starlette import status`. + + **FastAPI** предоставляет тот же `starlette.status` под псевдонимом `fastapi.status` для удобства разработчика. Но его источник - это непосредственно Starlette. + +## Теги + +Вы можете добавлять теги к вашим *операциям пути*, добавив параметр `tags` с `list` заполненным `str`-значениями (обычно в нём только одна строка): + +=== "Python 3.10+" + + ```Python hl_lines="15 20 25" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 22 27" + {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} + ``` + +Они будут добавлены в схему OpenAPI и будут использованы в автоматической документации интерфейса: + + + +### Теги с перечислениями + +Если у вас большое приложение, вы можете прийти к необходимости добавить **несколько тегов**, и возможно, вы захотите убедиться в том, что всегда используете **один и тот же тег** для связанных *операций пути*. + +В этих случаях, имеет смысл хранить теги в классе `Enum`. + +**FastAPI** поддерживает это так же, как и в случае с обычными строками: + +```Python hl_lines="1 8-10 13 18" +{!../../../docs_src/path_operation_configuration/tutorial002b.py!} +``` + +## Краткое и развёрнутое содержание + +Вы можете добавить параметры `summary` и `description`: + +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20-21" + {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} + ``` + +## Описание из строк документации + +Так как описания обычно длинные и содержат много строк, вы можете объявить описание *операции пути* в функции строки документации и **FastAPI** прочитает её отсюда. + +Вы можете использовать Markdown в строке документации, и он будет интерпретирован и отображён корректно (с учетом отступа в строке документации). + +=== "Python 3.10+" + + ```Python hl_lines="17-25" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="19-27" + {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} + ``` + +Он будет использован в интерактивной документации: + + + +## Описание ответа + +Вы можете указать описание ответа с помощью параметра `response_description`: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="21" + {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} + ``` + +!!! info "Дополнительная информация" + Помните, что `response_description` относится конкретно к ответу, а `description` относится к *операции пути* в целом. + +!!! check "Технические детали" + OpenAPI указывает, что каждой *операции пути* необходимо описание ответа. + + Если вдруг вы не укажете его, то **FastAPI** автоматически сгенерирует это описание с текстом "Successful response". + + + +## Обозначение *операции пути* как устаревшей + +Если вам необходимо пометить *операцию пути* как устаревшую, при этом не удаляя её, передайте параметр `deprecated`: + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +Он будет четко помечен как устаревший в интерактивной документации: + + + +Проверьте, как будут выглядеть устаревшие и не устаревшие *операции пути*: + + + +## Резюме + +Вы можете легко конфигурировать и добавлять метаданные в ваши *операции пути*, передавая параметры *декораторам операций пути*. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 24ab15726..3350a1a5e 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -81,6 +81,7 @@ nav: - tutorial/response-status-code.md - tutorial/query-params.md - tutorial/body-multiple-params.md + - tutorial/path-operation-configuration.md - tutorial/cors.md - tutorial/static-files.md - tutorial/debugging.md From b1f27c96c4e092882253dcd40cfd6ec03ad29eeb Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:35:04 +0000 Subject: [PATCH 1073/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9d0b94a57..96484a19b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). From 41ff599d4b9548a3a56a71431da7062a00de6f18 Mon Sep 17 00:00:00 2001 From: Lili_DL <97926049+lilidl-nft@users.noreply.github.com> Date: Thu, 22 Jun 2023 13:40:17 -0300 Subject: [PATCH 1074/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typo=20in=20Span?= =?UTF-8?q?ish=20translation=20for=20`docs/es/docs/tutorial/first-steps.md?= =?UTF-8?q?`=20(#9571)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/es/docs/tutorial/first-steps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/es/docs/tutorial/first-steps.md b/docs/es/docs/tutorial/first-steps.md index 110036e8c..efa61f994 100644 --- a/docs/es/docs/tutorial/first-steps.md +++ b/docs/es/docs/tutorial/first-steps.md @@ -181,7 +181,7 @@ $ uvicorn main:my_awesome_api --reload
-### Paso 3: crea un *operación de path* +### Paso 3: crea una *operación de path* #### Path From a3b1478221afc6185e47fd650d9d8958fd861896 Mon Sep 17 00:00:00 2001 From: jyothish-mohan <56919787+jyothish-mohan@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:10:32 +0530 Subject: [PATCH 1075/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Tweak=20wording?= =?UTF-8?q?=20in=20`docs/en/docs/tutorial/security/index.md`=20(#9561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/security/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md index 9aed2adb5..035b31736 100644 --- a/docs/en/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -26,7 +26,7 @@ That's what all the systems with "login with Facebook, Google, Twitter, GitHub" ### OAuth 1 -There was an OAuth 1, which is very different from OAuth2, and more complex, as it included directly specifications on how to encrypt the communication. +There was an OAuth 1, which is very different from OAuth2, and more complex, as it included direct specifications on how to encrypt the communication. It is not very popular or used nowadays. From 762ede2beca0b2d8ef95a4370a7fc79b62362a02 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:40:50 +0000 Subject: [PATCH 1076/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 96484a19b..ae23a74f9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). * 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). From 7217f167d4c511a85aac27aabe74aef91f3564e3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:41:05 +0000 Subject: [PATCH 1077/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ae23a74f9..fe9d92bb1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). * 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). * 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). From e5f3d6a5eb0f826f5d4ffe378a9d3c8c69d8388c Mon Sep 17 00:00:00 2001 From: Marcel Sander Date: Thu, 22 Jun 2023 18:44:05 +0200 Subject: [PATCH 1078/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20german=20blog=20?= =?UTF-8?q?post=20(Domain-driven=20Design=20mit=20Python=20und=20FastAPI)?= =?UTF-8?q?=20(#9261)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index af5810778..ad738df35 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -233,6 +233,10 @@ articles: link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92 title: Fastapi, Docker(Docker compose) and Postgres german: + - author: Marcel Sander (actidoo) + author_link: https://www.actidoo.com + link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design + title: Domain-driven Design mit Python und FastAPI - author: Nico Axtmann author_link: https://twitter.com/_nicoax link: https://blog.codecentric.de/2019/08/inbetriebnahme-eines-scikit-learn-modells-mit-onnx-und-fastapi/ From e76dd3e70de7f0cee79e46ab74eea423ebeb302a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:44:41 +0000 Subject: [PATCH 1079/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fe9d92bb1..d29ff0388 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). * ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). * 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). * 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). From 47342cdd183a7b3a2059209a1c94915603753aee Mon Sep 17 00:00:00 2001 From: TabarakoAkula <113298631+TabarakoAkula@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:52:24 +0300 Subject: [PATCH 1080/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/metadata.md`=20(#9681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/metadata.md | 111 ++++++++++++++++++++++++++++++ docs/ru/mkdocs.yml | 1 + 2 files changed, 112 insertions(+) create mode 100644 docs/ru/docs/tutorial/metadata.md diff --git a/docs/ru/docs/tutorial/metadata.md b/docs/ru/docs/tutorial/metadata.md new file mode 100644 index 000000000..331c96734 --- /dev/null +++ b/docs/ru/docs/tutorial/metadata.md @@ -0,0 +1,111 @@ +# URL-адреса метаданных и документации + +Вы можете настроить несколько конфигураций метаданных в вашем **FastAPI** приложении. + +## Метаданные для API + +Вы можете задать следующие поля, которые используются в спецификации OpenAPI и в UI автоматической документации API: + +| Параметр | Тип | Описание | +|------------|--|-------------| +| `title` | `str` | Заголовок API. | +| `description` | `str` | Краткое описание API. Может быть использован Markdown. | +| `version` | `string` | Версия API. Версия вашего собственного приложения, а не OpenAPI. К примеру `2.5.0`. | +| `terms_of_service` | `str` | Ссылка к условиям пользования API. Если указано, то это должен быть URL-адрес. | +| `contact` | `dict` | Контактная информация для открытого API. Может содержать несколько полей.
поля contact
ПараметрТипОписание
namestrИдентификационное имя контактного лица/организации.
urlstrURL указывающий на контактную информацию. ДОЛЖЕН быть в формате URL.
emailstrEmail адрес контактного лица/организации. ДОЛЖЕН быть в формате email адреса.
| +| `license_info` | `dict` | Информация о лицензии открытого API. Может содержать несколько полей.
поля license_info
ПараметрТипОписание
namestrОБЯЗАТЕЛЬНО (если установлен параметр license_info). Название лицензии, используемой для API
urlstrURL, указывающий на лицензию, используемую для API. ДОЛЖЕН быть в формате URL.
| + +Вы можете задать их следующим образом: + +```Python hl_lines="3-16 19-31" +{!../../../docs_src/metadata/tutorial001.py!} +``` + +!!! tip "Подсказка" + Вы можете использовать Markdown в поле `description`, и оно будет отображено в выводе. + +С этой конфигурацией автоматическая документация API будут выглядеть так: + + + +## Метаданные для тегов + +Вы также можете добавить дополнительные метаданные для различных тегов, используемых для группировки ваших операций пути с помощью параметра `openapi_tags`. + +Он принимает список, содержащий один словарь для каждого тега. + +Каждый словарь может содержать в себе: + +* `name` (**обязательно**): `str`-значение с тем же именем тега, которое вы используете в параметре `tags` в ваших *операциях пути* и `APIRouter`ах. +* `description`: `str`-значение с кратким описанием для тега. Может содержать Markdown и будет отображаться в UI документации. +* `externalDocs`: `dict`-значение описывающее внешнюю документацию. Включает в себя: + * `description`: `str`-значение с кратким описанием для внешней документации. + * `url` (**обязательно**): `str`-значение с URL-адресом для внешней документации. + +### Создание метаданных для тегов + +Давайте попробуем сделать это на примере с тегами для `users` и `items`. + +Создайте метаданные для ваших тегов и передайте их в параметре `openapi_tags`: + +```Python hl_lines="3-16 18" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +Помните, что вы можете использовать Markdown внутри описания, к примеру "login" будет отображен жирным шрифтом (**login**) и "fancy" будет отображаться курсивом (_fancy_). + +!!! tip "Подсказка" + Вам необязательно добавлять метаданные для всех используемых тегов + +### Используйте собственные теги +Используйте параметр `tags` с вашими *операциями пути* (и `APIRouter`ами), чтобы присвоить им различные теги: + +```Python hl_lines="21 26" +{!../../../docs_src/metadata/tutorial004.py!} +``` + +!!! info "Дополнительная информация" + Узнайте больше о тегах в [Конфигурации операции пути](../path-operation-configuration/#tags){.internal-link target=_blank}. + +### Проверьте документацию + +Теперь, если вы проверите документацию, вы увидите всю дополнительную информацию: + + + +### Порядок расположения тегов + +Порядок расположения словарей метаданных для каждого тега определяет также порядок, отображаемый в документах UI + +К примеру, несмотря на то, что `users` будут идти после `items` в алфавитном порядке, они отображаются раньше, потому что мы добавляем свои метаданные в качестве первого словаря в списке. + +## URL-адреса OpenAPI + +По умолчанию схема OpenAPI отображена по адресу `/openapi.json`. + +Но вы можете изменить это с помощью параметра `openapi_url`. + +К примеру, чтобы задать её отображение по адресу `/api/v1/openapi.json`: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial002.py!} +``` + +Если вы хотите отключить схему OpenAPI полностью, вы можете задать `openapi_url=None`, это также отключит пользовательские интерфейсы документации, которые его использует. + +## URL-адреса документации + +Вы можете изменить конфигурацию двух пользовательских интерфейсов документации, среди которых + +* **Swagger UI**: отображаемый по адресу `/docs`. + * Вы можете задать его URL с помощью параметра `docs_url`. + * Вы можете отключить это с помощью настройки `docs_url=None`. +* **ReDoc**: отображаемый по адресу `/redoc`. + * Вы можете задать его URL с помощью параметра `redoc_url`. + * Вы можете отключить это с помощью настройки `redoc_url=None`. + +К примеру, чтобы задать отображение Swagger UI по адресу `/documentation` и отключить ReDoc: + +```Python hl_lines="3" +{!../../../docs_src/metadata/tutorial003.py!} +``` diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 3350a1a5e..4a7512ac0 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -81,6 +81,7 @@ nav: - tutorial/response-status-code.md - tutorial/query-params.md - tutorial/body-multiple-params.md + - tutorial/metadata.md - tutorial/path-operation-configuration.md - tutorial/cors.md - tutorial/static-files.md From fafe670db6547b272d567272007f3f6de94131f3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 16:53:00 +0000 Subject: [PATCH 1081/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d29ff0388..d2ce322ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). * ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). * 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). From d82700c96dc49d81682877cf2438f75cd452d213 Mon Sep 17 00:00:00 2001 From: Pankaj Kumar <76695979+pankaj1707k@users.noreply.github.com> Date: Thu, 22 Jun 2023 22:31:28 +0530 Subject: [PATCH 1082/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20tooltips?= =?UTF-8?q?=20for=20light/dark=20theme=20toggler=20in=20docs=20(#9588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index b7cefee53..73df174d1 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight From 0dc9a377dcaf69c50a9d3c63d0d09aca700beffa Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:02:08 +0000 Subject: [PATCH 1083/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d2ce322ed..cfe51c17a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). * ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). From 68ce5b37dc4dba574aa91ac8f80e854fbe6738f3 Mon Sep 17 00:00:00 2001 From: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Date: Thu, 22 Jun 2023 23:04:16 +0600 Subject: [PATCH 1084/1881] =?UTF-8?q?=E2=9C=8F=20Rewording=20in=20`docs/en?= =?UTF-8?q?/docs/tutorial/debugging.md`=20(#9581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/debugging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/debugging.md b/docs/en/docs/tutorial/debugging.md index bda889c45..3deba54d5 100644 --- a/docs/en/docs/tutorial/debugging.md +++ b/docs/en/docs/tutorial/debugging.md @@ -64,7 +64,7 @@ from myapp import app # Some more code ``` -in that case, the automatic variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`. +in that case, the automatically created variable inside of `myapp.py` will not have the variable `__name__` with a value of `"__main__"`. So, the line: From c812b4229375eeb5c6b1fba26d01fbde219284af Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:04:50 +0000 Subject: [PATCH 1085/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cfe51c17a..ed1d94cfc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). * ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). From cfc06a3a3d2ebf4cb33d73aa40f62f0ac75ba25d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D1=8F=20=D0=BA=D0=BE=D1=82=D0=B8=D0=BA=20=D0=BF=D1=83?= =?UTF-8?q?=D1=80-=D0=BF=D1=83=D1=80?= Date: Thu, 22 Jun 2023 20:06:25 +0300 Subject: [PATCH 1086/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20on=20P?= =?UTF-8?q?ydantic=20using=20ujson=20internally=20(#5804)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- README.md | 1 - docs/az/docs/index.md | 1 - docs/de/docs/index.md | 1 - docs/en/docs/index.md | 1 - docs/es/docs/index.md | 1 - docs/fa/docs/index.md | 1 - docs/fr/docs/index.md | 1 - docs/he/docs/index.md | 1 - docs/id/docs/index.md | 1 - docs/it/docs/index.md | 1 - docs/ja/docs/index.md | 1 - docs/ko/docs/index.md | 1 - docs/nl/docs/index.md | 1 - docs/pl/docs/index.md | 1 - docs/pt/docs/index.md | 1 - docs/ru/docs/index.md | 1 - docs/sq/docs/index.md | 1 - docs/sv/docs/index.md | 1 - docs/tr/docs/index.md | 1 - docs/uk/docs/index.md | 1 - docs/zh/docs/index.md | 1 - 21 files changed, 21 deletions(-) diff --git a/README.md b/README.md index ee25f1803..7dc199367 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md index 282c15032..8b1c65194 100644 --- a/docs/az/docs/index.md +++ b/docs/az/docs/index.md @@ -441,7 +441,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md index 68fc8b753..f1c873d75 100644 --- a/docs/de/docs/index.md +++ b/docs/de/docs/index.md @@ -440,7 +440,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index 9a81f14d1..afd6d7138 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -445,7 +445,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 727a6617b..5b75880c0 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -433,7 +433,6 @@ Para entender más al respecto revisa la sección ujson - para "parsing" de JSON más rápido. * email_validator - para validación de emails. Usados por Starlette: diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index ebaa8085a..248084389 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -436,7 +436,6 @@ item: Item استفاده شده توسط Pydantic: -* ujson - برای "تجزیه (parse)" سریع‌تر JSON . * email_validator - برای اعتبارسنجی آدرس‌های ایمیل. استفاده شده توسط Starlette: diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 5ee8b462f..7c7547be1 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -445,7 +445,6 @@ Pour en savoir plus, consultez la section ujson - pour un "décodage" JSON plus rapide. * email_validator - pour la validation des adresses email. Utilisées par Starlette : diff --git a/docs/he/docs/index.md b/docs/he/docs/index.md index 19f2f2041..802dbe8b5 100644 --- a/docs/he/docs/index.md +++ b/docs/he/docs/index.md @@ -440,7 +440,6 @@ item: Item בשימוש Pydantic: -- ujson - "פרסור" JSON. - email_validator - לאימות כתובות אימייל. בשימוש Starlette: diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md index 66fc2859e..ed551f910 100644 --- a/docs/id/docs/index.md +++ b/docs/id/docs/index.md @@ -441,7 +441,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md index 9d95dd6d7..42c9a7e8c 100644 --- a/docs/it/docs/index.md +++ b/docs/it/docs/index.md @@ -438,7 +438,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index f3a159f70..a9c381a23 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -431,7 +431,6 @@ item: Item Pydantic によって使用されるもの: -- ujson - より速い JSON への"変換". - email_validator - E メールの検証 Starlette によって使用されるもの: diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index c64713705..a6991a9b8 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -437,7 +437,6 @@ item: Item Pydantic이 사용하는: -* ujson - 더 빠른 JSON "파싱". * email_validator - 이메일 유효성 검사. Starlette이 사용하는: diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md index 23143a96f..47d62f8c4 100644 --- a/docs/nl/docs/index.md +++ b/docs/nl/docs/index.md @@ -444,7 +444,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 98e1e82fc..bade7a88c 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -435,7 +435,6 @@ Aby dowiedzieć się o tym więcej, zobacz sekcję ujson - dla szybszego "parsowania" danych JSON. * email_validator - dla walidacji adresów email. Używane przez Starlette: diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 76668b4da..591e7f3d4 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -430,7 +430,6 @@ Para entender mais sobre performance, veja a seção ujson - para JSON mais rápido "parsing". * email_validator - para validação de email. Usados por Starlette: diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 14a6d5a8b..30c32e046 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -439,7 +439,6 @@ item: Item Используется Pydantic: -* ujson - для более быстрого JSON "парсинга". * email_validator - для проверки электронной почты. Используется Starlette: diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md index cff2c2804..a83b7b519 100644 --- a/docs/sq/docs/index.md +++ b/docs/sq/docs/index.md @@ -441,7 +441,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md index 23143a96f..47d62f8c4 100644 --- a/docs/sv/docs/index.md +++ b/docs/sv/docs/index.md @@ -444,7 +444,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 6bd30d709..2339337f3 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -449,7 +449,6 @@ Daha fazla bilgi için, bu bölüme bir göz at ujson - daha hızlı JSON "dönüşümü" için. * email_validator - email doğrulaması için. Starlette tarafında kullanılan: diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md index cff2c2804..a83b7b519 100644 --- a/docs/uk/docs/index.md +++ b/docs/uk/docs/index.md @@ -441,7 +441,6 @@ To understand more about it, see the section ujson - for faster JSON "parsing". * email_validator - for email validation. Used by Starlette: diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 4db3ef10c..1de2a8d36 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -437,7 +437,6 @@ item: Item 用于 Pydantic: -* ujson - 更快的 JSON 「解析」。 * email_validator - 用于 email 校验。 用于 Starlette: From 4842dfadcf1a68f77f6a26df1235cdf85387ae89 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:07:05 +0000 Subject: [PATCH 1087/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed1d94cfc..122976f96 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). * ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). * ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). From 56bc75372f970818502d3ec9b1ce08b15c702173 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:12:24 +0200 Subject: [PATCH 1088/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.8.5=20to=201.8.6=20(#9482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bdadcc6d3..b84c5bf17 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.8.5 + uses: pypa/gh-action-pypi-publish@v1.8.6 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From 586de94ca11c6edf914645a381a4a516e9c0aa31 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:12:59 +0000 Subject: [PATCH 1089/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 122976f96..e1b267d9e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). * ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). * ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). From 60343161ea502cac9039d4410686aa7a2e768153 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:26:01 +0200 Subject: [PATCH 1090/1881] =?UTF-8?q?=E2=AC=86=20Update=20pre-commit=20req?= =?UTF-8?q?uirement=20from=20<3.0.0,>=3D2.17.0=20to=20>=3D2.17.0,<4.0.0=20?= =?UTF-8?q?(#9251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cb9abb44a..49aae4466 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-tests.txt -r requirements-docs.txt uvicorn[standard] >=0.12.0,<0.21.0 -pre-commit >=2.17.0,<3.0.0 +pre-commit >=2.17.0,<4.0.0 From fdc713428e226d217dcda2b3418b4497aee3086e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:26:46 +0000 Subject: [PATCH 1091/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e1b267d9e..3ca3d7d9e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). * ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). From 6553243dbfcb0f0e938e6aa7b3e3c2d17730430b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:42:53 +0200 Subject: [PATCH 1092/1881] =?UTF-8?q?=E2=AC=86=20Bump=20mypy=20from=201.3.?= =?UTF-8?q?0=20to=201.4.0=20(#9719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 3ef3c4fd9..d7ef561fa 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,7 +1,7 @@ -e . pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 -mypy ==1.3.0 +mypy ==1.4.0 ruff ==0.0.272 black == 23.3.0 httpx >=0.23.0,<0.24.0 From a01c2ca3ddedbb743c6feb1b8d68b80b1d8ca692 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:43:29 +0000 Subject: [PATCH 1093/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3ca3d7d9e..caec549ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). * 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). From 836ac562034899146c3a744015f1df5703cccb66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:43:44 +0200 Subject: [PATCH 1094/1881] =?UTF-8?q?=E2=AC=86=20Update=20uvicorn[standard?= =?UTF-8?q?]=20requirement=20from=20<0.21.0,>=3D0.12.0=20to=20>=3D0.12.0, Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 49aae4466..7e746016a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -e .[all] -r requirements-tests.txt -r requirements-docs.txt -uvicorn[standard] >=0.12.0,<0.21.0 +uvicorn[standard] >=0.12.0,<0.23.0 pre-commit >=2.17.0,<4.0.0 From 41d774ed6d7f90da8cddc168a645bc00158a6a9b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:44:21 +0000 Subject: [PATCH 1095/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 caec549ae..171c46f7f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). From d1805ef466b507ad52a627a6fd4fea9b0b71a7b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 19:52:20 +0200 Subject: [PATCH 1096/1881] =?UTF-8?q?=E2=AC=86=20Bump=20ruff=20from=200.0.?= =?UTF-8?q?272=20to=200.0.275=20(#9721)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index d7ef561fa..5cedde84d 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,7 +2,7 @@ pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.0 -ruff ==0.0.272 +ruff ==0.0.275 black == 23.3.0 httpx >=0.23.0,<0.24.0 email_validator >=1.1.1,<2.0.0 From 2ffb08d0bc6a6e14a14d278c29dcc6e24b162f48 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 22 Jun 2023 17:52:55 +0000 Subject: [PATCH 1097/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 171c46f7f..b45ebeb03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump ruff from 0.0.272 to 0.0.275. PR [#9721](https://github.com/tiangolo/fastapi/pull/9721) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). From 8066f85b3f83d79c28b941af6160d3512edb8cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 22 Jun 2023 19:57:25 +0200 Subject: [PATCH 1098/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b45ebeb03..752b42d3e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,20 +2,25 @@ ## Latest Changes -* ⬆ Bump ruff from 0.0.272 to 0.0.275. PR [#9721](https://github.com/tiangolo/fastapi/pull/9721) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). +### Features + +* ✨ Allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). + +### Docs + * 📝 Update docs on Pydantic using ujson internally. PR [#5804](https://github.com/tiangolo/fastapi/pull/5804) by [@mvasilkov](https://github.com/mvasilkov). * ✏ Rewording in `docs/en/docs/tutorial/debugging.md`. PR [#9581](https://github.com/tiangolo/fastapi/pull/9581) by [@ivan-abc](https://github.com/ivan-abc). -* ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). -* 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 📝 Add german blog post (Domain-driven Design mit Python und FastAPI). PR [#9261](https://github.com/tiangolo/fastapi/pull/9261) by [@msander](https://github.com/msander). * ✏️ Tweak wording in `docs/en/docs/tutorial/security/index.md`. PR [#9561](https://github.com/tiangolo/fastapi/pull/9561) by [@jyothish-mohan](https://github.com/jyothish-mohan). +* 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). +* ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). +* 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/metadata.md`. PR [#9681](https://github.com/tiangolo/fastapi/pull/9681) by [@TabarakoAkula](https://github.com/TabarakoAkula). * 🌐 Fix typo in Spanish translation for `docs/es/docs/tutorial/first-steps.md`. PR [#9571](https://github.com/tiangolo/fastapi/pull/9571) by [@lilidl-nft](https://github.com/lilidl-nft). * 🌐 Add Russian translation for `docs/tutorial/path-operation-configuration.md`. PR [#9696](https://github.com/tiangolo/fastapi/pull/9696) by [@TabarakoAkula](https://github.com/TabarakoAkula). -* 📝 Update `Annotated` notes in `docs/en/docs/tutorial/schema-extra-example.md`. PR [#9620](https://github.com/tiangolo/fastapi/pull/9620) by [@Alexandrhub](https://github.com/Alexandrhub). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/security/index.md`. PR [#9666](https://github.com/tiangolo/fastapi/pull/9666) by [@lordqyxz](https://github.com/lordqyxz). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/settings.md`. PR [#9652](https://github.com/tiangolo/fastapi/pull/9652) by [@ChoyeonChern](https://github.com/ChoyeonChern). * 🌐 Add Chinese translations for `docs/zh/docs/advanced/websockets.md`. PR [#9651](https://github.com/tiangolo/fastapi/pull/9651) by [@ChoyeonChern](https://github.com/ChoyeonChern). @@ -24,12 +29,18 @@ * 🌐 Add Russian translation for `docs/tutorial/cors.md`. PR [#9608](https://github.com/tiangolo/fastapi/pull/9608) by [@ivan-abc](https://github.com/ivan-abc). * 🌐 Add Polish translation for `docs/pl/docs/features.md`. PR [#5348](https://github.com/tiangolo/fastapi/pull/5348) by [@mbroton](https://github.com/mbroton). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-nested-models.md`. PR [#9605](https://github.com/tiangolo/fastapi/pull/9605) by [@Alexandrhub](https://github.com/Alexandrhub). -* ✏️ Fix typo `Annotation` -> `Annotated` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#9625](https://github.com/tiangolo/fastapi/pull/9625) by [@mccricardo](https://github.com/mccricardo). + +### Internal + +* ⬆ Bump ruff from 0.0.272 to 0.0.275. PR [#9721](https://github.com/tiangolo/fastapi/pull/9721) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update uvicorn[standard] requirement from <0.21.0,>=0.12.0 to >=0.12.0,<0.23.0. PR [#9463](https://github.com/tiangolo/fastapi/pull/9463) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump mypy from 1.3.0 to 1.4.0. PR [#9719](https://github.com/tiangolo/fastapi/pull/9719) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Update pre-commit requirement from <3.0.0,>=2.17.0 to >=2.17.0,<4.0.0. PR [#9251](https://github.com/tiangolo/fastapi/pull/9251) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.5 to 1.8.6. PR [#9482](https://github.com/tiangolo/fastapi/pull/9482) by [@dependabot[bot]](https://github.com/apps/dependabot). +* ✏️ Fix tooltips for light/dark theme toggler in docs. PR [#9588](https://github.com/tiangolo/fastapi/pull/9588) by [@pankaj1707k](https://github.com/pankaj1707k). * 🔧 Set minimal hatchling version needed to build the package. PR [#9240](https://github.com/tiangolo/fastapi/pull/9240) by [@mgorny](https://github.com/mgorny). * 📝 Add repo link to PyPI. PR [#9559](https://github.com/tiangolo/fastapi/pull/9559) by [@JacobCoffee](https://github.com/JacobCoffee). * ✏️ Fix typos in data for tests. PR [#4958](https://github.com/tiangolo/fastapi/pull/4958) by [@ryanrussell](https://github.com/ryanrussell). -* 📝 Use in memory database for testing SQL in docs. PR [#1223](https://github.com/tiangolo/fastapi/pull/1223) by [@HarshaLaxman](https://github.com/HarshaLaxman). -* ✨ Add allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). * 🔧 Update sponsors, add Flint. PR [#9699](https://github.com/tiangolo/fastapi/pull/9699) by [@tiangolo](https://github.com/tiangolo). * 👷 Lint in CI only once, only with one version of Python, run tests with all of them. PR [#9686](https://github.com/tiangolo/fastapi/pull/9686) by [@tiangolo](https://github.com/tiangolo). From 4721405ef7c970bf3ba08259546dcc0b87cf22c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 22 Jun 2023 19:58:22 +0200 Subject: [PATCH 1099/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?98.0?= 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 752b42d3e..5a8610a09 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.98.0 + ### Features * ✨ Allow disabling `redirect_slashes` at the FastAPI app level. PR [#3432](https://github.com/tiangolo/fastapi/pull/3432) by [@cyberlis](https://github.com/cyberlis). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 46a056363..038e1ba86 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.97.0" +__version__ = "0.98.0" from starlette import status as status From 42d0d6e4a51273dca8eb2ab58eda8d840c87c6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Jun 2023 19:55:09 +0200 Subject: [PATCH 1100/1881] =?UTF-8?q?=F0=9F=91=B7=20Build=20and=20deploy?= =?UTF-8?q?=20docs=20only=20on=20docs=20changes=20(#9728)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 26 +++++++++++++++++++++++++- .github/workflows/preview-docs.yml | 5 +++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a0e83e5c8..fb1fa6f09 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -4,9 +4,33 @@ on: branches: - master pull_request: - types: [opened, synchronize] + types: + - opened + - synchronize jobs: + changes: + runs-on: ubuntu-latest + # Required permissions + permissions: + pull-requests: read + # Set job outputs to values from filter step + outputs: + docs: ${{ steps.filter.outputs.docs }} + steps: + - uses: actions/checkout@v3 + # For pull requests it's not necessary to checkout the code but for master it is + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + docs: + - README.md + - docs/** + - docs_src/** + - requirements-docs.txt build-docs: + needs: changes + if: ${{ needs.changes.outputs.docs == 'true' }} runs-on: ubuntu-latest steps: - name: Dump GitHub context diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/preview-docs.yml index 298f75b02..da98f5d2b 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/preview-docs.yml @@ -16,19 +16,23 @@ jobs: rm -rf ./site mkdir ./site - name: Download Artifact Docs + id: download uses: dawidd6/action-download-artifact@v2.27.0 with: + if_no_artifact_found: ignore github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} name: docs-zip path: ./site/ - name: Unzip docs + if: steps.download.outputs.found_artifact == 'true' run: | cd ./site unzip docs.zip rm -f docs.zip - name: Deploy to Netlify + if: steps.download.outputs.found_artifact == 'true' id: netlify uses: nwtgck/actions-netlify@v2.0.0 with: @@ -40,6 +44,7 @@ jobs: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} - name: Comment Deploy + if: steps.netlify.outputs.deploy-url != '' uses: ./.github/actions/comment-docs-preview-in-pr with: token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }} From 5a3bbb62de97f01bd4cfb64ed62592beef7523da Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Jun 2023 17:55:46 +0000 Subject: [PATCH 1101/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5a8610a09..405916d63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). ## 0.98.0 From 0c66ec7da9abec87a9961a3413c09d4b4031b06d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 23 Jun 2023 20:16:41 +0200 Subject: [PATCH 1102/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20MkDocs?= =?UTF-8?q?=20and=20MkDocs=20Material=20(#9729)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements-docs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index e9d0567ed..211212fba 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -mkdocs >=1.1.2,<2.0.0 -mkdocs-material >=8.1.4,<9.0.0 +mkdocs==1.4.3 +mkdocs-material==9.1.16 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 From 1471bc956cb4eff2207475c2ec2297a372d4b568 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Jun 2023 18:17:17 +0000 Subject: [PATCH 1103/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 405916d63..74b7f25ca 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). * 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). ## 0.98.0 From f61217a18a011c597f109be4e6033014c7ff92e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 01:51:56 +0200 Subject: [PATCH 1104/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20old=20interna?= =?UTF-8?q?l=20GitHub=20Action=20watch-previews=20that=20is=20no=20longer?= =?UTF-8?q?=20needed=20(#9730)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/watch-previews/Dockerfile | 7 -- .github/actions/watch-previews/action.yml | 10 -- .github/actions/watch-previews/app/main.py | 101 --------------------- 3 files changed, 118 deletions(-) delete mode 100644 .github/actions/watch-previews/Dockerfile delete mode 100644 .github/actions/watch-previews/action.yml delete mode 100644 .github/actions/watch-previews/app/main.py diff --git a/.github/actions/watch-previews/Dockerfile b/.github/actions/watch-previews/Dockerfile deleted file mode 100644 index b8cc64d94..000000000 --- a/.github/actions/watch-previews/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.7 - -RUN pip install httpx PyGithub "pydantic==1.5.1" - -COPY ./app /app - -CMD ["python", "/app/main.py"] diff --git a/.github/actions/watch-previews/action.yml b/.github/actions/watch-previews/action.yml deleted file mode 100644 index 5c09ad487..000000000 --- a/.github/actions/watch-previews/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Watch docs previews in PRs" -description: "Check PRs and trigger new docs deploys" -author: "Sebastián Ramírez " -inputs: - token: - description: 'Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }}' - required: true -runs: - using: 'docker' - image: 'Dockerfile' diff --git a/.github/actions/watch-previews/app/main.py b/.github/actions/watch-previews/app/main.py deleted file mode 100644 index 51285d02b..000000000 --- a/.github/actions/watch-previews/app/main.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging -from datetime import datetime -from pathlib import Path -from typing import List, Union - -import httpx -from github import Github -from github.NamedUser import NamedUser -from pydantic import BaseModel, BaseSettings, SecretStr - -github_api = "https://api.github.com" -netlify_api = "https://api.netlify.com" - - -class Settings(BaseSettings): - input_token: SecretStr - github_repository: str - github_event_path: Path - github_event_name: Union[str, None] = None - - -class Artifact(BaseModel): - id: int - node_id: str - name: str - size_in_bytes: int - url: str - archive_download_url: str - expired: bool - created_at: datetime - updated_at: datetime - - -class ArtifactResponse(BaseModel): - total_count: int - artifacts: List[Artifact] - - -def get_message(commit: str) -> str: - return f"Docs preview for commit {commit} at" - - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - settings = Settings() - logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_token.get_secret_value()) - repo = g.get_repo(settings.github_repository) - owner: NamedUser = repo.owner - headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"} - prs = list(repo.get_pulls(state="open")) - response = httpx.get( - f"{github_api}/repos/{settings.github_repository}/actions/artifacts", - headers=headers, - ) - data = response.json() - artifacts_response = ArtifactResponse.parse_obj(data) - for pr in prs: - logging.info("-----") - logging.info(f"Processing PR #{pr.number}: {pr.title}") - pr_comments = list(pr.get_issue_comments()) - pr_commits = list(pr.get_commits()) - last_commit = pr_commits[0] - for pr_commit in pr_commits: - if pr_commit.commit.author.date > last_commit.commit.author.date: - last_commit = pr_commit - commit = last_commit.commit.sha - logging.info(f"Last commit: {commit}") - message = get_message(commit) - notified = False - for pr_comment in pr_comments: - if message in pr_comment.body: - notified = True - logging.info(f"Docs preview was notified: {notified}") - if not notified: - artifact_name = f"docs-zip-{commit}" - use_artifact: Union[Artifact, None] = None - for artifact in artifacts_response.artifacts: - if artifact.name == artifact_name: - use_artifact = artifact - break - if not use_artifact: - logging.info("Artifact not available") - else: - logging.info(f"Existing artifact: {use_artifact.name}") - response = httpx.post( - "https://api.github.com/repos/tiangolo/fastapi/actions/workflows/preview-docs.yml/dispatches", - headers=headers, - json={ - "ref": "master", - "inputs": { - "pr": f"{pr.number}", - "name": artifact_name, - "commit": commit, - }, - }, - ) - logging.info( - f"Trigger sent, response status: {response.status_code} - content: {response.content}" - ) - logging.info("Finished") From 2848951082cf3301818b3a120b276dff91646458 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 23 Jun 2023 23:52:34 +0000 Subject: [PATCH 1105/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 74b7f25ca..6f43126b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). * 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). From c09e5cdfa70a6c0226e10d93595717bf3c0feedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 02:00:12 +0200 Subject: [PATCH 1106/1881] =?UTF-8?q?=F0=9F=91=B7=20Refactor=20Docs=20CI,?= =?UTF-8?q?=20run=20in=20multiple=20workers=20with=20a=20dynamic=20matrix?= =?UTF-8?q?=20to=20optimize=20speed=20(#9732)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 73 ++++++--- .../{preview-docs.yml => deploy-docs.yml} | 18 +-- .gitignore | 1 + scripts/docs.py | 152 +++++++++--------- scripts/zip-docs.sh | 11 -- 5 files changed, 142 insertions(+), 113 deletions(-) rename .github/workflows/{preview-docs.yml => deploy-docs.yml} (79%) delete mode 100644 scripts/zip-docs.sh diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index fb1fa6f09..c2880ef71 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -23,15 +23,45 @@ jobs: id: filter with: filters: | - docs: - - README.md - - docs/** - - docs_src/** - - requirements-docs.txt + docs: + - README.md + - docs/** + - docs_src/** + - requirements-docs.txt + langs: + needs: + - changes + runs-on: ubuntu-latest + outputs: + langs: ${{ steps.show-langs.outputs.langs }} + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 + - name: Install docs extras + if: steps.cache.outputs.cache-hit != 'true' + run: pip install -r requirements-docs.txt + - name: Export Language Codes + id: show-langs + run: | + echo "langs=$(python ./scripts/docs.py langs-json)" >> $GITHUB_OUTPUT + build-docs: - needs: changes + needs: + - changes + - langs if: ${{ needs.changes.outputs.docs == 'true' }} runs-on: ubuntu-latest + strategy: + matrix: + lang: ${{ fromJson(needs.langs.outputs.langs) }} steps: - name: Dump GitHub context env: @@ -53,21 +83,24 @@ jobs: - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + - name: Update Languages + run: python ./scripts/docs.py update-languages - name: Build Docs - run: python ./scripts/docs.py build-all - - name: Zip docs - run: bash ./scripts/zip-docs.sh + run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - uses: actions/upload-artifact@v3 with: - name: docs-zip - path: ./site/docs.zip - - name: Deploy to Netlify - uses: nwtgck/actions-netlify@v2.0.0 + name: docs-site + path: ./site/** + + # https://github.com/marketplace/actions/alls-green#why + docs-all-green: # This job does nothing and is only used for the branch protection + if: always() + needs: + - build-docs + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 with: - publish-dir: './site' - production-branch: master - github-token: ${{ secrets.FASTAPI_BUILD_DOCS_NETLIFY }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + jobs: ${{ toJSON(needs) }} + allowed-skips: build-docs diff --git a/.github/workflows/preview-docs.yml b/.github/workflows/deploy-docs.yml similarity index 79% rename from .github/workflows/preview-docs.yml rename to .github/workflows/deploy-docs.yml index da98f5d2b..312d835af 100644 --- a/.github/workflows/preview-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -1,4 +1,4 @@ -name: Preview Docs +name: Deploy Docs on: workflow_run: workflows: @@ -7,9 +7,13 @@ on: - completed jobs: - preview-docs: + deploy-docs: runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 - name: Clean site run: | @@ -23,21 +27,15 @@ jobs: github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} workflow: build-docs.yml run_id: ${{ github.event.workflow_run.id }} - name: docs-zip + name: docs-site path: ./site/ - - name: Unzip docs - if: steps.download.outputs.found_artifact == 'true' - run: | - cd ./site - unzip docs.zip - rm -f docs.zip - name: Deploy to Netlify if: steps.download.outputs.found_artifact == 'true' id: netlify uses: nwtgck/actions-netlify@v2.0.0 with: publish-dir: './site' - production-deploy: false + production-deploy: ${{ github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' }} github-token: ${{ secrets.FASTAPI_PREVIEW_DOCS_NETLIFY }} enable-commit-comment: false env: diff --git a/.gitignore b/.gitignore index a26bb5cd6..3cb64c047 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ Pipfile.lock env3.* env docs_build +site_build venv docs.zip archive.zip diff --git a/scripts/docs.py b/scripts/docs.py index e0953b8ed..c464f8dbe 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,3 +1,4 @@ +import json import os import re import shutil @@ -133,75 +134,83 @@ def build_lang( build_lang_path = build_dir_path / lang en_lang_path = Path("docs/en") site_path = Path("site").absolute() + build_site_path = Path("site_build").absolute() + build_site_dist_path = build_site_path / lang if lang == "en": dist_path = site_path else: dist_path: Path = site_path / lang shutil.rmtree(build_lang_path, ignore_errors=True) shutil.copytree(lang_path, build_lang_path) - shutil.copytree(en_docs_path / "data", build_lang_path / "data") - overrides_src = en_docs_path / "overrides" - overrides_dest = build_lang_path / "overrides" - for path in overrides_src.iterdir(): - dest_path = overrides_dest / path.name - if not dest_path.exists(): - shutil.copy(path, dest_path) - en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) - nav = en_config["nav"] - lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load( - lang_config_path.read_text(encoding="utf-8") - ) - lang_nav = lang_config["nav"] - # Exclude first 2 entries FastAPI and Languages, for custom handling - use_nav = nav[2:] - lang_use_nav = lang_nav[2:] - file_to_nav = get_file_to_nav_map(use_nav) - sections = get_sections(use_nav) - lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - for file in file_to_nav: - file_path = Path(file) - lang_file_path: Path = build_lang_path / "docs" / file_path - en_file_path: Path = en_lang_path / "docs" / file_path - lang_file_path.parent.mkdir(parents=True, exist_ok=True) - if not lang_file_path.is_file(): - en_text = en_file_path.read_text(encoding="utf-8") - lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text, encoding="utf-8") - file_key = file_to_nav[file] - use_lang_file_to_nav[file] = file_key - if file_key: - composite_key = () - new_key = () - for key_part in file_key: - composite_key += (key_part,) - key_first_file = sections[composite_key] - if key_first_file in lang_file_to_nav: - new_key = lang_file_to_nav[key_first_file] - else: - new_key += (key_part,) - use_lang_file_to_nav[file] = new_key - key_to_section = {(): []} - for file, orig_file_key in file_to_nav.items(): - if file in use_lang_file_to_nav: - file_key = use_lang_file_to_nav[file] - else: - file_key = orig_file_key - section = get_key_section(key_to_section=key_to_section, key=file_key) - section.append(file) - new_nav = key_to_section[()] - export_lang_nav = [lang_nav[0], nav[1]] + new_nav - lang_config["nav"] = export_lang_nav - build_lang_config_path: Path = build_lang_path / mkdocs_name - build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + if not lang == "en": + shutil.copytree(en_docs_path / "data", build_lang_path / "data") + overrides_src = en_docs_path / "overrides" + overrides_dest = build_lang_path / "overrides" + for path in overrides_src.iterdir(): + dest_path = overrides_dest / path.name + if not dest_path.exists(): + shutil.copy(path, dest_path) + en_config_path: Path = en_lang_path / mkdocs_name + en_config: dict = mkdocs.utils.yaml_load( + en_config_path.read_text(encoding="utf-8") + ) + nav = en_config["nav"] + lang_config_path: Path = lang_path / mkdocs_name + lang_config: dict = mkdocs.utils.yaml_load( + lang_config_path.read_text(encoding="utf-8") + ) + lang_nav = lang_config["nav"] + # Exclude first 2 entries FastAPI and Languages, for custom handling + use_nav = nav[2:] + lang_use_nav = lang_nav[2:] + file_to_nav = get_file_to_nav_map(use_nav) + sections = get_sections(use_nav) + lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) + for file in file_to_nav: + file_path = Path(file) + lang_file_path: Path = build_lang_path / "docs" / file_path + en_file_path: Path = en_lang_path / "docs" / file_path + lang_file_path.parent.mkdir(parents=True, exist_ok=True) + if not lang_file_path.is_file(): + en_text = en_file_path.read_text(encoding="utf-8") + lang_text = get_text_with_translate_missing(en_text) + lang_file_path.write_text(lang_text, encoding="utf-8") + file_key = file_to_nav[file] + use_lang_file_to_nav[file] = file_key + if file_key: + composite_key = () + new_key = () + for key_part in file_key: + composite_key += (key_part,) + key_first_file = sections[composite_key] + if key_first_file in lang_file_to_nav: + new_key = lang_file_to_nav[key_first_file] + else: + new_key += (key_part,) + use_lang_file_to_nav[file] = new_key + key_to_section = {(): []} + for file, orig_file_key in file_to_nav.items(): + if file in use_lang_file_to_nav: + file_key = use_lang_file_to_nav[file] + else: + file_key = orig_file_key + section = get_key_section(key_to_section=key_to_section, key=file_key) + section.append(file) + new_nav = key_to_section[()] + export_lang_nav = [lang_nav[0], nav[1]] + new_nav + lang_config["nav"] = export_lang_nav + build_lang_config_path: Path = build_lang_path / mkdocs_name + build_lang_config_path.write_text( + yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", + ) current_dir = os.getcwd() os.chdir(build_lang_path) - subprocess.run(["mkdocs", "build", "--site-dir", dist_path], check=True) + shutil.rmtree(build_site_dist_path, ignore_errors=True) + shutil.rmtree(dist_path, ignore_errors=True) + subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) + shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) os.chdir(current_dir) typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) @@ -271,18 +280,8 @@ def build_all(): Build mkdocs site for en, and then build each language inside, end result is located at directory ./site/ with each language inside. """ - site_path = Path("site").absolute() update_languages(lang=None) - current_dir = os.getcwd() - os.chdir(en_docs_path) - typer.echo("Building docs for: en") - subprocess.run(["mkdocs", "build", "--site-dir", site_path], check=True) - os.chdir(current_dir) - langs = [] - for lang in get_lang_paths(): - if lang == en_docs_path or not lang.is_dir(): - continue - langs.append(lang.name) + langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 typer.echo(f"Using process pool size: {process_pool_size}") @@ -397,6 +396,15 @@ def update_config(lang: str): ) +@app.command() +def langs_json(): + langs = [] + for lang_path in get_lang_paths(): + if lang_path.is_dir(): + langs.append(lang_path.name) + print(json.dumps(langs)) + + def get_key_section( *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] ) -> list: diff --git a/scripts/zip-docs.sh b/scripts/zip-docs.sh deleted file mode 100644 index 47c3b0977..000000000 --- a/scripts/zip-docs.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -x -set -e - -cd ./site - -if [ -f docs.zip ]; then - rm -rf docs.zip -fi -zip -r docs.zip ./ From 7d865c9487eb8d7e6bcb508b933b17a5a6e8586b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 00:00:47 +0000 Subject: [PATCH 1107/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6f43126b0..0a5f51e98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). * 👷 Build and deploy docs only on docs changes. PR [#9728](https://github.com/tiangolo/fastapi/pull/9728) by [@tiangolo](https://github.com/tiangolo). From dd590f46ad932533bd6f28ea388a6e687683fb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 14:28:43 +0200 Subject: [PATCH 1108/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20MkDocs=20for?= =?UTF-8?q?=20other=20languages=20(#9734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 4 ++-- docs/cs/mkdocs.yml | 10 ++++++++-- docs/de/mkdocs.yml | 4 ++-- docs/em/mkdocs.yml | 7 +++++-- docs/es/mkdocs.yml | 4 ++-- docs/fa/mkdocs.yml | 4 ++-- docs/fr/mkdocs.yml | 4 ++-- docs/he/mkdocs.yml | 4 ++-- docs/hy/mkdocs.yml | 4 ++-- docs/id/mkdocs.yml | 4 ++-- docs/it/mkdocs.yml | 4 ++-- docs/ja/mkdocs.yml | 4 ++-- docs/ko/mkdocs.yml | 4 ++-- docs/lo/mkdocs.yml | 7 +++++-- docs/nl/mkdocs.yml | 4 ++-- docs/pl/mkdocs.yml | 4 ++-- docs/pt/mkdocs.yml | 4 ++-- docs/ru/mkdocs.yml | 4 ++-- docs/sq/mkdocs.yml | 4 ++-- docs/sv/mkdocs.yml | 4 ++-- docs/ta/mkdocs.yml | 4 ++-- docs/tr/mkdocs.yml | 4 ++-- docs/uk/mkdocs.yml | 4 ++-- docs/zh/mkdocs.yml | 4 ++-- 24 files changed, 60 insertions(+), 48 deletions(-) diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index 1d2930494..b846b91f8 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index 539d7d65d..c303d8f6a 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -42,6 +42,7 @@ nav: - az: /az/ - cs: /cs/ - de: /de/ + - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ @@ -51,6 +52,7 @@ nav: - it: /it/ - ja: /ja/ - ko: /ko/ + - lo: /lo/ - nl: /nl/ - pl: /pl/ - pt: /pt/ @@ -108,6 +110,8 @@ extra: name: cs - link: /de/ name: de + - link: /em/ + name: 😉 - link: /es/ name: es - español - link: /fa/ @@ -126,6 +130,8 @@ extra: name: ja - 日本語 - link: /ko/ name: ko - 한국어 + - link: /lo/ + name: lo - ພາສາລາວ - link: /nl/ name: nl - link: /pl/ diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index e475759a8..4be982509 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index 2c48de93a..bceef0d65 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -212,6 +213,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index 8152c91e3..e01f55b3a 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 2a966f664..5c5b5e3e1 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 0b73d3cae..5714a74cb 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index b8a674812..39e533342 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index 19d747c31..64e5ab876 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 460cb6914..acd93df48 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index b3a48482b..4074dff5a 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 91b9a6658..56dc4ff4b 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index aec1c7569..d91f0dd12 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 450ebcd2b..2ec3d6a2f 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight @@ -40,6 +40,7 @@ nav: - Languages: - en: / - az: /az/ + - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ @@ -105,6 +106,8 @@ extra: name: en - English - link: /az/ name: az + - link: /cs/ + name: cs - link: /de/ name: de - link: /em/ diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 96c93abff..52039bbb5 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 5ca1bbfef..3b1e82c66 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 023944618..fc933db94 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 4a7512ac0..dbae5ac95 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 092c50816..d3038644f 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 215b32f18..1409b49dc 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index 4b96d2cad..5c63d659f 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 5811f793e..125341fc6 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 5e22570b1..33e6fff40 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index a6afb3039..b64228d2c 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -11,14 +11,14 @@ theme: accent: amber toggle: icon: material/lightbulb - name: Switch to light mode + name: Switch to dark mode - media: '(prefers-color-scheme: dark)' scheme: slate primary: teal accent: amber toggle: icon: material/lightbulb-outline - name: Switch to dark mode + name: Switch to light mode features: - search.suggest - search.highlight From 8cee653ad820761efaf1eca1f31971335fd15c94 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 12:29:17 +0000 Subject: [PATCH 1109/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0a5f51e98..816765a8c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade MkDocs and MkDocs Material. PR [#9729](https://github.com/tiangolo/fastapi/pull/9729) by [@tiangolo](https://github.com/tiangolo). From dfa56f743ac0443c3f252b9e98ce925c4d630620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 14:30:57 +0200 Subject: [PATCH 1110/1881] =?UTF-8?q?=F0=9F=91=B7=20Make=20cron=20jobs=20r?= =?UTF-8?q?un=20only=20on=20main=20repo,=20not=20on=20forks,=20to=20avoid?= =?UTF-8?q?=20error=20notifications=20from=20missing=20tokens=20(#9735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 3 ++- .github/workflows/label-approved.yml | 1 + .github/workflows/people.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 617105b6e..324623103 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -2,7 +2,7 @@ name: Issue Manager on: schedule: - - cron: "0 0 * * *" + - cron: "10 3 * * *" issue_comment: types: - created @@ -16,6 +16,7 @@ on: jobs: issue-manager: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: tiangolo/issue-manager@0.4.0 diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 4a73b02aa..976d29f74 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -6,6 +6,7 @@ on: jobs: label-approved: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: docker://tiangolo/label-approved:0.0.2 diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index b167c268f..15ea464a1 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -12,6 +12,7 @@ on: jobs: fastapi-people: + if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 From 3aea9acc6879be81d25209937ec15502abb49958 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 12:31:54 +0000 Subject: [PATCH 1111/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 816765a8c..ccf12b334 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove old internal GitHub Action watch-previews that is no longer needed. PR [#9730](https://github.com/tiangolo/fastapi/pull/9730) by [@tiangolo](https://github.com/tiangolo). From 51d3a8ff127fd1ba6c34039961debd38597e403d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 24 Jun 2023 16:47:15 +0200 Subject: [PATCH 1112/1881] =?UTF-8?q?=F0=9F=94=A8=20Add=20MkDocs=20hook=20?= =?UTF-8?q?that=20renames=20sections=20based=20on=20the=20first=20index=20?= =?UTF-8?q?file=20(#9737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/mkdocs.yml | 3 ++ docs/cs/mkdocs.yml | 3 ++ docs/de/mkdocs.yml | 3 ++ docs/em/docs/advanced/index.md | 2 +- docs/em/docs/advanced/security/index.md | 2 +- docs/em/docs/deployment/index.md | 2 +- docs/em/docs/tutorial/dependencies/index.md | 2 +- docs/em/docs/tutorial/index.md | 2 +- docs/em/docs/tutorial/security/index.md | 2 +- docs/em/mkdocs.yml | 3 ++ docs/en/docs/advanced/index.md | 2 +- docs/en/docs/advanced/security/index.md | 2 +- docs/en/docs/deployment/index.md | 2 +- docs/en/docs/tutorial/dependencies/index.md | 2 +- docs/en/docs/tutorial/index.md | 2 +- docs/en/docs/tutorial/security/index.md | 2 +- docs/en/mkdocs.yml | 3 ++ docs/es/docs/advanced/index.md | 2 +- docs/es/docs/tutorial/index.md | 2 +- docs/es/mkdocs.yml | 3 ++ docs/fa/mkdocs.yml | 3 ++ docs/fr/docs/advanced/index.md | 2 +- docs/fr/docs/deployment/index.md | 2 +- docs/fr/mkdocs.yml | 3 ++ docs/he/mkdocs.yml | 3 ++ docs/hy/mkdocs.yml | 3 ++ docs/id/mkdocs.yml | 3 ++ docs/it/mkdocs.yml | 3 ++ docs/ja/docs/advanced/index.md | 2 +- docs/ja/docs/deployment/index.md | 2 +- docs/ja/docs/tutorial/index.md | 2 +- docs/ja/mkdocs.yml | 3 ++ docs/ko/docs/tutorial/index.md | 2 +- docs/ko/mkdocs.yml | 3 ++ docs/lo/mkdocs.yml | 3 ++ docs/nl/mkdocs.yml | 3 ++ docs/pl/docs/tutorial/index.md | 2 +- docs/pl/mkdocs.yml | 3 ++ docs/pt/docs/advanced/index.md | 2 +- docs/pt/docs/deployment/index.md | 2 +- docs/pt/docs/tutorial/index.md | 2 +- docs/pt/docs/tutorial/security/index.md | 2 +- docs/pt/mkdocs.yml | 3 ++ docs/ru/docs/deployment/index.md | 2 +- docs/ru/docs/tutorial/index.md | 2 +- docs/ru/mkdocs.yml | 3 ++ docs/sq/mkdocs.yml | 3 ++ docs/sv/mkdocs.yml | 3 ++ docs/ta/mkdocs.yml | 3 ++ docs/tr/mkdocs.yml | 3 ++ docs/uk/mkdocs.yml | 3 ++ docs/zh/docs/advanced/index.md | 2 +- docs/zh/docs/advanced/security/index.md | 2 +- docs/zh/docs/tutorial/dependencies/index.md | 2 +- docs/zh/docs/tutorial/index.md | 2 +- docs/zh/docs/tutorial/security/index.md | 2 +- docs/zh/mkdocs.yml | 3 ++ scripts/mkdocs_hooks.py | 38 +++++++++++++++++++++ 58 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 scripts/mkdocs_hooks.py diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index b846b91f8..c9f467768 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index c303d8f6a..358f0ccf2 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index 4be982509..bdbaa36e3 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -159,3 +160,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/em/docs/advanced/index.md b/docs/em/docs/advanced/index.md index 6a43a09e7..abe8d357c 100644 --- a/docs/em/docs/advanced/index.md +++ b/docs/em/docs/advanced/index.md @@ -1,4 +1,4 @@ -# 🏧 👩‍💻 🦮 - 🎶 +# 🏧 👩‍💻 🦮 ## 🌖 ⚒ diff --git a/docs/em/docs/advanced/security/index.md b/docs/em/docs/advanced/security/index.md index 20ee85553..f2bb66df4 100644 --- a/docs/em/docs/advanced/security/index.md +++ b/docs/em/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# 🏧 💂‍♂ - 🎶 +# 🏧 💂‍♂ ## 🌖 ⚒ diff --git a/docs/em/docs/deployment/index.md b/docs/em/docs/deployment/index.md index 1010c589f..9bcf427b6 100644 --- a/docs/em/docs/deployment/index.md +++ b/docs/em/docs/deployment/index.md @@ -1,4 +1,4 @@ -# 🛠️ - 🎶 +# 🛠️ 🛠️ **FastAPI** 🈸 📶 ⏩. diff --git a/docs/em/docs/tutorial/dependencies/index.md b/docs/em/docs/tutorial/dependencies/index.md index f1c28c573..ffd38d716 100644 --- a/docs/em/docs/tutorial/dependencies/index.md +++ b/docs/em/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# 🔗 - 🥇 🔁 +# 🔗 **FastAPI** ✔️ 📶 🏋️ ✋️ 🏋️ **🔗 💉** ⚙️. diff --git a/docs/em/docs/tutorial/index.md b/docs/em/docs/tutorial/index.md index 8536dc3ee..26b4c1913 100644 --- a/docs/em/docs/tutorial/index.md +++ b/docs/em/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 🔰 - 👩‍💻 🦮 - 🎶 +# 🔰 - 👩‍💻 🦮 👉 🔰 🎦 👆 ❔ ⚙️ **FastAPI** ⏮️ 🌅 🚮 ⚒, 🔁 🔁. diff --git a/docs/em/docs/tutorial/security/index.md b/docs/em/docs/tutorial/security/index.md index 5b507af3e..d76f7203f 100644 --- a/docs/em/docs/tutorial/security/index.md +++ b/docs/em/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# 💂‍♂ 🎶 +# 💂‍♂ 📤 📚 🌌 🍵 💂‍♂, 🤝 & ✔. diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index bceef0d65..8b6b3997c 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -265,3 +266,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/en/docs/advanced/index.md b/docs/en/docs/advanced/index.md index 917f4a62e..467f0833e 100644 --- a/docs/en/docs/advanced/index.md +++ b/docs/en/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Advanced User Guide - Intro +# Advanced User Guide ## Additional Features diff --git a/docs/en/docs/advanced/security/index.md b/docs/en/docs/advanced/security/index.md index 0c94986b5..c18baf64b 100644 --- a/docs/en/docs/advanced/security/index.md +++ b/docs/en/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# Advanced Security - Intro +# Advanced Security ## Additional Features diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index f0fd001cd..6c43d8abb 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Deployment - Intro +# Deployment Deploying a **FastAPI** application is relatively easy. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index 4f5ecea66..f6f4bced0 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# Dependencies - First Steps +# Dependencies **FastAPI** has a very powerful but intuitive **Dependency Injection** system. diff --git a/docs/en/docs/tutorial/index.md b/docs/en/docs/tutorial/index.md index 8b4a9df9b..75665324d 100644 --- a/docs/en/docs/tutorial/index.md +++ b/docs/en/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - User Guide - Intro +# Tutorial - User Guide This tutorial shows you how to use **FastAPI** with most of its features, step by step. diff --git a/docs/en/docs/tutorial/security/index.md b/docs/en/docs/tutorial/security/index.md index 035b31736..659a94dc3 100644 --- a/docs/en/docs/tutorial/security/index.md +++ b/docs/en/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Security Intro +# Security There are many ways to handle security, authentication and authorization. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 73df174d1..40dfb1661 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -265,3 +266,5 @@ extra_css: extra_javascript: - js/termynal.js - js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/es/docs/advanced/index.md b/docs/es/docs/advanced/index.md index 1bee540f2..ba1d20b0d 100644 --- a/docs/es/docs/advanced/index.md +++ b/docs/es/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guía de Usuario Avanzada - Introducción +# Guía de Usuario Avanzada ## Características Adicionales diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md index e3671f381..1cff8b4e3 100644 --- a/docs/es/docs/tutorial/index.md +++ b/docs/es/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guía de Usuario - Introducción +# Tutorial - Guía de Usuario Este tutorial te muestra cómo usar **FastAPI** con la mayoría de sus características paso a paso. diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index e01f55b3a..d8aa9c494 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -168,3 +169,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 5c5b5e3e1..287521ab3 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/fr/docs/advanced/index.md b/docs/fr/docs/advanced/index.md index 41737889a..f4fa5ecf6 100644 --- a/docs/fr/docs/advanced/index.md +++ b/docs/fr/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guide de l'utilisateur avancé - Introduction +# Guide de l'utilisateur avancé ## Caractéristiques supplémentaires diff --git a/docs/fr/docs/deployment/index.md b/docs/fr/docs/deployment/index.md index e855adfa3..e2014afe9 100644 --- a/docs/fr/docs/deployment/index.md +++ b/docs/fr/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Déploiement - Intro +# Déploiement Le déploiement d'une application **FastAPI** est relativement simple. diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 5714a74cb..67e5383ed 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -187,3 +188,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index 39e533342..b390875ea 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index 64e5ab876..e5af7dd30 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index acd93df48..6cc2cf045 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index 4074dff5a..f7de769ee 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ja/docs/advanced/index.md b/docs/ja/docs/advanced/index.md index 676f60359..0732fc405 100644 --- a/docs/ja/docs/advanced/index.md +++ b/docs/ja/docs/advanced/index.md @@ -1,4 +1,4 @@ -# ユーザーガイド 応用編 +# 高度なユーザーガイド ## さらなる機能 diff --git a/docs/ja/docs/deployment/index.md b/docs/ja/docs/deployment/index.md index 40710a93a..897956e38 100644 --- a/docs/ja/docs/deployment/index.md +++ b/docs/ja/docs/deployment/index.md @@ -1,4 +1,4 @@ -# デプロイ - イントロ +# デプロイ **FastAPI** 製のアプリケーションは比較的容易にデプロイできます。 diff --git a/docs/ja/docs/tutorial/index.md b/docs/ja/docs/tutorial/index.md index a2dd59c9b..856cde44b 100644 --- a/docs/ja/docs/tutorial/index.md +++ b/docs/ja/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# チュートリアル - ユーザーガイド - はじめに +# チュートリアル - ユーザーガイド このチュートリアルは**FastAPI**のほぼすべての機能の使い方を段階的に紹介します。 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index 56dc4ff4b..f21d731f9 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -202,3 +203,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index d6db525e8..deb5ca8f2 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 자습서 - 사용자 안내서 - 도입부 +# 자습서 - 사용자 안내서 이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index d91f0dd12..0a1e6b639 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -172,3 +173,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 2ec3d6a2f..7f9253d6c 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index 52039bbb5..e74e1a6e3 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/pl/docs/tutorial/index.md b/docs/pl/docs/tutorial/index.md index ed8752a95..f8c5c6022 100644 --- a/docs/pl/docs/tutorial/index.md +++ b/docs/pl/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Samouczek - Wprowadzenie +# Samouczek Ten samouczek pokaże Ci, krok po kroku, jak używać większości funkcji **FastAPI**. diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 3b1e82c66..588eddf97 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -162,3 +163,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/pt/docs/advanced/index.md b/docs/pt/docs/advanced/index.md index d1a57c6d1..7e276f732 100644 --- a/docs/pt/docs/advanced/index.md +++ b/docs/pt/docs/advanced/index.md @@ -1,4 +1,4 @@ -# Guia de Usuário Avançado - Introdução +# Guia de Usuário Avançado ## Recursos Adicionais diff --git a/docs/pt/docs/deployment/index.md b/docs/pt/docs/deployment/index.md index 1ff0e44a0..6b4290d1d 100644 --- a/docs/pt/docs/deployment/index.md +++ b/docs/pt/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Implantação - Introdução +# Implantação A implantação de uma aplicação **FastAPI** é relativamente simples. diff --git a/docs/pt/docs/tutorial/index.md b/docs/pt/docs/tutorial/index.md index b1abd32bc..5fc0485a0 100644 --- a/docs/pt/docs/tutorial/index.md +++ b/docs/pt/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Tutorial - Guia de Usuário - Introdução +# Tutorial - Guia de Usuário Esse tutorial mostra como usar o **FastAPI** com a maior parte de seus recursos, passo a passo. diff --git a/docs/pt/docs/tutorial/security/index.md b/docs/pt/docs/tutorial/security/index.md index 70f864040..f94a8ab62 100644 --- a/docs/pt/docs/tutorial/security/index.md +++ b/docs/pt/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# Introdução à segurança +# Segurança Há várias formas de lidar segurança, autenticação e autorização. diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index fc933db94..54520642e 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -199,3 +200,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ru/docs/deployment/index.md b/docs/ru/docs/deployment/index.md index 4dc4e482e..d214a9d62 100644 --- a/docs/ru/docs/deployment/index.md +++ b/docs/ru/docs/deployment/index.md @@ -1,4 +1,4 @@ -# Развёртывание - Введение +# Развёртывание Развернуть приложение **FastAPI** довольно просто. diff --git a/docs/ru/docs/tutorial/index.md b/docs/ru/docs/tutorial/index.md index 4277a6c4f..ea3a1c37a 100644 --- a/docs/ru/docs/tutorial/index.md +++ b/docs/ru/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# Учебник - Руководство пользователя - Введение +# Учебник - Руководство пользователя В этом руководстве шаг за шагом показано, как использовать **FastApi** с большинством его функций. diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index dbae5ac95..66c7687b0 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -197,3 +198,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index d3038644f..64f3dec2e 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 1409b49dc..8604a06f6 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml index 5c63d659f..4000d9a41 100644 --- a/docs/ta/mkdocs.yml +++ b/docs/ta/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 125341fc6..408b3ec29 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -163,3 +164,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 33e6fff40..49516cebf 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -158,3 +159,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/docs/zh/docs/advanced/index.md b/docs/zh/docs/advanced/index.md index d71838cd7..824f91f47 100644 --- a/docs/zh/docs/advanced/index.md +++ b/docs/zh/docs/advanced/index.md @@ -1,4 +1,4 @@ -# 高级用户指南 - 简介 +# 高级用户指南 ## 额外特性 diff --git a/docs/zh/docs/advanced/security/index.md b/docs/zh/docs/advanced/security/index.md index 962523c09..fdc8075c7 100644 --- a/docs/zh/docs/advanced/security/index.md +++ b/docs/zh/docs/advanced/security/index.md @@ -1,4 +1,4 @@ -# 高级安全 - 介绍 +# 高级安全 ## 附加特性 diff --git a/docs/zh/docs/tutorial/dependencies/index.md b/docs/zh/docs/tutorial/dependencies/index.md index c717da0f6..7a133061d 100644 --- a/docs/zh/docs/tutorial/dependencies/index.md +++ b/docs/zh/docs/tutorial/dependencies/index.md @@ -1,4 +1,4 @@ -# 依赖项 - 第一步 +# 依赖项 FastAPI 提供了简单易用,但功能强大的**依赖注入**系统。 diff --git a/docs/zh/docs/tutorial/index.md b/docs/zh/docs/tutorial/index.md index 6093caeb6..6180d3de3 100644 --- a/docs/zh/docs/tutorial/index.md +++ b/docs/zh/docs/tutorial/index.md @@ -1,4 +1,4 @@ -# 教程 - 用户指南 - 简介 +# 教程 - 用户指南 本教程将一步步向你展示如何使用 **FastAPI** 的绝大部分特性。 diff --git a/docs/zh/docs/tutorial/security/index.md b/docs/zh/docs/tutorial/security/index.md index 8f302a16c..0595f5f63 100644 --- a/docs/zh/docs/tutorial/security/index.md +++ b/docs/zh/docs/tutorial/security/index.md @@ -1,4 +1,4 @@ -# 安全性简介 +# 安全性 有许多方法可以处理安全性、身份认证和授权等问题。 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index b64228d2c..39f989790 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -23,6 +23,7 @@ theme: - search.suggest - search.highlight - content.tabs.link + - navigation.indexes icon: repo: fontawesome/brands/github-alt logo: https://fastapi.tiangolo.com/img/icon-white.svg @@ -223,3 +224,5 @@ extra_css: extra_javascript: - https://fastapi.tiangolo.com/js/termynal.js - https://fastapi.tiangolo.com/js/custom.js +hooks: +- ../../scripts/mkdocs_hooks.py diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py new file mode 100644 index 000000000..f09e9a99d --- /dev/null +++ b/scripts/mkdocs_hooks.py @@ -0,0 +1,38 @@ +from typing import Any, List, Union + +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import Files +from mkdocs.structure.nav import Link, Navigation, Section +from mkdocs.structure.pages import Page + + +def generate_renamed_section_items( + items: List[Union[Page, Section, Link]], *, config: MkDocsConfig +) -> List[Union[Page, Section, Link]]: + new_items: List[Union[Page, Section, Link]] = [] + for item in items: + if isinstance(item, Section): + new_title = item.title + new_children = generate_renamed_section_items(item.children, config=config) + first_child = new_children[0] + if isinstance(first_child, Page): + if first_child.file.src_path.endswith("index.md"): + # Read the source so that the title is parsed and available + first_child.read_source(config=config) + new_title = first_child.title or new_title + # Creating a new section makes it render it collapsed by default + # no idea why, so, let's just modify the existing one + # new_section = Section(title=new_title, children=new_children) + item.title = new_title + item.children = new_children + new_items.append(item) + else: + new_items.append(item) + return new_items + + +def on_nav( + nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any +) -> Navigation: + new_items = generate_renamed_section_items(nav.items, config=config) + return Navigation(items=new_items, pages=nav.pages) From c563b5bcf11a35f36a5f9345facf52b39ca7e9dd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 24 Jun 2023 14:47:59 +0000 Subject: [PATCH 1113/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ccf12b334..88f2ddf2d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). * 👷 Refactor Docs CI, run in multiple workers with a dynamic matrix to optimize speed. PR [#9732](https://github.com/tiangolo/fastapi/pull/9732) by [@tiangolo](https://github.com/tiangolo). From 5656ed09efea3451145087f63e402a0d024622b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 25 Jun 2023 14:33:58 +0200 Subject: [PATCH 1114/1881] =?UTF-8?q?=E2=9C=A8=20Refactor=20docs=20for=20b?= =?UTF-8?q?uilding=20scripts,=20use=20MkDocs=20hooks,=20simplify=20(remove?= =?UTF-8?q?)=20configs=20for=20languages=20(#9742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Add MkDocs hooks to re-use all config from en, and auto-generate missing docs files form en * 🔧 Update MkDocs config for es * 🔧 Simplify configs for all languages * ✨ Compute available languages from MkDocs Material for config overrides in hooks * 🔧 Update config for MkDocs for en, to make paths compatible for other languages * ♻️ Refactor scripts/docs.py to remove all custom logic that is now handled by the MkDocs hooks * 🔧 Remove ta language as it's incomplete (no translations and causing errors) * 🔥 Remove ta lang, no translations available * 🔥 Remove dummy overrides directories, no longer needed * ✨ Use the same missing-translation.md file contents for hooks * ⏪️ Restore and refactor new-lang command * 📝 Update docs for contributing with new simplified workflow for translations * 🔊 Enable logs so that MkDocs can show its standard output on the docs.py script --- docs/az/mkdocs.yml | 164 +-------------------- docs/az/overrides/.gitignore | 0 docs/cs/mkdocs.yml | 164 +-------------------- docs/cs/overrides/.gitignore | 0 docs/de/mkdocs.yml | 165 +-------------------- docs/de/overrides/.gitignore | 0 docs/em/mkdocs.yml | 271 +---------------------------------- docs/em/overrides/.gitignore | 0 docs/en/docs/contributing.md | 132 ++++++----------- docs/en/mkdocs.yml | 7 +- docs/es/mkdocs.yml | 174 +--------------------- docs/es/overrides/.gitignore | 0 docs/fa/mkdocs.yml | 164 +-------------------- docs/fa/overrides/.gitignore | 0 docs/fr/mkdocs.yml | 193 +------------------------ docs/fr/overrides/.gitignore | 0 docs/he/mkdocs.yml | 164 +-------------------- docs/he/overrides/.gitignore | 0 docs/hy/mkdocs.yml | 164 +-------------------- docs/hy/overrides/.gitignore | 0 docs/id/mkdocs.yml | 164 +-------------------- docs/id/overrides/.gitignore | 0 docs/it/mkdocs.yml | 164 +-------------------- docs/it/overrides/.gitignore | 0 docs/ja/mkdocs.yml | 208 +-------------------------- docs/ja/overrides/.gitignore | 0 docs/ko/mkdocs.yml | 178 +---------------------- docs/ko/overrides/.gitignore | 0 docs/lo/mkdocs.yml | 164 +-------------------- docs/lo/overrides/.gitignore | 0 docs/nl/mkdocs.yml | 164 +-------------------- docs/nl/overrides/.gitignore | 0 docs/pl/mkdocs.yml | 168 +--------------------- docs/pl/overrides/.gitignore | 0 docs/pt/mkdocs.yml | 205 +------------------------- docs/pt/overrides/.gitignore | 0 docs/ru/mkdocs.yml | 203 +------------------------- docs/ru/overrides/.gitignore | 0 docs/sq/mkdocs.yml | 164 +-------------------- docs/sq/overrides/.gitignore | 0 docs/sv/mkdocs.yml | 164 +-------------------- docs/sv/overrides/.gitignore | 0 docs/ta/mkdocs.yml | 163 --------------------- docs/ta/overrides/.gitignore | 0 docs/tr/mkdocs.yml | 169 +--------------------- docs/tr/overrides/.gitignore | 0 docs/uk/mkdocs.yml | 164 +-------------------- docs/uk/overrides/.gitignore | 0 docs/zh/mkdocs.yml | 229 +---------------------------- docs/zh/overrides/.gitignore | 0 scripts/docs.py | 245 +++++-------------------------- scripts/mkdocs_hooks.py | 96 ++++++++++++- 52 files changed, 202 insertions(+), 4572 deletions(-) delete mode 100644 docs/az/overrides/.gitignore delete mode 100644 docs/cs/overrides/.gitignore delete mode 100644 docs/de/overrides/.gitignore delete mode 100644 docs/em/overrides/.gitignore delete mode 100644 docs/es/overrides/.gitignore delete mode 100644 docs/fa/overrides/.gitignore delete mode 100644 docs/fr/overrides/.gitignore delete mode 100644 docs/he/overrides/.gitignore delete mode 100644 docs/hy/overrides/.gitignore delete mode 100644 docs/id/overrides/.gitignore delete mode 100644 docs/it/overrides/.gitignore delete mode 100644 docs/ja/overrides/.gitignore delete mode 100644 docs/ko/overrides/.gitignore delete mode 100644 docs/lo/overrides/.gitignore delete mode 100644 docs/nl/overrides/.gitignore delete mode 100644 docs/pl/overrides/.gitignore delete mode 100644 docs/pt/overrides/.gitignore delete mode 100644 docs/ru/overrides/.gitignore delete mode 100644 docs/sq/overrides/.gitignore delete mode 100644 docs/sv/overrides/.gitignore delete mode 100644 docs/ta/mkdocs.yml delete mode 100644 docs/ta/overrides/.gitignore delete mode 100644 docs/tr/overrides/.gitignore delete mode 100644 docs/uk/overrides/.gitignore delete mode 100644 docs/zh/overrides/.gitignore diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml index c9f467768..de18856f4 100644 --- a/docs/az/mkdocs.yml +++ b/docs/az/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/az/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/az/overrides/.gitignore b/docs/az/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml index 358f0ccf2..de18856f4 100644 --- a/docs/cs/mkdocs.yml +++ b/docs/cs/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/cs/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: cs -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/cs/overrides/.gitignore b/docs/cs/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/de/mkdocs.yml b/docs/de/mkdocs.yml index bdbaa36e3..de18856f4 100644 --- a/docs/de/mkdocs.yml +++ b/docs/de/mkdocs.yml @@ -1,164 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/de/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: de -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/de/overrides/.gitignore b/docs/de/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/em/mkdocs.yml b/docs/em/mkdocs.yml index 8b6b3997c..de18856f4 100644 --- a/docs/em/mkdocs.yml +++ b/docs/em/mkdocs.yml @@ -1,270 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/em/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- 🔰 - 👩‍💻 🦮: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - 🔗: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - tutorial/dependencies/dependencies-with-yield.md - - 💂‍♂: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/background-tasks.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 🏧 👩‍💻 🦮: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/additional-responses.md - - advanced/response-cookies.md - - advanced/response-headers.md - - advanced/response-change-status-code.md - - advanced/advanced-dependencies.md - - 🏧 💂‍♂: - - advanced/security/index.md - - advanced/security/oauth2-scopes.md - - advanced/security/http-basic-auth.md - - advanced/using-request-directly.md - - advanced/dataclasses.md - - advanced/middleware.md - - advanced/sql-databases-peewee.md - - advanced/async-sql-databases.md - - advanced/nosql-databases.md - - advanced/sub-applications.md - - advanced/behind-a-proxy.md - - advanced/templates.md - - advanced/graphql.md - - advanced/websockets.md - - advanced/events.md - - advanced/custom-request-and-route.md - - advanced/testing-websockets.md - - advanced/testing-events.md - - advanced/testing-dependencies.md - - advanced/testing-database.md - - advanced/async-tests.md - - advanced/settings.md - - advanced/conditional-openapi.md - - advanced/extending-openapi.md - - advanced/openapi-callbacks.md - - advanced/wsgi.md - - advanced/generate-clients.md -- async.md -- 🛠️: - - deployment/index.md - - deployment/versions.md - - deployment/https.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 -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -- release-notes.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/em/overrides/.gitignore b/docs/em/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 660914a08..f968489ae 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -195,6 +195,21 @@ It will serve the documentation on `http://127.0.0.1:8008`. That way, you can edit the documentation/source files and see the changes live. +!!! tip + Alternatively, you can perform the same steps that scripts does manually. + + Go into the language directory, for the main docs in English it's at `docs/en/`: + + ```console + $ cd docs/en/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + #### Typer CLI (optional) The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. @@ -245,13 +260,15 @@ Here are the steps to help with translations. Check the docs about adding a pull request review to approve it or request changes. -* Check in the issues to see if there's one coordinating translations for your language. +* Check if there's a GitHub Discussion to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. * Add a single pull request per page translated. That will make it much easier for others to review it. For the languages I don't speak, I'll wait for several others to review the translation before merging. * You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it. + * You could check in the GitHub Discussions for your language. + * Or you can filter the existing PRs by the ones with the label for your language, for example, for Spanish, the label is `lang-es`. * Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work. @@ -283,11 +300,24 @@ $ python ./scripts/docs.py live es
+!!! tip + Alternatively, you can perform the same steps that scripts does manually. + + Go into the language directory, for the Spanish translations it's at `docs/es/`: + + ```console + $ cd docs/es/ + ``` + + Then run `mkdocs` in that directory: + + ```console + $ mkdocs serve --dev-addr 8008 + ``` + Now you can go to http://127.0.0.1:8008 and see your changes live. -If you look at the FastAPI docs website, you will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. - -But when you run it locally like this, you will only see the pages that are already translated. +You will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. @@ -306,46 +336,6 @@ docs/es/docs/features.md !!! tip Notice that the only change in the path and file name is the language code, from `en` to `es`. -* Now open the MkDocs config file for English at: - -``` -docs/en/mkdocs.yml -``` - -* Find the place where that `docs/features.md` is located in the config file. Somewhere like: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -* Open the MkDocs config file for the language you are editing, e.g.: - -``` -docs/es/mkdocs.yml -``` - -* Add it there at the exact same location it was for English, e.g.: - -```YAML hl_lines="8" -site_name: FastAPI -# More stuff -nav: -- FastAPI: index.md -- Languages: - - en: / - - es: /es/ -- features.md -``` - -Make sure that if there are other entries, the new entry with your translation is exactly in the same order as in the English version. - If you go to your browser you will see that now the docs show your new section. 🎉 Now you can translate it all and see how it looks as you save the file. @@ -367,55 +357,32 @@ The next step is to run the script to generate a new translation directory: $ python ./scripts/docs.py new-lang ht Successfully initialized: docs/ht -Updating ht -Updating en ```
Now you can check in your code editor the newly created directory `docs/ht/`. +That command created a file `docs/ht/mkdocs.yml` with a simple config that inherits everything from the `en` version: + +```yaml +INHERIT: ../en/mkdocs.yml +``` + !!! tip - Create a first pull request with just this, to set up the configuration for the new language, before adding translations. + You could also simply create that file with those contents manually. - That way others can help with other pages while you work on the first one. 🚀 +That command also created a dummy file `docs/ht/index.md` for the main page, you can start by translating that one. -Start by translating the main page, `docs/ht/index.md`. +You can continue with the previous instructions for an "Existing Language" for that process. -Then you can continue with the previous instructions, for an "Existing Language". - -##### New Language not supported - -If when running the live server script you get an error about the language not being supported, something like: - -``` - raise TemplateNotFound(template) -jinja2.exceptions.TemplateNotFound: partials/language/xx.html -``` - -That means that the theme doesn't support that language (in this case, with a fake 2-letter code of `xx`). - -But don't worry, you can set the theme language to English and then translate the content of the docs. - -If you need to do that, edit the `mkdocs.yml` for your new language, it will have something like: - -```YAML hl_lines="5" -site_name: FastAPI -# More stuff -theme: - # More stuff - language: xx -``` - -Change that language from `xx` (from your language code) to `en`. - -Then you can start the live server again. +You can make the first pull request with those two files, `docs/ht/mkdocs.yml` and `docs/ht/index.md`. 🎉 #### Preview the result -When you use the script at `./scripts/docs.py` with the `live` command it only shows the files and translations available for the current language. +You can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). -But once you are done, you can test it all as it would look online. +Once you are done, you can also test it all as it would look online, including all the other languages. To do that, first build all the docs: @@ -425,19 +392,14 @@ To do that, first build all the docs: // Use the command "build-all", this will take a bit $ python ./scripts/docs.py build-all -Updating es -Updating en Building docs for: en Building docs for: es Successfully built docs for: es -Copying en index.md to README.md ```
-That generates all the docs at `./docs_build/` for each language. This includes adding any files with missing translations, with a note saying that "this file doesn't have a translation yet". But you don't have to do anything with that directory. - -Then it builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. +This builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. Then you can serve that with the command `serve`: diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 40dfb1661..a21848a66 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -3,7 +3,7 @@ site_description: FastAPI framework, high performance, easy to learn, fast to co site_url: https://fastapi.tiangolo.com/ theme: name: material - custom_dir: overrides + custom_dir: ../en/overrides palette: - media: '(prefers-color-scheme: light)' scheme: default @@ -35,7 +35,7 @@ edit_uri: '' plugins: - search - markdownextradata: - data: data + data: ../en/data nav: - FastAPI: index.md - Languages: @@ -60,7 +60,6 @@ nav: - ru: /ru/ - sq: /sq/ - sv: /sv/ - - ta: /ta/ - tr: /tr/ - uk: /uk/ - zh: /zh/ @@ -252,8 +251,6 @@ extra: name: sq - shqip - link: /sv/ name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - link: /tr/ name: tr - Türkçe - link: /uk/ diff --git a/docs/es/mkdocs.yml b/docs/es/mkdocs.yml index d8aa9c494..de18856f4 100644 --- a/docs/es/mkdocs.yml +++ b/docs/es/mkdocs.yml @@ -1,173 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/es/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: es -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- python-types.md -- Tutorial - Guía de Usuario: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md -- Guía de Usuario Avanzada: - - advanced/index.md -- async.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/es/overrides/.gitignore b/docs/es/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/fa/mkdocs.yml b/docs/fa/mkdocs.yml index 287521ab3..de18856f4 100644 --- a/docs/fa/mkdocs.yml +++ b/docs/fa/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fa/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fa -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/fa/overrides/.gitignore b/docs/fa/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/fr/mkdocs.yml b/docs/fr/mkdocs.yml index 67e5383ed..de18856f4 100644 --- a/docs/fr/mkdocs.yml +++ b/docs/fr/mkdocs.yml @@ -1,192 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/fr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: fr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Tutoriel - Guide utilisateur: - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/background-tasks.md - - tutorial/debugging.md -- Guide utilisateur avancé: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/additional-responses.md -- async.md -- Déploiement: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/fr/overrides/.gitignore b/docs/fr/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/he/mkdocs.yml b/docs/he/mkdocs.yml index b390875ea..de18856f4 100644 --- a/docs/he/mkdocs.yml +++ b/docs/he/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/he/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: he -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/he/overrides/.gitignore b/docs/he/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml index e5af7dd30..de18856f4 100644 --- a/docs/hy/mkdocs.yml +++ b/docs/hy/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/hy/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: hy -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/hy/overrides/.gitignore b/docs/hy/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/id/mkdocs.yml b/docs/id/mkdocs.yml index 6cc2cf045..de18856f4 100644 --- a/docs/id/mkdocs.yml +++ b/docs/id/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/id/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: id -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/id/overrides/.gitignore b/docs/id/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml index f7de769ee..de18856f4 100644 --- a/docs/it/mkdocs.yml +++ b/docs/it/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/it/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: it -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/it/overrides/.gitignore b/docs/it/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ja/mkdocs.yml b/docs/ja/mkdocs.yml index f21d731f9..de18856f4 100644 --- a/docs/ja/mkdocs.yml +++ b/docs/ja/mkdocs.yml @@ -1,207 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ja/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: ja -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- チュートリアル - ユーザーガイド: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/request-forms.md - - tutorial/body-updates.md - - セキュリティ: - - tutorial/security/first-steps.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 高度なユーザーガイド: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/nosql-databases.md - - advanced/websockets.md - - advanced/conditional-openapi.md -- async.md -- デプロイ: - - deployment/index.md - - deployment/versions.md - - deployment/deta.md - - deployment/docker.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ja/overrides/.gitignore b/docs/ja/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ko/mkdocs.yml b/docs/ko/mkdocs.yml index 0a1e6b639..de18856f4 100644 --- a/docs/ko/mkdocs.yml +++ b/docs/ko/mkdocs.yml @@ -1,177 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ko/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- 자습서 - 사용자 안내서: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/header-params.md - - tutorial/path-params-numeric-validations.md - - tutorial/response-status-code.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/encoder.md - - tutorial/cors.md - - 의존성: - - tutorial/dependencies/classes-as-dependencies.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ko/overrides/.gitignore b/docs/ko/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml index 7f9253d6c..de18856f4 100644 --- a/docs/lo/mkdocs.yml +++ b/docs/lo/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/lo/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/lo/overrides/.gitignore b/docs/lo/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml index e74e1a6e3..de18856f4 100644 --- a/docs/nl/mkdocs.yml +++ b/docs/nl/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/nl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: nl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/nl/overrides/.gitignore b/docs/nl/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/pl/mkdocs.yml b/docs/pl/mkdocs.yml index 588eddf97..de18856f4 100644 --- a/docs/pl/mkdocs.yml +++ b/docs/pl/mkdocs.yml @@ -1,167 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pl/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pl -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- Samouczek: - - tutorial/index.md - - tutorial/first-steps.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/pl/overrides/.gitignore b/docs/pl/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/pt/mkdocs.yml b/docs/pt/mkdocs.yml index 54520642e..de18856f4 100644 --- a/docs/pt/mkdocs.yml +++ b/docs/pt/mkdocs.yml @@ -1,204 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/pt/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: pt -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- Tutorial - Guia de Usuário: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/extra-data-types.md - - tutorial/extra-models.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/path-operation-configuration.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/encoder.md - - Segurança: - - tutorial/security/index.md - - tutorial/background-tasks.md - - tutorial/static-files.md - - Guia de Usuário Avançado: - - advanced/index.md - - advanced/events.md -- Implantação: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/deta.md - - deployment/docker.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/pt/overrides/.gitignore b/docs/pt/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ru/mkdocs.yml b/docs/ru/mkdocs.yml index 66c7687b0..de18856f4 100644 --- a/docs/ru/mkdocs.yml +++ b/docs/ru/mkdocs.yml @@ -1,202 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ru/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: ru -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Учебник - руководство пользователя: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-fields.md - - tutorial/background-tasks.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/testing.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/query-params.md - - tutorial/body-multiple-params.md - - tutorial/metadata.md - - tutorial/path-operation-configuration.md - - tutorial/cors.md - - tutorial/static-files.md - - tutorial/debugging.md - - tutorial/schema-extra-example.md - - tutorial/body-nested-models.md -- async.md -- Развёртывание: - - deployment/index.md - - deployment/versions.md - - deployment/concepts.md - - deployment/https.md - - deployment/manually.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- contributing.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/ru/overrides/.gitignore b/docs/ru/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml index 64f3dec2e..de18856f4 100644 --- a/docs/sq/mkdocs.yml +++ b/docs/sq/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sq/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/sq/overrides/.gitignore b/docs/sq/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml index 8604a06f6..de18856f4 100644 --- a/docs/sv/mkdocs.yml +++ b/docs/sv/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/sv/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: sv -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/sv/overrides/.gitignore b/docs/sv/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/ta/mkdocs.yml b/docs/ta/mkdocs.yml deleted file mode 100644 index 4000d9a41..000000000 --- a/docs/ta/mkdocs.yml +++ /dev/null @@ -1,163 +0,0 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/ta/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: en -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py diff --git a/docs/ta/overrides/.gitignore b/docs/ta/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/tr/mkdocs.yml b/docs/tr/mkdocs.yml index 408b3ec29..de18856f4 100644 --- a/docs/tr/mkdocs.yml +++ b/docs/tr/mkdocs.yml @@ -1,168 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/tr/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: tr -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- Tutorial - User Guide: - - tutorial/first-steps.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/tr/overrides/.gitignore b/docs/tr/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml index 49516cebf..de18856f4 100644 --- a/docs/uk/mkdocs.yml +++ b/docs/uk/mkdocs.yml @@ -1,163 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/uk/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: uk -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/uk/overrides/.gitignore b/docs/uk/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/zh/mkdocs.yml b/docs/zh/mkdocs.yml index 39f989790..de18856f4 100644 --- a/docs/zh/mkdocs.yml +++ b/docs/zh/mkdocs.yml @@ -1,228 +1 @@ -site_name: FastAPI -site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production -site_url: https://fastapi.tiangolo.com/zh/ -theme: - name: material - custom_dir: overrides - palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: teal - accent: amber - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: teal - accent: amber - toggle: - icon: material/lightbulb-outline - name: Switch to light mode - features: - - search.suggest - - search.highlight - - content.tabs.link - - navigation.indexes - icon: - repo: fontawesome/brands/github-alt - logo: https://fastapi.tiangolo.com/img/icon-white.svg - favicon: https://fastapi.tiangolo.com/img/favicon.png - language: zh -repo_name: tiangolo/fastapi -repo_url: https://github.com/tiangolo/fastapi -edit_uri: '' -plugins: -- search -- markdownextradata: - data: data -nav: -- FastAPI: index.md -- Languages: - - en: / - - az: /az/ - - cs: /cs/ - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - hy: /hy/ - - id: /id/ - - it: /it/ - - ja: /ja/ - - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - - ta: /ta/ - - tr: /tr/ - - uk: /uk/ - - zh: /zh/ -- features.md -- fastapi-people.md -- python-types.md -- 教程 - 用户指南: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/middleware.md - - tutorial/body-nested-models.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - 依赖项: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - 安全性: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- 高级用户指南: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/response-cookies.md - - advanced/response-change-status-code.md - - advanced/settings.md - - advanced/response-headers.md - - advanced/websockets.md - - advanced/wsgi.md - - 高级安全: - - advanced/security/index.md -- contributing.md -- help-fastapi.md -- benchmarks.md -markdown_extensions: -- toc: - permalink: true -- markdown.extensions.codehilite: - guess_lang: false -- mdx_include: - base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: - alternate_style: true -- attr_list -- md_in_html -extra: - analytics: - provider: google - property: G-YNEVN69SC3 - social: - - icon: fontawesome/brands/github-alt - link: https://github.com/tiangolo/fastapi - - icon: fontawesome/brands/discord - link: https://discord.gg/VQjSZaeJmf - - icon: fontawesome/brands/twitter - link: https://twitter.com/fastapi - - icon: fontawesome/brands/linkedin - link: https://www.linkedin.com/in/tiangolo - - icon: fontawesome/brands/dev - link: https://dev.to/tiangolo - - icon: fontawesome/brands/medium - link: https://medium.com/@tiangolo - - icon: fontawesome/solid/globe - link: https://tiangolo.com - alternate: - - link: / - name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - - link: /de/ - name: de - - link: /em/ - name: 😉 - - link: /es/ - name: es - español - - link: /fa/ - name: fa - - link: /fr/ - name: fr - français - - link: /he/ - name: he - - link: /hy/ - name: hy - - link: /id/ - name: id - - link: /it/ - name: it - italiano - - link: /ja/ - name: ja - 日本語 - - link: /ko/ - name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - - link: /pl/ - name: pl - - link: /pt/ - name: pt - português - - link: /ru/ - name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - - link: /ta/ - name: ta - தமிழ் - - link: /tr/ - name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - - link: /zh/ - name: zh - 汉语 -extra_css: -- https://fastapi.tiangolo.com/css/termynal.css -- https://fastapi.tiangolo.com/css/custom.css -extra_javascript: -- https://fastapi.tiangolo.com/js/termynal.js -- https://fastapi.tiangolo.com/js/custom.js -hooks: -- ../../scripts/mkdocs_hooks.py +INHERIT: ../en/mkdocs.yml diff --git a/docs/zh/overrides/.gitignore b/docs/zh/overrides/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/docs.py b/scripts/docs.py index c464f8dbe..5615a8572 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -1,4 +1,5 @@ import json +import logging import os import re import shutil @@ -6,7 +7,7 @@ import subprocess from http.server import HTTPServer, SimpleHTTPRequestHandler from multiprocessing import Pool from pathlib import Path -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Union import mkdocs.commands.build import mkdocs.commands.serve @@ -16,6 +17,8 @@ import typer import yaml from jinja2 import Template +logging.basicConfig(level=logging.INFO) + app = typer.Typer() mkdocs_name = "mkdocs.yml" @@ -27,19 +30,21 @@ missing_translation_snippet = """ docs_path = Path("docs") en_docs_path = Path("docs/en") en_config_path: Path = en_docs_path / mkdocs_name +site_path = Path("site").absolute() +build_site_path = Path("site_build").absolute() -def get_en_config() -> dict: +def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) -def get_lang_paths(): +def get_lang_paths() -> List[Path]: return sorted(docs_path.iterdir()) -def lang_callback(lang: Optional[str]): +def lang_callback(lang: Optional[str]) -> Union[str, None]: if lang is None: - return + return None if not lang.isalpha() or len(lang) != 2: typer.echo("Use a 2 letter language code, like: es") raise typer.Abort() @@ -54,35 +59,6 @@ def complete_existing_lang(incomplete: str): yield lang_path.name -def get_base_lang_config(lang: str): - en_config = get_en_config() - fastapi_url_base = "https://fastapi.tiangolo.com/" - new_config = en_config.copy() - new_config["site_url"] = en_config["site_url"] + f"{lang}/" - new_config["theme"]["logo"] = fastapi_url_base + en_config["theme"]["logo"] - new_config["theme"]["favicon"] = fastapi_url_base + en_config["theme"]["favicon"] - new_config["theme"]["language"] = lang - new_config["nav"] = en_config["nav"][:2] - extra_css = [] - css: str - for css in en_config["extra_css"]: - if css.startswith("http"): - extra_css.append(css) - else: - extra_css.append(fastapi_url_base + css) - new_config["extra_css"] = extra_css - - extra_js = [] - js: str - for js in en_config["extra_javascript"]: - if js.startswith("http"): - extra_js.append(js) - else: - extra_js.append(fastapi_url_base + js) - new_config["extra_javascript"] = extra_js - return new_config - - @app.command() def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): """ @@ -95,12 +71,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): typer.echo(f"The language was already created: {lang}") raise typer.Abort() new_path.mkdir() - new_config = get_base_lang_config(lang) new_config_path: Path = Path(new_path) / mkdocs_name - new_config_path.write_text( - yaml.dump(new_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + new_config_path.write_text("INHERIT: ../en/mkdocs.yml\n", encoding="utf-8") new_config_docs_path: Path = new_path / "docs" new_config_docs_path.mkdir() en_index_path: Path = en_docs_path / "docs" / "index.md" @@ -108,11 +80,8 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): en_index_content = en_index_path.read_text(encoding="utf-8") new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" new_index_path.write_text(new_index_content, encoding="utf-8") - new_overrides_gitignore_path = new_path / "overrides" / ".gitignore" - new_overrides_gitignore_path.parent.mkdir(parents=True, exist_ok=True) - new_overrides_gitignore_path.write_text("") typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) - update_languages(lang=None) + update_languages() @app.command() @@ -120,95 +89,29 @@ def build_lang( lang: str = typer.Argument( ..., callback=lang_callback, autocompletion=complete_existing_lang ) -): +) -> None: """ - Build the docs for a language, filling missing pages with translation notifications. + Build the docs for a language. """ lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") raise typer.Abort() typer.echo(f"Building docs for: {lang}") - build_dir_path = Path("docs_build") - build_dir_path.mkdir(exist_ok=True) - build_lang_path = build_dir_path / lang - en_lang_path = Path("docs/en") - site_path = Path("site").absolute() - build_site_path = Path("site_build").absolute() build_site_dist_path = build_site_path / lang if lang == "en": dist_path = site_path + # Don't remove en dist_path as it might already contain other languages. + # When running build_all(), that function already removes site_path. + # All this is only relevant locally, on GitHub Actions all this is done through + # artifacts and multiple workflows, so it doesn't matter if directories are + # removed or not. else: - dist_path: Path = site_path / lang - shutil.rmtree(build_lang_path, ignore_errors=True) - shutil.copytree(lang_path, build_lang_path) - if not lang == "en": - shutil.copytree(en_docs_path / "data", build_lang_path / "data") - overrides_src = en_docs_path / "overrides" - overrides_dest = build_lang_path / "overrides" - for path in overrides_src.iterdir(): - dest_path = overrides_dest / path.name - if not dest_path.exists(): - shutil.copy(path, dest_path) - en_config_path: Path = en_lang_path / mkdocs_name - en_config: dict = mkdocs.utils.yaml_load( - en_config_path.read_text(encoding="utf-8") - ) - nav = en_config["nav"] - lang_config_path: Path = lang_path / mkdocs_name - lang_config: dict = mkdocs.utils.yaml_load( - lang_config_path.read_text(encoding="utf-8") - ) - lang_nav = lang_config["nav"] - # Exclude first 2 entries FastAPI and Languages, for custom handling - use_nav = nav[2:] - lang_use_nav = lang_nav[2:] - file_to_nav = get_file_to_nav_map(use_nav) - sections = get_sections(use_nav) - lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) - for file in file_to_nav: - file_path = Path(file) - lang_file_path: Path = build_lang_path / "docs" / file_path - en_file_path: Path = en_lang_path / "docs" / file_path - lang_file_path.parent.mkdir(parents=True, exist_ok=True) - if not lang_file_path.is_file(): - en_text = en_file_path.read_text(encoding="utf-8") - lang_text = get_text_with_translate_missing(en_text) - lang_file_path.write_text(lang_text, encoding="utf-8") - file_key = file_to_nav[file] - use_lang_file_to_nav[file] = file_key - if file_key: - composite_key = () - new_key = () - for key_part in file_key: - composite_key += (key_part,) - key_first_file = sections[composite_key] - if key_first_file in lang_file_to_nav: - new_key = lang_file_to_nav[key_first_file] - else: - new_key += (key_part,) - use_lang_file_to_nav[file] = new_key - key_to_section = {(): []} - for file, orig_file_key in file_to_nav.items(): - if file in use_lang_file_to_nav: - file_key = use_lang_file_to_nav[file] - else: - file_key = orig_file_key - section = get_key_section(key_to_section=key_to_section, key=file_key) - section.append(file) - new_nav = key_to_section[()] - export_lang_nav = [lang_nav[0], nav[1]] + new_nav - lang_config["nav"] = export_lang_nav - build_lang_config_path: Path = build_lang_path / mkdocs_name - build_lang_config_path.write_text( - yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), - encoding="utf-8", - ) + dist_path = site_path / lang + shutil.rmtree(dist_path, ignore_errors=True) current_dir = os.getcwd() - os.chdir(build_lang_path) + os.chdir(lang_path) shutil.rmtree(build_site_dist_path, ignore_errors=True) - shutil.rmtree(dist_path, ignore_errors=True) subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) os.chdir(current_dir) @@ -227,7 +130,7 @@ index_sponsors_template = """ """ -def generate_readme_content(): +def generate_readme_content() -> str: en_index = en_docs_path / "docs" / "index.md" content = en_index.read_text("utf-8") match_start = re.search(r"", content) @@ -247,7 +150,7 @@ def generate_readme_content(): @app.command() -def generate_readme(): +def generate_readme() -> None: """ Generate README.md content from main index.md """ @@ -258,7 +161,7 @@ def generate_readme(): @app.command() -def verify_readme(): +def verify_readme() -> None: """ Verify README.md content from main index.md """ @@ -275,12 +178,13 @@ def verify_readme(): @app.command() -def build_all(): +def build_all() -> None: """ Build mkdocs site for en, and then build each language inside, end result is located at directory ./site/ with each language inside. """ - update_languages(lang=None) + update_languages() + shutil.rmtree(site_path, ignore_errors=True) langs = [lang.name for lang in get_lang_paths() if lang.is_dir()] cpu_count = os.cpu_count() or 1 process_pool_size = cpu_count * 4 @@ -289,34 +193,16 @@ def build_all(): p.map(build_lang, langs) -def update_single_lang(lang: str): - lang_path = docs_path / lang - typer.echo(f"Updating {lang_path.name}") - update_config(lang_path.name) - - @app.command() -def update_languages( - lang: str = typer.Argument( - None, callback=lang_callback, autocompletion=complete_existing_lang - ) -): +def update_languages() -> None: """ Update the mkdocs.yml file Languages section including all the available languages. - - The LANG argument is a 2-letter language code. If it's not provided, update all the - mkdocs.yml files (for all the languages). """ - if lang is None: - for lang_path in get_lang_paths(): - if lang_path.is_dir(): - update_single_lang(lang_path.name) - else: - update_single_lang(lang) + update_config() @app.command() -def serve(): +def serve() -> None: """ A quick server to preview a built site with translations. @@ -342,7 +228,7 @@ def live( lang: str = typer.Argument( None, callback=lang_callback, autocompletion=complete_existing_lang ) -): +) -> None: """ Serve with livereload a docs site for a specific language. @@ -359,18 +245,8 @@ def live( mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") -def update_config(lang: str): - lang_path: Path = docs_path / lang - config_path = lang_path / mkdocs_name - current_config: dict = mkdocs.utils.yaml_load( - config_path.read_text(encoding="utf-8") - ) - if lang == "en": - config = get_en_config() - else: - config = get_base_lang_config(lang) - config["nav"] = current_config["nav"] - config["theme"]["language"] = current_config["theme"]["language"] +def update_config() -> None: + config = get_en_config() languages = [{"en": "/"}] alternate: List[Dict[str, str]] = config["extra"].get("alternate", []) alternate_dict = {alt["link"]: alt["name"] for alt in alternate} @@ -390,7 +266,7 @@ def update_config(lang: str): new_alternate.append({"link": url, "name": use_name}) config["nav"][1] = {"Languages": languages} config["extra"]["alternate"] = new_alternate - config_path.write_text( + en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), encoding="utf-8", ) @@ -405,56 +281,5 @@ def langs_json(): print(json.dumps(langs)) -def get_key_section( - *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] -) -> list: - if key in key_to_section: - return key_to_section[key] - super_key = key[:-1] - title = key[-1] - super_section = get_key_section(key_to_section=key_to_section, key=super_key) - new_section = [] - super_section.append({title: new_section}) - key_to_section[key] = new_section - return new_section - - -def get_text_with_translate_missing(text: str) -> str: - lines = text.splitlines() - lines.insert(1, missing_translation_snippet) - new_text = "\n".join(lines) - return new_text - - -def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: - file_to_nav = {} - for item in nav: - if type(item) is str: - file_to_nav[item] = () - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sub_file_to_nav = get_file_to_nav_map(sub_nav) - for k, v in sub_file_to_nav.items(): - file_to_nav[k] = (item_key,) + v - return file_to_nav - - -def get_sections(nav: list) -> Dict[Tuple[str, ...], str]: - sections = {} - for item in nav: - if type(item) is str: - continue - elif type(item) is dict: - item_key = list(item.keys())[0] - sub_nav = item[item_key] - sections[(item_key,)] = sub_nav[0] - sub_sections = get_sections(sub_nav) - for k, v in sub_sections.items(): - new_key = (item_key,) + k - sections[new_key] = v - return sections - - if __name__ == "__main__": app() diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index f09e9a99d..008751f8a 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -1,11 +1,88 @@ +from functools import lru_cache +from pathlib import Path from typing import Any, List, Union +import material from mkdocs.config.defaults import MkDocsConfig -from mkdocs.structure.files import Files +from mkdocs.structure.files import File, Files from mkdocs.structure.nav import Link, Navigation, Section from mkdocs.structure.pages import Page +@lru_cache() +def get_missing_translation_content(docs_dir: str) -> str: + docs_dir_path = Path(docs_dir) + missing_translation_path = docs_dir_path.parent.parent / "missing-translation.md" + return missing_translation_path.read_text(encoding="utf-8") + + +@lru_cache() +def get_mkdocs_material_langs() -> List[str]: + material_path = Path(material.__file__).parent + material_langs_path = material_path / "partials" / "languages" + langs = [file.stem for file in material_langs_path.glob("*.html")] + return langs + + +class EnFile(File): + pass + + +def on_config(config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig: + available_langs = get_mkdocs_material_langs() + dir_path = Path(config.docs_dir) + lang = dir_path.parent.name + if lang in available_langs: + config.theme["language"] = lang + if not (config.site_url or "").endswith(f"{lang}/") and not lang == "en": + config.site_url = f"{config.site_url}{lang}/" + return config + + +def resolve_file(*, item: str, files: Files, config: MkDocsConfig) -> None: + item_path = Path(config.docs_dir) / item + if not item_path.is_file(): + en_src_dir = (Path(config.docs_dir) / "../../en/docs").resolve() + potential_path = en_src_dir / item + if potential_path.is_file(): + files.append( + EnFile( + path=item, + src_dir=str(en_src_dir), + dest_dir=config.site_dir, + use_directory_urls=config.use_directory_urls, + ) + ) + + +def resolve_files(*, items: List[Any], files: Files, config: MkDocsConfig) -> None: + for item in items: + if isinstance(item, str): + resolve_file(item=item, files=files, config=config) + elif isinstance(item, dict): + assert len(item) == 1 + values = list(item.values()) + if not values: + continue + if isinstance(values[0], str): + resolve_file(item=values[0], files=files, config=config) + elif isinstance(values[0], list): + resolve_files(items=values[0], files=files, config=config) + else: + raise ValueError(f"Unexpected value: {values}") + + +def on_files(files: Files, *, config: MkDocsConfig) -> Files: + resolve_files(items=config.nav or [], files=files, config=config) + if "logo" in config.theme: + resolve_file(item=config.theme["logo"], files=files, config=config) + if "favicon" in config.theme: + resolve_file(item=config.theme["favicon"], files=files, config=config) + resolve_files(items=config.extra_css, files=files, config=config) + resolve_files(items=config.extra_javascript, files=files, config=config) + return files + + def generate_renamed_section_items( items: List[Union[Page, Section, Link]], *, config: MkDocsConfig ) -> List[Union[Page, Section, Link]]: @@ -36,3 +113,20 @@ def on_nav( ) -> Navigation: new_items = generate_renamed_section_items(nav.items, config=config) return Navigation(items=new_items, pages=nav.pages) + + +def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: + return page + + +def on_page_markdown( + markdown: str, *, page: Page, config: MkDocsConfig, files: Files +) -> str: + if isinstance(page.file, EnFile): + missing_translation_content = get_missing_translation_content(config.docs_dir) + header = "" + body = markdown + if markdown.startswith("#"): + header, _, body = markdown.partition("\n\n") + return f"{header}\n\n{missing_translation_content}\n\n{body}" + return markdown From be8e704e46e26bc0ef8f331e90ef5574860897af Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 25 Jun 2023 12:34:39 +0000 Subject: [PATCH 1115/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 88f2ddf2d..b8c9a1106 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update MkDocs for other languages. PR [#9734](https://github.com/tiangolo/fastapi/pull/9734) by [@tiangolo](https://github.com/tiangolo). From b107b6a0968d4fad394b94ff883a5bcfd00bebca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 25 Jun 2023 14:57:19 +0200 Subject: [PATCH 1116/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20languages=20w?= =?UTF-8?q?ithout=20translations=20(#9743)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Remove lang directories for empty translations * 🔥 Remove untranslated langs from main config --- docs/az/docs/index.md | 465 ----------------------------------------- docs/az/mkdocs.yml | 1 - docs/cs/docs/index.md | 473 ------------------------------------------ docs/cs/mkdocs.yml | 1 - docs/en/mkdocs.yml | 27 --- docs/hy/docs/index.md | 467 ----------------------------------------- docs/hy/mkdocs.yml | 1 - docs/it/docs/index.md | 462 ----------------------------------------- docs/it/mkdocs.yml | 1 - docs/lo/docs/index.md | 469 ----------------------------------------- docs/lo/mkdocs.yml | 1 - docs/nl/docs/index.md | 467 ----------------------------------------- docs/nl/mkdocs.yml | 1 - docs/sq/docs/index.md | 465 ----------------------------------------- docs/sq/mkdocs.yml | 1 - docs/sv/docs/index.md | 467 ----------------------------------------- docs/sv/mkdocs.yml | 1 - docs/uk/docs/index.md | 465 ----------------------------------------- docs/uk/mkdocs.yml | 1 - 19 files changed, 4236 deletions(-) delete mode 100644 docs/az/docs/index.md delete mode 100644 docs/az/mkdocs.yml delete mode 100644 docs/cs/docs/index.md delete mode 100644 docs/cs/mkdocs.yml delete mode 100644 docs/hy/docs/index.md delete mode 100644 docs/hy/mkdocs.yml delete mode 100644 docs/it/docs/index.md delete mode 100644 docs/it/mkdocs.yml delete mode 100644 docs/lo/docs/index.md delete mode 100644 docs/lo/mkdocs.yml delete mode 100644 docs/nl/docs/index.md delete mode 100644 docs/nl/mkdocs.yml delete mode 100644 docs/sq/docs/index.md delete mode 100644 docs/sq/mkdocs.yml delete mode 100644 docs/sv/docs/index.md delete mode 100644 docs/sv/mkdocs.yml delete mode 100644 docs/uk/docs/index.md delete mode 100644 docs/uk/mkdocs.yml diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md deleted file mode 100644 index 8b1c65194..000000000 --- a/docs/az/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on `requests` and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/az/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/cs/docs/index.md b/docs/cs/docs/index.md deleted file mode 100644 index bde72f851..000000000 --- a/docs/cs/docs/index.md +++ /dev/null @@ -1,473 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" - -
Deon Pillsbury - Cisco (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/cs/mkdocs.yml b/docs/cs/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/cs/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a21848a66..c39d656ff 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -40,28 +40,19 @@ nav: - FastAPI: index.md - Languages: - en: / - - az: /az/ - - cs: /cs/ - de: /de/ - em: /em/ - es: /es/ - fa: /fa/ - fr: /fr/ - he: /he/ - - hy: /hy/ - id: /id/ - - it: /it/ - ja: /ja/ - ko: /ko/ - - lo: /lo/ - - nl: /nl/ - pl: /pl/ - pt: /pt/ - ru: /ru/ - - sq: /sq/ - - sv: /sv/ - tr: /tr/ - - uk: /uk/ - zh: /zh/ - features.md - fastapi-people.md @@ -211,10 +202,6 @@ extra: alternate: - link: / name: en - English - - link: /az/ - name: az - - link: /cs/ - name: cs - link: /de/ name: de - link: /em/ @@ -227,34 +214,20 @@ extra: name: fr - français - link: /he/ name: he - - link: /hy/ - name: hy - link: /id/ name: id - - link: /it/ - name: it - italiano - link: /ja/ name: ja - 日本語 - link: /ko/ name: ko - 한국어 - - link: /lo/ - name: lo - ພາສາລາວ - - link: /nl/ - name: nl - link: /pl/ name: pl - link: /pt/ name: pt - português - link: /ru/ name: ru - русский язык - - link: /sq/ - name: sq - shqip - - link: /sv/ - name: sv - svenska - link: /tr/ name: tr - Türkçe - - link: /uk/ - name: uk - українська мова - link: /zh/ name: zh - 汉语 extra_css: diff --git a/docs/hy/docs/index.md b/docs/hy/docs/index.md deleted file mode 100644 index cc82b33cf..000000000 --- a/docs/hy/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/hy/mkdocs.yml b/docs/hy/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/hy/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md deleted file mode 100644 index 42c9a7e8c..000000000 --- a/docs/it/docs/index.md +++ /dev/null @@ -1,462 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Build Status - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from fastapi import FastAPI -from typing import Optional - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: str = Optional[None]): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="7 12" -from fastapi import FastAPI -from typing import Optional - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="2 7-10 23-25" -from fastapi import FastAPI -from pydantic import BaseModel -from typing import Optional - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: bool = Optional[None] - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/it/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/lo/docs/index.md b/docs/lo/docs/index.md deleted file mode 100644 index 9a81f14d1..000000000 --- a/docs/lo/docs/index.md +++ /dev/null @@ -1,469 +0,0 @@ -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" - -
Deon Pillsbury - Cisco (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.7+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* ujson - for faster JSON "parsing". -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/lo/mkdocs.yml b/docs/lo/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/lo/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/nl/docs/index.md b/docs/nl/docs/index.md deleted file mode 100644 index 47d62f8c4..000000000 --- a/docs/nl/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/nl/mkdocs.yml b/docs/nl/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/nl/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/sq/docs/index.md b/docs/sq/docs/index.md deleted file mode 100644 index a83b7b519..000000000 --- a/docs/sq/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/sq/mkdocs.yml b/docs/sq/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/sq/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/sv/docs/index.md b/docs/sv/docs/index.md deleted file mode 100644 index 47d62f8c4..000000000 --- a/docs/sv/docs/index.md +++ /dev/null @@ -1,467 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - - - Supported Python versions - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* **GraphQL** integration with Strawberry and other libraries. -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install "fastapi[all]"`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/sv/mkdocs.yml b/docs/sv/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/sv/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md deleted file mode 100644 index a83b7b519..000000000 --- a/docs/uk/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml deleted file mode 100644 index de18856f4..000000000 --- a/docs/uk/mkdocs.yml +++ /dev/null @@ -1 +0,0 @@ -INHERIT: ../en/mkdocs.yml From afc237ad530297041f21d8ecdbe66ee1b7d52802 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 25 Jun 2023 12:57:53 +0000 Subject: [PATCH 1117/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b8c9a1106..900822809 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). * 👷 Make cron jobs run only on main repo, not on forks, to avoid error notifications from missing tokens. PR [#9735](https://github.com/tiangolo/fastapi/pull/9735) by [@tiangolo](https://github.com/tiangolo). From ed297bb2e044a0cc5cb013673447778057b731d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 26 Jun 2023 16:05:43 +0200 Subject: [PATCH 1118/1881] =?UTF-8?q?=E2=9C=A8=20Add=20Material=20for=20Mk?= =?UTF-8?q?Docs=20Insiders=20features=20and=20cards=20(#9748)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ➕ Add dependencies for MkDocs Insiders * 🙈 Add Insider's .cache to .gitignore * 🔧 Update MkDocs configs for Insiders * 💄 Add custom Insiders card layout, while the custom logo is provided from upstream * 🔨 Update docs.py script to dynamically enable insiders if it's installed * 👷 Add cache for MkDocs Material Insiders' cards * 🔊 Add a small log to the docs CLI * 🔊 Tweak logs, only after exporting languages * 🐛 Fix accessing non existing env var * 🔧 Invalidate deps cache * 🔧 Tweak cache IDs * 👷 Update cache for installing insiders * 🔊 Log insiders * 💚 Invalidate cache * 👷 Tweak cache keys * 👷 Trigger CI and test cache * 🔥 Remove cache comment * ⚡️ Optimize cache usage for first runs of docs * 👷 Tweak cache for MkDocs Material cards * 💚 Trigger CI to test cache --- .github/workflows/build-docs.yml | 12 +- .gitignore | 1 + docs/en/layouts/custom.yml | 228 ++++++++++++++++++++++++++++++ docs/en/mkdocs.insiders.yml | 7 + docs/en/mkdocs.maybe-insiders.yml | 3 + docs/en/mkdocs.no-insiders.yml | 0 docs/en/mkdocs.yml | 10 +- requirements-docs.txt | 6 + scripts/docs.py | 20 +++ 9 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 docs/en/layouts/custom.yml create mode 100644 docs/en/mkdocs.insiders.yml create mode 100644 docs/en/mkdocs.maybe-insiders.yml create mode 100644 docs/en/mkdocs.no-insiders.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index c2880ef71..a155ecfec 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -44,10 +44,14 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt + # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps + - name: Install Material for MkDocs Insiders + if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' + run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Export Language Codes id: show-langs run: | @@ -76,7 +80,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v03 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt @@ -85,6 +89,10 @@ jobs: run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Update Languages run: python ./scripts/docs.py update-languages + - uses: actions/cache@v3 + with: + key: mkdocs-cards-${{ matrix.lang }}-${{ github.ref }} + path: docs/${{ matrix.lang }}/.cache - name: Build Docs run: python ./scripts/docs.py build-lang ${{ matrix.lang }} - uses: actions/upload-artifact@v3 diff --git a/.gitignore b/.gitignore index 3cb64c047..d380d16b7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ archive.zip # vim temporary files *~ .*.sw? +.cache diff --git a/docs/en/layouts/custom.yml b/docs/en/layouts/custom.yml new file mode 100644 index 000000000..aad81af28 --- /dev/null +++ b/docs/en/layouts/custom.yml @@ -0,0 +1,228 @@ +# Copyright (c) 2016-2023 Martin Donath + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +# ----------------------------------------------------------------------------- +# Configuration +# ----------------------------------------------------------------------------- + +# The same default card with a a configurable logo + +# Definitions +definitions: + + # Background image + - &background_image >- + {{ layout.background_image or "" }} + + # Background color (default: indigo) + - &background_color >- + {%- if layout.background_color -%} + {{ layout.background_color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ef5552", + "pink": "#e92063", + "purple": "#ab47bd", + "deep-purple": "#7e56c2", + "indigo": "#4051b5", + "blue": "#2094f3", + "light-blue": "#02a6f2", + "cyan": "#00bdd6", + "teal": "#009485", + "green": "#4cae4f", + "light-green": "#8bc34b", + "lime": "#cbdc38", + "yellow": "#ffec3d", + "amber": "#ffc105", + "orange": "#ffa724", + "deep-orange": "#ff6e42", + "brown": "#795649", + "grey": "#757575", + "blue-grey": "#546d78", + "black": "#000000", + "white": "#ffffff" + }[primary] or "#4051b5" }} + {%- endif -%} + + # Text color (default: white) + - &color >- + {%- if layout.color -%} + {{ layout.color }} + {%- else -%} + {%- set palette = config.theme.palette or {} -%} + {%- if not palette is mapping -%} + {%- set palette = palette | first -%} + {%- endif -%} + {%- set primary = palette.get("primary", "indigo") -%} + {%- set primary = primary.replace(" ", "-") -%} + {{ { + "red": "#ffffff", + "pink": "#ffffff", + "purple": "#ffffff", + "deep-purple": "#ffffff", + "indigo": "#ffffff", + "blue": "#ffffff", + "light-blue": "#ffffff", + "cyan": "#ffffff", + "teal": "#ffffff", + "green": "#ffffff", + "light-green": "#ffffff", + "lime": "#000000", + "yellow": "#000000", + "amber": "#000000", + "orange": "#000000", + "deep-orange": "#ffffff", + "brown": "#ffffff", + "grey": "#ffffff", + "blue-grey": "#ffffff", + "black": "#ffffff", + "white": "#000000" + }[primary] or "#ffffff" }} + {%- endif -%} + + # Font family (default: Roboto) + - &font_family >- + {%- if layout.font_family -%} + {{ layout.font_family }} + {%- elif config.theme.font != false -%} + {{ config.theme.font.get("text", "Roboto") }} + {%- else -%} + Roboto + {%- endif -%} + + # Site name + - &site_name >- + {{ config.site_name }} + + # Page title + - &page_title >- + {{ page.meta.get("title", page.title) }} + + # Page title with site name + - &page_title_with_site_name >- + {%- if not page.is_homepage -%} + {{ page.meta.get("title", page.title) }} - {{ config.site_name }} + {%- else -%} + {{ page.meta.get("title", page.title) }} + {%- endif -%} + + # Page description + - &page_description >- + {{ page.meta.get("description", config.site_description) or "" }} + + + # Start of custom modified logic + # Logo + - &logo >- + {%- if layout.logo -%} + {{ layout.logo }} + {%- elif config.theme.logo -%} + {{ config.docs_dir }}/{{ config.theme.logo }} + {%- endif -%} + # End of custom modified logic + + # Logo (icon) + - &logo_icon >- + {{ config.theme.icon.logo or "" }} + +# Meta tags +tags: + + # Open Graph + og:type: website + og:title: *page_title_with_site_name + og:description: *page_description + og:image: "{{ image.url }}" + og:image:type: "{{ image.type }}" + og:image:width: "{{ image.width }}" + og:image:height: "{{ image.height }}" + og:url: "{{ page.canonical_url }}" + + # Twitter + twitter:card: summary_large_image + twitter.title: *page_title_with_site_name + twitter:description: *page_description + twitter:image: "{{ image.url }}" + +# ----------------------------------------------------------------------------- +# Specification +# ----------------------------------------------------------------------------- + +# Card size and layers +size: { width: 1200, height: 630 } +layers: + + # Background + - background: + image: *background_image + color: *background_color + + # Logo + - size: { width: 144, height: 144 } + offset: { x: 992, y: 64 } + background: + image: *logo + icon: + value: *logo_icon + color: *color + + # Site name + - size: { width: 832, height: 42 } + offset: { x: 64, y: 64 } + typography: + content: *site_name + color: *color + font: + family: *font_family + style: Bold + + # Page title + - size: { width: 832, height: 310 } + offset: { x: 62, y: 160 } + typography: + content: *page_title + align: start + color: *color + line: + amount: 3 + height: 1.25 + font: + family: *font_family + style: Bold + + # Page description + - size: { width: 832, height: 64 } + offset: { x: 64, y: 512 } + typography: + content: *page_description + align: start + color: *color + line: + amount: 2 + height: 1.5 + font: + family: *font_family + style: Regular diff --git a/docs/en/mkdocs.insiders.yml b/docs/en/mkdocs.insiders.yml new file mode 100644 index 000000000..d204974b8 --- /dev/null +++ b/docs/en/mkdocs.insiders.yml @@ -0,0 +1,7 @@ +plugins: + social: + cards_layout_dir: ../en/layouts + cards_layout: custom + cards_layout_options: + logo: ../en/docs/img/icon-white.svg + typeset: diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.maybe-insiders.yml new file mode 100644 index 000000000..8e6271334 --- /dev/null +++ b/docs/en/mkdocs.maybe-insiders.yml @@ -0,0 +1,3 @@ +# Define this here and not in the main mkdocs.yml file because that one is auto +# updated and written, and the script would remove the env var +INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] diff --git a/docs/en/mkdocs.no-insiders.yml b/docs/en/mkdocs.no-insiders.yml new file mode 100644 index 000000000..e69de29bb diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index c39d656ff..bdadb167e 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -1,3 +1,4 @@ +INHERIT: ../en/mkdocs.maybe-insiders.yml site_name: FastAPI site_description: FastAPI framework, high performance, easy to learn, fast to code, ready for production site_url: https://fastapi.tiangolo.com/ @@ -24,6 +25,11 @@ theme: - search.highlight - content.tabs.link - navigation.indexes + - content.tooltips + - navigation.path + - content.code.annotate + - content.code.copy + - content.code.select icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -33,8 +39,8 @@ repo_name: tiangolo/fastapi repo_url: https://github.com/tiangolo/fastapi edit_uri: '' plugins: -- search -- markdownextradata: + search: null + markdownextradata: data: ../en/data nav: - FastAPI: index.md diff --git a/requirements-docs.txt b/requirements-docs.txt index 211212fba..2c5f71ec0 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -6,3 +6,9 @@ mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 typer[all] >=0.6.1,<0.8.0 pyyaml >=5.3.1,<7.0.0 +# For Material for MkDocs, Chinese search +jieba==0.42.1 +# For image processing by Material for MkDocs +pillow==9.5.0 +# For image processing by Material for MkDocs +cairosvg==2.7.0 diff --git a/scripts/docs.py b/scripts/docs.py index 5615a8572..20838be6a 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -4,7 +4,9 @@ import os import re import shutil import subprocess +from functools import lru_cache from http.server import HTTPServer, SimpleHTTPRequestHandler +from importlib import metadata from multiprocessing import Pool from pathlib import Path from typing import Any, Dict, List, Optional, Union @@ -34,6 +36,12 @@ site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() +@lru_cache() +def is_mkdocs_insiders() -> bool: + version = metadata.version("mkdocs-material") + return "insiders" in version + + def get_en_config() -> Dict[str, Any]: return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) @@ -59,6 +67,14 @@ def complete_existing_lang(incomplete: str): yield lang_path.name +@app.callback() +def callback() -> None: + if is_mkdocs_insiders(): + os.environ["INSIDERS_FILE"] = "../en/mkdocs.insiders.yml" + # For MacOS with insiders and Cairo + os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = "/opt/homebrew/lib" + + @app.command() def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): """ @@ -93,6 +109,10 @@ def build_lang( """ Build the docs for a language. """ + insiders_env_file = os.environ.get("INSIDERS_FILE") + print(f"Insiders file {insiders_env_file}") + if is_mkdocs_insiders(): + print("Using insiders") lang_path: Path = Path("docs") / lang if not lang_path.is_dir(): typer.echo(f"The language translation doesn't seem to exist yet: {lang}") From d1c5c5c97c147016623edcc65e30e19769ca0ada Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 26 Jun 2023 14:06:24 +0000 Subject: [PATCH 1119/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 900822809..f0d321e3b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). * 🔨 Add MkDocs hook that renames sections based on the first index file. PR [#9737](https://github.com/tiangolo/fastapi/pull/9737) by [@tiangolo](https://github.com/tiangolo). From 872af100f5e8c81b109fe29e7d70a36f520193ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 26 Jun 2023 18:02:34 +0200 Subject: [PATCH 1120/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20form=20for=20the?= =?UTF-8?q?=20FastAPI=20and=20friends=20newsletter=20(#9749)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/newsletter.md | 4 ++-- docs/en/mkdocs.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/newsletter.md b/docs/en/docs/newsletter.md index 6403f31e6..782db1353 100644 --- a/docs/en/docs/newsletter.md +++ b/docs/en/docs/newsletter.md @@ -1,5 +1,5 @@ # FastAPI and friends newsletter - + - + diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index bdadb167e..21300b9dc 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -165,6 +165,7 @@ nav: - external-links.md - benchmarks.md - help-fastapi.md +- newsletter.md - contributing.md - release-notes.md markdown_extensions: From 47524eee1bd16d2680ff0a49706ad72d742e4e30 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 26 Jun 2023 16:03:19 +0000 Subject: [PATCH 1121/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f0d321e3b..e55bf48c7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). From 81772b46a8bcd1fe531508c67240489533c26417 Mon Sep 17 00:00:00 2001 From: Sergei Glazkov Date: Tue, 27 Jun 2023 04:00:19 +0300 Subject: [PATCH 1122/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/response-model.md`=20(#967?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: s.glazkov Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Alexandr Co-authored-by: ivan-abc <36765187+ivan-abc@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/ru/docs/tutorial/response-model.md | 480 ++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 docs/ru/docs/tutorial/response-model.md diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md new file mode 100644 index 000000000..c5e111790 --- /dev/null +++ b/docs/ru/docs/tutorial/response-model.md @@ -0,0 +1,480 @@ +# Модель ответа - Возвращаемый тип + +Вы можете объявить тип ответа, указав аннотацию **возвращаемого значения** для *функции операции пути*. + +FastAPI позволяет использовать **аннотации типов** таким же способом, как и для ввода данных в **параметры** функции, вы можете использовать модели Pydantic, списки, словари, скалярные типы (такие, как int, bool и т.д.). + +=== "Python 3.10+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/response_model/tutorial001_01_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18 23" + {!> ../../../docs_src/response_model/tutorial001_01.py!} + ``` + +FastAPI будет использовать этот возвращаемый тип для: + +* **Валидации** ответа. + * Если данные невалидны (например, отсутствует одно из полей), это означает, что код *вашего* приложения работает некорректно и функция возвращает не то, что вы ожидаете. В таком случае приложение вернет server error вместо того, чтобы отправить неправильные данные. Таким образом, вы и ваши пользователи можете быть уверены, что получите корректные данные в том виде, в котором они ожидаются. +* Добавьте **JSON схему** для ответа внутри *операции пути* OpenAPI. + * Она будет использована для **автоматически генерируемой документации**. + * А также - для автоматической кодогенерации пользователями. + +Но самое важное: + +* Ответ будет **ограничен и отфильтрован** - т.е. в нем останутся только те данные, которые определены в возвращаемом типе. + * Это особенно важно для **безопасности**, далее мы рассмотрим эту тему подробнее. + +## Параметр `response_model` + +Бывают случаи, когда вам необходимо (или просто хочется) возвращать данные, которые не полностью соответствуют объявленному типу. + +Допустим, вы хотите, чтобы ваша функция **возвращала словарь (dict)** или объект из базы данных, но при этом **объявляете выходной тип как модель Pydantic**. Тогда именно указанная модель будет использована для автоматической документации, валидации и т.п. для объекта, который вы вернули (например, словаря или объекта из базы данных). + +Но если указать аннотацию возвращаемого типа, статическая проверка типов будет выдавать ошибку (абсолютно корректную в данном случае). Она будет говорить о том, что ваша функция должна возвращать данные одного типа (например, dict), а в аннотации вы объявили другой тип (например, модель Pydantic). + +В таком случае можно использовать параметр `response_model` внутри *декоратора операции пути* вместо аннотации возвращаемого значения функции. + +Параметр `response_model` может быть указан для любой *операции пути*: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* и др. + +=== "Python 3.10+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` + +!!! note "Технические детали" + Помните, что параметр `response_model` является параметром именно декоратора http-методов (`get`, `post`, и т.п.). Не следует его указывать для *функций операций пути*, как вы бы поступили с другими параметрами или с телом запроса. + +`response_model` принимает те же типы, которые можно указать для какого-либо поля в модели Pydantic. Таким образом, это может быть как одиночная модель Pydantic, так и `список (list)` моделей Pydantic. Например, `List[Item]`. + +FastAPI будет использовать значение `response_model` для того, чтобы автоматически генерировать документацию, производить валидацию и т.п. А также для **конвертации и фильтрации выходных данных** в объявленный тип. + +!!! tip "Подсказка" + Если вы используете анализаторы типов со строгой проверкой (например, mypy), можно указать `Any` в качестве типа возвращаемого значения функции. + + Таким образом вы информируете ваш редактор кода, что намеренно возвращаете данные неопределенного типа. Но возможности FastAPI, такие как автоматическая генерация документации, валидация, фильтрация и т.д. все так же будут работать, просто используя параметр `response_model`. + +### Приоритет `response_model` + +Если одновременно указать аннотацию типа для ответа функции и параметр `response_model` - последний будет иметь больший приоритет и FastAPI будет использовать именно его. + +Таким образом вы можете объявить корректные аннотации типов к вашим функциям, даже если они возвращают тип, отличающийся от указанного в `response_model`. Они будут считаны во время статической проверки типов вашими помощниками, например, mypy. При этом вы все так же используете возможности FastAPI для автоматической документации, валидации и т.д. благодаря `response_model`. + +Вы можете указать значение `response_model=None`, чтобы отключить создание модели ответа для данной *операции пути*. Это может понадобиться, если вы добавляете аннотации типов для данных, не являющихся валидными полями Pydantic. Мы увидим пример кода для такого случая в одном из разделов ниже. + +## Получить и вернуть один и тот же тип данных + +Здесь мы объявили модель `UserIn`, которая хранит пользовательский пароль в открытом виде: + +=== "Python 3.10+" + + ```Python hl_lines="7 9" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +!!! info "Информация" + Чтобы использовать `EmailStr`, прежде необходимо установить `email_validator`. + Используйте `pip install email-validator` + или `pip install pydantic[email]`. + +Далее мы используем нашу модель в аннотациях типа как для аргумента функции, так и для выходного значения: + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/response_model/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/response_model/tutorial002.py!} + ``` + +Теперь всякий раз, когда клиент создает пользователя с паролем, API будет возвращать его пароль в ответе. + +В данном случае это не такая уж большая проблема, поскольку ответ получит тот же самый пользователь, который и создал пароль. + +Но что если мы захотим использовать эту модель для какой-либо другой *операции пути*? Мы можем, сами того не желая, отправить пароль любому другому пользователю. + +!!! danger "Осторожно" + Никогда не храните пароли пользователей в открытом виде, а также никогда не возвращайте их в ответе, как в примере выше. В противном случае - убедитесь, что вы хорошо продумали и учли все возможные риски такого подхода и вам известно, что вы делаете. + +## Создание модели для ответа + +Вместо этого мы можем создать входную модель, хранящую пароль в открытом виде и выходную модель без пароля: + +=== "Python 3.10+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +В таком случае, даже несмотря на то, что наша *функция операции пути* возвращает тот же самый объект пользователя с паролем, полученным на вход: + +=== "Python 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +...мы указали в `response_model` модель `UserOut`, в которой отсутствует поле, содержащее пароль - и он будет исключен из ответа: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` + +Таким образом **FastAPI** позаботится о фильтрации ответа и исключит из него всё, что не указано в выходной модели (при помощи Pydantic). + +### `response_model` или возвращаемый тип данных + +В нашем примере модели входных данных и выходных данных различаются. И если мы укажем аннотацию типа выходного значения функции как `UserOut` - проверка типов выдаст ошибку из-за того, что мы возвращаем некорректный тип. Поскольку это 2 разных класса. + +Поэтому в нашем примере мы можем объявить тип ответа только в параметре `response_model`. + +...но продолжайте читать дальше, чтобы узнать как можно это обойти. + +## Возвращаемый тип и Фильтрация данных + +Продолжим рассматривать предыдущий пример. Мы хотели **аннотировать входные данные одним типом**, а выходное значение - **другим типом**. + +Мы хотим, чтобы FastAPI продолжал **фильтровать** данные, используя `response_model`. + +В прошлом примере, т.к. входной и выходной типы являлись разными классами, мы были вынуждены использовать параметр `response_model`. И как следствие, мы лишались помощи статических анализаторов для проверки ответа функции. + +Но в подавляющем большинстве случаев мы будем хотеть, чтобы модель ответа лишь **фильтровала/удаляла** некоторые данные из ответа, как в нашем примере. + +И в таких случаях мы можем использовать классы и наследование, чтобы пользоваться преимуществами **аннотаций типов** и получать более полную статическую проверку типов. Но при этом все так же получать **фильтрацию ответа** от FastAPI. + +=== "Python 3.10+" + + ```Python hl_lines="7-10 13-14 18" + {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-13 15-16 20" + {!> ../../../docs_src/response_model/tutorial003_01.py!} + ``` + +Таким образом, мы получаем поддержку редактора кода и mypy в части типов, сохраняя при этом фильтрацию данных от FastAPI. + +Как это возможно? Давайте разберемся. 🤓 + +### Аннотации типов и инструменты для их проверки + +Для начала давайте рассмотрим как наш редактор кода, mypy и другие помощники разработчика видят аннотации типов. + +У модели `BaseUser` есть некоторые поля. Затем `UserIn` наследуется от `BaseUser` и добавляет новое поле `password`. Таким образом модель будет включать в себя все поля из первой модели (родителя), а также свои собственные. + +Мы аннотируем возвращаемый тип функции как `BaseUser`, но фактически мы будем возвращать объект типа `UserIn`. + +Редакторы, mypy и другие инструменты не будут иметь возражений против такого подхода, поскольку `UserIn` является подклассом `BaseUser`. Это означает, что такой тип будет *корректным*, т.к. ответ может быть чем угодно, если это будет `BaseUser`. + +### Фильтрация Данных FastAPI + +FastAPI знает тип ответа функции, так что вы можете быть уверены, что на выходе будут **только** те поля, которые вы указали. + +FastAPI совместно с Pydantic выполнит некоторую магию "под капотом", чтобы убедиться, что те же самые правила наследования классов не используются для фильтрации возвращаемых данных, в противном случае вы могли бы в конечном итоге вернуть гораздо больше данных, чем ожидали. + +Таким образом, вы можете получить все самое лучшее из обоих миров: аннотации типов с **поддержкой инструментов для разработки** и **фильтрацию данных**. + +## Автоматическая документация + +Если посмотреть на сгенерированную документацию, вы можете убедиться, что в ней присутствуют обе JSON схемы - как для входной модели, так и для выходной: + + + +И также обе модели будут использованы в интерактивной документации API: + + + +## Другие аннотации типов + +Бывают случаи, когда вы возвращаете что-то, что не является валидным типом для Pydantic и вы указываете аннотацию ответа функции только для того, чтобы работала поддержка различных инструментов (редактор кода, mypy и др.). + +### Возвращаем Response + +Самый частый сценарий использования - это [возвращать Response напрямую, как описано в расширенной документации](../advanced/response-directly.md){.internal-link target=_blank}. + +```Python hl_lines="8 10-11" +{!> ../../../docs_src/response_model/tutorial003_02.py!} +``` + +Это поддерживается FastAPI по-умолчанию, т.к. аннотация проставлена в классе (или подклассе) `Response`. + +И ваши помощники разработки также будут счастливы, т.к. оба класса `RedirectResponse` и `JSONResponse` являются подклассами `Response`. Таким образом мы получаем корректную аннотацию типа. + +### Подкласс Response в аннотации типа + +Вы также можете указать подкласс `Response` в аннотации типа: + +```Python hl_lines="8-9" +{!> ../../../docs_src/response_model/tutorial003_03.py!} +``` + +Это сработает, потому что `RedirectResponse` является подклассом `Response` и FastAPI автоматически обработает этот простейший случай. + +### Некорректные аннотации типов + +Но когда вы возвращаете какой-либо другой произвольный объект, который не является допустимым типом Pydantic (например, объект из базы данных), и вы аннотируете его подобным образом для функции, FastAPI попытается создать из этого типа модель Pydantic и потерпит неудачу. + +То же самое произошло бы, если бы у вас было что-то вроде Union различных типов и один или несколько из них не являлись бы допустимыми типами для Pydantic. Например, такой вариант приведет к ошибке 💥: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/response_model/tutorial003_04.py!} + ``` + +...такой код вызовет ошибку, потому что в аннотации указан неподдерживаемый Pydantic тип. А также этот тип не является классом или подклассом `Response`. + +### Возможно ли отключить генерацию модели ответа? + +Продолжим рассматривать предыдущий пример. Допустим, что вы хотите отказаться от автоматической валидации ответа, документации, фильтрации и т.д. + +Но в то же время, хотите сохранить аннотацию возвращаемого типа для функции, чтобы обеспечить работу помощников и анализаторов типов (например, mypy). + +В таком случае, вы можете отключить генерацию модели ответа, указав `response_model=None`: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/response_model/tutorial003_05.py!} + ``` + +Тогда FastAPI не станет генерировать модель ответа и вы сможете сохранить такую аннотацию типа, которая вам требуется, никак не влияя на работу FastAPI. 🤓 + +## Параметры модели ответа + +Модель ответа может иметь значения по умолчанию, например: + +=== "Python 3.10+" + + ```Python hl_lines="9 11-12" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11 13-14" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +* `description: Union[str, None] = None` (или `str | None = None` в Python 3.10), где `None` является значением по умолчанию. +* `tax: float = 10.5`, где `10.5` является значением по умолчанию. +* `tags: List[str] = []`, где пустой список `[]` является значением по умолчанию. + +но вы, возможно, хотели бы исключить их из ответа, если данные поля не были заданы явно. + +Например, у вас есть модель с множеством необязательных полей в NoSQL базе данных, но вы не хотите отправлять в качестве ответа очень длинный JSON с множеством значений по умолчанию. + +### Используйте параметр `response_model_exclude_unset` + +Установите для *декоратора операции пути* параметр `response_model_exclude_unset=True`: + +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial004.py!} + ``` + +и тогда значения по умолчанию не будут включены в ответ. В нем будут только те поля, значения которых фактически были установлены. + +Итак, если вы отправите запрос на данную *операцию пути* для элемента, с ID = `Foo` - ответ (с исключенными значениями по-умолчанию) будет таким: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "Информация" + "Под капотом" FastAPI использует метод `.dict()` у объектов моделей Pydantic с параметром `exclude_unset`, чтобы достичь такого эффекта. + +!!! info "Информация" + Вы также можете использовать: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + как описано в документации Pydantic для параметров `exclude_defaults` и `exclude_none`. + +#### Если значение поля отличается от значения по-умолчанию + +Если для некоторых полей модели, имеющих значения по-умолчанию, значения были явно установлены - как для элемента с ID = `Bar`, ответ будет таким: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +они не будут исключены из ответа. + +#### Если значение поля совпадает с его значением по умолчанию + +Если данные содержат те же значения, которые являются для этих полей по умолчанию, но были установлены явно - как для элемента с ID = `baz`, ответ будет таким: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPI достаточно умен (на самом деле, это заслуга Pydantic), чтобы понять, что, хотя `description`, `tax` и `tags` хранят такие же данные, какие должны быть по умолчанию - для них эти значения были установлены явно (а не получены из значений по умолчанию). + +И поэтому, они также будут включены в JSON ответа. + +!!! tip "Подсказка" + Значением по умолчанию может быть что угодно, не только `None`. + + Им может быть и список (`[]`), значение 10.5 типа `float`, и т.п. + +### `response_model_include` и `response_model_exclude` + +Вы также можете использовать параметры *декоратора операции пути*, такие, как `response_model_include` и `response_model_exclude`. + +Они принимают аргументы типа `set`, состоящий из строк (`str`) с названиями атрибутов, которые либо требуется включить в ответ (при этом исключив все остальные), либо наоборот исключить (оставив в ответе все остальные поля). + +Это можно использовать как быстрый способ исключить данные из ответа, не создавая отдельную модель Pydantic. + +!!! tip "Подсказка" + Но по-прежнему рекомендуется следовать изложенным выше советам и использовать несколько моделей вместо данных параметров. + + Потому как JSON схема OpenAPI, генерируемая вашим приложением (а также документация) все еще будет содержать все поля, даже если вы использовали `response_model_include` или `response_model_exclude` и исключили некоторые атрибуты. + + То же самое применимо к параметру `response_model_by_alias`. + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial005_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial005.py!} + ``` + +!!! tip "Подсказка" + При помощи кода `{"name","description"}` создается объект множества (`set`) с двумя строковыми значениями. + + Того же самого можно достичь используя `set(["name", "description"])`. + +#### Что если использовать `list` вместо `set`? + +Если вы забыли про `set` и использовали структуру `list` или `tuple`, FastAPI автоматически преобразует этот объект в `set`, чтобы обеспечить корректную работу: + +=== "Python 3.10+" + + ```Python hl_lines="29 35" + {!> ../../../docs_src/response_model/tutorial006_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="31 37" + {!> ../../../docs_src/response_model/tutorial006.py!} + ``` + +## Резюме + +Используйте параметр `response_model` у *декоратора операции пути* для того, чтобы задать модель ответа и в большей степени для того, чтобы быть уверенным, что приватная информация будет отфильтрована. + +А также используйте `response_model_exclude_unset`, чтобы возвращать только те значения, которые были заданы явно. From 317cef3f8a4ff41a019e8558e97b4c64a2769f51 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:00:55 +0000 Subject: [PATCH 1123/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e55bf48c7..75ce0bbe6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). From 6ba4492670381eb5cc08f49a19a83a51485ddc07 Mon Sep 17 00:00:00 2001 From: mojtaba <121169359+mojtabapaso@users.noreply.github.com> Date: Tue, 27 Jun 2023 04:32:00 +0330 Subject: [PATCH 1124/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20transl?= =?UTF-8?q?ation=20for=20`docs/fa/docs/advanced/sub-applications.md`=20(#9?= =?UTF-8?q?692)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Amin Alaee Co-authored-by: Sebastián Ramírez --- docs/fa/docs/advanced/sub-applications.md | 72 +++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/fa/docs/advanced/sub-applications.md diff --git a/docs/fa/docs/advanced/sub-applications.md b/docs/fa/docs/advanced/sub-applications.md new file mode 100644 index 000000000..f3a948414 --- /dev/null +++ b/docs/fa/docs/advanced/sub-applications.md @@ -0,0 +1,72 @@ +# زیر برنامه ها - اتصال + +اگر نیاز دارید که دو برنامه مستقل FastAPI، با OpenAPI مستقل و رابط‌های کاربری اسناد خود داشته باشید، می‌توانید یک برنامه +اصلی داشته باشید و یک (یا چند) زیر برنامه را به آن متصل کنید. + +## اتصال (mount) به یک برنامه **FastAPI** + +کلمه "Mounting" به معنای افزودن یک برنامه کاملاً مستقل در یک مسیر خاص است، که پس از آن مدیریت همه چیز در آن مسیر، با path operations (عملیات های مسیر) اعلام شده در آن زیر برنامه می باشد. + +### برنامه سطح بالا + +ابتدا برنامه اصلی سطح بالا، **FastAPI** و path operations آن را ایجاد کنید: + + +```Python hl_lines="3 6-8" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### زیر برنامه + +سپس، زیر برنامه خود و path operations آن را ایجاد کنید. + +این زیر برنامه فقط یکی دیگر از برنامه های استاندارد FastAPI است، اما این برنامه ای است که متصل می شود: + +```Python hl_lines="11 14-16" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### اتصال زیر برنامه + +در برنامه سطح بالا `app` اتصال زیر برنامه `subapi` در این نمونه `/subapi` در مسیر قرار میدهد و میشود: + +```Python hl_lines="11 19" +{!../../../docs_src/sub_applications/tutorial001.py!} +``` + +### اسناد API خودکار را بررسی کنید + +برنامه را با استفاده از ‘uvicorn‘ اجرا کنید، اگر فایل شما ‘main.py‘ نام دارد، دستور زیر را وارد کنید: +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +صفحه مستندات را در آدرس http://127.0.0.1:8000/docs باز کنید. + +اسناد API خودکار برنامه اصلی را مشاهده خواهید کرد که فقط شامل path operations خود می شود: + + + +و سپس اسناد زیر برنامه را در آدرس http://127.0.0.1:8000/subapi/docs. باز کنید. + +اسناد API خودکار برای زیر برنامه را خواهید دید، که فقط شامل path operations خود می شود، همه در زیر مسیر `/subapi` قرار دارند: + + + +اگر سعی کنید با هر یک از این دو رابط کاربری تعامل داشته باشید، آنها به درستی کار می کنند، زیرا مرورگر می تواند با هر یک از برنامه ها یا زیر برنامه های خاص صحبت کند. + +### جرئیات فنی : `root_path` + +هنگامی که یک زیر برنامه را همانطور که در بالا توضیح داده شد متصل می کنید, FastAPI با استفاده از مکانیزمی از مشخصات ASGI به نام `root_path` ارتباط مسیر mount را برای زیر برنامه انجام می دهد. + +به این ترتیب، زیر برنامه می داند که از آن پیشوند مسیر برای رابط کاربری اسناد (docs UI) استفاده کند. + +و زیر برنامه ها نیز می تواند زیر برنامه های متصل شده خود را داشته باشد و همه چیز به درستی کار کند، زیرا FastAPI تمام این مسیرهای `root_path` را به طور خودکار مدیریت می کند. + +در بخش [پشت پراکسی](./behind-a-proxy.md){.internal-link target=_blank}. درباره `root_path` و نحوه استفاده درست از آن بیشتر خواهید آموخت. From a95af9466967cefdd1433650745264b4d2db5a4f Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:02:34 +0000 Subject: [PATCH 1125/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 75ce0bbe6..190526bd1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). From eb312758d8c10ac723f4954cd0b86e07fa9ac3eb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:06:02 +0200 Subject: [PATCH 1126/1881] =?UTF-8?q?=E2=AC=86=20[pre-commit.ci]=20pre-com?= =?UTF-8?q?mit=20autoupdate=20(#9259)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/pyupgrade: v3.3.1 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.7.0) - [github.com/charliermarsh/ruff-pre-commit: v0.0.272 → v0.0.275](https://github.com/charliermarsh/ruff-pre-commit/compare/v0.0.272...v0.0.275) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a8a03136..9f7085f72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,14 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.7.0 hooks: - id: pyupgrade args: - --py3-plus - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.272 + rev: v0.0.275 hooks: - id: ruff args: From 5e7d45af16ba0b6fb1a954d44f714a6016ec9e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 27 Jun 2023 03:06:27 +0200 Subject: [PATCH 1127/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20missing=20tra?= =?UTF-8?q?nslation=20dummy=20pages,=20no=20longer=20necessary=20(#9751)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/index.md | 463 ----------------------------------------- docs/id/docs/index.md | 465 ------------------------------------------ docs/tr/docs/index.md | 4 - 3 files changed, 932 deletions(-) delete mode 100644 docs/de/docs/index.md delete mode 100644 docs/id/docs/index.md diff --git a/docs/de/docs/index.md b/docs/de/docs/index.md deleted file mode 100644 index f1c873d75..000000000 --- a/docs/de/docs/index.md +++ /dev/null @@ -1,463 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Union - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Union - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Union[bool, None] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Union[str, None] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * extremely easy tests based on `requests` and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/id/docs/index.md b/docs/id/docs/index.md deleted file mode 100644 index ed551f910..000000000 --- a/docs/id/docs/index.md +++ /dev/null @@ -1,465 +0,0 @@ - -{!../../../docs/missing-translation.md!} - - -

- FastAPI -

-

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

-

- - Test - - - Coverage - - - Package version - -

- ---- - -**Documentation**: https://fastapi.tiangolo.com - -**Source Code**: https://github.com/tiangolo/fastapi - ---- - -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints. - -The key features are: - -* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic). [One of the fastest Python frameworks available](#performance). - -* **Fast to code**: Increase the speed to develop features by about 200% to 300%. * -* **Fewer bugs**: Reduce about 40% of human (developer) induced errors. * -* **Intuitive**: Great editor support. Completion everywhere. Less time debugging. -* **Easy**: Designed to be easy to use and learn. Less time reading docs. -* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs. -* **Robust**: Get production-ready code. With automatic interactive documentation. -* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema. - -* estimation based on tests on an internal development team, building production applications. - -## Sponsors - - - -{% if sponsors %} -{% for sponsor in sponsors.gold -%} - -{% endfor -%} -{%- for sponsor in sponsors.silver -%} - -{% endfor %} -{% endif %} - - - -Other sponsors - -## Opinions - -"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" - -
Kabir Khan - Microsoft (ref)
- ---- - -"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" - -
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
- ---- - -"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" - -
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
- ---- - -"_I’m over the moon excited about **FastAPI**. It’s so fun!_" - -
Brian Okken - Python Bytes podcast host (ref)
- ---- - -"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" - -
Timothy Crosley - Hug creator (ref)
- ---- - -"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" - -"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" - -
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
- ---- - -## **Typer**, the FastAPI of CLIs - - - -If you are building a CLI app to be used in the terminal instead of a web API, check out **Typer**. - -**Typer** is FastAPI's little sibling. And it's intended to be the **FastAPI of CLIs**. ⌨️ 🚀 - -## Requirements - -Python 3.7+ - -FastAPI stands on the shoulders of giants: - -* Starlette for the web parts. -* Pydantic for the data parts. - -## Installation - -
- -```console -$ pip install fastapi - ----> 100% -``` - -
- -You will also need an ASGI server, for production such as Uvicorn or Hypercorn. - -
- -```console -$ pip install "uvicorn[standard]" - ----> 100% -``` - -
- -## Example - -### Create it - -* Create a file `main.py` with: - -```Python -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -
-Or use async def... - -If your code uses `async` / `await`, use `async def`: - -```Python hl_lines="9 14" -from typing import Optional - -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -async def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -async def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} -``` - -**Note**: - -If you don't know, check the _"In a hurry?"_ section about `async` and `await` in the docs. - -
- -### Run it - -Run the server with: - -
- -```console -$ uvicorn main:app --reload - -INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) -INFO: Started reloader process [28720] -INFO: Started server process [28722] -INFO: Waiting for application startup. -INFO: Application startup complete. -``` - -
- -
-About the command uvicorn main:app --reload... - -The command `uvicorn main:app` refers to: - -* `main`: the file `main.py` (the Python "module"). -* `app`: the object created inside of `main.py` with the line `app = FastAPI()`. -* `--reload`: make the server restart after code changes. Only do this for development. - -
- -### Check it - -Open your browser at http://127.0.0.1:8000/items/5?q=somequery. - -You will see the JSON response as: - -```JSON -{"item_id": 5, "q": "somequery"} -``` - -You already created an API that: - -* Receives HTTP requests in the _paths_ `/` and `/items/{item_id}`. -* Both _paths_ take `GET` operations (also known as HTTP _methods_). -* The _path_ `/items/{item_id}` has a _path parameter_ `item_id` that should be an `int`. -* The _path_ `/items/{item_id}` has an optional `str` _query parameter_ `q`. - -### Interactive API docs - -Now go to http://127.0.0.1:8000/docs. - -You will see the automatic interactive API documentation (provided by Swagger UI): - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) - -### Alternative API docs - -And now, go to http://127.0.0.1:8000/redoc. - -You will see the alternative automatic documentation (provided by ReDoc): - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) - -## Example upgrade - -Now modify the file `main.py` to receive a body from a `PUT` request. - -Declare the body using standard Python types, thanks to Pydantic. - -```Python hl_lines="4 9-12 25-27" -from typing import Optional - -from fastapi import FastAPI -from pydantic import BaseModel - -app = FastAPI() - - -class Item(BaseModel): - name: str - price: float - is_offer: Optional[bool] = None - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): - return {"item_id": item_id, "q": q} - - -@app.put("/items/{item_id}") -def update_item(item_id: int, item: Item): - return {"item_name": item.name, "item_id": item_id} -``` - -The server should reload automatically (because you added `--reload` to the `uvicorn` command above). - -### Interactive API docs upgrade - -Now go to http://127.0.0.1:8000/docs. - -* The interactive API documentation will be automatically updated, including the new body: - -![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) - -* Click on the button "Try it out", it allows you to fill the parameters and directly interact with the API: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) - -* Then click on the "Execute" button, the user interface will communicate with your API, send the parameters, get the results and show them on the screen: - -![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) - -### Alternative API docs upgrade - -And now, go to http://127.0.0.1:8000/redoc. - -* The alternative documentation will also reflect the new query parameter and body: - -![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) - -### Recap - -In summary, you declare **once** the types of parameters, body, etc. as function parameters. - -You do that with standard modern Python types. - -You don't have to learn a new syntax, the methods or classes of a specific library, etc. - -Just standard **Python 3.6+**. - -For example, for an `int`: - -```Python -item_id: int -``` - -or for a more complex `Item` model: - -```Python -item: Item -``` - -...and with that single declaration you get: - -* Editor support, including: - * Completion. - * Type checks. -* Validation of data: - * Automatic and clear errors when the data is invalid. - * Validation even for deeply nested JSON objects. -* Conversion of input data: coming from the network to Python data and types. Reading from: - * JSON. - * Path parameters. - * Query parameters. - * Cookies. - * Headers. - * Forms. - * Files. -* Conversion of output data: converting from Python data and types to network data (as JSON): - * Convert Python types (`str`, `int`, `float`, `bool`, `list`, etc). - * `datetime` objects. - * `UUID` objects. - * Database models. - * ...and many more. -* Automatic interactive API documentation, including 2 alternative user interfaces: - * Swagger UI. - * ReDoc. - ---- - -Coming back to the previous code example, **FastAPI** will: - -* Validate that there is an `item_id` in the path for `GET` and `PUT` requests. -* Validate that the `item_id` is of type `int` for `GET` and `PUT` requests. - * If it is not, the client will see a useful, clear error. -* Check if there is an optional query parameter named `q` (as in `http://127.0.0.1:8000/items/foo?q=somequery`) for `GET` requests. - * As the `q` parameter is declared with `= None`, it is optional. - * Without the `None` it would be required (as is the body in the case with `PUT`). -* For `PUT` requests to `/items/{item_id}`, Read the body as JSON: - * Check that it has a required attribute `name` that should be a `str`. - * Check that it has a required attribute `price` that has to be a `float`. - * Check that it has an optional attribute `is_offer`, that should be a `bool`, if present. - * All this would also work for deeply nested JSON objects. -* Convert from and to JSON automatically. -* Document everything with OpenAPI, that can be used by: - * Interactive documentation systems. - * Automatic client code generation systems, for many languages. -* Provide 2 interactive documentation web interfaces directly. - ---- - -We just scratched the surface, but you already get the idea of how it all works. - -Try changing the line with: - -```Python - return {"item_name": item.name, "item_id": item_id} -``` - -...from: - -```Python - ... "item_name": item.name ... -``` - -...to: - -```Python - ... "item_price": item.price ... -``` - -...and see how your editor will auto-complete the attributes and know their types: - -![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) - -For a more complete example including more features, see the Tutorial - User Guide. - -**Spoiler alert**: the tutorial - user guide includes: - -* Declaration of **parameters** from other different places as: **headers**, **cookies**, **form fields** and **files**. -* How to set **validation constraints** as `maximum_length` or `regex`. -* A very powerful and easy to use **Dependency Injection** system. -* Security and authentication, including support for **OAuth2** with **JWT tokens** and **HTTP Basic** auth. -* More advanced (but equally easy) techniques for declaring **deeply nested JSON models** (thanks to Pydantic). -* Many extra features (thanks to Starlette) as: - * **WebSockets** - * **GraphQL** - * extremely easy tests based on HTTPX and `pytest` - * **CORS** - * **Cookie Sessions** - * ...and more. - -## Performance - -Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as one of the fastest Python frameworks available, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) - -To understand more about it, see the section Benchmarks. - -## Optional Dependencies - -Used by Pydantic: - -* email_validator - for email validation. - -Used by Starlette: - -* httpx - Required if you want to use the `TestClient`. -* jinja2 - Required if you want to use the default template configuration. -* python-multipart - Required if you want to support form "parsing", with `request.form()`. -* itsdangerous - Required for `SessionMiddleware` support. -* pyyaml - Required for Starlette's `SchemaGenerator` support (you probably don't need it with FastAPI). -* graphene - Required for `GraphQLApp` support. -* ujson - Required if you want to use `UJSONResponse`. - -Used by FastAPI / Starlette: - -* uvicorn - for the server that loads and serves your application. -* orjson - Required if you want to use `ORJSONResponse`. - -You can install all of these with `pip install fastapi[all]`. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index 2339337f3..e74efbc2f 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -1,7 +1,3 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

From dffca555ff69fa445c7435809352ba28333f8b7b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:06:48 +0000 Subject: [PATCH 1128/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 190526bd1..522ee253c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). * 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). From 6c143b930dfe66c2a83073e801296ea651126ca1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:07:03 +0000 Subject: [PATCH 1129/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 522ee253c..f8ad8d179 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). From 9debdc97ef854aef1911059ad8d89c54d9661453 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:08:43 +0200 Subject: [PATCH 1130/1881] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20?= =?UTF-8?q?from=209.1.16=20to=209.1.17=20(#9746)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.16 to 9.1.17. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.16...9.1.17) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 2c5f71ec0..df60ca4df 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . mkdocs==1.4.3 -mkdocs-material==9.1.16 +mkdocs-material==9.1.17 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 From 706d74b6ad60ee773b8705ba7d15636369d8b4c8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:10:40 +0000 Subject: [PATCH 1131/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f8ad8d179..f14ca10f3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). * 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). From 782b1c49a9eeed34004b44e5d3b67f3315392c68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 03:13:10 +0200 Subject: [PATCH 1132/1881] =?UTF-8?q?=E2=AC=86=20Update=20httpx=20requirem?= =?UTF-8?q?ent=20from=20<0.24.0,>=3D0.23.0=20to=20>=3D0.23.0,<0.25.0=20(#9?= =?UTF-8?q?724)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the requirements on [httpx](https://github.com/encode/httpx) to permit the latest version. - [Release notes](https://github.com/encode/httpx/releases) - [Changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/httpx/compare/0.23.0...0.24.1) --- updated-dependencies: - dependency-name: httpx dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index 5cedde84d..4b34fcc2c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.0 ruff ==0.0.275 black == 23.3.0 -httpx >=0.23.0,<0.24.0 +httpx >=0.23.0,<0.25.0 email_validator >=1.1.1,<2.0.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel From d409c05d6ff3411640e92b0d39a9fb2b3f1cbea7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 27 Jun 2023 01:14:01 +0000 Subject: [PATCH 1133/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f14ca10f3..808caf2d4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). From 1f21b16e03260d6935c73fa609b7017357abaf34 Mon Sep 17 00:00:00 2001 From: Carson Crane Date: Wed, 28 Jun 2023 09:39:10 -0700 Subject: [PATCH 1134/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`de?= =?UTF-8?q?que`=20objects=20and=20children=20in=20`jsonable=5Fencoder`=20(?= =?UTF-8?q?#9433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- fastapi/encoders.py | 4 ++-- tests/test_jsonable_encoder.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 2f95bcbf6..94f41bfa1 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,5 +1,5 @@ import dataclasses -from collections import defaultdict +from collections import defaultdict, deque from enum import Enum from pathlib import PurePath from types import GeneratorType @@ -124,7 +124,7 @@ def jsonable_encoder( ) encoded_dict[encoded_key] = encoded_value return encoded_dict - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)): encoded_list = [] for item in obj: encoded_list.append( diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index f4fdcf601..1f43c33c7 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,3 +1,4 @@ +from collections import deque from dataclasses import dataclass from datetime import datetime, timezone from enum import Enum @@ -237,3 +238,12 @@ def test_encode_model_with_path(model_with_path): def test_encode_root(): model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" + + +def test_encode_deque_encodes_child_models(): + class Model(BaseModel): + test: str + + dq = deque([Model(test="test")]) + + assert jsonable_encoder(dq)[0]["test"] == "test" From 0f390cd4b57b47fb53989094dc87d18ce337058a Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 28 Jun 2023 16:39:44 +0000 Subject: [PATCH 1135/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 808caf2d4..5565b8022 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). From 0a8423d792bda91ab74c9c8b0021c9a9388cbd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 18:23:02 +0200 Subject: [PATCH 1136/1881] =?UTF-8?q?=F0=9F=94=A8=20Enable=20linenums=20in?= =?UTF-8?q?=20MkDocs=20Material=20during=20local=20live=20development=20to?= =?UTF-8?q?=20simplify=20highlighting=20code=20(#9769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.maybe-insiders.yml | 3 +++ docs/en/mkdocs.yml | 20 ++++++++++---------- scripts/docs.py | 2 ++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/en/mkdocs.maybe-insiders.yml b/docs/en/mkdocs.maybe-insiders.yml index 8e6271334..37fd9338e 100644 --- a/docs/en/mkdocs.maybe-insiders.yml +++ b/docs/en/mkdocs.maybe-insiders.yml @@ -1,3 +1,6 @@ # Define this here and not in the main mkdocs.yml file because that one is auto # updated and written, and the script would remove the env var INHERIT: !ENV [INSIDERS_FILE, '../en/mkdocs.no-insiders.yml'] +markdown_extensions: + pymdownx.highlight: + linenums: !ENV [LINENUMS, false] diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 21300b9dc..64dc40372 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -169,24 +169,24 @@ nav: - contributing.md - release-notes.md markdown_extensions: -- toc: + toc: permalink: true -- markdown.extensions.codehilite: + markdown.extensions.codehilite: guess_lang: false -- mdx_include: + mdx_include: base_path: docs -- admonition -- codehilite -- extra -- pymdownx.superfences: + admonition: + codehilite: + extra: + pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format '' -- pymdownx.tabbed: + pymdownx.tabbed: alternate_style: true -- attr_list -- md_in_html + attr_list: + md_in_html: extra: analytics: provider: google diff --git a/scripts/docs.py b/scripts/docs.py index 20838be6a..968dd9a3d 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -258,6 +258,8 @@ def live( Takes an optional LANG argument with the name of the language to serve, by default en. """ + # Enable line numbers during local development to make it easier to highlight + os.environ["LINENUMS"] = "true" if lang is None: lang = "en" lang_path: Path = docs_path / lang From 02fc9e8a63361fa25e8787d5a0da069890da62c6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 30 Jun 2023 16:23:36 +0000 Subject: [PATCH 1137/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5565b8022..fca7502cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7dad5a820bfe99e49a6cfaefde537e09644c2c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:25:16 +0200 Subject: [PATCH 1138/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Ope?= =?UTF-8?q?nAPI=203.1.0=20(#9770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Update OpenAPI models for JSON Schema 2020-12 and OpenAPI 3.1.0 * ✨ Add support for summary and webhooks * ✨ Update JSON Schema for UploadFiles * ⏪️ Revert making paths optional, to ensure always correctness * ⏪️ Keep UploadFile as format: binary for compatibility with the rest of Pydantic bytes fields in v1 * ✨ Update version of OpenAPI generated to 3.1.0 * ✨ Update the version of Swagger UI * 📝 Update docs about extending OpenAPI * 📝 Update docs and links to refer to OpenAPI 3.1.0 * ✨ Update logic for handling webhooks * ♻️ Update parameter functions and classes, deprecate example and make examples the main field * ✅ Update tests for OpenAPI 3.1.0 * 📝 Update examples for OpenAPI metadata * ✅ Add and update tests for OpenAPI metadata * 📝 Add source example for webhooks * 📝 Update docs for metadata * 📝 Update docs for Schema extra * 📝 Add docs for webhooks * 🔧 Add webhooks docs to MkDocs * ✅ Update tests for extending OpenAPI * ✅ Add tests for webhooks * ♻️ Refactor generation of OpenAPI and JSON Schema with params * 📝 Update source examples for field examples * ✅ Update tests for examples * ➕ Make sure the minimum version of typing-extensions installed has deprecated() (already a dependency of Pydantic) * ✏️ Fix typo in Webhooks example code * 🔥 Remove commented out code of removed nullable field * 🗑️ Add deprecation warnings for example argument * ✅ Update tests to check for deprecation warnings * ✅ Add test for webhooks with security schemes, for coverage * 🍱 Update image for metadata, with new summary * 🍱 Add docs image for Webhooks * 📝 Update docs for webhooks, add docs UI image --- docs/en/docs/advanced/additional-responses.md | 4 +- docs/en/docs/advanced/behind-a-proxy.md | 4 +- docs/en/docs/advanced/extending-openapi.md | 16 +- docs/en/docs/advanced/openapi-callbacks.md | 4 +- docs/en/docs/advanced/openapi-webhooks.md | 51 ++ .../path-operation-advanced-configuration.md | 2 +- .../en/docs/img/tutorial/metadata/image01.png | Bin 90062 -> 86437 bytes .../img/tutorial/openapi-webhooks/image01.png | Bin 0 -> 86925 bytes docs/en/docs/tutorial/first-steps.md | 2 +- docs/en/docs/tutorial/metadata.md | 15 +- docs/en/docs/tutorial/path-params.md | 2 +- docs/en/docs/tutorial/schema-extra-example.md | 120 +++-- docs/en/mkdocs.yml | 1 + docs_src/extending_openapi/tutorial001.py | 3 +- docs_src/metadata/tutorial001.py | 1 + docs_src/metadata/tutorial001_1.py | 38 ++ docs_src/openapi_webhooks/tutorial001.py | 25 + docs_src/schema_extra_example/tutorial001.py | 14 +- .../schema_extra_example/tutorial001_py310.py | 14 +- docs_src/schema_extra_example/tutorial002.py | 8 +- .../schema_extra_example/tutorial002_py310.py | 8 +- docs_src/schema_extra_example/tutorial003.py | 14 +- .../schema_extra_example/tutorial003_an.py | 14 +- .../tutorial003_an_py310.py | 14 +- .../tutorial003_an_py39.py | 14 +- .../schema_extra_example/tutorial003_py310.py | 14 +- docs_src/schema_extra_example/tutorial004.py | 10 +- .../schema_extra_example/tutorial004_an.py | 10 +- .../tutorial004_an_py310.py | 10 +- .../tutorial004_an_py39.py | 10 +- .../schema_extra_example/tutorial004_py310.py | 10 +- fastapi/applications.py | 8 +- fastapi/openapi/docs.py | 4 +- fastapi/openapi/models.py | 93 +++- fastapi/openapi/utils.py | 38 +- fastapi/param_functions.py | 73 ++- fastapi/params.py | 108 +++- pyproject.toml | 1 + tests/test_additional_properties.py | 2 +- tests/test_additional_response_extra.py | 2 +- tests/test_additional_responses_bad.py | 2 +- ...onal_responses_custom_model_in_callback.py | 2 +- ...tional_responses_custom_validationerror.py | 2 +- ...ional_responses_default_validationerror.py | 2 +- ...est_additional_responses_response_class.py | 2 +- tests/test_additional_responses_router.py | 2 +- tests/test_annotated.py | 2 +- tests/test_application.py | 2 +- tests/test_custom_route_class.py | 2 +- tests/test_dependency_duplicates.py | 2 +- tests/test_deprecated_openapi_prefix.py | 2 +- tests/test_duplicate_models_openapi.py | 2 +- tests/test_enforce_once_required_parameter.py | 2 +- tests/test_extra_routes.py | 2 +- tests/test_filter_pydantic_sub_model.py | 2 +- tests/test_generate_unique_id_function.py | 14 +- tests/test_get_request_body.py | 2 +- .../test_include_router_defaults_overrides.py | 2 +- .../test_modules_same_name_body/test_main.py | 2 +- tests/test_multi_body_errors.py | 2 +- tests/test_multi_query_errors.py | 2 +- .../test_openapi_query_parameter_extension.py | 2 +- tests/test_openapi_route_extensions.py | 2 +- tests/test_openapi_servers.py | 2 +- tests/test_param_in_path_and_dependency.py | 2 +- tests/test_param_include_in_schema.py | 2 +- tests/test_put_no_body.py | 2 +- tests/test_repeated_dependency_schema.py | 2 +- tests/test_repeated_parameter_alias.py | 2 +- tests/test_reponse_set_reponse_code_empty.py | 2 +- ...test_request_body_parameters_media_type.py | 2 +- tests/test_response_by_alias.py | 2 +- tests/test_response_class_no_mediatype.py | 2 +- tests/test_response_code_no_body.py | 2 +- ...est_response_model_as_return_annotation.py | 2 +- tests/test_response_model_sub_types.py | 2 +- tests/test_schema_extra_examples.py | 508 ++++++++---------- tests/test_security_api_key_cookie.py | 2 +- ...est_security_api_key_cookie_description.py | 2 +- .../test_security_api_key_cookie_optional.py | 2 +- tests/test_security_api_key_header.py | 2 +- ...est_security_api_key_header_description.py | 2 +- .../test_security_api_key_header_optional.py | 2 +- tests/test_security_api_key_query.py | 2 +- ...test_security_api_key_query_description.py | 2 +- tests/test_security_api_key_query_optional.py | 2 +- tests/test_security_http_base.py | 2 +- tests/test_security_http_base_description.py | 2 +- tests/test_security_http_base_optional.py | 2 +- tests/test_security_http_basic_optional.py | 2 +- tests/test_security_http_basic_realm.py | 2 +- ...t_security_http_basic_realm_description.py | 2 +- tests/test_security_http_bearer.py | 2 +- .../test_security_http_bearer_description.py | 2 +- tests/test_security_http_bearer_optional.py | 2 +- tests/test_security_http_digest.py | 2 +- .../test_security_http_digest_description.py | 2 +- tests/test_security_http_digest_optional.py | 2 +- tests/test_security_oauth2.py | 2 +- ...curity_oauth2_authorization_code_bearer.py | 2 +- ...2_authorization_code_bearer_description.py | 2 +- tests/test_security_oauth2_optional.py | 2 +- ...st_security_oauth2_optional_description.py | 2 +- ...ecurity_oauth2_password_bearer_optional.py | 2 +- ...h2_password_bearer_optional_description.py | 2 +- tests/test_security_openid_connect.py | 2 +- ...est_security_openid_connect_description.py | 2 +- .../test_security_openid_connect_optional.py | 2 +- tests/test_starlette_exception.py | 2 +- tests/test_sub_callbacks.py | 2 +- tests/test_tuples.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial002.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial004.py | 2 +- .../test_tutorial001.py | 2 +- .../test_behind_a_proxy/test_tutorial001.py | 2 +- .../test_behind_a_proxy/test_tutorial002.py | 2 +- .../test_behind_a_proxy/test_tutorial003.py | 2 +- .../test_behind_a_proxy/test_tutorial004.py | 2 +- .../test_bigger_applications/test_main.py | 2 +- .../test_bigger_applications/test_main_an.py | 2 +- .../test_main_an_py39.py | 2 +- .../test_body/test_tutorial001.py | 2 +- .../test_body/test_tutorial001_py310.py | 2 +- .../test_body_fields/test_tutorial001.py | 2 +- .../test_body_fields/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_tutorial009.py | 2 +- .../test_tutorial009_py39.py | 2 +- .../test_body_updates/test_tutorial001.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_tutorial001_py39.py | 2 +- .../test_tutorial001.py | 2 +- .../test_cookie_params/test_tutorial001.py | 2 +- .../test_cookie_params/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_custom_response/test_tutorial001.py | 2 +- .../test_custom_response/test_tutorial001b.py | 2 +- .../test_custom_response/test_tutorial004.py | 2 +- .../test_custom_response/test_tutorial005.py | 2 +- .../test_custom_response/test_tutorial006.py | 2 +- .../test_custom_response/test_tutorial006b.py | 2 +- .../test_custom_response/test_tutorial006c.py | 2 +- .../test_dataclasses/test_tutorial001.py | 2 +- .../test_dataclasses/test_tutorial002.py | 2 +- .../test_dataclasses/test_tutorial003.py | 2 +- .../test_dependencies/test_tutorial001.py | 2 +- .../test_dependencies/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_dependencies/test_tutorial004.py | 2 +- .../test_dependencies/test_tutorial004_an.py | 2 +- .../test_tutorial004_an_py310.py | 2 +- .../test_tutorial004_an_py39.py | 2 +- .../test_tutorial004_py310.py | 2 +- .../test_dependencies/test_tutorial006.py | 2 +- .../test_dependencies/test_tutorial006_an.py | 2 +- .../test_tutorial006_an_py39.py | 2 +- .../test_dependencies/test_tutorial012.py | 2 +- .../test_dependencies/test_tutorial012_an.py | 2 +- .../test_tutorial012_an_py39.py | 2 +- .../test_events/test_tutorial001.py | 2 +- .../test_events/test_tutorial002.py | 2 +- .../test_events/test_tutorial003.py | 2 +- .../test_tutorial001.py | 5 +- .../test_extra_data_types/test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_extra_models/test_tutorial003.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_extra_models/test_tutorial004.py | 2 +- .../test_tutorial004_py39.py | 2 +- .../test_extra_models/test_tutorial005.py | 2 +- .../test_tutorial005_py39.py | 2 +- .../test_first_steps/test_tutorial001.py | 2 +- .../test_generate_clients/test_tutorial003.py | 2 +- .../test_handling_errors/test_tutorial001.py | 2 +- .../test_handling_errors/test_tutorial002.py | 2 +- .../test_handling_errors/test_tutorial003.py | 2 +- .../test_handling_errors/test_tutorial004.py | 2 +- .../test_handling_errors/test_tutorial005.py | 2 +- .../test_handling_errors/test_tutorial006.py | 2 +- .../test_header_params/test_tutorial001.py | 2 +- .../test_header_params/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py310.py | 2 +- .../test_tutorial001_py310.py | 2 +- .../test_header_params/test_tutorial002.py | 2 +- .../test_header_params/test_tutorial002_an.py | 2 +- .../test_tutorial002_an_py310.py | 2 +- .../test_tutorial002_an_py39.py | 2 +- .../test_tutorial002_py310.py | 2 +- .../test_header_params/test_tutorial003.py | 2 +- .../test_header_params/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_metadata/test_tutorial001.py | 3 +- .../test_metadata/test_tutorial001_1.py | 49 ++ .../test_metadata/test_tutorial004.py | 2 +- .../test_tutorial001.py | 2 +- .../test_openapi_webhooks/__init__.py | 0 .../test_openapi_webhooks/test_tutorial001.py | 117 ++++ .../test_tutorial001.py | 2 +- .../test_tutorial002.py | 2 +- .../test_tutorial003.py | 2 +- .../test_tutorial004.py | 2 +- .../test_tutorial005.py | 2 +- .../test_tutorial006.py | 2 +- .../test_tutorial007.py | 2 +- .../test_tutorial002b.py | 2 +- .../test_tutorial005.py | 2 +- .../test_tutorial005_py310.py | 2 +- .../test_tutorial005_py39.py | 2 +- .../test_tutorial006.py | 2 +- .../test_path_params/test_tutorial004.py | 2 +- .../test_path_params/test_tutorial005.py | 2 +- .../test_query_params/test_tutorial005.py | 2 +- .../test_query_params/test_tutorial006.py | 2 +- .../test_tutorial006_py310.py | 2 +- .../test_tutorial010.py | 2 +- .../test_tutorial010_an.py | 2 +- .../test_tutorial010_an_py310.py | 2 +- .../test_tutorial010_an_py39.py | 2 +- .../test_tutorial010_py310.py | 2 +- .../test_tutorial011.py | 2 +- .../test_tutorial011_an.py | 2 +- .../test_tutorial011_an_py310.py | 2 +- .../test_tutorial011_an_py39.py | 2 +- .../test_tutorial011_py310.py | 2 +- .../test_tutorial011_py39.py | 2 +- .../test_tutorial012.py | 2 +- .../test_tutorial012_an.py | 2 +- .../test_tutorial012_an_py39.py | 2 +- .../test_tutorial012_py39.py | 2 +- .../test_tutorial013.py | 2 +- .../test_tutorial013_an.py | 2 +- .../test_tutorial013_an_py39.py | 2 +- .../test_tutorial014.py | 2 +- .../test_tutorial014_an.py | 2 +- .../test_tutorial014_an_py310.py | 2 +- .../test_tutorial014_an_py39.py | 2 +- .../test_tutorial014_py310.py | 2 +- .../test_request_files/test_tutorial001.py | 2 +- .../test_request_files/test_tutorial001_02.py | 2 +- .../test_tutorial001_02_an.py | 2 +- .../test_tutorial001_02_an_py310.py | 2 +- .../test_tutorial001_02_an_py39.py | 2 +- .../test_tutorial001_02_py310.py | 2 +- .../test_request_files/test_tutorial001_03.py | 2 +- .../test_tutorial001_03_an.py | 2 +- .../test_tutorial001_03_an_py39.py | 2 +- .../test_request_files/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_request_files/test_tutorial002.py | 2 +- .../test_request_files/test_tutorial002_an.py | 2 +- .../test_tutorial002_an_py39.py | 2 +- .../test_tutorial002_py39.py | 2 +- .../test_request_files/test_tutorial003.py | 2 +- .../test_request_files/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py39.py | 2 +- .../test_tutorial003_py39.py | 2 +- .../test_request_forms/test_tutorial001.py | 2 +- .../test_request_forms/test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_tutorial001.py | 2 +- .../test_tutorial001_an.py | 2 +- .../test_tutorial001_an_py39.py | 2 +- .../test_response_model/test_tutorial003.py | 2 +- .../test_tutorial003_01.py | 2 +- .../test_tutorial003_01_py310.py | 2 +- .../test_tutorial003_02.py | 2 +- .../test_tutorial003_03.py | 2 +- .../test_tutorial003_05.py | 2 +- .../test_tutorial003_05_py310.py | 2 +- .../test_tutorial003_py310.py | 2 +- .../test_response_model/test_tutorial004.py | 2 +- .../test_tutorial004_py310.py | 2 +- .../test_tutorial004_py39.py | 2 +- .../test_response_model/test_tutorial005.py | 2 +- .../test_tutorial005_py310.py | 2 +- .../test_response_model/test_tutorial006.py | 2 +- .../test_tutorial006_py310.py | 2 +- .../test_tutorial004.py | 51 +- .../test_tutorial004_an.py | 51 +- .../test_tutorial004_an_py310.py | 51 +- .../test_tutorial004_an_py39.py | 51 +- .../test_tutorial004_py310.py | 51 +- .../test_security/test_tutorial001.py | 2 +- .../test_security/test_tutorial001_an.py | 2 +- .../test_security/test_tutorial001_an_py39.py | 2 +- .../test_security/test_tutorial003.py | 2 +- .../test_security/test_tutorial003_an.py | 2 +- .../test_tutorial003_an_py310.py | 2 +- .../test_security/test_tutorial003_an_py39.py | 2 +- .../test_security/test_tutorial003_py310.py | 2 +- .../test_security/test_tutorial005.py | 2 +- .../test_security/test_tutorial005_an.py | 2 +- .../test_tutorial005_an_py310.py | 2 +- .../test_security/test_tutorial005_an_py39.py | 2 +- .../test_security/test_tutorial005_py310.py | 2 +- .../test_security/test_tutorial005_py39.py | 2 +- .../test_security/test_tutorial006.py | 2 +- .../test_security/test_tutorial006_an.py | 2 +- .../test_security/test_tutorial006_an_py39.py | 2 +- .../test_sql_databases/test_sql_databases.py | 2 +- .../test_sql_databases_middleware.py | 2 +- .../test_sql_databases_middleware_py310.py | 2 +- .../test_sql_databases_middleware_py39.py | 2 +- .../test_sql_databases_py310.py | 2 +- .../test_sql_databases_py39.py | 2 +- .../test_sql_databases_peewee.py | 2 +- .../test_sub_applications/test_tutorial001.py | 4 +- tests/test_tutorial/test_testing/test_main.py | 2 +- .../test_testing/test_tutorial001.py | 2 +- tests/test_union_body.py | 2 +- tests/test_union_inherited_body.py | 2 +- tests/test_webhooks_security.py | 126 +++++ 335 files changed, 1533 insertions(+), 891 deletions(-) create mode 100644 docs/en/docs/advanced/openapi-webhooks.md create mode 100644 docs/en/docs/img/tutorial/openapi-webhooks/image01.png create mode 100644 docs_src/metadata/tutorial001_1.py create mode 100644 docs_src/openapi_webhooks/tutorial001.py create mode 100644 tests/test_tutorial/test_metadata/test_tutorial001_1.py create mode 100644 tests/test_tutorial/test_openapi_webhooks/__init__.py create mode 100644 tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py create mode 100644 tests/test_webhooks_security.py diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index dca5f6a98..624036ce9 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -236,5 +236,5 @@ For example: To see what exactly you can include in the responses, you can check these sections in the OpenAPI specification: -* OpenAPI Responses Object, it includes the `Response Object`. -* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. +* OpenAPI Responses Object, it includes the `Response Object`. +* OpenAPI Response Object, you can include anything from this directly in each response inside your `responses` parameter. Including `description`, `headers`, `content` (inside of this is that you declare different media types and JSON Schemas), and `links`. diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 03198851a..e7af77f3d 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -46,7 +46,7 @@ The docs UI would also need the OpenAPI schema to declare that this API `server` ```JSON hl_lines="4-8" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { @@ -298,7 +298,7 @@ Will generate an OpenAPI schema like: ```JSON hl_lines="5-7" { - "openapi": "3.0.2", + "openapi": "3.1.0", // More stuff here "servers": [ { diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index 36619696b..c47f939af 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -29,10 +29,14 @@ And that function `get_openapi()` receives as parameters: * `title`: The OpenAPI title, shown in the docs. * `version`: The version of your API, e.g. `2.5.0`. -* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`. -* `description`: The description of your API. +* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. +* `summary`: A short summary of the API. +* `description`: The description of your API, this can include markdown and will be shown in the docs. * `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. +!!! info + The parameter `summary` is available in OpenAPI 3.1.0 and above, supported by FastAPI 0.99.0 and above. + ## Overriding the defaults Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. @@ -51,7 +55,7 @@ First, write all your **FastAPI** application as normally: Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: -```Python hl_lines="2 15-20" +```Python hl_lines="2 15-21" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -59,7 +63,7 @@ Then, use the same utility function to generate the OpenAPI schema, inside a `cu Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema: -```Python hl_lines="21-23" +```Python hl_lines="22-24" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -71,7 +75,7 @@ That way, your application won't have to generate the schema every time a user o It will be generated only once, and then the same cached schema will be used for the next requests. -```Python hl_lines="13-14 24-25" +```Python hl_lines="13-14 25-26" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` @@ -79,7 +83,7 @@ It will be generated only once, and then the same cached schema will be used for Now you can replace the `.openapi()` method with your new function. -```Python hl_lines="28" +```Python hl_lines="29" {!../../../docs_src/extending_openapi/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 71924ce8b..37339eae5 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -103,11 +103,11 @@ It should look just like a normal FastAPI *path operation*: There are 2 main differences from a normal *path operation*: * It doesn't need to have any actual code, because your app will never call this code. It's only used to document the *external API*. So, the function could just have `pass`. -* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. +* The *path* can contain an OpenAPI 3 expression (see more below) where it can use variables with parameters and parts of the original request sent to *your API*. ### The callback path expression -The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. +The callback *path* can have an OpenAPI 3 expression that can contain parts of the original request sent to *your API*. In this case, it's the `str`: diff --git a/docs/en/docs/advanced/openapi-webhooks.md b/docs/en/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..63cbdc610 --- /dev/null +++ b/docs/en/docs/advanced/openapi-webhooks.md @@ -0,0 +1,51 @@ +# OpenAPI Webhooks + +There are cases where you want to tell your API **users** that your app could call *their* app (sending a request) with some data, normally to **notify** of some type of **event**. + +This means that instead of the normal process of your users sending requests to your API, it's **your API** (or your app) that could **send requests to their system** (to their API, their app). + +This is normally called a **webhook**. + +## Webhooks steps + +The process normally is that **you define** in your code what is the message that you will send, the **body of the request**. + +You also define in some way at which **moments** your app will send those requests or events. + +And **your users** define in some way (for example in a web dashboard somewhere) the **URL** where your app should send those requests. + +All the **logic** about how to register the URLs for webhooks and the code to actually send those requests is up to you. You write it however you want to in **your own code**. + +## Documenting webhooks with **FastAPI** and OpenAPI + +With **FastAPI**, using OpenAPI, you can define the names of these webhooks, the types of HTTP operations that your app can send (e.g. `POST`, `PUT`, etc.) and the request **bodies** that your app would send. + +This can make it a lot easier for your users to **implement their APIs** to receive your **webhook** requests, they might even be able to autogenerate some of their own API code. + +!!! info + Webhooks are available in OpenAPI 3.1.0 and above, supported by FastAPI `0.99.0` and above. + +## An app with webhooks + +When you create a **FastAPI** application, there is a `webhooks` attribute that you can use to define *webhooks*, the same way you would define *path operations*, for example with `@app.webhooks.post()`. + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_webhooks/tutorial001.py!} +``` + +The webhooks that you define will end up in the **OpenAPI** schema and the automatic **docs UI**. + +!!! info + The `app.webhooks` object is actually just an `APIRouter`, the same type you would use when structuring your app with multiple files. + +Notice that with webhooks you are actually not declaring a *path* (like `/items/`), the text you pass there is just an **identifier** of the webhook (the name of the event), for example in `@app.webhooks.post("new-subscription")`, the webhook name is `new-subscription`. + +This is because it is expected that **your users** would define the actual **URL path** where they want to receive the webhook request in some other way (e.g. a web dashboard). + +### Check the docs + +Now you can start your app with Uvicorn and go to http://127.0.0.1:8000/docs. + +You will see your docs have the normal *path operations* and now also some **webhooks**: + + diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index a1c902ef2..6d9a5fe70 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -97,7 +97,7 @@ And if you see the resulting OpenAPI (at `/openapi.json` in your API), you will ```JSON hl_lines="22" { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" diff --git a/docs/en/docs/img/tutorial/metadata/image01.png b/docs/en/docs/img/tutorial/metadata/image01.png index b7708a3fd98ad2170d880781896623ec8ed80204..4146a8607b5b4278b9d826c5d9e45ee9ff183ce3 100644 GIT binary patch literal 86437 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 literal 90062 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 diff --git a/docs/en/docs/img/tutorial/openapi-webhooks/image01.png b/docs/en/docs/img/tutorial/openapi-webhooks/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..25ced48186d8395b0406b9a66e75afb17aa83fd5 GIT binary patch literal 86925 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�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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/tutorial/first-steps.md b/docs/en/docs/tutorial/first-steps.md index 6ca5f39eb..cfa159329 100644 --- a/docs/en/docs/tutorial/first-steps.md +++ b/docs/en/docs/tutorial/first-steps.md @@ -99,7 +99,7 @@ It will show a JSON starting with something like: ```JSON { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index cf13e7470..e75b4a0b9 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -9,15 +9,16 @@ You can set the following fields that are used in the OpenAPI specification and | Parameter | Type | Description | |------------|------|-------------| | `title` | `str` | The title of the API. | +| `summary` | `str` | A short summary of the API. Available since OpenAPI 3.1.0, FastAPI 0.99.0. | | `description` | `str` | A short description of the API. It can use Markdown. | | `version` | `string` | The version of the API. This is the version of your own application, not of OpenAPI. For example `2.5.0`. | | `terms_of_service` | `str` | A URL to the Terms of Service for the API. If provided, this has to be a URL. | | `contact` | `dict` | The contact information for the exposed API. It can contain several fields.
contact fields
ParameterTypeDescription
namestrThe identifying name of the contact person/organization.
urlstrThe URL pointing to the contact information. MUST be in the format of a URL.
emailstrThe email address of the contact person/organization. MUST be in the format of an email address.
| -| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| +| `license_info` | `dict` | The license information for the exposed API. It can contain several fields.
license_info fields
ParameterTypeDescription
namestrREQUIRED (if a license_info is set). The license name used for the API.
identifierstrAn SPDX license expression for the API. The identifier field is mutually exclusive of the url field. Available since OpenAPI 3.1.0, FastAPI 0.99.0.
urlstrA URL to the license used for the API. MUST be in the format of a URL.
| You can set them as follows: -```Python hl_lines="3-16 19-31" +```Python hl_lines="3-16 19-32" {!../../../docs_src/metadata/tutorial001.py!} ``` @@ -28,6 +29,16 @@ With this configuration, the automatic API docs would look like: +## License identifier + +Since OpenAPI 3.1.0 and FastAPI 0.99.0, you can also set the `license_info` with an `identifier` instead of a `url`. + +For example: + +```Python hl_lines="31" +{!../../../docs_src/metadata/tutorial001_1.py!} +``` + ## Metadata for tags You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`. diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index a0d70692e..6594a7a8b 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -83,7 +83,7 @@ And when you open your browser at
OpenAPI standard, there are many compatible tools. +And because the generated schema is from the OpenAPI standard, there are many compatible tools. Because of this, **FastAPI** itself provides an alternative API documentation (using ReDoc), which you can access at http://127.0.0.1:8000/redoc: diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index e0f7ed256..6cf8bf181 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -6,17 +6,17 @@ Here are several ways to do it. ## Pydantic `schema_extra` -You can declare an `example` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: +You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: === "Python 3.10+" - ```Python hl_lines="13-21" + ```Python hl_lines="13-23" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` === "Python 3.6+" - ```Python hl_lines="15-23" + ```Python hl_lines="15-25" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} ``` @@ -27,11 +27,16 @@ That extra info will be added as-is to the output **JSON Schema** for that model For example you could use it to add metadata for a frontend user interface, etc. +!!! info + OpenAPI 3.1.0 (used since FastAPI 0.99.0) added support for `examples`, which is part of the **JSON Schema** standard. + + Before that, it only supported the keyword `example` with a single example. That is still supported by OpenAPI 3.1.0, but is deprecated and is not part of the JSON Schema standard. So you are encouraged to migrate `example` to `examples`. 🤓 + + You can read more at the end of this page. + ## `Field` additional arguments -When using `Field()` with Pydantic models, you can also declare extra info for the **JSON Schema** by passing any other arbitrary arguments to the function. - -You can use this to add `example` for each field: +When using `Field()` with Pydantic models, you can also declare additional `examples`: === "Python 3.10+" @@ -45,10 +50,7 @@ You can use this to add `example` for each field: {!> ../../../docs_src/schema_extra_example/tutorial002.py!} ``` -!!! warning - Keep in mind that those extra arguments passed won't add any validation, only extra information, for documentation purposes. - -## `example` and `examples` in OpenAPI +## `examples` in OpenAPI When using any of: @@ -60,27 +62,27 @@ When using any of: * `Form()` * `File()` -you can also declare a data `example` or a group of `examples` with additional information that will be added to **OpenAPI**. +you can also declare a group of `examples` with additional information that will be added to **OpenAPI**. -### `Body` with `example` +### `Body` with `examples` -Here we pass an `example` of the data expected in `Body()`: +Here we pass `examples` containing one example of the data expected in `Body()`: === "Python 3.10+" - ```Python hl_lines="22-27" + ```Python hl_lines="22-29" {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} ``` === "Python 3.9+" - ```Python hl_lines="22-27" + ```Python hl_lines="22-29" {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} ``` === "Python 3.6+" - ```Python hl_lines="23-28" + ```Python hl_lines="23-30" {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} ``` @@ -89,7 +91,7 @@ Here we pass an `example` of the data expected in `Body()`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="18-23" + ```Python hl_lines="18-25" {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` @@ -98,7 +100,7 @@ Here we pass an `example` of the data expected in `Body()`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="20-25" + ```Python hl_lines="20-27" {!> ../../../docs_src/schema_extra_example/tutorial003.py!} ``` @@ -110,16 +112,7 @@ With any of the methods above it would look like this in the `/docs`: ### `Body` with multiple `examples` -Alternatively to the single `example`, you can pass `examples` using a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. - -The keys of the `dict` identify each example, and each value is another `dict`. - -Each specific example `dict` in the `examples` can contain: - -* `summary`: Short description for the example. -* `description`: A long description that can contain Markdown text. -* `value`: This is the actual example shown, e.g. a `dict`. -* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. +You can of course also pass multiple `examples`: === "Python 3.10+" @@ -165,25 +158,76 @@ With `examples` added to `Body()` the `/docs` would look like: ## Technical Details +!!! tip + If you are already using **FastAPI** version **0.99.0 or above**, you can probably **skip** these details. + + They are more relevant for older versions, before OpenAPI 3.1.0 was available. + + You can consider this a brief OpenAPI and JSON Schema **history lesson**. 🤓 + !!! warning These are very technical details about the standards **JSON Schema** and **OpenAPI**. If the ideas above already work for you, that might be enough, and you probably don't need these details, feel free to skip them. -When you add an example inside of a Pydantic model, using `schema_extra` or `Field(example="something")` that example is added to the **JSON Schema** for that Pydantic model. +Before OpenAPI 3.1.0, OpenAPI used an older and modified version of **JSON Schema**. + +JSON Schema didn't have `examples`, so OpenAPI added it's own `example` field to its own modified version. + +OpenAPI also added `example` and `examples` fields to other parts of the specification: + +* `Parameter Object` (in the specification) that was used by FastAPI's: + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` +* `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification) that was used by FastAPI's: + * `Body()` + * `File()` + * `Form()` + +### OpenAPI's `examples` field + +The shape of this field `examples` from OpenAPI is a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. + +The keys of the `dict` identify each example, and each value is another `dict`. + +Each specific example `dict` in the `examples` can contain: + +* `summary`: Short description for the example. +* `description`: A long description that can contain Markdown text. +* `value`: This is the actual example shown, e.g. a `dict`. +* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. + +This applies to those other parts of the OpenAPI specification apart from JSON Schema. + +### JSON Schema's `examples` field + +But then JSON Schema added an `examples` field to a new version of the specification. + +And then the new OpenAPI 3.1.0 was based on the latest version (JSON Schema 2020-12) that included this new field `examples`. + +And now this new `examples` field takes precedence over the old single (and custom) `example` field, that is now deprecated. + +This new `examples` field in JSON Schema is **just a `list`** of examples, not a dict with extra metadata as in the other places in OpenAPI (described above). + +!!! info + Even after OpenAPI 3.1.0 was released with this new simpler integration with JSON Schema, for a while, Swagger UI, the tool that provides the automatic docs, didn't support OpenAPI 3.1.0 (it does since version 5.0.0 🎉). + + Because of that, versions of FastAPI previous to 0.99.0 still used versions of OpenAPI lower than 3.1.0. + +### Pydantic and FastAPI `examples` + +When you add `examples` inside of a Pydantic model, using `schema_extra` or `Field(examples=["something"])` that example is added to the **JSON Schema** for that Pydantic model. And that **JSON Schema** of the Pydantic model is included in the **OpenAPI** of your API, and then it's used in the docs UI. -**JSON Schema** doesn't really have a field `example` in the standards. Recent versions of JSON Schema define a field `examples`, but OpenAPI 3.0.3 is based on an older version of JSON Schema that didn't have `examples`. +In versions of FastAPI before 0.99.0 (0.99.0 and above use the newer OpenAPI 3.1.0) when you used `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples were not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they were added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). -So, OpenAPI 3.0.3 defined its own `example` for the modified version of **JSON Schema** it uses, for the same purpose (but it's a single `example`, not `examples`), and that's what is used by the API docs UI (using Swagger UI). +But now that FastAPI 0.99.0 and above uses OpenAPI 3.1.0, that uses JSON Schema 2020-12, and Swagger UI 5.0.0 and above, everything is more consistent and the examples are included in JSON Schema. -So, although `example` is not part of JSON Schema, it is part of OpenAPI's custom version of JSON Schema, and that's what will be used by the docs UI. +### Summary -But when you use `example` or `examples` with any of the other utilities (`Query()`, `Body()`, etc.) those examples are not added to the JSON Schema that describes that data (not even to OpenAPI's own version of JSON Schema), they are added directly to the *path operation* declaration in OpenAPI (outside the parts of OpenAPI that use JSON Schema). +I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅 -For `Path()`, `Query()`, `Header()`, and `Cookie()`, the `example` or `examples` are added to the OpenAPI definition, to the `Parameter Object` (in the specification). - -And for `Body()`, `File()`, and `Form()`, the `example` or `examples` are equivalently added to the OpenAPI definition, to the `Request Body Object`, in the field `content`, on the `Media Type Object` (in the specification). - -On the other hand, there's a newer version of OpenAPI: **3.1.0**, recently released. It is based on the latest JSON Schema and most of the modifications from OpenAPI's custom version of JSON Schema are removed, in exchange of the features from the recent versions of JSON Schema, so all these small differences are reduced. Nevertheless, Swagger UI currently doesn't support OpenAPI 3.1.0, so, for now, it's better to continue using the ideas above. +In short, **upgrade to FastAPI 0.99.0 or above**, and things are much **simpler, consistent, and intuitive**, and you don't have to know all these historic details. 😎 diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 64dc40372..030bbe5d3 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -147,6 +147,7 @@ nav: - advanced/conditional-openapi.md - advanced/extending-openapi.md - advanced/openapi-callbacks.md + - advanced/openapi-webhooks.md - advanced/wsgi.md - advanced/generate-clients.md - async.md diff --git a/docs_src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001.py index 561e95898..35e31c0e0 100644 --- a/docs_src/extending_openapi/tutorial001.py +++ b/docs_src/extending_openapi/tutorial001.py @@ -15,7 +15,8 @@ def custom_openapi(): openapi_schema = get_openapi( title="Custom title", version="2.5.0", - description="This is a very custom OpenAPI schema", + summary="This is a very custom OpenAPI schema", + description="Here's a longer description of the custom **OpenAPI** schema", routes=app.routes, ) openapi_schema["info"]["x-logo"] = { diff --git a/docs_src/metadata/tutorial001.py b/docs_src/metadata/tutorial001.py index 3fba9e7d1..76656e81b 100644 --- a/docs_src/metadata/tutorial001.py +++ b/docs_src/metadata/tutorial001.py @@ -18,6 +18,7 @@ You will be able to: app = FastAPI( title="ChimichangApp", description=description, + summary="Deadpool's favorite app. Nuff said.", version="0.0.1", terms_of_service="http://example.com/terms/", contact={ diff --git a/docs_src/metadata/tutorial001_1.py b/docs_src/metadata/tutorial001_1.py new file mode 100644 index 000000000..a8f5b9458 --- /dev/null +++ b/docs_src/metadata/tutorial001_1.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI + +description = """ +ChimichangApp API helps you do awesome stuff. 🚀 + +## Items + +You can **read items**. + +## Users + +You will be able to: + +* **Create users** (_not implemented_). +* **Read users** (_not implemented_). +""" + +app = FastAPI( + title="ChimichangApp", + description=description, + summary="Deadpool's favorite app. Nuff said.", + version="0.0.1", + terms_of_service="http://example.com/terms/", + contact={ + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + license_info={ + "name": "Apache 2.0", + "identifier": "MIT", + }, +) + + +@app.get("/items/") +async def read_items(): + return [{"name": "Katana"}] diff --git a/docs_src/openapi_webhooks/tutorial001.py b/docs_src/openapi_webhooks/tutorial001.py new file mode 100644 index 000000000..5016f5b00 --- /dev/null +++ b/docs_src/openapi_webhooks/tutorial001.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Subscription(BaseModel): + username: str + montly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription(body: Subscription): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +@app.get("/users/") +def read_users(): + return ["Rick", "Morty"] diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index a5ae28127..6ab96ff85 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -14,12 +14,14 @@ class Item(BaseModel): class Config: schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py index 77ceedd60..ec83f1112 100644 --- a/docs_src/schema_extra_example/tutorial001_py310.py +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -12,12 +12,14 @@ class Item(BaseModel): class Config: schema_extra = { - "example": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - } + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] } diff --git a/docs_src/schema_extra_example/tutorial002.py b/docs_src/schema_extra_example/tutorial002.py index 6de434f81..70f06567c 100644 --- a/docs_src/schema_extra_example/tutorial002.py +++ b/docs_src/schema_extra_example/tutorial002.py @@ -7,10 +7,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: Union[str, None] = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: Union[float, None] = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: Union[str, None] = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: Union[float, None] = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial002_py310.py b/docs_src/schema_extra_example/tutorial002_py310.py index e84928bb1..27d786867 100644 --- a/docs_src/schema_extra_example/tutorial002_py310.py +++ b/docs_src/schema_extra_example/tutorial002_py310.py @@ -5,10 +5,10 @@ app = FastAPI() class Item(BaseModel): - name: str = Field(example="Foo") - description: str | None = Field(default=None, example="A very nice Item") - price: float = Field(example=35.4) - tax: float | None = Field(default=None, example=3.2) + name: str = Field(examples=["Foo"]) + description: str | None = Field(default=None, examples=["A very nice Item"]) + price: float = Field(examples=[35.4]) + tax: float | None = Field(default=None, examples=[3.2]) @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial003.py b/docs_src/schema_extra_example/tutorial003.py index ce1736bba..385f3de8a 100644 --- a/docs_src/schema_extra_example/tutorial003.py +++ b/docs_src/schema_extra_example/tutorial003.py @@ -17,12 +17,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial003_an.py b/docs_src/schema_extra_example/tutorial003_an.py index 1dec555a9..23675aba1 100644 --- a/docs_src/schema_extra_example/tutorial003_an.py +++ b/docs_src/schema_extra_example/tutorial003_an.py @@ -20,12 +20,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_an_py310.py b/docs_src/schema_extra_example/tutorial003_an_py310.py index 9edaddfb8..bbd2e171e 100644 --- a/docs_src/schema_extra_example/tutorial003_an_py310.py +++ b/docs_src/schema_extra_example/tutorial003_an_py310.py @@ -19,12 +19,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_an_py39.py b/docs_src/schema_extra_example/tutorial003_an_py39.py index fe08847d9..472808561 100644 --- a/docs_src/schema_extra_example/tutorial003_an_py39.py +++ b/docs_src/schema_extra_example/tutorial003_an_py39.py @@ -19,12 +19,14 @@ async def update_item( item: Annotated[ Item, Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial003_py310.py b/docs_src/schema_extra_example/tutorial003_py310.py index 1e137101d..2d31619be 100644 --- a/docs_src/schema_extra_example/tutorial003_py310.py +++ b/docs_src/schema_extra_example/tutorial003_py310.py @@ -15,12 +15,14 @@ class Item(BaseModel): async def update_item( item_id: int, item: Item = Body( - example={ - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + examples=[ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index b67edf30c..eb49293fd 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -18,8 +18,8 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -29,7 +29,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -37,14 +37,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py index 82c9a92ac..567ec4702 100644 --- a/docs_src/schema_extra_example/tutorial004_an.py +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -21,8 +21,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -32,7 +32,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -40,14 +40,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py index 01f1a486c..026654835 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py310.py +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -20,8 +20,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -31,7 +31,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -39,14 +39,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py index d50e8aa5f..06219ede8 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py39.py +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -20,8 +20,8 @@ async def update_item( item: Annotated[ Item, Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -31,7 +31,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -39,14 +39,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ], ): diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index 100a30860..ef2b9d8cb 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -16,8 +16,8 @@ async def update_item( *, item_id: int, item: Item = Body( - examples={ - "normal": { + examples=[ + { "summary": "A normal example", "description": "A **normal** item works correctly.", "value": { @@ -27,7 +27,7 @@ async def update_item( "tax": 3.2, }, }, - "converted": { + { "summary": "An example with converted data", "description": "FastAPI can convert price `strings` to actual `numbers` automatically", "value": { @@ -35,14 +35,14 @@ async def update_item( "price": "35.4", }, }, - "invalid": { + { "summary": "Invalid data is rejected with an error", "value": { "name": "Baz", "price": "thirty five point four", }, }, - }, + ], ), ): results = {"item_id": item_id, "item": item} diff --git a/fastapi/applications.py b/fastapi/applications.py index 9b161c5ec..88f861c1e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -55,6 +55,7 @@ class FastAPI(Starlette): debug: bool = False, routes: Optional[List[BaseRoute]] = None, title: str = "FastAPI", + summary: Optional[str] = None, description: str = "", version: str = "0.1.0", openapi_url: Optional[str] = "/openapi.json", @@ -85,6 +86,7 @@ class FastAPI(Starlette): root_path_in_servers: bool = True, responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, callbacks: Optional[List[BaseRoute]] = None, + webhooks: Optional[routing.APIRouter] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, swagger_ui_parameters: Optional[Dict[str, Any]] = None, @@ -95,6 +97,7 @@ class FastAPI(Starlette): ) -> None: self.debug = debug self.title = title + self.summary = summary self.description = description self.version = version self.terms_of_service = terms_of_service @@ -110,7 +113,7 @@ class FastAPI(Starlette): self.swagger_ui_parameters = swagger_ui_parameters self.servers = servers or [] self.extra = extra - self.openapi_version = "3.0.2" + self.openapi_version = "3.1.0" self.openapi_schema: Optional[Dict[str, Any]] = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" @@ -123,6 +126,7 @@ class FastAPI(Starlette): "automatic. Check the docs at " "https://fastapi.tiangolo.com/advanced/sub-applications/" ) + self.webhooks = webhooks or routing.APIRouter() self.root_path = root_path or openapi_prefix self.state: State = State() self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} @@ -215,11 +219,13 @@ class FastAPI(Starlette): title=self.title, version=self.version, openapi_version=self.openapi_version, + summary=self.summary, description=self.description, terms_of_service=self.terms_of_service, contact=self.contact, license_info=self.license_info, routes=self.routes, + webhooks=self.webhooks.routes, tags=self.openapi_tags, servers=self.servers, ) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index bf335118f..81f67dcc5 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -17,8 +17,8 @@ def get_swagger_ui_html( *, openapi_url: str, title: str, - swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js", - swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css", + swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", + swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", oauth2_redirect_url: Optional[str] = None, init_oauth: Optional[Dict[str, Any]] = None, diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 81a24f389..7420d3b55 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,9 +1,10 @@ from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field -from typing_extensions import Literal +from typing_extensions import Annotated, Literal +from typing_extensions import deprecated as typing_deprecated try: import email_validator # type: ignore @@ -37,6 +38,7 @@ class Contact(BaseModel): class License(BaseModel): name: str + identifier: Optional[str] = None url: Optional[AnyUrl] = None class Config: @@ -45,6 +47,7 @@ class License(BaseModel): class Info(BaseModel): title: str + summary: Optional[str] = None description: Optional[str] = None termsOfService: Optional[str] = None contact: Optional[Contact] = None @@ -56,7 +59,7 @@ class Info(BaseModel): class ServerVariable(BaseModel): - enum: Optional[List[str]] = None + enum: Annotated[Optional[List[str]], Field(min_items=1)] = None default: str description: Optional[str] = None @@ -102,9 +105,42 @@ class ExternalDocumentation(BaseModel): class Schema(BaseModel): + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-the-json-schema-core-vocabu + # Core Vocabulary + schema_: Optional[str] = Field(default=None, alias="$schema") + vocabulary: Optional[str] = Field(default=None, alias="$vocabulary") + id: Optional[str] = Field(default=None, alias="$id") + anchor: Optional[str] = Field(default=None, alias="$anchor") + dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") - title: Optional[str] = None - multipleOf: Optional[float] = None + dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") + defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs") + comment: Optional[str] = Field(default=None, alias="$comment") + # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s + # A Vocabulary for Applying Subschemas + allOf: Optional[List["Schema"]] = None + anyOf: Optional[List["Schema"]] = None + oneOf: Optional[List["Schema"]] = None + not_: Optional["Schema"] = Field(default=None, alias="not") + if_: Optional["Schema"] = Field(default=None, alias="if") + then: Optional["Schema"] = None + else_: Optional["Schema"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "Schema"]] = None + prefixItems: Optional[List["Schema"]] = None + items: Optional[Union["Schema", List["Schema"]]] = None + contains: Optional["Schema"] = None + properties: Optional[Dict[str, "Schema"]] = None + patternProperties: Optional[Dict[str, "Schema"]] = None + additionalProperties: Optional["Schema"] = None + propertyNames: Optional["Schema"] = None + unevaluatedItems: Optional["Schema"] = None + unevaluatedProperties: Optional["Schema"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural + # A Vocabulary for Structural Validation + type: Optional[str] = None + enum: Optional[List[Any]] = None + const: Optional[Any] = None + multipleOf: Optional[float] = Field(default=None, gt=0) maximum: Optional[float] = None exclusiveMaximum: Optional[float] = None minimum: Optional[float] = None @@ -115,29 +151,41 @@ class Schema(BaseModel): maxItems: Optional[int] = Field(default=None, ge=0) minItems: Optional[int] = Field(default=None, ge=0) uniqueItems: Optional[bool] = None + maxContains: Optional[int] = Field(default=None, ge=0) + minContains: Optional[int] = Field(default=None, ge=0) maxProperties: Optional[int] = Field(default=None, ge=0) minProperties: Optional[int] = Field(default=None, ge=0) required: Optional[List[str]] = None - enum: Optional[List[Any]] = None - type: Optional[str] = None - allOf: Optional[List["Schema"]] = None - oneOf: Optional[List["Schema"]] = None - anyOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(default=None, alias="not") - items: Optional[Union["Schema", List["Schema"]]] = None - properties: Optional[Dict[str, "Schema"]] = None - additionalProperties: Optional[Union["Schema", Reference, bool]] = None - description: Optional[str] = None + dependentRequired: Optional[Dict[str, Set[str]]] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-vocabularies-for-semantic-c + # Vocabularies for Semantic Content With "format" format: Optional[str] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-the-conten + # A Vocabulary for the Contents of String-Encoded Data + contentEncoding: Optional[str] = None + contentMediaType: Optional[str] = None + contentSchema: Optional["Schema"] = None + # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta + # A Vocabulary for Basic Meta-Data Annotations + title: Optional[str] = None + description: Optional[str] = None default: Optional[Any] = None - nullable: Optional[bool] = None - discriminator: Optional[Discriminator] = None + deprecated: Optional[bool] = None readOnly: Optional[bool] = None writeOnly: Optional[bool] = None + examples: Optional[List[Any]] = None + # Ref: OpenAPI 3.1.0: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#schema-object + # Schema Object + discriminator: Optional[Discriminator] = None xml: Optional[XML] = None externalDocs: Optional[ExternalDocumentation] = None - example: Optional[Any] = None - deprecated: Optional[bool] = None + example: Annotated[ + Optional[Any], + typing_deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = None class Config: extra: str = "allow" @@ -248,7 +296,7 @@ class Operation(BaseModel): parameters: Optional[List[Union[Parameter, Reference]]] = None requestBody: Optional[Union[RequestBody, Reference]] = None # Using Any for Specification Extensions - responses: Dict[str, Union[Response, Any]] + responses: Optional[Dict[str, Union[Response, Any]]] = None callbacks: Optional[Dict[str, Union[Dict[str, "PathItem"], Reference]]] = None deprecated: Optional[bool] = None security: Optional[List[Dict[str, List[str]]]] = None @@ -375,6 +423,7 @@ class Components(BaseModel): links: Optional[Dict[str, Union[Link, Reference]]] = None # Using Any for Specification Extensions callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None + pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None class Config: extra = "allow" @@ -392,9 +441,11 @@ class Tag(BaseModel): class OpenAPI(BaseModel): openapi: str info: Info + jsonSchemaDialect: Optional[str] = None servers: Optional[List[Server]] = None # Using Any for Specification Extensions - paths: Dict[str, Union[PathItem, Any]] + paths: Optional[Dict[str, Union[PathItem, Any]]] = None + webhooks: Optional[Dict[str, Union[PathItem, Reference]]] = None components: Optional[Components] = None security: Optional[List[Dict[str, List[str]]]] = None tags: Optional[List[Tag]] = None diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 6d736647b..609fe4389 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -106,9 +106,7 @@ def get_openapi_operation_parameters( } if field_info.description: parameter["description"] = field_info.description - if field_info.examples: - parameter["examples"] = jsonable_encoder(field_info.examples) - elif field_info.example != Undefined: + if field_info.example != Undefined: parameter["example"] = jsonable_encoder(field_info.example) if field_info.deprecated: parameter["deprecated"] = field_info.deprecated @@ -134,9 +132,7 @@ def get_openapi_operation_request_body( if required: request_body_oai["required"] = required request_media_content: Dict[str, Any] = {"schema": body_schema} - if field_info.examples: - request_media_content["examples"] = jsonable_encoder(field_info.examples) - elif field_info.example != Undefined: + if field_info.example != Undefined: request_media_content["example"] = jsonable_encoder(field_info.example) request_body_oai["content"] = {request_media_type: request_media_content} return request_body_oai @@ -392,9 +388,11 @@ def get_openapi( *, title: str, version: str, - openapi_version: str = "3.0.2", + openapi_version: str = "3.1.0", + summary: Optional[str] = None, description: Optional[str] = None, routes: Sequence[BaseRoute], + webhooks: Optional[Sequence[BaseRoute]] = None, tags: Optional[List[Dict[str, Any]]] = None, servers: Optional[List[Dict[str, Union[str, Any]]]] = None, terms_of_service: Optional[str] = None, @@ -402,6 +400,8 @@ def get_openapi( license_info: Optional[Dict[str, Union[str, Any]]] = None, ) -> Dict[str, Any]: info: Dict[str, Any] = {"title": title, "version": version} + if summary: + info["summary"] = summary if description: info["description"] = description if terms_of_service: @@ -415,13 +415,14 @@ def get_openapi( output["servers"] = servers components: Dict[str, Dict[str, Any]] = {} paths: Dict[str, Dict[str, Any]] = {} + webhook_paths: Dict[str, Dict[str, Any]] = {} operation_ids: Set[str] = set() - flat_models = get_flat_models_from_routes(routes) + flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or [])) model_name_map = get_model_name_map(flat_models) definitions = get_model_definitions( flat_models=flat_models, model_name_map=model_name_map ) - for route in routes: + for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( route=route, model_name_map=model_name_map, operation_ids=operation_ids @@ -436,11 +437,30 @@ def get_openapi( ) if path_definitions: definitions.update(path_definitions) + for webhook in webhooks or []: + if isinstance(webhook, routing.APIRoute): + result = get_openapi_path( + route=webhook, + model_name_map=model_name_map, + operation_ids=operation_ids, + ) + if result: + path, security_schemes, path_definitions = result + if path: + webhook_paths.setdefault(webhook.path_format, {}).update(path) + if security_schemes: + components.setdefault("securitySchemes", {}).update( + security_schemes + ) + if path_definitions: + definitions.update(path_definitions) if definitions: components["schemas"] = {k: definitions[k] for k in sorted(definitions)} if components: output["components"] = components output["paths"] = paths + if webhook_paths: + output["webhooks"] = webhook_paths if tags: output["tags"] = tags return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True) # type: ignore diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 75f054e9d..2f5818c85 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,7 +1,8 @@ -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, List, Optional, Sequence from fastapi import params from pydantic.fields import Undefined +from typing_extensions import Annotated, deprecated def Path( # noqa: N802 @@ -17,8 +18,14 @@ def Path( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -56,8 +63,14 @@ def Query( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -96,8 +109,14 @@ def Header( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -136,8 +155,14 @@ def Cookie( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -177,8 +202,14 @@ def Body( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.Body( @@ -215,8 +246,14 @@ def Form( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.Form( @@ -252,8 +289,14 @@ def File( # noqa: N802 min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ) -> Any: return params.File( diff --git a/fastapi/params.py b/fastapi/params.py index 16c5c309a..4069f2cda 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,7 +1,9 @@ +import warnings from enum import Enum -from typing import Any, Callable, Dict, Optional, Sequence +from typing import Any, Callable, List, Optional, Sequence from pydantic.fields import FieldInfo, Undefined +from typing_extensions import Annotated, deprecated class ParamTypes(Enum): @@ -28,16 +30,30 @@ class Param(FieldInfo): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, ): self.deprecated = deprecated + if example is not Undefined: + warnings.warn( + "`example` has been depreacated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=1, + ) self.example = example - self.examples = examples self.include_in_schema = include_in_schema + extra_kwargs = {**extra} + if examples: + extra_kwargs["examples"] = examples super().__init__( default=default, alias=alias, @@ -50,7 +66,7 @@ class Param(FieldInfo): min_length=min_length, max_length=max_length, regex=regex, - **extra, + **extra_kwargs, ) def __repr__(self) -> str: @@ -74,8 +90,14 @@ class Path(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -119,8 +141,14 @@ class Query(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -163,8 +191,14 @@ class Header(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -207,8 +241,14 @@ class Cookie(Param): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, deprecated: Optional[bool] = None, include_in_schema: bool = True, **extra: Any, @@ -250,14 +290,28 @@ class Body(FieldInfo): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): self.embed = embed self.media_type = media_type + if example is not Undefined: + warnings.warn( + "`example` has been depreacated, please use `examples` instead", + category=DeprecationWarning, + stacklevel=1, + ) self.example = example - self.examples = examples + extra_kwargs = {**extra} + if examples is not None: + extra_kwargs["examples"] = examples super().__init__( default=default, alias=alias, @@ -270,7 +324,7 @@ class Body(FieldInfo): min_length=min_length, max_length=max_length, regex=regex, - **extra, + **extra_kwargs, ) def __repr__(self) -> str: @@ -293,8 +347,14 @@ class Form(Body): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): super().__init__( @@ -333,8 +393,14 @@ class File(Form): min_length: Optional[int] = None, max_length: Optional[int] = None, regex: Optional[str] = None, - example: Any = Undefined, - examples: Optional[Dict[str, Any]] = None, + examples: Optional[List[Any]] = None, + example: Annotated[ + Optional[Any], + deprecated( + "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " + "although still supported. Use examples instead." + ), + ] = Undefined, **extra: Any, ): super().__init__( diff --git a/pyproject.toml b/pyproject.toml index 5c0d3c48e..61dbf7629 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ classifiers = [ dependencies = [ "starlette>=0.27.0,<0.28.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", + "typing-extensions>=4.5.0" ] dynamic = ["version"] diff --git a/tests/test_additional_properties.py b/tests/test_additional_properties.py index 516a569e4..be14d10ed 100644 --- a/tests/test_additional_properties.py +++ b/tests/test_additional_properties.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/foo": { diff --git a/tests/test_additional_response_extra.py b/tests/test_additional_response_extra.py index d62638c8f..55be19bad 100644 --- a/tests/test_additional_response_extra.py +++ b/tests/test_additional_response_extra.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_additional_responses_bad.py b/tests/test_additional_responses_bad.py index d2eb4d7a1..36df07f46 100644 --- a/tests/test_additional_responses_bad.py +++ b/tests/test_additional_responses_bad.py @@ -11,7 +11,7 @@ async def a(): openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 5c08eaa6d..397159142 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_additional_responses_custom_validationerror.py b/tests/test_additional_responses_custom_validationerror.py index 052602768..9fec5c96d 100644 --- a/tests/test_additional_responses_custom_validationerror.py +++ b/tests/test_additional_responses_custom_validationerror.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/{id}": { diff --git a/tests/test_additional_responses_default_validationerror.py b/tests/test_additional_responses_default_validationerror.py index 58de46ff6..153f04f57 100644 --- a/tests/test_additional_responses_default_validationerror.py +++ b/tests/test_additional_responses_default_validationerror.py @@ -16,7 +16,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/{id}": { diff --git a/tests/test_additional_responses_response_class.py b/tests/test_additional_responses_response_class.py index 6746760f0..68753561c 100644 --- a/tests/test_additional_responses_response_class.py +++ b/tests/test_additional_responses_response_class.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_additional_responses_router.py b/tests/test_additional_responses_router.py index 58d54b733..71cabc7c3 100644 --- a/tests/test_additional_responses_router.py +++ b/tests/test_additional_responses_router.py @@ -85,7 +85,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_annotated.py b/tests/test_annotated.py index a4f42b038..5a70c4541 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -118,7 +118,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/default": { diff --git a/tests/test_application.py b/tests/test_application.py index e5f2f4387..b036e67af 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -55,7 +55,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/api_route": { diff --git a/tests/test_custom_route_class.py b/tests/test_custom_route_class.py index d1b18ef1d..55374584b 100644 --- a/tests/test_custom_route_class.py +++ b/tests/test_custom_route_class.py @@ -75,7 +75,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/": { diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 285fdf1ab..4f4f3166c 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -86,7 +86,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/with-duplicates": { diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py index 688b9837f..ec7366d2a 100644 --- a/tests/test_deprecated_openapi_prefix.py +++ b/tests/test_deprecated_openapi_prefix.py @@ -22,7 +22,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_duplicate_models_openapi.py b/tests/test_duplicate_models_openapi.py index 116b2006a..83e86d231 100644 --- a/tests/test_duplicate_models_openapi.py +++ b/tests/test_duplicate_models_openapi.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_enforce_once_required_parameter.py b/tests/test_enforce_once_required_parameter.py index bf05aa585..b64f8341b 100644 --- a/tests/test_enforce_once_required_parameter.py +++ b/tests/test_enforce_once_required_parameter.py @@ -57,7 +57,7 @@ expected_schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/foo": { "get": { diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index c0db62c19..fa95d061c 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model.py index 15b15f862..6ee928d07 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/model/{name}": { diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index 0b519f859..c5ef5182b 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -48,7 +48,7 @@ def test_top_level_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -249,7 +249,7 @@ def test_router_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -450,7 +450,7 @@ def test_router_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -661,7 +661,7 @@ def test_subrouter_top_level_include_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -928,7 +928,7 @@ def test_router_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1136,7 +1136,7 @@ def test_app_path_operation_overrides_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { @@ -1353,7 +1353,7 @@ def test_callback_override_generate_unique_id(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_get_request_body.py b/tests/test_get_request_body.py index 541147fa8..cc567b88f 100644 --- a/tests/test_get_request_body.py +++ b/tests/test_get_request_body.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/product": { diff --git a/tests/test_include_router_defaults_overrides.py b/tests/test_include_router_defaults_overrides.py index ced56c84d..33baa25e6 100644 --- a/tests/test_include_router_defaults_overrides.py +++ b/tests/test_include_router_defaults_overrides.py @@ -443,7 +443,7 @@ def test_openapi(): assert issubclass(w[-1].category, UserWarning) assert "Duplicate Operation ID" in str(w[-1].message) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/override1": { diff --git a/tests/test_modules_same_name_body/test_main.py b/tests/test_modules_same_name_body/test_main.py index 1ebcaee8c..cc165bdca 100644 --- a/tests/test_modules_same_name_body/test_main.py +++ b/tests/test_modules_same_name_body/test_main.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a/compute": { diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 358684bc5..96043aa35 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -80,7 +80,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index e7a833f2b..c1f0678d0 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 8a9086ebe..6f62e6726 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -42,7 +42,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_openapi_route_extensions.py b/tests/test_openapi_route_extensions.py index 943dc43f2..3a3099436 100644 --- a/tests/test_openapi_route_extensions.py +++ b/tests/test_openapi_route_extensions.py @@ -22,7 +22,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 26abeaa12..11cd795ac 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/", "description": "Default, relative server"}, diff --git a/tests/test_param_in_path_and_dependency.py b/tests/test_param_in_path_and_dependency.py index 0aef7ac7b..08eb0f40f 100644 --- a/tests/test_param_in_path_and_dependency.py +++ b/tests/test_param_in_path_and_dependency.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/{user_id}": { diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py index d0c29f7b2..26201e9e2 100644 --- a/tests/test_param_include_in_schema.py +++ b/tests/test_param_include_in_schema.py @@ -34,7 +34,7 @@ async def hidden_query( openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/hidden_cookie": { diff --git a/tests/test_put_no_body.py b/tests/test_put_no_body.py index a02d1152c..8f4c82532 100644 --- a/tests/test_put_no_body.py +++ b/tests/test_put_no_body.py @@ -28,7 +28,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_repeated_dependency_schema.py b/tests/test_repeated_dependency_schema.py index ca0305184..d7d0dfa05 100644 --- a/tests/test_repeated_dependency_schema.py +++ b/tests/test_repeated_dependency_schema.py @@ -50,7 +50,7 @@ schema = { } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/": { "get": { diff --git a/tests/test_repeated_parameter_alias.py b/tests/test_repeated_parameter_alias.py index c656a161d..fd72eaab2 100644 --- a/tests/test_repeated_parameter_alias.py +++ b/tests/test_repeated_parameter_alias.py @@ -58,7 +58,7 @@ def test_openapi_schema(): } }, "info": {"title": "FastAPI", "version": "0.1.0"}, - "openapi": "3.0.2", + "openapi": "3.1.0", "paths": { "/{repeated_alias}": { "get": { diff --git a/tests/test_reponse_set_reponse_code_empty.py b/tests/test_reponse_set_reponse_code_empty.py index 14770fed0..bf3aa758c 100644 --- a/tests/test_reponse_set_reponse_code_empty.py +++ b/tests/test_reponse_set_reponse_code_empty.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/{id}": { diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 32f6c6a72..8424bf551 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -41,7 +41,7 @@ def test_openapi_schema(): assert response.status_code == 200, response.text # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/products": { diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index 1861a40fa..c3ff5b1d2 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -138,7 +138,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/dict": { diff --git a/tests/test_response_class_no_mediatype.py b/tests/test_response_class_no_mediatype.py index 2d75c7535..706929ac3 100644 --- a/tests/test_response_class_no_mediatype.py +++ b/tests/test_response_class_no_mediatype.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_response_code_no_body.py b/tests/test_response_code_no_body.py index 3851a325d..3ca8708f1 100644 --- a/tests/test_response_code_no_body.py +++ b/tests/test_response_code_no_body.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/a": { diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 7decdff7d..7a0cf47ec 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -507,7 +507,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/no_response_model-no_annotation-return_model": { diff --git a/tests/test_response_model_sub_types.py b/tests/test_response_model_sub_types.py index e462006ff..660bcee1b 100644 --- a/tests/test_response_model_sub_types.py +++ b/tests/test_response_model_sub_types.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/valid1": { diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 41021a983..45caa1615 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,240 +1,220 @@ from typing import Union +import pytest from fastapi import Body, Cookie, FastAPI, Header, Path, Query from fastapi.testclient import TestClient from pydantic import BaseModel -app = FastAPI() +def create_app(): + app = FastAPI() -class Item(BaseModel): - data: str + class Item(BaseModel): + data: str - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + class Config: + schema_extra = {"example": {"data": "Data in schema_extra"}} + @app.post("/schema_extra/") + def schema_extra(item: Item): + return item -@app.post("/schema_extra/") -def schema_extra(item: Item): - return item + with pytest.warns(DeprecationWarning): + @app.post("/example/") + def example(item: Item = Body(example={"data": "Data in Body example"})): + return item -@app.post("/example/") -def example(item: Item = Body(example={"data": "Data in Body example"})): - return item + @app.post("/examples/") + def examples( + item: Item = Body( + examples=[ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + ) + ): + return item + with pytest.warns(DeprecationWarning): -@app.post("/examples/") -def examples( - item: Item = Body( - examples={ - "example1": { - "summary": "example1 summary", - "value": {"data": "Data in Body examples, example1"}, - }, - "example2": {"value": {"data": "Data in Body examples, example2"}}, - }, - ) -): - return item + @app.post("/example_examples/") + def example_examples( + item: Item = Body( + example={"data": "Overridden example"}, + examples=[ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + ) + ): + return item + # TODO: enable these tests once/if Form(embed=False) is supported + # TODO: In that case, define if File() should support example/examples too + # @app.post("/form_example") + # def form_example(firstname: str = Form(example="John")): + # return firstname -@app.post("/example_examples/") -def example_examples( - item: Item = Body( - example={"data": "Overridden example"}, - examples={ - "example1": {"value": {"data": "examples example_examples 1"}}, - "example2": {"value": {"data": "examples example_examples 2"}}, - }, - ) -): - return item + # @app.post("/form_examples") + # def form_examples( + # lastname: str = Form( + # ..., + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname + # @app.post("/form_example_examples") + # def form_example_examples( + # lastname: str = Form( + # ..., + # example="Doe overridden", + # examples={ + # "example1": {"summary": "last name summary", "value": "Doe"}, + # "example2": {"value": "Doesn't"}, + # }, + # ), + # ): + # return lastname -# TODO: enable these tests once/if Form(embed=False) is supported -# TODO: In that case, define if File() should support example/examples too -# @app.post("/form_example") -# def form_example(firstname: str = Form(example="John")): -# return firstname + with pytest.warns(DeprecationWarning): + @app.get("/path_example/{item_id}") + def path_example( + item_id: str = Path( + example="item_1", + ), + ): + return item_id -# @app.post("/form_examples") -# def form_examples( -# lastname: str = Form( -# ..., -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname + @app.get("/path_examples/{item_id}") + def path_examples( + item_id: str = Path( + examples=["item_1", "item_2"], + ), + ): + return item_id + with pytest.warns(DeprecationWarning): -# @app.post("/form_example_examples") -# def form_example_examples( -# lastname: str = Form( -# ..., -# example="Doe overridden", -# examples={ -# "example1": {"summary": "last name summary", "value": "Doe"}, -# "example2": {"value": "Doesn't"}, -# }, -# ), -# ): -# return lastname + @app.get("/path_example_examples/{item_id}") + def path_example_examples( + item_id: str = Path( + example="item_overridden", + examples=["item_1", "item_2"], + ), + ): + return item_id + with pytest.warns(DeprecationWarning): -@app.get("/path_example/{item_id}") -def path_example( - item_id: str = Path( - example="item_1", - ), -): - return item_id + @app.get("/query_example/") + def query_example( + data: Union[str, None] = Query( + default=None, + example="query1", + ), + ): + return data + @app.get("/query_examples/") + def query_examples( + data: Union[str, None] = Query( + default=None, + examples=["query1", "query2"], + ), + ): + return data -@app.get("/path_examples/{item_id}") -def path_examples( - item_id: str = Path( - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id + with pytest.warns(DeprecationWarning): + @app.get("/query_example_examples/") + def query_example_examples( + data: Union[str, None] = Query( + default=None, + example="query_overridden", + examples=["query1", "query2"], + ), + ): + return data -@app.get("/path_example_examples/{item_id}") -def path_example_examples( - item_id: str = Path( - example="item_overridden", - examples={ - "example1": {"summary": "item ID summary", "value": "item_1"}, - "example2": {"value": "item_2"}, - }, - ), -): - return item_id + with pytest.warns(DeprecationWarning): + @app.get("/header_example/") + def header_example( + data: Union[str, None] = Header( + default=None, + example="header1", + ), + ): + return data -@app.get("/query_example/") -def query_example( - data: Union[str, None] = Query( - default=None, - example="query1", - ), -): - return data + @app.get("/header_examples/") + def header_examples( + data: Union[str, None] = Header( + default=None, + examples=[ + "header1", + "header2", + ], + ), + ): + return data + with pytest.warns(DeprecationWarning): -@app.get("/query_examples/") -def query_examples( - data: Union[str, None] = Query( - default=None, - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data + @app.get("/header_example_examples/") + def header_example_examples( + data: Union[str, None] = Header( + default=None, + example="header_overridden", + examples=["header1", "header2"], + ), + ): + return data + with pytest.warns(DeprecationWarning): -@app.get("/query_example_examples/") -def query_example_examples( - data: Union[str, None] = Query( - default=None, - example="query_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "query1"}, - "example2": {"value": "query2"}, - }, - ), -): - return data + @app.get("/cookie_example/") + def cookie_example( + data: Union[str, None] = Cookie( + default=None, + example="cookie1", + ), + ): + return data + @app.get("/cookie_examples/") + def cookie_examples( + data: Union[str, None] = Cookie( + default=None, + examples=["cookie1", "cookie2"], + ), + ): + return data -@app.get("/header_example/") -def header_example( - data: Union[str, None] = Header( - default=None, - example="header1", - ), -): - return data + with pytest.warns(DeprecationWarning): + @app.get("/cookie_example_examples/") + def cookie_example_examples( + data: Union[str, None] = Cookie( + default=None, + example="cookie_overridden", + examples=["cookie1", "cookie2"], + ), + ): + return data -@app.get("/header_examples/") -def header_examples( - data: Union[str, None] = Header( - default=None, - examples={ - "example1": {"summary": "header example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/header_example_examples/") -def header_example_examples( - data: Union[str, None] = Header( - default=None, - example="header_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "header1"}, - "example2": {"value": "header2"}, - }, - ), -): - return data - - -@app.get("/cookie_example/") -def cookie_example( - data: Union[str, None] = Cookie( - default=None, - example="cookie1", - ), -): - return data - - -@app.get("/cookie_examples/") -def cookie_examples( - data: Union[str, None] = Cookie( - default=None, - examples={ - "example1": {"summary": "cookie example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -@app.get("/cookie_example_examples/") -def cookie_example_examples( - data: Union[str, None] = Cookie( - default=None, - example="cookie_overridden", - examples={ - "example1": {"summary": "Query example 1", "value": "cookie1"}, - "example2": {"value": "cookie2"}, - }, - ), -): - return data - - -client = TestClient(app) + return app def test_call_api(): + app = create_app() + client = TestClient(app) response = client.post("/schema_extra/", json={"data": "Foo"}) assert response.status_code == 200, response.text response = client.post("/example/", json={"data": "Foo"}) @@ -277,10 +257,12 @@ def test_openapi_schema(): * Body(example={}) overrides schema_extra in pydantic model * Body(examples{}) overrides Body(example={}) and schema_extra in pydantic model """ + app = create_app() + client = TestClient(app) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/schema_extra/": { @@ -351,20 +333,14 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "summary": "example1 summary", - "value": { - "data": "Data in Body examples, example1" - }, - }, - "example2": { - "value": { - "data": "Data in Body examples, example2" - } - }, - }, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } } }, "required": True, @@ -394,15 +370,15 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "example1": { - "value": {"data": "examples example_examples 1"} - }, - "example2": { - "value": {"data": "examples example_examples 2"} - }, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], }, + "example": {"data": "Overridden example"}, } }, "required": True, @@ -463,13 +439,10 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], }, "name": "item_id", "in": "path", @@ -500,14 +473,12 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": {"title": "Item Id", "type": "string"}, - "examples": { - "example1": { - "summary": "item ID summary", - "value": "item_1", - }, - "example2": {"value": "item_2"}, + "schema": { + "title": "Item Id", + "type": "string", + "examples": ["item_1", "item_2"], }, + "example": "item_overridden", "name": "item_id", "in": "path", } @@ -568,13 +539,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], }, "name": "data", "in": "query", @@ -605,14 +573,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "query1", - }, - "example2": {"value": "query2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], }, + "example": "query_overridden", "name": "data", "in": "query", } @@ -642,7 +608,7 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": {"type": "string", "title": "Data"}, "example": "header1", "name": "data", "in": "header", @@ -673,13 +639,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "header example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], }, "name": "data", "in": "header", @@ -710,14 +673,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "header1", - }, - "example2": {"value": "header2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], }, + "example": "header_overridden", "name": "data", "in": "header", } @@ -747,7 +708,7 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": {"type": "string", "title": "Data"}, "example": "cookie1", "name": "data", "in": "cookie", @@ -778,13 +739,10 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "cookie example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["cookie1", "cookie2"], }, "name": "data", "in": "cookie", @@ -815,14 +773,12 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, - "examples": { - "example1": { - "summary": "Query example 1", - "value": "cookie1", - }, - "example2": {"value": "cookie2"}, + "schema": { + "type": "string", + "title": "Data", + "examples": ["cookie1", "cookie2"], }, + "example": "cookie_overridden", "name": "data", "in": "cookie", } diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index b1c648c55..4ddb8e2ee 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index ac8b4abf8..d99d616e0 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_cookie_optional.py b/tests/test_security_api_key_cookie_optional.py index b8c440c9d..cb5590168 100644 --- a/tests/test_security_api_key_cookie_optional.py +++ b/tests/test_security_api_key_cookie_optional.py @@ -48,7 +48,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 96ad80b54..1ff883703 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 382f53dd7..27f9d0f29 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_header_optional.py b/tests/test_security_api_key_header_optional.py index adfb20ba0..6f9682a64 100644 --- a/tests/test_security_api_key_header_optional.py +++ b/tests/test_security_api_key_header_optional.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index da98eafd6..dc7a0a621 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 3c08afc5f..35dc7743a 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_api_key_query_optional.py b/tests/test_security_api_key_query_optional.py index 99a26cfd0..4cc134bd4 100644 --- a/tests/test_security_api_key_query_optional.py +++ b/tests/test_security_api_key_query_optional.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index d3a754203..51928bafd 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index 3d7d15016..bc79f3242 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index 180c9110e..dd4d76843 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_optional.py b/tests/test_security_http_basic_optional.py index 7e7fcac32..9b6cb6c45 100644 --- a/tests/test_security_http_basic_optional.py +++ b/tests/test_security_http_basic_optional.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_realm.py b/tests/test_security_http_basic_realm.py index 470afd662..9fc33971a 100644 --- a/tests/test_security_http_basic_realm.py +++ b/tests/test_security_http_basic_realm.py @@ -52,7 +52,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_basic_realm_description.py b/tests/test_security_http_basic_realm_description.py index 44289007b..02122442e 100644 --- a/tests/test_security_http_basic_realm_description.py +++ b/tests/test_security_http_basic_realm_description.py @@ -52,7 +52,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index f24869fc3..5b9e2d691 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 6d5ad0b8e..2f11c3a14 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -37,7 +37,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_bearer_optional.py b/tests/test_security_http_bearer_optional.py index b596ac730..943da2ee2 100644 --- a/tests/test_security_http_bearer_optional.py +++ b/tests/test_security_http_bearer_optional.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 2a25efe02..133d35763 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 721f7cfde..4e31a0c00 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_http_digest_optional.py b/tests/test_security_http_digest_optional.py index d4c3597bc..1e6eb8bd7 100644 --- a/tests/test_security_http_digest_optional.py +++ b/tests/test_security_http_digest_optional.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 23dce7cfa..73d1b7d94 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -135,7 +135,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_authorization_code_bearer.py b/tests/test_security_oauth2_authorization_code_bearer.py index 6df81528d..f2097b149 100644 --- a/tests/test_security_oauth2_authorization_code_bearer.py +++ b/tests/test_security_oauth2_authorization_code_bearer.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_authorization_code_bearer_description.py b/tests/test_security_oauth2_authorization_code_bearer_description.py index c119abde4..5386fbbd9 100644 --- a/tests/test_security_oauth2_authorization_code_bearer_description.py +++ b/tests/test_security_oauth2_authorization_code_bearer_description.py @@ -44,7 +44,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index 3ef9f4a8d..b24c1b58f 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -139,7 +139,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index b6425fde4..cda635151 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -140,7 +140,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login": { diff --git a/tests/test_security_oauth2_password_bearer_optional.py b/tests/test_security_oauth2_password_bearer_optional.py index e5dcbb553..4c9362c3e 100644 --- a/tests/test_security_oauth2_password_bearer_optional.py +++ b/tests/test_security_oauth2_password_bearer_optional.py @@ -41,7 +41,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_oauth2_password_bearer_optional_description.py b/tests/test_security_oauth2_password_bearer_optional_description.py index 9ff48e715..6e6ea846c 100644 --- a/tests/test_security_oauth2_password_bearer_optional_description.py +++ b/tests/test_security_oauth2_password_bearer_optional_description.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 206de6574..1e322e640 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -47,7 +47,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 5884de793..44cf57f86 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -49,7 +49,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_security_openid_connect_optional.py b/tests/test_security_openid_connect_optional.py index 8ac719118..e817434b0 100644 --- a/tests/test_security_openid_connect_optional.py +++ b/tests/test_security_openid_connect_optional.py @@ -53,7 +53,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_starlette_exception.py b/tests/test_starlette_exception.py index 96f835b93..229fe8016 100644 --- a/tests/test_starlette_exception.py +++ b/tests/test_starlette_exception.py @@ -80,7 +80,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/http-no-body-statuscode-exception": { diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index c5a0237e8..dce3ea5e2 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -87,7 +87,7 @@ def test_openapi_schema(): with client: response = client.get("/openapi.json") assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/invoices/": { diff --git a/tests/test_tuples.py b/tests/test_tuples.py index 4fa1f7a13..c37a25ca6 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -86,7 +86,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/model-with-tuple/": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial001.py b/tests/test_tutorial/test_additional_responses/test_tutorial001.py index 3d6267023..3afeaff84 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial001.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial001.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 6182ed507..8e084e152 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial003.py b/tests/test_tutorial/test_additional_responses/test_tutorial003.py index 77568d9d4..bd34d2938 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial003.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial003.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 3fbd91e5c..5fc8b81ca 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 3da362a50..8126cdcc6 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/notes/": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py index 7533a1b68..a070f850f 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py index 930ab3bf5..ce791e215 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index ae8f1a495..0ae9f4f93 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/api/v1"}, diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index e67ad1cb1..576a411a4 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -15,7 +15,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "https://stag.example.com", "description": "Staging environment"}, diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index a13decd75..7da663435 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -166,7 +166,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py index 64e19c3f3..8f42d9dd1 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -166,7 +166,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py index 70c86b4d7..44694e371 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -181,7 +181,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index cd1209ade..469198e0f 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -200,7 +200,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index 5ebcbbf57..a68b4e044 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -215,7 +215,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index a7ea0e949..4999cbf6b 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py index 32f996ecb..011946d07 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py index 20e032fcd..e7dcb54e9 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py index e3baf5f2b..f1015a03b 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 4c2f48674..29c8ef4e9 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index 496ab38fb..ce41a4283 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index 74a8a9b2e..acc4cfadc 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -50,7 +50,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py index 9c764b6d1..a8dc02a6c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index 0cca29433..f31fee78e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 3b61e717e..0e46df253 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -58,7 +58,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index b34377a28..8555cf88c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -90,7 +90,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py index 9b8d5e15b..f4d300cc5 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -90,7 +90,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py index f8af555fc..afe2b2c20 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py index 06e2c3146..033d5892e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index 82c5fb101..8fcc00013 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index 378c24197..ac39cd93f 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/index-weights/": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 5ca63a92b..0800abe29 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -41,7 +41,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/index-weights/": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 939bf44e0..151b4b917 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -34,7 +34,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 5f50f2071..c4b4b9df3 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index d4fdabce6..940b4b3b8 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index c1d8fd805..a43394ab1 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -33,7 +33,7 @@ def test_default_openapi(): assert response.status_code == 200, response.text response = client.get("/openapi.json") assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index c3511d129..902bed843 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index f4f94c09d..aa5807844 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py index a80f10f81..ffb55d4e1 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py index 1be898c09..9bc38effd 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index 7ba542c90..bb2953ef6 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001.py b/tests/test_tutorial/test_custom_response/test_tutorial001.py index da2ca8d62..fc8362467 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial001b.py b/tests/test_tutorial/test_custom_response/test_tutorial001b.py index f681f5a9d..91e5c501e 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial001b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial001b.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial004.py index ef0ba3446..de60574f5 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial004.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial005.py b/tests/test_tutorial/test_custom_response/test_tutorial005.py index e4b5c1546..889bf3e92 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial005.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial005.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006.py b/tests/test_tutorial/test_custom_response/test_tutorial006.py index 9f1b07bee..2d0a2cd3f 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/typer": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006b.py b/tests/test_tutorial/test_custom_response/test_tutorial006b.py index cf204cbc0..1739fd457 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006b.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006b.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/fastapi": { diff --git a/tests/test_tutorial/test_custom_response/test_tutorial006c.py b/tests/test_tutorial/test_custom_response/test_tutorial006c.py index f196899ac..51aa1833d 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial006c.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial006c.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/pydantic": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index 26ae04c51..e20c0efe9 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -34,7 +34,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 675e826b1..e122239d8 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -22,7 +22,7 @@ def test_openapi_schema(): assert response.status_code == 200 data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/next": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index f3378fe62..204426e8b 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -55,7 +55,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/authors/{author_id}/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index 974b9304f..a8e564ebe 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -26,7 +26,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py index b1ca27ff8..4e6a329f4 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -26,7 +26,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py index 70bed03f6..205aee908 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py index 9c5723be8..73593ea55 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 1bcde4e9f..10bf84fb5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index 298bc290d..d16fd9ef7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py index f985be8df..46fe97fb2 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -64,7 +64,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index fc0286702..c6a0fc665 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 1e37673ed..30431cd29 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index ab936ccdc..9793c8c33 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -72,7 +72,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 2e9c82d71..6fac9f8eb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py index 919066dca..810537e48 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py index c23718479..f17cbcfc7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -66,7 +66,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index b92b96c01..af1fcde55 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py index 2ddb7bb53..c33d51d87 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -99,7 +99,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py index 595c83a53..d7bd756b5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -115,7 +115,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index 52f9beed5..a5bb299ac 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index 882d41aa5..81cbf4ab6 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -17,7 +17,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_events/test_tutorial003.py b/tests/test_tutorial/test_events/test_tutorial003.py index b2820b63c..0ad1a1f8b 100644 --- a/tests/test_tutorial/test_events/test_tutorial003.py +++ b/tests/test_tutorial/test_events/test_tutorial003.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/predict": { diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py index 6e71bb2de..a85a31350 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_extending_openapi/test_tutorial001.py @@ -15,11 +15,12 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "Custom title", + "summary": "This is a very custom OpenAPI schema", + "description": "Here's a longer description of the custom **OpenAPI** schema", "version": "2.5.0", - "description": "This is a very custom OpenAPI schema", "x-logo": { "url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" }, diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 07a834990..39d2005ab 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py index 76836d447..3e497a291 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -30,7 +30,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py index 158ee01b3..b539cf3d6 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py index 5be6452ee..efd31e63d 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 5413fe428..733d9f406 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index f08bf4c50..21192b7db 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -28,7 +28,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index 407c71787..c17ddbbe1 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -38,7 +38,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004.py b/tests/test_tutorial/test_extra_models/test_tutorial004.py index 47790ba8f..71f6a8c70 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004.py @@ -18,7 +18,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py index a98700172..5475b92e1 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial004_py39.py @@ -27,7 +27,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005.py b/tests/test_tutorial/test_extra_models/test_tutorial005.py index 7c094b253..b0861c37f 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/keyword-weights/": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py index b40386450..7278e93c3 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial005_py39.py @@ -24,7 +24,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/keyword-weights/": { diff --git a/tests/test_tutorial/test_first_steps/test_tutorial001.py b/tests/test_tutorial/test_first_steps/test_tutorial001.py index ea37aec53..6cc9fc228 100644 --- a/tests/test_tutorial/test_first_steps/test_tutorial001.py +++ b/tests/test_tutorial/test_first_steps/test_tutorial001.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_generate_clients/test_tutorial003.py b/tests/test_tutorial/test_generate_clients/test_tutorial003.py index 8b22eab9e..1cd9678a1 100644 --- a/tests/test_tutorial/test_generate_clients/test_tutorial003.py +++ b/tests/test_tutorial/test_generate_clients/test_tutorial003.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial001.py b/tests/test_tutorial/test_handling_errors/test_tutorial001.py index 99a1053ca..8809c135b 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial001.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial002.py b/tests/test_tutorial/test_handling_errors/test_tutorial002.py index 091c74f4d..efd86ebde 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial002.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial002.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items-header/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial003.py b/tests/test_tutorial/test_handling_errors/test_tutorial003.py index 1639cb1d8..4763f68f3 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial003.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial003.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/unicorns/{name}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 246f3b94c..0c0988c64 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -32,7 +32,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index af47fd1a4..f356178ac 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 4a39bd102..4dd1adf43 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 80f502d6a..030159dcf 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -24,7 +24,7 @@ def test_openapi(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py index f0ad7b816..3755ab758 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -24,7 +24,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py index d095c7123..207b3b02b 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index bf176bba2..bf51982b7 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 516abda8b..545fc836b 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py index 97493e604..cfd581e33 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py index e0c60342a..c8d61e42e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -43,7 +43,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py index c1bc5faf8..85150d4a9 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py index 81871b8c6..f189d85b5 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -46,7 +46,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index 99dd9e25f..b2fc17b8f 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -26,7 +26,7 @@ def test_openapi_schema(): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py index 4477da7a8..87fa839e2 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -26,7 +26,7 @@ def test_openapi_schema(): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py index b52304a2b..ef6c268c5 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py index dffdd1622..6525fd50c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py index 64ef7b22a..b404ce5d8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -34,7 +34,7 @@ def test_openapi_schema(client: TestClient): assert response.status_code == 200 # insert_assert(response.json()) assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_metadata/test_tutorial001.py b/tests/test_tutorial/test_metadata/test_tutorial001.py index f1ddc3259..04e8ff82b 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial001.py +++ b/tests/test_tutorial/test_metadata/test_tutorial001.py @@ -15,9 +15,10 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": { "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", "termsOfService": "http://example.com/terms/", "contact": { diff --git a/tests/test_tutorial/test_metadata/test_tutorial001_1.py b/tests/test_tutorial/test_metadata/test_tutorial001_1.py new file mode 100644 index 000000000..3efb1c432 --- /dev/null +++ b/tests/test_tutorial/test_metadata/test_tutorial001_1.py @@ -0,0 +1,49 @@ +from fastapi.testclient import TestClient + +from docs_src.metadata.tutorial001_1 import app + +client = TestClient(app) + + +def test_items(): + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [{"name": "Katana"}] + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": { + "title": "ChimichangApp", + "summary": "Deadpool's favorite app. Nuff said.", + "description": "\nChimichangApp API helps you do awesome stuff. 🚀\n\n## Items\n\nYou can **read items**.\n\n## Users\n\nYou will be able to:\n\n* **Create users** (_not implemented_).\n* **Read users** (_not implemented_).\n", + "termsOfService": "http://example.com/terms/", + "contact": { + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + }, + "license": { + "name": "Apache 2.0", + "identifier": "MIT", + }, + "version": "0.0.1", + }, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py index f7f47a558..507220371 100644 --- a/tests/test_tutorial/test_metadata/test_tutorial004.py +++ b/tests/test_tutorial/test_metadata/test_tutorial004.py @@ -16,7 +16,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index c6cdc6064..a6e898c49 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/invoices/": { diff --git a/tests/test_tutorial/test_openapi_webhooks/__init__.py b/tests/test_tutorial/test_openapi_webhooks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py new file mode 100644 index 000000000..9111fdb2f --- /dev/null +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -0,0 +1,117 @@ +from fastapi.testclient import TestClient + +from docs_src.openapi_webhooks.tutorial001 import app + +client = TestClient(app) + + +def test_get(): + response = client.get("/users/") + assert response.status_code == 200, response.text + assert response.json() == ["Rick", "Morty"] + + +def test_dummy_webhook(): + # Just for coverage + app.webhooks.routes[0].endpoint({}) + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Read Users", + "operationId": "read_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "montly_fee": {"type": "number", "title": "Montly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "montly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py index c1cdbee24..95542398e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial001.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py index fdaddd018..d1388c367 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial002.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py index 782c64a84..313bb2a04 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial003.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": {}, } diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index f5fd868eb..cd9fc520e 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py index 52379c01e..07e2d7d20 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial005.py @@ -14,7 +14,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py index deb6b0910..f92c59015 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial006.py @@ -22,7 +22,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 470956a77..3b88a38c2 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -56,7 +56,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py index 76e44b5e5..58dec5769 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial002b.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index cf8e203a0..30278caf8 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index 497fc9024..cf59d354c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -30,7 +30,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 09fac44c4..a93ea8807 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -30,7 +30,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py index e90771f24..91180d109 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial006.py @@ -24,7 +24,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial004.py b/tests/test_tutorial/test_path_params/test_tutorial004.py index ab0455bf5..acbeaca76 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial004.py +++ b/tests/test_tutorial/test_path_params/test_tutorial004.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/{file_path}": { diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 3401f2253..b9b58c961 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -51,7 +51,7 @@ def test_openapi(): assert response.status_code == 200, response.text data = response.json() assert data == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/models/{model_name}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 3c408449b..6c2cba7e1 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -35,7 +35,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 7fe58a990..626637903 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -60,7 +60,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index b90c0a6c8..b6fb2f39e 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -67,7 +67,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index c41f554ba..370ae0ff0 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py index dc8028f81..1f76ef314 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -45,7 +45,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py index 496b95b79..3a06b4bc7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py index 2005e5043..1e6f93093 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py index 8147d768e..63524d291 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index d6d69c169..164ec1193 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py index 3a53d422e..2afaafd92 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py index f00df2879..fafd38337 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py index 895fb8e9f..f3fb47528 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 4f4b1fd55..21f348f2b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index d85bb6231..f2c2a5a33 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py index 7bc020540..1436db384 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py index be5557f6a..270763f1d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py index d9512e193..548391683 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py index b2a2c8d1d..e7d745154 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial012_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py index 4a0b9e8b5..1ba1fdf61 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py index 71e4638ae..343261748 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py index 4e90db358..537d6325b 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial013_an_py39.py @@ -33,7 +33,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py index 7686c07b3..7bce7590c 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py index e739044a8..2182e87b7 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py index 73f0ba78b..344004d01 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py index e2c149992..5d4f6df3d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_an_py39.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py index 07f30b739..dad49fb12 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial014_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 3269801ef..84c736180 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 4b6edfa06..8ebe4eafd 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py index 0c34620e3..5da8b320b 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -43,7 +43,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py index 04442c76f..166f59b1a 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py index f5249ef5b..02ea604b2 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index f690d107b..c753e14d1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -55,7 +55,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03.py b/tests/test_tutorial/test_request_files/test_tutorial001_03.py index 4af659a11..f02170814 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py index 91dbc60b9..acfb749ce 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an.py @@ -31,7 +31,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py index 7c4ad326c..36e5faac1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_03_an_py39.py @@ -39,7 +39,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py index 80c288ed6..6eb2d55dc 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -66,7 +66,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py index 4dc1f752c..4e3ef6869 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -76,7 +76,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 6868be328..65a8a9e61 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -77,7 +77,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py index ca1f62ea3..52a8e1964 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -77,7 +77,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py index e593ae75d..6594e0116 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py @@ -96,7 +96,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index bacd25b97..bfe964604 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -96,7 +96,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003.py b/tests/test_tutorial/test_request_files/test_tutorial003.py index e2d69184b..85cd03a59 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an.py b/tests/test_tutorial/test_request_files/test_tutorial003_an.py index f199d4d2f..0327a2db6 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an.py @@ -54,7 +54,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py index 51fa83470..3aa68c621 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_an_py39.py @@ -82,7 +82,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py index 32b028909..238bb39cd 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial003_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial003_py39.py @@ -82,7 +82,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 721c97fb1..4a2a7abe9 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py index 6b8abab19..347361344 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py index 5862524ad..e65a8823e 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py @@ -81,7 +81,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/login/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index dcc44cf09..be12656d2 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -112,7 +112,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py index f11e36984..a5fcb3a94 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py @@ -112,7 +112,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py index 9b5e8681c..6eacb2fcf 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -131,7 +131,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/files/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 4a95cb2b5..9cb0419a3 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index a055bc688..8b8fe514a 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -27,7 +27,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index 8cb4ac208..01dc8e71c 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_02.py b/tests/test_tutorial/test_response_model/test_tutorial003_02.py index 6ccb054b8..eabd20345 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_02.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_02.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_03.py b/tests/test_tutorial/test_response_model/test_tutorial003_03.py index ba4c0f275..970ff5845 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_03.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_03.py @@ -15,7 +15,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/teleport": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05.py b/tests/test_tutorial/test_response_model/test_tutorial003_05.py index d7c232e75..c7a39cc74 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05.py @@ -21,7 +21,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py index a89f1dad8..f80d62572 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_05_py310.py @@ -31,7 +31,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/portal": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 1f4ec9057..602147b13 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -36,7 +36,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/user/": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 8cb7dc9cf..07af29207 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -36,7 +36,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 2ba8143ba..90147fbdd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 97df0a238..740a49590 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -44,7 +44,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 76662f793..e8c8946c5 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 1b1cf4175..388e030bd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -35,7 +35,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 3a759fde4..548a3dbd8 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -25,7 +25,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 07e84cc82..075bb8079 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -35,7 +35,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}/name": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 784517d69..dea136fb2 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -41,31 +41,34 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 222a4edfe..571feb19f 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -23,7 +23,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -41,31 +41,34 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index 1eacd640a..e25531794 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index 632f2cbe0..dafc5afad 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index c99cb75c8..29004218b 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -32,7 +32,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/{item_id}": { @@ -50,31 +50,34 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"}, - "examples": { - "normal": { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, }, - }, - "converted": { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - "invalid": { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", + { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, }, - }, - }, + { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + ], + } } }, "required": True, diff --git a/tests/test_tutorial/test_security/test_tutorial001.py b/tests/test_tutorial/test_security/test_tutorial001.py index a7f55b78b..417bed8f7 100644 --- a/tests/test_tutorial/test_security/test_tutorial001.py +++ b/tests/test_tutorial/test_security/test_tutorial001.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an.py b/tests/test_tutorial/test_security/test_tutorial001_an.py index fc48703aa..59460da7f 100644 --- a/tests/test_tutorial/test_security/test_tutorial001_an.py +++ b/tests/test_tutorial/test_security/test_tutorial001_an.py @@ -29,7 +29,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py index 345e0be0f..d8e712773 100644 --- a/tests/test_tutorial/test_security/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial001_an_py39.py @@ -40,7 +40,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index c10f928eb..cb5cdaa04 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py index 41872fda0..26e68a029 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -70,7 +70,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py index 02bd748c8..1250d4afb 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py index 7e74afafb..b74cfdc54 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py index a463751f5..8a75d2321 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_py310.py @@ -86,7 +86,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index ccb5b3c9f..4e4b6afe8 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -179,7 +179,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index e851f47fe..51cc8329a 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -179,7 +179,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index cca581980..b0d0fed12 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index eae851457..26deaaf3c 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index cdbd8f75e..e93f34c3b 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 45b2a2341..737a8548f 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -207,7 +207,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/token": { diff --git a/tests/test_tutorial/test_security/test_tutorial006.py b/tests/test_tutorial/test_security/test_tutorial006.py index 73cbdc538..dc459b6fd 100644 --- a/tests/test_tutorial/test_security/test_tutorial006.py +++ b/tests/test_tutorial/test_security/test_tutorial006.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an.py b/tests/test_tutorial/test_security/test_tutorial006_an.py index 5f970ed01..52ddd938f 100644 --- a/tests/test_tutorial/test_security/test_tutorial006_an.py +++ b/tests/test_tutorial/test_security/test_tutorial006_an.py @@ -42,7 +42,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py index 7d7a851ac..52b22e573 100644 --- a/tests/test_tutorial/test_security/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial006_an_py39.py @@ -54,7 +54,7 @@ def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/me": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index a2628f3c3..d927940da 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -89,7 +89,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index 02501b1a2..08d7b3533 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -91,7 +91,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py index 67b92ea4a..493fb3b6b 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py @@ -105,7 +105,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py index a2af20ea2..7b56685bc 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py @@ -105,7 +105,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py index 2f918cfd8..43c2b272f 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py @@ -104,7 +104,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py index f2eefe41d..fd33517db 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py @@ -104,7 +104,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py index d2470a2db..ac6c427ca 100644 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py @@ -97,7 +97,7 @@ def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/users/": { diff --git a/tests/test_tutorial/test_sub_applications/test_tutorial001.py b/tests/test_tutorial/test_sub_applications/test_tutorial001.py index 00e9aec57..0790d207b 100644 --- a/tests/test_tutorial/test_sub_applications/test_tutorial001.py +++ b/tests/test_tutorial/test_sub_applications/test_tutorial001.py @@ -5,7 +5,7 @@ from docs_src.sub_applications.tutorial001 import app client = TestClient(app) openapi_schema_main = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/app": { @@ -23,7 +23,7 @@ openapi_schema_main = { }, } openapi_schema_sub = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/sub": { diff --git a/tests/test_tutorial/test_testing/test_main.py b/tests/test_tutorial/test_testing/test_main.py index 937ce75e4..fe3498081 100644 --- a/tests/test_tutorial/test_testing/test_main.py +++ b/tests/test_tutorial/test_testing/test_main.py @@ -9,7 +9,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_tutorial/test_testing/test_tutorial001.py b/tests/test_tutorial/test_testing/test_tutorial001.py index f3db70af2..471e896c9 100644 --- a/tests/test_tutorial/test_testing/test_tutorial001.py +++ b/tests/test_tutorial/test_testing/test_tutorial001.py @@ -9,7 +9,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/": { diff --git a/tests/test_union_body.py b/tests/test_union_body.py index bc1e74432..57a14b574 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index 988b920aa..c2a37d3dd 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -39,7 +39,7 @@ def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/items/": { diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py new file mode 100644 index 000000000..a1c7b18fb --- /dev/null +++ b/tests/test_webhooks_security.py @@ -0,0 +1,126 @@ +from datetime import datetime + +from fastapi import FastAPI, Security +from fastapi.security import HTTPBearer +from fastapi.testclient import TestClient +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + +bearer_scheme = HTTPBearer() + + +class Subscription(BaseModel): + username: str + montly_fee: float + start_date: datetime + + +@app.webhooks.post("new-subscription") +def new_subscription( + body: Subscription, token: Annotated[str, Security(bearer_scheme)] +): + """ + When a new user subscribes to your service we'll send you a POST request with this + data to the URL that you register for the event `new-subscription` in the dashboard. + """ + + +client = TestClient(app) + + +def test_dummy_webhook(): + # Just for coverage + new_subscription(body={}, token="Bearer 123") + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": {}, + "webhooks": { + "new-subscription": { + "post": { + "summary": "New Subscription", + "description": "When a new user subscribes to your service we'll send you a POST request with this\ndata to the URL that you register for the event `new-subscription` in the dashboard.", + "operationId": "new_subscriptionnew_subscription_post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Subscription"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "security": [{"HTTPBearer": []}], + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Subscription": { + "properties": { + "username": {"type": "string", "title": "Username"}, + "montly_fee": {"type": "number", "title": "Montly Fee"}, + "start_date": { + "type": "string", + "format": "date-time", + "title": "Start Date", + }, + }, + "type": "object", + "required": ["username", "montly_fee", "start_date"], + "title": "Subscription", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + }, + "securitySchemes": {"HTTPBearer": {"type": "http", "scheme": "bearer"}}, + }, + } From b757211299b19e2715513826c41b7f48da2e51eb Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 30 Jun 2023 18:25:53 +0000 Subject: [PATCH 1139/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fca7502cc..edf8ef13e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). * 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). From efc2bcc57ac6a711a835f55159252c220db74213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:54:25 +0200 Subject: [PATCH 1140/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index edf8ef13e..18abf0ee4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,16 +2,32 @@ ## Latest Changes +### Features + * ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). -* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). + * New support for documenting **webhooks**, read the new docs here: Advanced User Guide: OpenAPI Webhooks. + * Upgrade OpenAPI 3.1.0, this uses JSON Schema 2020-12. + * Upgrade Swagger UI to version 5.x.x, that supports OpenAPI 3.1.0. + * Updated `examples` field in `Query()`, `Cookie()`, `Body()`, etc. based on the latest JSON Schema and OpenAPI. Now it takes a list of examples and they are included directly in the JSON Schema, not outside. Read more about it (including the historical technical details) in the updated docs: Tutorial: Declare Request Example Data. + * ✨ Add support for `deque` objects and children in `jsonable_encoder`. PR [#9433](https://github.com/tiangolo/fastapi/pull/9433) by [@cranium](https://github.com/cranium). + +### Docs + +* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). + +### Translations + +* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). + +### Internal + +* 🔨 Enable linenums in MkDocs Material during local live development to simplify highlighting code. PR [#9769](https://github.com/tiangolo/fastapi/pull/9769) by [@tiangolo](https://github.com/tiangolo). * ⬆ Update httpx requirement from <0.24.0,>=0.23.0 to >=0.23.0,<0.25.0. PR [#9724](https://github.com/tiangolo/fastapi/pull/9724) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.16 to 9.1.17. PR [#9746](https://github.com/tiangolo/fastapi/pull/9746) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔥 Remove missing translation dummy pages, no longer necessary. PR [#9751](https://github.com/tiangolo/fastapi/pull/9751) by [@tiangolo](https://github.com/tiangolo). * ⬆ [pre-commit.ci] pre-commit autoupdate. PR [#9259](https://github.com/tiangolo/fastapi/pull/9259) by [@pre-commit-ci[bot]](https://github.com/apps/pre-commit-ci). -* 🌐 Add Persian translation for `docs/fa/docs/advanced/sub-applications.md`. PR [#9692](https://github.com/tiangolo/fastapi/pull/9692) by [@mojtabapaso](https://github.com/mojtabapaso). -* 🌐 Add Russian translation for `docs/ru/docs/tutorial/response-model.md`. PR [#9675](https://github.com/tiangolo/fastapi/pull/9675) by [@glsglsgls](https://github.com/glsglsgls). -* 📝 Fix form for the FastAPI and friends newsletter. PR [#9749](https://github.com/tiangolo/fastapi/pull/9749) by [@tiangolo](https://github.com/tiangolo). * ✨ Add Material for MkDocs Insiders features and cards. PR [#9748](https://github.com/tiangolo/fastapi/pull/9748) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove languages without translations. PR [#9743](https://github.com/tiangolo/fastapi/pull/9743) by [@tiangolo](https://github.com/tiangolo). * ✨ Refactor docs for building scripts, use MkDocs hooks, simplify (remove) configs for languages. PR [#9742](https://github.com/tiangolo/fastapi/pull/9742) by [@tiangolo](https://github.com/tiangolo). From 983f1d34dbc9443fcd12159647aabdeabc5032a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 30 Jun 2023 20:55:17 +0200 Subject: [PATCH 1141/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?99.0?= 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 18abf0ee4..b68bd5f00 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.99.0 + ### Features * ✨ Add support for OpenAPI 3.1.0. PR [#9770](https://github.com/tiangolo/fastapi/pull/9770) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 038e1ba86..9c4316e69 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.98.0" +__version__ = "0.99.0" from starlette import status as status From 4d83f984cc42831e3883285e9bedc4d69f4c1ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 1 Jul 2023 18:43:29 +0200 Subject: [PATCH 1142/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20source=20exam?= =?UTF-8?q?ples=20to=20use=20new=20JSON=20Schema=20examples=20field=20(#97?= =?UTF-8?q?76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update source examples to use new JSON Schema examples field * ✅ Update tests for JSON Schema examples * 📝 Update highlights in JSON Schema examples --- docs/en/docs/tutorial/schema-extra-example.md | 10 +++---- docs_src/schema_extra_example/tutorial004.py | 27 ++++++------------- .../schema_extra_example/tutorial004_an.py | 27 ++++++------------- .../tutorial004_an_py310.py | 27 ++++++------------- .../tutorial004_an_py39.py | 27 ++++++------------- .../schema_extra_example/tutorial004_py310.py | 27 ++++++------------- .../test_tutorial004.py | 25 +++++------------ .../test_tutorial004_an.py | 25 +++++------------ .../test_tutorial004_an_py310.py | 25 +++++------------ .../test_tutorial004_an_py39.py | 25 +++++------------ .../test_tutorial004_py310.py | 25 +++++------------ 11 files changed, 80 insertions(+), 190 deletions(-) diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 6cf8bf181..86ccb1f5a 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -116,19 +116,19 @@ You can of course also pass multiple `examples`: === "Python 3.10+" - ```Python hl_lines="23-49" + ```Python hl_lines="23-38" {!> ../../../docs_src/schema_extra_example/tutorial004_an_py310.py!} ``` === "Python 3.9+" - ```Python hl_lines="23-49" + ```Python hl_lines="23-38" {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} ``` === "Python 3.6+" - ```Python hl_lines="24-50" + ```Python hl_lines="24-39" {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} ``` @@ -137,7 +137,7 @@ You can of course also pass multiple `examples`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="19-45" + ```Python hl_lines="19-34" {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` @@ -146,7 +146,7 @@ You can of course also pass multiple `examples`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="21-47" + ```Python hl_lines="21-36" {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` diff --git a/docs_src/schema_extra_example/tutorial004.py b/docs_src/schema_extra_example/tutorial004.py index eb49293fd..75514a3e9 100644 --- a/docs_src/schema_extra_example/tutorial004.py +++ b/docs_src/schema_extra_example/tutorial004.py @@ -20,29 +20,18 @@ async def update_item( item: Item = Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an.py b/docs_src/schema_extra_example/tutorial004_an.py index 567ec4702..e817302a2 100644 --- a/docs_src/schema_extra_example/tutorial004_an.py +++ b/docs_src/schema_extra_example/tutorial004_an.py @@ -23,29 +23,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an_py310.py b/docs_src/schema_extra_example/tutorial004_an_py310.py index 026654835..650da3187 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py310.py +++ b/docs_src/schema_extra_example/tutorial004_an_py310.py @@ -22,29 +22,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_an_py39.py b/docs_src/schema_extra_example/tutorial004_an_py39.py index 06219ede8..dc5a8fe49 100644 --- a/docs_src/schema_extra_example/tutorial004_an_py39.py +++ b/docs_src/schema_extra_example/tutorial004_an_py39.py @@ -22,29 +22,18 @@ async def update_item( Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/docs_src/schema_extra_example/tutorial004_py310.py b/docs_src/schema_extra_example/tutorial004_py310.py index ef2b9d8cb..05996ac2a 100644 --- a/docs_src/schema_extra_example/tutorial004_py310.py +++ b/docs_src/schema_extra_example/tutorial004_py310.py @@ -18,29 +18,18 @@ async def update_item( item: Item = Body( examples=[ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": { - "name": "Bar", - "price": "35.4", - }, + "name": "Bar", + "price": "35.4", }, { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], ), diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index dea136fb2..313cd51d6 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -46,26 +46,15 @@ def test_openapi_schema(): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 571feb19f..353401b78 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -46,26 +46,15 @@ def test_openapi_schema(): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index e25531794..79f4e1e1e 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index dafc5afad..1ee120705 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index 29004218b..b77368400 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -55,26 +55,15 @@ def test_openapi_schema(client: TestClient): "title": "Item", "examples": [ { - "summary": "A normal example", - "description": "A **normal** item works correctly.", - "value": { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, }, + {"name": "Bar", "price": "35.4"}, { - "summary": "An example with converted data", - "description": "FastAPI can convert price `strings` to actual `numbers` automatically", - "value": {"name": "Bar", "price": "35.4"}, - }, - { - "summary": "Invalid data is rejected with an error", - "value": { - "name": "Baz", - "price": "thirty five point four", - }, + "name": "Baz", + "price": "thirty five point four", }, ], } From 0f105d90769bb2fe4c79ff0f571f33db803fb16f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 1 Jul 2023 16:44:12 +0000 Subject: [PATCH 1143/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b68bd5f00..983c0e218 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From 07e1dea467c9654ea771bfef23cb3bf9654feb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 17:58:23 +0200 Subject: [PATCH 1144/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20JSON=20Schema=20?= =?UTF-8?q?accepting=20bools=20as=20valid=20JSON=20Schemas,=20e.g.=20`addi?= =?UTF-8?q?tionalProperties:=20false`=20(#9781)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. additionalProperties: false * ✅ Add test to ensure additionalProperties can be false * ♻️ Tweak OpenAPI models to support Pydantic v1's JSON Schema for tuples --- fastapi/openapi/models.py | 46 +++++---- tests/test_additional_properties_bool.py | 115 +++++++++++++++++++++++ 2 files changed, 142 insertions(+), 19 deletions(-) create mode 100644 tests/test_additional_properties_bool.py diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 7420d3b55..a2ea53607 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -114,27 +114,30 @@ class Schema(BaseModel): dynamicAnchor: Optional[str] = Field(default=None, alias="$dynamicAnchor") ref: Optional[str] = Field(default=None, alias="$ref") dynamicRef: Optional[str] = Field(default=None, alias="$dynamicRef") - defs: Optional[Dict[str, "Schema"]] = Field(default=None, alias="$defs") + defs: Optional[Dict[str, "SchemaOrBool"]] = Field(default=None, alias="$defs") comment: Optional[str] = Field(default=None, alias="$comment") # Ref: JSON Schema 2020-12: https://json-schema.org/draft/2020-12/json-schema-core.html#name-a-vocabulary-for-applying-s # A Vocabulary for Applying Subschemas - allOf: Optional[List["Schema"]] = None - anyOf: Optional[List["Schema"]] = None - oneOf: Optional[List["Schema"]] = None - not_: Optional["Schema"] = Field(default=None, alias="not") - if_: Optional["Schema"] = Field(default=None, alias="if") - then: Optional["Schema"] = None - else_: Optional["Schema"] = Field(default=None, alias="else") - dependentSchemas: Optional[Dict[str, "Schema"]] = None - prefixItems: Optional[List["Schema"]] = None - items: Optional[Union["Schema", List["Schema"]]] = None - contains: Optional["Schema"] = None - properties: Optional[Dict[str, "Schema"]] = None - patternProperties: Optional[Dict[str, "Schema"]] = None - additionalProperties: Optional["Schema"] = None - propertyNames: Optional["Schema"] = None - unevaluatedItems: Optional["Schema"] = None - unevaluatedProperties: Optional["Schema"] = None + allOf: Optional[List["SchemaOrBool"]] = None + anyOf: Optional[List["SchemaOrBool"]] = None + oneOf: Optional[List["SchemaOrBool"]] = None + not_: Optional["SchemaOrBool"] = Field(default=None, alias="not") + if_: Optional["SchemaOrBool"] = Field(default=None, alias="if") + then: Optional["SchemaOrBool"] = None + else_: Optional["SchemaOrBool"] = Field(default=None, alias="else") + dependentSchemas: Optional[Dict[str, "SchemaOrBool"]] = None + prefixItems: Optional[List["SchemaOrBool"]] = None + # TODO: uncomment and remove below when deprecating Pydantic v1 + # It generales a list of schemas for tuples, before prefixItems was available + # items: Optional["SchemaOrBool"] = None + items: Optional[Union["SchemaOrBool", List["SchemaOrBool"]]] = None + contains: Optional["SchemaOrBool"] = None + properties: Optional[Dict[str, "SchemaOrBool"]] = None + patternProperties: Optional[Dict[str, "SchemaOrBool"]] = None + additionalProperties: Optional["SchemaOrBool"] = None + propertyNames: Optional["SchemaOrBool"] = None + unevaluatedItems: Optional["SchemaOrBool"] = None + unevaluatedProperties: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-structural # A Vocabulary for Structural Validation type: Optional[str] = None @@ -164,7 +167,7 @@ class Schema(BaseModel): # A Vocabulary for the Contents of String-Encoded Data contentEncoding: Optional[str] = None contentMediaType: Optional[str] = None - contentSchema: Optional["Schema"] = None + contentSchema: Optional["SchemaOrBool"] = None # Ref: JSON Schema Validation 2020-12: https://json-schema.org/draft/2020-12/json-schema-validation.html#name-a-vocabulary-for-basic-meta # A Vocabulary for Basic Meta-Data Annotations title: Optional[str] = None @@ -191,6 +194,11 @@ class Schema(BaseModel): extra: str = "allow" +# Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents +# A JSON Schema MUST be an object or a boolean. +SchemaOrBool = Union[Schema, bool] + + class Example(BaseModel): summary: Optional[str] = None description: Optional[str] = None diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py new file mode 100644 index 000000000..e35c26342 --- /dev/null +++ b/tests/test_additional_properties_bool.py @@ -0,0 +1,115 @@ +from typing import Union + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + + +class FooBaseModel(BaseModel): + class Config: + extra = "forbid" + + +class Foo(FooBaseModel): + pass + + +app = FastAPI() + + +@app.post("/") +async def post( + foo: Union[Foo, None] = None, +): + return foo + + +client = TestClient(app) + + +def test_call_invalid(): + response = client.post("/", json={"foo": {"bar": "baz"}}) + assert response.status_code == 422 + + +def test_call_valid(): + response = client.post("/", json={}) + assert response.status_code == 200 + assert response.json() == {} + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "post": { + "summary": "Post", + "operationId": "post__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Foo"} + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Foo": { + "properties": {}, + "additionalProperties": False, + "type": "object", + "title": "Foo", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } From 6bd4f5353154b53920c4f313d2b635107c696be5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 2 Jul 2023 15:59:00 +0000 Subject: [PATCH 1145/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 983c0e218..c13023ce3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo). * 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From 8a198fc1ed2647f1047099e8b19864ec13040e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 18:00:12 +0200 Subject: [PATCH 1146/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c13023ce3..f22146f4b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,15 @@ ## Latest Changes + +## 0.99.1 + +### Fixes + * 🐛 Fix JSON Schema accepting bools as valid JSON Schemas, e.g. `additionalProperties: false`. PR [#9781](https://github.com/tiangolo/fastapi/pull/9781) by [@tiangolo](https://github.com/tiangolo). + +### Docs + * 📝 Update source examples to use new JSON Schema examples field. PR [#9776](https://github.com/tiangolo/fastapi/pull/9776) by [@tiangolo](https://github.com/tiangolo). ## 0.99.0 From dd4e78ca7b09abdf0d4646fe4697316c021a8b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 2 Jul 2023 18:00:39 +0200 Subject: [PATCH 1147/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?99.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 9c4316e69..2d1bac2e1 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.99.0" +__version__ = "0.99.1" from starlette import status as status From 0976185af96ab2ee39c949c0456be616b01f8669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:12:13 +0200 Subject: [PATCH 1148/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pyd?= =?UTF-8?q?antic=20v2=20(#9816)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Pydantic v2 migration, initial implementation (#9500) * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * 👷 Add CI for both Pydantic v1 and v2 (#9688) * 👷 Test and install Pydantic v1 and v2 in CI * 💚 Tweak CI config for Pydantic v1 and v2 * 💚 Fix Pydantic v2 specification in CI * 🐛 Fix type annotations for compatibility with Python 3.7 * 💚 Install Pydantic v2 for lints * 🐛 Fix type annotations for Pydantic v2 * 💚 Re-use test cache for lint * ♻️ Refactor internals for test coverage and performance (#9691) * ♻️ Tweak import of Annotated from typing_extensions, they are installed anyway * ♻️ Refactor _compat to define functions for Pydantic v1 or v2 once instead of checking inside * ✅ Add test for UploadFile for Pydantic v2 * ♻️ Refactor types and remove logic for impossible cases * ✅ Add missing tests from test refactor for path params * ✅ Add tests for new decimal encoder * 💡 Add TODO comment for decimals in encoders * 🔥 Remove unneeded dummy function * 🔥 Remove section of code in field_annotation_is_scalar covered by sub-call to field_annotation_is_complex * ♻️ Refactor and tweak variables and types in _compat * ✅ Add tests for corner cases and compat with Pydantic v1 and v2 * ♻️ Refactor type annotations * 🔖 Release version 0.100.0-beta1 * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them (#9707) * ♻️ Refactor parts that use optional requirements to make them compatible with installations without them * ♻️ Update JSON Schema for email field without email-validator installed * 🐛 Fix support for Pydantic v2.0, small changes in their final release (#9771) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez * 🔖 Release version 0.100.0-beta2 * ✨ OpenAPI 3.1.0 with Pydantic v2, merge `master` (#9773) * ➕ Add dirty-equals as a testing dependency (#9778) ➕ Add dirty-equals as a testing dependency, it seems it got lsot at some point * 🔀 Merge master, fix valid JSON Schema accepting bools (#9782) * ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on the Pydantic side (#9787) ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on Pydantic side * ♻️ Deprecate parameter `regex`, use `pattern` instead (#9786) * 📝 Update docs to deprecate regex, recommend pattern * ♻️ Update examples to use new pattern instead of regex * 📝 Add new example with deprecated regex * ♻️ Add deprecation notes and warnings for regex * ✅ Add tests for regex deprecation * ✅ Update tests for compatibility with Pydantic v1 * ✨ Update docs to use Pydantic v2 settings and add note and example about v1 (#9788) * ➕ Add pydantic-settings to all extras * 📝 Update docs for Pydantic settings * 📝 Update Settings source examples to use Pydantic v2, and add a Pydantic v1 version * ✅ Add tests for settings with Pydantic v1 and v2 * 🔥 Remove solved TODO comment * ♻️ Update conditional OpenAPI to use new Pydantic v2 settings * ✅ Update tests to import Annotated from typing_extensions for Python < 3.9 (#9795) * ➕ Add pydantic-extra-types to fastapi[extra] * ➕ temp: Install Pydantic from source to test JSON Schema metadata fixes (#9777) * ➕ Install Pydantic from source, from branch for JSON Schema with metadata * ➕ Update dependencies, install Pydantic main * ➕ Fix dependency URL for Pydantic from source * ➕ Add pydantic-settings for test requirements * 💡 Add TODO comments to re-enable Pydantic main (not from source) (#9796) * ✨ Add new Pydantic Field param options to Query, Cookie, Body, etc. (#9797) * 📝 Add docs for Pydantic v2 for `docs/en/docs/advanced/path-operation-advanced-configuration.md` (#9798) * 📝 Update docs in examples for settings with Pydantic v2 (#9799) * 📝 Update JSON Schema `examples` docs with Pydantic v2 (#9800) * ♻️ Use new Pydantic v2 JSON Schema generator (#9813) Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> * ♻️ Tweak type annotations and Pydantic version range (#9801) * 📌 Re-enable GA Pydantic, for v2, require minimum 2.0.2 (#9814) * 🔖 Release version 0.100.0-beta3 * 🔥 Remove duplicate type declaration from merge conflicts (#9832) * 👷‍♂️ Run tests with Pydantic v2 GA (#9830) 👷 Run tests for Pydantic v2 GA * 📝 Add notes to docs expecting Pydantic v2 and future updates (#9833) * 📝 Update index with new extras * 📝 Update release notes --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Pastukhov Nikita --- .github/workflows/test.yml | 13 +- README.md | 2 + docs/en/docs/advanced/async-sql-databases.md | 7 + docs/en/docs/advanced/nosql-databases.md | 7 + .../path-operation-advanced-configuration.md | 34 +- docs/en/docs/advanced/settings.md | 72 +- docs/en/docs/advanced/sql-databases-peewee.md | 7 + docs/en/docs/advanced/testing-database.md | 7 + docs/en/docs/index.md | 2 + docs/en/docs/release-notes.md | 73 + .../tutorial/query-params-str-validations.md | 18 +- docs/en/docs/tutorial/schema-extra-example.md | 36 +- docs/en/docs/tutorial/sql-databases.md | 7 + docs_src/conditional_openapi/tutorial001.py | 2 +- docs_src/extra_models/tutorial003.py | 4 +- docs_src/extra_models/tutorial003_py310.py | 4 +- .../tutorial007.py | 4 +- .../tutorial007_pv1.py | 34 + .../tutorial004.py | 2 +- .../tutorial004_an.py | 2 +- .../tutorial004_an_py310.py | 2 +- .../tutorial004_an_py310_regex.py | 17 + .../tutorial004_an_py39.py | 2 +- .../tutorial004_py310.py | 2 +- .../tutorial010.py | 2 +- .../tutorial010_an.py | 2 +- .../tutorial010_an_py310.py | 2 +- .../tutorial010_an_py39.py | 2 +- .../tutorial010_py310.py | 2 +- docs_src/schema_extra_example/tutorial001.py | 5 +- .../schema_extra_example/tutorial001_pv1.py | 31 + .../schema_extra_example/tutorial001_py310.py | 5 +- .../tutorial001_py310_pv1.py | 29 + docs_src/settings/app01/config.py | 2 +- docs_src/settings/app02/config.py | 2 +- docs_src/settings/app02_an/config.py | 2 +- docs_src/settings/app02_an_py39/config.py | 2 +- docs_src/settings/app03/config.py | 2 +- docs_src/settings/app03_an/config.py | 5 +- docs_src/settings/app03_an/config_pv1.py | 10 + docs_src/settings/app03_an_py39/config.py | 2 +- docs_src/settings/tutorial001.py | 2 +- docs_src/settings/tutorial001_pv1.py | 21 + fastapi/__init__.py | 2 +- fastapi/_compat.py | 616 +++++++ fastapi/applications.py | 43 +- fastapi/datastructures.py | 33 +- fastapi/dependencies/models.py | 2 +- fastapi/dependencies/utils.py | 196 +-- fastapi/encoders.py | 102 +- fastapi/exceptions.py | 28 +- fastapi/openapi/constants.py | 1 + fastapi/openapi/models.py | 247 ++- fastapi/openapi/utils.py | 124 +- fastapi/param_functions.py | 263 ++- fastapi/params.py | 381 ++++- fastapi/routing.py | 131 +- fastapi/security/oauth2.py | 28 +- fastapi/types.py | 10 +- fastapi/utils.py | 126 +- pyproject.toml | 10 +- requirements-tests.txt | 4 +- tests/test_additional_properties_bool.py | 26 +- ...onal_responses_custom_model_in_callback.py | 26 +- tests/test_annotated.py | 49 +- tests/test_application.py | 23 +- tests/test_compat.py | 93 ++ tests/test_custom_schema_fields.py | 15 +- tests/test_datastructures.py | 6 + tests/test_datetime_custom_encoder.py | 59 +- tests/test_dependency_duplicates.py | 35 +- tests/test_dependency_overrides.py | 666 +++++--- tests/test_extra_routes.py | 10 +- .../__init__.py | 0 .../test_filter_pydantic_sub_model/app_pv1.py | 35 + .../test_filter_pydantic_sub_model_pv1.py} | 54 +- tests/test_filter_pydantic_sub_model_pv2.py | 182 +++ tests/test_infer_param_optionality.py | 277 +++- tests/test_inherited_custom_class.py | 92 +- tests/test_jsonable_encoder.py | 152 +- tests/test_multi_body_errors.py | 160 +- tests/test_multi_query_errors.py | 55 +- .../test_openapi_query_parameter_extension.py | 21 +- tests/test_openapi_servers.py | 15 +- tests/test_params_repr.py | 135 +- tests/test_path.py | 1413 ++++++++++++++--- tests/test_query.py | 448 +++++- tests/test_read_with_orm_mode.py | 113 +- tests/test_regex_deprecated_body.py | 182 +++ tests/test_regex_deprecated_params.py | 165 ++ ...test_request_body_parameters_media_type.py | 1 - tests/test_response_by_alias.py | 28 +- ...est_response_model_as_return_annotation.py | 16 +- tests/test_response_model_data_filter.py | 81 + ...sponse_model_data_filter_no_inheritance.py | 83 + tests/test_schema_extra_examples.py | 227 ++- tests/test_security_oauth2.py | 213 ++- tests/test_security_oauth2_optional.py | 213 ++- ...st_security_oauth2_optional_description.py | 213 ++- tests/test_skip_defaults.py | 2 +- tests/test_sub_callbacks.py | 43 +- tests/test_tuples.py | 91 +- .../test_tutorial002.py | 12 +- .../test_tutorial004.py | 12 +- .../test_tutorial001.py | 4 + .../test_behind_a_proxy/test_tutorial003.py | 18 +- .../test_behind_a_proxy/test_tutorial004.py | 18 +- .../test_bigger_applications/test_main.py | 472 ++++-- .../test_bigger_applications/test_main_an.py | 472 ++++-- .../test_main_an_py39.py | 470 ++++-- .../test_body/test_tutorial001.py | 457 ++++-- .../test_body/test_tutorial001_py310.py | 448 ++++-- .../test_body_fields/test_tutorial001.py | 159 +- .../test_body_fields/test_tutorial001_an.py | 159 +- .../test_tutorial001_an_py310.py | 145 +- .../test_tutorial001_an_py39.py | 145 +- .../test_tutorial001_py310.py | 145 +- .../test_tutorial001.py | 151 +- .../test_tutorial001_an.py | 151 +- .../test_tutorial001_an_py310.py | 140 +- .../test_tutorial001_an_py39.py | 140 +- .../test_tutorial001_py310.py | 140 +- .../test_tutorial003.py | 252 ++- .../test_tutorial003_an.py | 252 ++- .../test_tutorial003_an_py310.py | 240 ++- .../test_tutorial003_an_py39.py | 240 ++- .../test_tutorial003_py310.py | 240 ++- .../test_tutorial009.py | 54 +- .../test_tutorial009_py39.py | 35 +- .../test_body_updates/test_tutorial001.py | 49 +- .../test_tutorial001_py310.py | 34 +- .../test_tutorial001_py39.py | 34 +- .../test_tutorial001.py | 23 +- .../test_cookie_params/test_tutorial001.py | 12 +- .../test_cookie_params/test_tutorial001_an.py | 12 +- .../test_tutorial001_an_py310.py | 12 +- .../test_tutorial001_an_py39.py | 12 +- .../test_tutorial001_py310.py | 12 +- .../test_tutorial002.py | 43 +- .../test_dataclasses/test_tutorial001.py | 57 +- .../test_dataclasses/test_tutorial002.py | 44 +- .../test_dataclasses/test_tutorial003.py | 33 +- .../test_dependencies/test_tutorial001.py | 23 +- .../test_dependencies/test_tutorial001_an.py | 23 +- .../test_tutorial001_an_py310.py | 23 +- .../test_tutorial001_an_py39.py | 23 +- .../test_tutorial001_py310.py | 23 +- .../test_dependencies/test_tutorial004.py | 12 +- .../test_dependencies/test_tutorial004_an.py | 12 +- .../test_tutorial004_an_py310.py | 12 +- .../test_tutorial004_an_py39.py | 12 +- .../test_tutorial004_py310.py | 12 +- .../test_dependencies/test_tutorial006.py | 52 +- .../test_dependencies/test_tutorial006_an.py | 52 +- .../test_tutorial006_an_py39.py | 52 +- .../test_dependencies/test_tutorial012.py | 102 +- .../test_dependencies/test_tutorial012_an.py | 102 +- .../test_tutorial012_an_py39.py | 102 +- .../test_extra_data_types/test_tutorial001.py | 108 +- .../test_tutorial001_an.py | 108 +- .../test_tutorial001_an_py310.py | 108 +- .../test_tutorial001_an_py39.py | 108 +- .../test_tutorial001_py310.py | 108 +- .../test_handling_errors/test_tutorial004.py | 18 +- .../test_handling_errors/test_tutorial005.py | 38 +- .../test_handling_errors/test_tutorial006.py | 35 +- .../test_header_params/test_tutorial001.py | 14 +- .../test_header_params/test_tutorial001_an.py | 12 +- .../test_tutorial001_an_py310.py | 12 +- .../test_tutorial001_py310.py | 12 +- .../test_header_params/test_tutorial002.py | 12 +- .../test_header_params/test_tutorial002_an.py | 12 +- .../test_tutorial002_an_py310.py | 12 +- .../test_tutorial002_an_py39.py | 12 +- .../test_tutorial002_py310.py | 12 +- .../test_header_params/test_tutorial003.py | 24 +- .../test_header_params/test_tutorial003_an.py | 24 +- .../test_tutorial003_an_py310.py | 24 +- .../test_tutorial003_an_py39.py | 30 +- .../test_tutorial003_py310.py | 24 +- .../test_tutorial001.py | 43 +- .../test_tutorial004.py | 23 +- .../test_tutorial007.py | 35 +- .../test_tutorial007_pv1.py | 106 ++ .../test_tutorial005.py | 23 +- .../test_tutorial005_py310.py | 23 +- .../test_tutorial005_py39.py | 23 +- .../test_path_params/test_tutorial005.py | 110 +- .../test_query_params/test_tutorial005.py | 55 +- .../test_query_params/test_tutorial006.py | 133 +- .../test_tutorial006_py310.py | 123 +- .../test_tutorial010.py | 129 +- .../test_tutorial010_an.py | 129 +- .../test_tutorial010_an_py310.py | 120 +- .../test_tutorial010_an_py39.py | 120 +- .../test_tutorial010_py310.py | 120 +- .../test_tutorial011.py | 23 +- .../test_tutorial011_an.py | 23 +- .../test_tutorial011_an_py310.py | 23 +- .../test_tutorial011_an_py39.py | 23 +- .../test_tutorial011_py310.py | 23 +- .../test_tutorial011_py39.py | 23 +- .../test_request_files/test_tutorial001.py | 52 +- .../test_request_files/test_tutorial001_02.py | 67 +- .../test_tutorial001_02_an.py | 67 +- .../test_tutorial001_02_an_py310.py | 67 +- .../test_tutorial001_02_an_py39.py | 67 +- .../test_tutorial001_02_py310.py | 67 +- .../test_request_files/test_tutorial001_an.py | 63 +- .../test_tutorial001_an_py39.py | 63 +- .../test_request_files/test_tutorial002.py | 63 +- .../test_request_files/test_tutorial002_an.py | 63 +- .../test_tutorial002_an_py39.py | 63 +- .../test_tutorial002_py39.py | 52 +- .../test_request_forms/test_tutorial001.py | 194 ++- .../test_request_forms/test_tutorial001_an.py | 194 ++- .../test_tutorial001_an_py39.py | 195 ++- .../test_tutorial001.py | 252 ++- .../test_tutorial001_an.py | 252 ++- .../test_tutorial001_an_py39.py | 227 ++- .../test_response_model/test_tutorial003.py | 23 +- .../test_tutorial003_01.py | 23 +- .../test_tutorial003_01_py310.py | 23 +- .../test_tutorial003_py310.py | 23 +- .../test_response_model/test_tutorial004.py | 12 +- .../test_tutorial004_py310.py | 12 +- .../test_tutorial004_py39.py | 12 +- .../test_response_model/test_tutorial005.py | 12 +- .../test_tutorial005_py310.py | 12 +- .../test_response_model/test_tutorial006.py | 12 +- .../test_tutorial006_py310.py | 12 +- .../test_tutorial001.py | 133 ++ .../test_tutorial001_pv1.py | 127 ++ .../test_tutorial001_py310.py | 135 ++ .../test_tutorial001_py310_pv1.py | 129 ++ .../test_tutorial004.py | 80 +- .../test_tutorial004_an.py | 80 +- .../test_tutorial004_an_py310.py | 80 +- .../test_tutorial004_an_py39.py | 80 +- .../test_tutorial004_py310.py | 80 +- .../test_security/test_tutorial003.py | 45 +- .../test_security/test_tutorial003_an.py | 45 +- .../test_tutorial003_an_py310.py | 45 +- .../test_security/test_tutorial003_an_py39.py | 45 +- .../test_security/test_tutorial003_py310.py | 45 +- .../test_security/test_tutorial005.py | 78 +- .../test_security/test_tutorial005_an.py | 78 +- .../test_tutorial005_an_py310.py | 78 +- .../test_security/test_tutorial005_an_py39.py | 78 +- .../test_security/test_tutorial005_py310.py | 78 +- .../test_security/test_tutorial005_py39.py | 78 +- .../test_tutorial/test_settings/test_app02.py | 11 +- .../test_settings/test_tutorial001.py | 19 + .../test_settings/test_tutorial001_pv1.py | 19 + .../test_sql_databases/test_sql_databases.py | 41 +- .../test_sql_databases_middleware.py | 41 +- .../test_sql_databases_middleware_py310.py | 41 +- .../test_sql_databases_middleware_py39.py | 41 +- .../test_sql_databases_py310.py | 41 +- .../test_sql_databases_py39.py | 41 +- .../test_testing_databases.py | 4 + .../test_testing_databases_py310.py | 4 +- .../test_testing_databases_py39.py | 4 +- .../test_sql_databases_peewee.py | 10 + tests/test_union_body.py | 14 +- tests/test_union_inherited_body.py | 25 +- tests/test_validate_response.py | 11 +- tests/test_validate_response_dataclass.py | 8 +- .../__init__.py | 0 .../app_pv1.py} | 30 - .../app_pv2.py | 51 + .../test_validate_response_recursive_pv1.py | 33 + .../test_validate_response_recursive_pv2.py | 33 + tests/utils.py | 3 + 274 files changed, 16798 insertions(+), 4648 deletions(-) create mode 100644 docs_src/path_operation_advanced_configuration/tutorial007_pv1.py create mode 100644 docs_src/query_params_str_validations/tutorial004_an_py310_regex.py create mode 100644 docs_src/schema_extra_example/tutorial001_pv1.py create mode 100644 docs_src/schema_extra_example/tutorial001_py310_pv1.py create mode 100644 docs_src/settings/app03_an/config_pv1.py create mode 100644 docs_src/settings/tutorial001_pv1.py create mode 100644 fastapi/_compat.py create mode 100644 tests/test_compat.py create mode 100644 tests/test_filter_pydantic_sub_model/__init__.py create mode 100644 tests/test_filter_pydantic_sub_model/app_pv1.py rename tests/{test_filter_pydantic_sub_model.py => test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py} (81%) create mode 100644 tests/test_filter_pydantic_sub_model_pv2.py create mode 100644 tests/test_regex_deprecated_body.py create mode 100644 tests/test_regex_deprecated_params.py create mode 100644 tests/test_response_model_data_filter.py create mode 100644 tests/test_response_model_data_filter_no_inheritance.py create mode 100644 tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py create mode 100644 tests/test_tutorial/test_settings/test_tutorial001.py create mode 100644 tests/test_tutorial/test_settings/test_tutorial001_pv1.py create mode 100644 tests/test_validate_response_recursive/__init__.py rename tests/{test_validate_response_recursive.py => test_validate_response_recursive/app_pv1.py} (58%) create mode 100644 tests/test_validate_response_recursive/app_pv2.py create mode 100644 tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py create mode 100644 tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84f101424..b95358d01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,10 +25,12 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt + - name: Install Pydantic v2 + run: pip install "pydantic>=2.0.2,<3.0.0" - name: Lint run: bash scripts/lint.sh @@ -37,6 +39,7 @@ jobs: strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: - uses: actions/checkout@v3 @@ -51,10 +54,16 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt + - name: Install Pydantic v1 + if: matrix.pydantic-version == 'pydantic-v1' + run: pip install "pydantic>=1.10.0,<2.0.0" + - name: Install Pydantic v2 + if: matrix.pydantic-version == 'pydantic-v2' + run: pip install "pydantic>=2.0.2,<3.0.0" - run: mkdir coverage - name: Test run: bash scripts/test.sh diff --git a/README.md b/README.md index 7dc199367..36c71081e 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,8 @@ To understand more about it, see the section email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/en/docs/advanced/async-sql-databases.md b/docs/en/docs/advanced/async-sql-databases.md index 93c288e1b..12549a190 100644 --- a/docs/en/docs/advanced/async-sql-databases.md +++ b/docs/en/docs/advanced/async-sql-databases.md @@ -1,5 +1,12 @@ # Async SQL (Relational) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will include Pydantic v2 and will use SQLModel once it is updated to use Pydantic v2 as well. + You can also use `encode/databases` with **FastAPI** to connect to databases using `async` and `await`. It is compatible with: diff --git a/docs/en/docs/advanced/nosql-databases.md b/docs/en/docs/advanced/nosql-databases.md index 6cc5a9385..606db35c7 100644 --- a/docs/en/docs/advanced/nosql-databases.md +++ b/docs/en/docs/advanced/nosql-databases.md @@ -1,5 +1,12 @@ # NoSQL (Distributed / Big Data) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1. + + The new docs will hopefully use Pydantic v2 and will use ODMantic with MongoDB. + **FastAPI** can also be integrated with any NoSQL. Here we'll see an example using **Couchbase**, a document based NoSQL database. diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 6d9a5fe70..7ca88d43e 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -150,9 +150,20 @@ And you could do this even if the data type in the request is not JSON. For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON: -```Python hl_lines="17-22 24" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="17-22 24" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`. Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. @@ -160,9 +171,20 @@ Then we use the request directly, and extract the body as `bytes`. This means th And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content: -```Python hl_lines="26-33" -{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!} + ``` + +=== "Pydantic v1" + + ```Python hl_lines="26-33" + {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!} + ``` + +!!! info + In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`. !!! tip Here we re-use the same Pydantic model. diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 60ec9c92c..8f6c7da93 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -125,7 +125,34 @@ That means that any value read in Python from an environment variable will be a ## Pydantic `Settings` -Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. +Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with Pydantic: Settings management. + +### Install `pydantic-settings` + +First, install the `pydantic-settings` package: + +
+ +```console +$ pip install pydantic-settings +---> 100% +``` + +
+ +It also comes included when you install the `all` extras with: + +
+ +```console +$ pip install "fastapi[all]" +---> 100% +``` + +
+ +!!! info + In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality. ### Create the `Settings` object @@ -135,9 +162,20 @@ The same way as with Pydantic models, you declare class attributes with type ann You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`. -```Python hl_lines="2 5-8 11" -{!../../../docs_src/settings/tutorial001.py!} -``` +=== "Pydantic v2" + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001.py!} + ``` + +=== "Pydantic v1" + + !!! info + In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`. + + ```Python hl_lines="2 5-8 11" + {!> ../../../docs_src/settings/tutorial001_pv1.py!} + ``` !!! tip If you want something quick to copy and paste, don't use this example, use the last one below. @@ -306,14 +344,28 @@ APP_NAME="ChimichangApp" And then update your `config.py` with: -```Python hl_lines="9-10" -{!../../../docs_src/settings/app03/config.py!} -``` +=== "Pydantic v2" -Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use. + ```Python hl_lines="9" + {!> ../../../docs_src/settings/app03_an/config.py!} + ``` -!!! tip - The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config + !!! tip + The `model_config` attribute is used just for Pydantic configuration. You can read more at Pydantic Model Config. + +=== "Pydantic v1" + + ```Python hl_lines="9-10" + {!> ../../../docs_src/settings/app03_an/config_pv1.py!} + ``` + + !!! tip + The `Config` class is used just for Pydantic configuration. You can read more at Pydantic Model Config. + +!!! info + In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`. + +Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use. ### Creating the `Settings` only once with `lru_cache` diff --git a/docs/en/docs/advanced/sql-databases-peewee.md b/docs/en/docs/advanced/sql-databases-peewee.md index b4ea61367..6a469634f 100644 --- a/docs/en/docs/advanced/sql-databases-peewee.md +++ b/docs/en/docs/advanced/sql-databases-peewee.md @@ -5,6 +5,13 @@ Feel free to skip this. + Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives. + +!!! info + These docs assume Pydantic v1. + + Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes. + If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM. If you already have a code base that uses Peewee ORM, you can check here how to use it with **FastAPI**. diff --git a/docs/en/docs/advanced/testing-database.md b/docs/en/docs/advanced/testing-database.md index 13a6959b6..1c0669b9c 100644 --- a/docs/en/docs/advanced/testing-database.md +++ b/docs/en/docs/advanced/testing-database.md @@ -1,5 +1,12 @@ # Testing a Database +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing. You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc. diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index afd6d7138..ebd74bc8f 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -446,6 +446,8 @@ To understand more about it, see the section email_validator - for email validation. +* pydantic-settings - for settings management. +* pydantic-extra-types - for extra types to be used with Pydantic. Used by Starlette: diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f22146f4b..f4ce74404 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,79 @@ ## Latest Changes +✨ Support for **Pydantic v2** ✨ + +Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: + +* Improved **correctness** in corner cases. +* **Safer** types. +* Better **performance** and **less energy** consumption. +* Better **extensibility**. +* etc. + +...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀 + +In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯 + +When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade. + +The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign. + +### Migration + +Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/). + +For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic). + +A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes. + +### Pydantic v1 + +**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while. + +This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI. + +There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**. + +### Changes + +* There are **new parameter** fields supported by Pydantic `Field()` for: + + * `Path()` + * `Query()` + * `Header()` + * `Cookie()` + * `Body()` + * `Form()` + * `File()` + +* The new parameter fields are: + + * `default_factory` + * `alias_priority` + * `validation_alias` + * `serialization_alias` + * `discriminator` + * `strict` + * `multiple_of` + * `allow_inf_nan` + * `max_digits` + * `decimal_places` + * `json_schema_extra` + +...you can read about them in the Pydantic docs. + +* The parameter `regex` has been deprecated and replaced by `pattern`. + * You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions). +* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict. + * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). +* When you install `"fastapi[all]"` it now also includes: + * pydantic-settings - for settings management. + * pydantic-extra-types - for extra types to be used with Pydantic. +* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly. + * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). ## 0.99.1 diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 549e6c75b..f87adddcb 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -277,7 +277,7 @@ You can also add a parameter `min_length`: ## Add regular expressions -You can define a regular expression that the parameter should match: +You can define a regular expression `pattern` that the parameter should match: === "Python 3.10+" @@ -315,7 +315,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!} + ``` + +But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓 + ## Default values You can, of course, use default values other than `None`. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 86ccb1f5a..39d184763 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -4,24 +4,48 @@ You can declare examples of the data your app can receive. Here are several ways to do it. -## Pydantic `schema_extra` +## Extra JSON Schema data in Pydantic models -You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization: +You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema. -=== "Python 3.10+" +=== "Python 3.10+ Pydantic v2" - ```Python hl_lines="13-23" + ```Python hl_lines="13-24" {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.10+ Pydantic v1" - ```Python hl_lines="15-25" + ```Python hl_lines="13-23" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!} + ``` + +=== "Python 3.6+ Pydantic v2" + + ```Python hl_lines="15-26" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} ``` +=== "Python 3.6+ Pydantic v1" + + ```Python hl_lines="15-25" + {!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!} + ``` + That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs. +=== "Pydantic v2" + + In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in Pydantic's docs: Model Config. + + You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + +=== "Pydantic v1" + + In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. + + You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + !!! tip You could use the same technique to extend the JSON Schema and add your own custom extra info. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index fd66c5add..6e0e5dc06 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -1,5 +1,12 @@ # SQL (Relational) Databases +!!! info + These docs are about to be updated. 🎉 + + The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0. + + The new docs will include Pydantic v2 and will use SQLModel (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well. + **FastAPI** doesn't require you to use a SQL (relational) database. But you can use any relational database that you want. diff --git a/docs_src/conditional_openapi/tutorial001.py b/docs_src/conditional_openapi/tutorial001.py index 717e723e8..eedb0d274 100644 --- a/docs_src/conditional_openapi/tutorial001.py +++ b/docs_src/conditional_openapi/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/extra_models/tutorial003.py b/docs_src/extra_models/tutorial003.py index 065439acc..06675cbc0 100644 --- a/docs_src/extra_models/tutorial003.py +++ b/docs_src/extra_models/tutorial003.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/extra_models/tutorial003_py310.py b/docs_src/extra_models/tutorial003_py310.py index 065439acc..06675cbc0 100644 --- a/docs_src/extra_models/tutorial003_py310.py +++ b/docs_src/extra_models/tutorial003_py310.py @@ -12,11 +12,11 @@ class BaseItem(BaseModel): class CarItem(BaseItem): - type = "car" + type: str = "car" class PlaneItem(BaseItem): - type = "plane" + type: str = "plane" size: int diff --git a/docs_src/path_operation_advanced_configuration/tutorial007.py b/docs_src/path_operation_advanced_configuration/tutorial007.py index d51752bb8..972ddbd2c 100644 --- a/docs_src/path_operation_advanced_configuration/tutorial007.py +++ b/docs_src/path_operation_advanced_configuration/tutorial007.py @@ -16,7 +16,7 @@ class Item(BaseModel): "/items/", openapi_extra={ "requestBody": { - "content": {"application/x-yaml": {"schema": Item.schema()}}, + "content": {"application/x-yaml": {"schema": Item.model_json_schema()}}, "required": True, }, }, @@ -28,7 +28,7 @@ async def create_item(request: Request): except yaml.YAMLError: raise HTTPException(status_code=422, detail="Invalid YAML") try: - item = Item.parse_obj(data) + item = Item.model_validate(data) except ValidationError as e: raise HTTPException(status_code=422, detail=e.errors()) return item diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py new file mode 100644 index 000000000..d51752bb8 --- /dev/null +++ b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py @@ -0,0 +1,34 @@ +from typing import List + +import yaml +from fastapi import FastAPI, HTTPException, Request +from pydantic import BaseModel, ValidationError + +app = FastAPI() + + +class Item(BaseModel): + name: str + tags: List[str] + + +@app.post( + "/items/", + openapi_extra={ + "requestBody": { + "content": {"application/x-yaml": {"schema": Item.schema()}}, + "required": True, + }, + }, +) +async def create_item(request: Request): + raw_body = await request.body() + try: + data = yaml.safe_load(raw_body) + except yaml.YAMLError: + raise HTTPException(status_code=422, detail="Invalid YAML") + try: + item = Item.parse_obj(data) + except ValidationError as e: + raise HTTPException(status_code=422, detail=e.errors()) + return item diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py index 5a7129816..3639b6c38 100644 --- a/docs_src/query_params_str_validations/tutorial004.py +++ b/docs_src/query_params_str_validations/tutorial004.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Union[str, None] = Query( - default=None, min_length=3, max_length=50, regex="^fixedquery$" + default=None, min_length=3, max_length=50, pattern="^fixedquery$" ) ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py index 5346b997b..24698c7b3 100644 --- a/docs_src/query_params_str_validations/tutorial004_an.py +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -9,7 +9,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py index 8fd375b3d..b7b629ee8 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py new file mode 100644 index 000000000..8fd375b3d --- /dev/null +++ b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py @@ -0,0 +1,17 @@ +from typing import Annotated + +from fastapi import FastAPI, Query + +app = FastAPI() + + +@app.get("/items/") +async def read_items( + q: Annotated[ + str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") + ] = None +): + results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + if q: + results.update({"q": q}) + return results diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py index 2fd82db75..8e9a6fc32 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -8,7 +8,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: Annotated[ - Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$") + Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") ] = None ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py index 180a2e511..f80798bcb 100644 --- a/docs_src/query_params_str_validations/tutorial004_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -6,7 +6,7 @@ app = FastAPI() @app.get("/items/") async def read_items( q: str - | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$") + | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$") ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py index 35443d194..3314f8b6d 100644 --- a/docs_src/query_params_str_validations/tutorial010.py +++ b/docs_src/query_params_str_validations/tutorial010.py @@ -14,7 +14,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ) ): diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py index 8995f3f57..c5df00897 100644 --- a/docs_src/query_params_str_validations/tutorial010_an.py +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -16,7 +16,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py index cfa81926c..a8e8c099b 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -15,7 +15,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py index 220eaabf4..955880dd6 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -15,7 +15,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ), ] = None diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py index f2839516e..9ea7b3c49 100644 --- a/docs_src/query_params_str_validations/tutorial010_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -13,7 +13,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, max_length=50, - regex="^fixedquery$", + pattern="^fixedquery$", deprecated=True, ) ): diff --git a/docs_src/schema_extra_example/tutorial001.py b/docs_src/schema_extra_example/tutorial001.py index 6ab96ff85..32a66db3a 100644 --- a/docs_src/schema_extra_example/tutorial001.py +++ b/docs_src/schema_extra_example/tutorial001.py @@ -12,8 +12,8 @@ class Item(BaseModel): price: float tax: Union[float, None] = None - class Config: - schema_extra = { + model_config = { + "json_schema_extra": { "examples": [ { "name": "Foo", @@ -23,6 +23,7 @@ class Item(BaseModel): } ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_pv1.py b/docs_src/schema_extra_example/tutorial001_pv1.py new file mode 100644 index 000000000..6ab96ff85 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_pv1.py @@ -0,0 +1,31 @@ +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial001_py310.py b/docs_src/schema_extra_example/tutorial001_py310.py index ec83f1112..84aa5fc12 100644 --- a/docs_src/schema_extra_example/tutorial001_py310.py +++ b/docs_src/schema_extra_example/tutorial001_py310.py @@ -10,8 +10,8 @@ class Item(BaseModel): price: float tax: float | None = None - class Config: - schema_extra = { + model_config = { + "json_schema_extra": { "examples": [ { "name": "Foo", @@ -21,6 +21,7 @@ class Item(BaseModel): } ] } + } @app.put("/items/{item_id}") diff --git a/docs_src/schema_extra_example/tutorial001_py310_pv1.py b/docs_src/schema_extra_example/tutorial001_py310_pv1.py new file mode 100644 index 000000000..ec83f1112 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial001_py310_pv1.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + class Config: + schema_extra = { + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ] + } + + +@app.put("/items/{item_id}") +async def update_item(item_id: int, item: Item): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/settings/app01/config.py b/docs_src/settings/app01/config.py index defede9db..b31b8811d 100644 --- a/docs_src/settings/app01/config.py +++ b/docs_src/settings/app01/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02/config.py b/docs_src/settings/app02/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02/config.py +++ b/docs_src/settings/app02/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02_an/config.py b/docs_src/settings/app02_an/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02_an/config.py +++ b/docs_src/settings/app02_an/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app02_an_py39/config.py b/docs_src/settings/app02_an_py39/config.py index 9a7829135..e17b5035d 100644 --- a/docs_src/settings/app02_an_py39/config.py +++ b/docs_src/settings/app02_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03/config.py b/docs_src/settings/app03/config.py index e1c3ee300..942aea3e5 100644 --- a/docs_src/settings/app03/config.py +++ b/docs_src/settings/app03/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/app03_an/config.py b/docs_src/settings/app03_an/config.py index e1c3ee300..08f8f88c2 100644 --- a/docs_src/settings/app03_an/config.py +++ b/docs_src/settings/app03_an/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -6,5 +6,4 @@ class Settings(BaseSettings): admin_email: str items_per_user: int = 50 - class Config: - env_file = ".env" + model_config = SettingsConfigDict(env_file=".env") diff --git a/docs_src/settings/app03_an/config_pv1.py b/docs_src/settings/app03_an/config_pv1.py new file mode 100644 index 000000000..e1c3ee300 --- /dev/null +++ b/docs_src/settings/app03_an/config_pv1.py @@ -0,0 +1,10 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + class Config: + env_file = ".env" diff --git a/docs_src/settings/app03_an_py39/config.py b/docs_src/settings/app03_an_py39/config.py index e1c3ee300..942aea3e5 100644 --- a/docs_src/settings/app03_an_py39/config.py +++ b/docs_src/settings/app03_an_py39/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001.py b/docs_src/settings/tutorial001.py index 0cfd1b663..d48c4c060 100644 --- a/docs_src/settings/tutorial001.py +++ b/docs_src/settings/tutorial001.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): diff --git a/docs_src/settings/tutorial001_pv1.py b/docs_src/settings/tutorial001_pv1.py new file mode 100644 index 000000000..0cfd1b663 --- /dev/null +++ b/docs_src/settings/tutorial001_pv1.py @@ -0,0 +1,21 @@ +from fastapi import FastAPI +from pydantic import BaseSettings + + +class Settings(BaseSettings): + app_name: str = "Awesome API" + admin_email: str + items_per_user: int = 50 + + +settings = Settings() +app = FastAPI() + + +@app.get("/info") +async def info(): + return { + "app_name": settings.app_name, + "admin_email": settings.admin_email, + "items_per_user": settings.items_per_user, + } diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 2d1bac2e1..5eb3c4de2 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.99.1" +__version__ = "0.100.0-beta3" from starlette import status as status diff --git a/fastapi/_compat.py b/fastapi/_compat.py new file mode 100644 index 000000000..2233fe33c --- /dev/null +++ b/fastapi/_compat.py @@ -0,0 +1,616 @@ +from collections import deque +from copy import copy +from dataclasses import dataclass, is_dataclass +from enum import Enum +from typing import ( + Any, + Callable, + Deque, + Dict, + FrozenSet, + List, + Mapping, + Sequence, + Set, + Tuple, + Type, + Union, +) + +from fastapi.exceptions import RequestErrorModel +from fastapi.types import IncEx, ModelNameMap, UnionType +from pydantic import BaseModel, create_model +from pydantic.version import VERSION as PYDANTIC_VERSION +from starlette.datastructures import UploadFile +from typing_extensions import Annotated, Literal, get_args, get_origin + +PYDANTIC_V2 = PYDANTIC_VERSION.startswith("2.") + + +sequence_annotation_to_type = { + Sequence: list, + List: list, + list: list, + Tuple: tuple, + tuple: tuple, + Set: set, + set: set, + FrozenSet: frozenset, + frozenset: frozenset, + Deque: deque, + deque: deque, +} + +sequence_types = tuple(sequence_annotation_to_type.keys()) + +if PYDANTIC_V2: + from pydantic import PydanticSchemaGenerationError as PydanticSchemaGenerationError + from pydantic import TypeAdapter + from pydantic import ValidationError as ValidationError + from pydantic._internal._schema_generation_shared import ( # type: ignore[attr-defined] + GetJsonSchemaHandler as GetJsonSchemaHandler, + ) + from pydantic._internal._typing_extra import eval_type_lenient + from pydantic._internal._utils import lenient_issubclass as lenient_issubclass + from pydantic.fields import FieldInfo + from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema + from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue + from pydantic_core import CoreSchema as CoreSchema + from pydantic_core import MultiHostUrl as MultiHostUrl + from pydantic_core import PydanticUndefined, PydanticUndefinedType + from pydantic_core import Url as Url + from pydantic_core.core_schema import ( + general_plain_validator_function as general_plain_validator_function, + ) + + Required = PydanticUndefined + Undefined = PydanticUndefined + UndefinedType = PydanticUndefinedType + evaluate_forwardref = eval_type_lenient + Validator = Any + + class BaseConfig: + pass + + class ErrorWrapper(Exception): + pass + + @dataclass + class ModelField: + field_info: FieldInfo + name: str + mode: Literal["validation", "serialization"] = "validation" + + @property + def alias(self) -> str: + a = self.field_info.alias + return a if a is not None else self.name + + @property + def required(self) -> bool: + return self.field_info.is_required() + + @property + def default(self) -> Any: + return self.get_default() + + @property + def type_(self) -> Any: + return self.field_info.annotation + + def __post_init__(self) -> None: + self._type_adapter: TypeAdapter[Any] = TypeAdapter( + Annotated[self.field_info.annotation, self.field_info] + ) + + def get_default(self) -> Any: + if self.field_info.is_required(): + return Undefined + return self.field_info.get_default(call_default_factory=True) + + def validate( + self, + value: Any, + values: Dict[str, Any] = {}, # noqa: B006 + *, + loc: Tuple[Union[int, str], ...] = (), + ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]: + try: + return ( + self._type_adapter.validate_python(value, from_attributes=True), + None, + ) + except ValidationError as exc: + return None, _regenerate_error_with_loc( + errors=exc.errors(), loc_prefix=loc + ) + + def serialize( + self, + value: Any, + *, + mode: Literal["json", "python"] = "json", + include: Union[IncEx, None] = None, + exclude: Union[IncEx, None] = None, + by_alias: bool = True, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + ) -> Any: + # What calls this code passes a value that already called + # self._type_adapter.validate_python(value) + return self._type_adapter.dump_python( + value, + mode=mode, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + def __hash__(self) -> int: + # Each ModelField is unique for our purposes, to allow making a dict from + # ModelField to its JSON Schema. + return id(self) + + def get_annotation_from_field_info( + annotation: Any, field_info: FieldInfo, field_name: str + ) -> Any: + return annotation + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + return errors # type: ignore[return-value] + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.model_rebuild() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.model_dump(mode=mode, **kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.model_config + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + ) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + json_schema = field_mapping[(field, field.mode)] + if "$ref" not in json_schema: + # TODO remove when deprecating Pydantic v1 + # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 + json_schema[ + "title" + ] = field.field_info.title or field.alias.title().replace("_", " ") + return json_schema + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + return {} + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + inputs = [ + (field, field.mode, field._type_adapter.core_schema) for field in fields + ] + field_mapping, definitions = schema_generator.generate_definitions( + inputs=inputs + ) + return field_mapping, definitions # type: ignore[return-value] + + def is_scalar_field(field: ModelField) -> bool: + from fastapi import params + + return field_annotation_is_scalar( + field.field_info.annotation + ) and not isinstance(field.field_info, params.Body) + + def is_sequence_field(field: ModelField) -> bool: + return field_annotation_is_sequence(field.field_info.annotation) + + def is_scalar_sequence_field(field: ModelField) -> bool: + return field_annotation_is_scalar_sequence(field.field_info.annotation) + + def is_bytes_field(field: ModelField) -> bool: + return is_bytes_or_nonable_bytes_annotation(field.type_) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return is_bytes_sequence_annotation(field.type_) + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return type(field_info).from_annotation(annotation) + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + origin_type = ( + get_origin(field.field_info.annotation) or field.field_info.annotation + ) + assert issubclass(origin_type, sequence_types) # type: ignore[arg-type] + return sequence_annotation_to_type[origin_type](value) # type: ignore[no-any-return] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + error = ValidationError.from_exception_data( + "Field required", [{"type": "missing", "loc": loc, "input": {}}] + ).errors()[0] + error["input"] = None + return error # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + field_params = {f.name: (f.field_info.annotation, f.field_info) for f in fields} + BodyModel: Type[BaseModel] = create_model(model_name, **field_params) # type: ignore[call-overload] + return BodyModel + +else: + from fastapi.openapi.constants import REF_PREFIX as REF_PREFIX + from pydantic import AnyUrl as Url # noqa: F401 + from pydantic import ( # type: ignore[assignment] + BaseConfig as BaseConfig, # noqa: F401 + ) + from pydantic import ValidationError as ValidationError # noqa: F401 + from pydantic.class_validators import ( # type: ignore[no-redef] + Validator as Validator, # noqa: F401 + ) + from pydantic.error_wrappers import ( # type: ignore[no-redef] + ErrorWrapper as ErrorWrapper, # noqa: F401 + ) + from pydantic.errors import MissingError + from pydantic.fields import ( # type: ignore[attr-defined] + SHAPE_FROZENSET, + SHAPE_LIST, + SHAPE_SEQUENCE, + SHAPE_SET, + SHAPE_SINGLETON, + SHAPE_TUPLE, + SHAPE_TUPLE_ELLIPSIS, + ) + from pydantic.fields import FieldInfo as FieldInfo + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + ModelField as ModelField, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Required as Required, # noqa: F401 + ) + from pydantic.fields import ( # type: ignore[no-redef,attr-defined] + Undefined as Undefined, + ) + from pydantic.fields import ( # type: ignore[no-redef, attr-defined] + UndefinedType as UndefinedType, # noqa: F401 + ) + from pydantic.networks import ( # type: ignore[no-redef] + MultiHostDsn as MultiHostUrl, # noqa: F401 + ) + from pydantic.schema import ( + field_schema, + get_flat_models_from_fields, + get_model_name_map, + model_process_schema, + ) + from pydantic.schema import ( # type: ignore[no-redef] # noqa: F401 + get_annotation_from_field_info as get_annotation_from_field_info, + ) + from pydantic.typing import ( # type: ignore[no-redef] + evaluate_forwardref as evaluate_forwardref, # noqa: F401 + ) + from pydantic.utils import ( # type: ignore[no-redef] + lenient_issubclass as lenient_issubclass, # noqa: F401 + ) + + GetJsonSchemaHandler = Any # type: ignore[assignment,misc] + JsonSchemaValue = Dict[str, Any] # type: ignore[misc] + CoreSchema = Any # type: ignore[assignment,misc] + + sequence_shapes = { + SHAPE_LIST, + SHAPE_SET, + SHAPE_FROZENSET, + SHAPE_TUPLE, + SHAPE_SEQUENCE, + SHAPE_TUPLE_ELLIPSIS, + } + sequence_shape_to_type = { + SHAPE_LIST: list, + SHAPE_SET: set, + SHAPE_TUPLE: tuple, + SHAPE_SEQUENCE: list, + SHAPE_TUPLE_ELLIPSIS: list, + } + + @dataclass + class GenerateJsonSchema: # type: ignore[no-redef] + ref_template: str + + class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] + pass + + def general_plain_validator_function( # type: ignore[misc] + function: Callable[..., Any], + *, + ref: Union[str, None] = None, + metadata: Any = None, + serialization: Any = None, + ) -> Any: + return {} + + def get_model_definitions( + *, + flat_models: Set[Union[Type[BaseModel], Type[Enum]]], + model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + ) -> Dict[str, Any]: + definitions: Dict[str, Dict[str, Any]] = {} + for model in flat_models: + m_schema, m_definitions, m_nested_models = model_process_schema( + model, model_name_map=model_name_map, ref_prefix=REF_PREFIX + ) + definitions.update(m_definitions) + model_name = model_name_map[model] + if "description" in m_schema: + m_schema["description"] = m_schema["description"].split("\f")[0] + definitions[model_name] = m_schema + return definitions + + def is_pv1_scalar_field(field: ModelField) -> bool: + from fastapi import params + + field_info = field.field_info + if not ( + field.shape == SHAPE_SINGLETON # type: ignore[attr-defined] + and not lenient_issubclass(field.type_, BaseModel) + and not lenient_issubclass(field.type_, dict) + and not field_annotation_is_sequence(field.type_) + and not is_dataclass(field.type_) + and not isinstance(field_info, params.Body) + ): + return False + if field.sub_fields: # type: ignore[attr-defined] + if not all( + is_pv1_scalar_field(f) + for f in field.sub_fields # type: ignore[attr-defined] + ): + return False + return True + + def is_pv1_scalar_sequence_field(field: ModelField) -> bool: + if (field.shape in sequence_shapes) and not lenient_issubclass( # type: ignore[attr-defined] + field.type_, BaseModel + ): + if field.sub_fields is not None: # type: ignore[attr-defined] + for sub_field in field.sub_fields: # type: ignore[attr-defined] + if not is_pv1_scalar_field(sub_field): + return False + return True + if _annotation_is_sequence(field.type_): + return True + return False + + def _normalize_errors(errors: Sequence[Any]) -> List[Dict[str, Any]]: + use_errors: List[Any] = [] + for error in errors: + if isinstance(error, ErrorWrapper): + new_errors = ValidationError( # type: ignore[call-arg] + errors=[error], model=RequestErrorModel + ).errors() + use_errors.extend(new_errors) + elif isinstance(error, list): + use_errors.extend(_normalize_errors(error)) + else: + use_errors.append(error) + return use_errors + + def _model_rebuild(model: Type[BaseModel]) -> None: + model.update_forward_refs() + + def _model_dump( + model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any + ) -> Any: + return model.dict(**kwargs) + + def _get_model_config(model: BaseModel) -> Any: + return model.__config__ # type: ignore[attr-defined] + + def get_schema_from_model_field( + *, + field: ModelField, + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + ) -> Dict[str, Any]: + # This expects that GenerateJsonSchema was already used to generate the definitions + return field_schema( # type: ignore[no-any-return] + field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + )[0] + + def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: + models = get_flat_models_from_fields(fields, known_models=set()) + return get_model_name_map(models) # type: ignore[no-any-return] + + def get_definitions( + *, + fields: List[ModelField], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + ) -> Tuple[ + Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], + Dict[str, Dict[str, Any]], + ]: + models = get_flat_models_from_fields(fields, known_models=set()) + return {}, get_model_definitions( + flat_models=models, model_name_map=model_name_map + ) + + def is_scalar_field(field: ModelField) -> bool: + return is_pv1_scalar_field(field) + + def is_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes or _annotation_is_sequence(field.type_) # type: ignore[attr-defined] + + def is_scalar_sequence_field(field: ModelField) -> bool: + return is_pv1_scalar_sequence_field(field) + + def is_bytes_field(field: ModelField) -> bool: + return lenient_issubclass(field.type_, bytes) + + def is_bytes_sequence_field(field: ModelField) -> bool: + return field.shape in sequence_shapes and lenient_issubclass(field.type_, bytes) # type: ignore[attr-defined] + + def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: + return copy(field_info) + + def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: + return sequence_shape_to_type[field.shape](value) # type: ignore[no-any-return,attr-defined] + + def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]: + missing_field_error = ErrorWrapper(MissingError(), loc=loc) # type: ignore[call-arg] + new_error = ValidationError([missing_field_error], RequestErrorModel) + return new_error.errors()[0] # type: ignore[return-value] + + def create_body_model( + *, fields: Sequence[ModelField], model_name: str + ) -> Type[BaseModel]: + BodyModel = create_model(model_name) + for f in fields: + BodyModel.__fields__[f.name] = f # type: ignore[index] + return BodyModel + + +def _regenerate_error_with_loc( + *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...] +) -> List[Dict[str, Any]]: + updated_loc_errors: List[Any] = [ + {**err, "loc": loc_prefix + err.get("loc", ())} + for err in _normalize_errors(errors) + ] + + return updated_loc_errors + + +def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + if lenient_issubclass(annotation, (str, bytes)): + return False + return lenient_issubclass(annotation, sequence_types) + + +def field_annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool: + return _annotation_is_sequence(annotation) or _annotation_is_sequence( + get_origin(annotation) + ) + + +def value_is_sequence(value: Any) -> bool: + return isinstance(value, sequence_types) and not isinstance(value, (str, bytes)) # type: ignore[arg-type] + + +def _annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + return ( + lenient_issubclass(annotation, (BaseModel, Mapping, UploadFile)) + or _annotation_is_sequence(annotation) + or is_dataclass(annotation) + ) + + +def field_annotation_is_complex(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + return any(field_annotation_is_complex(arg) for arg in get_args(annotation)) + + return ( + _annotation_is_complex(annotation) + or _annotation_is_complex(origin) + or hasattr(origin, "__pydantic_core_schema__") + or hasattr(origin, "__get_pydantic_core_schema__") + ) + + +def field_annotation_is_scalar(annotation: Any) -> bool: + # handle Ellipsis here to make tuple[int, ...] work nicely + return annotation is Ellipsis or not field_annotation_is_complex(annotation) + + +def field_annotation_is_scalar_sequence(annotation: Union[Type[Any], None]) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one_scalar_sequence = False + for arg in get_args(annotation): + if field_annotation_is_scalar_sequence(arg): + at_least_one_scalar_sequence = True + continue + elif not field_annotation_is_scalar(arg): + return False + return at_least_one_scalar_sequence + return field_annotation_is_sequence(annotation) and all( + field_annotation_is_scalar(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_bytes_or_nonable_bytes_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, bytes): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, bytes): + return True + return False + + +def is_uploadfile_or_nonable_uploadfile_annotation(annotation: Any) -> bool: + if lenient_issubclass(annotation, UploadFile): + return True + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + for arg in get_args(annotation): + if lenient_issubclass(arg, UploadFile): + return True + return False + + +def is_bytes_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_bytes_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_bytes_or_nonable_bytes_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) + + +def is_uploadfile_sequence_annotation(annotation: Any) -> bool: + origin = get_origin(annotation) + if origin is Union or origin is UnionType: + at_least_one = False + for arg in get_args(annotation): + if is_uploadfile_sequence_annotation(arg): + at_least_one = True + continue + return at_least_one + return field_annotation_is_sequence(annotation) and all( + is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation) + for sub_annotation in get_args(annotation) + ) diff --git a/fastapi/applications.py b/fastapi/applications.py index 88f861c1e..e32cfa03d 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -15,7 +15,6 @@ from typing import ( from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder -from fastapi.encoders import DictIntStrAny, SetIntStr from fastapi.exception_handlers import ( http_exception_handler, request_validation_exception_handler, @@ -31,7 +30,7 @@ from fastapi.openapi.docs import ( ) from fastapi.openapi.utils import get_openapi from fastapi.params import Depends -from fastapi.types import DecoratedCallable +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import generate_unique_id from starlette.applications import Starlette from starlette.datastructures import State @@ -305,8 +304,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -363,8 +362,8 @@ class FastAPI(Starlette): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -484,8 +483,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -539,8 +538,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -594,8 +593,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -649,8 +648,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -704,8 +703,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -759,8 +758,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -814,8 +813,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -869,8 +868,8 @@ class FastAPI(Starlette): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b20a25ab6..3c96c56c7 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,5 +1,12 @@ -from typing import Any, Callable, Dict, Iterable, Type, TypeVar +from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + general_plain_validator_function, +) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 from starlette.datastructures import FormData as FormData # noqa: F401 @@ -21,8 +28,28 @@ class UploadFile(StarletteUploadFile): return v @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update({"type": "string", "format": "binary"}) + def _validate(cls, __input_value: Any, _: Any) -> "UploadFile": + if not isinstance(__input_value, StarletteUploadFile): + raise ValueError(f"Expected UploadFile, received: {type(__input_value)}") + return cast(UploadFile, __input_value) + + if not PYDANTIC_V2: + + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update({"type": "string", "format": "binary"}) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "binary"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return general_plain_validator_function(cls._validate) class DefaultPlaceholder: diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py index 443590b9c..61ef00638 100644 --- a/fastapi/dependencies/models.py +++ b/fastapi/dependencies/models.py @@ -1,7 +1,7 @@ from typing import Any, Callable, List, Optional, Sequence +from fastapi._compat import ModelField from fastapi.security.base import SecurityBase -from pydantic.fields import ModelField class SecurityRequirement: diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index f131001ce..e2915268c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,7 +1,6 @@ -import dataclasses import inspect from contextlib import contextmanager -from copy import copy, deepcopy +from copy import deepcopy from typing import ( Any, Callable, @@ -20,6 +19,31 @@ from typing import ( import anyio from fastapi import params +from fastapi._compat import ( + PYDANTIC_V2, + ErrorWrapper, + ModelField, + Required, + Undefined, + _regenerate_error_with_loc, + copy_field_info, + create_body_model, + evaluate_forwardref, + field_annotation_is_scalar, + get_annotation_from_field_info, + get_missing_field_error, + is_bytes_field, + is_bytes_sequence_field, + is_scalar_field, + is_scalar_sequence_field, + is_sequence_field, + is_uploadfile_or_nonable_uploadfile_annotation, + is_uploadfile_sequence_annotation, + lenient_issubclass, + sequence_types, + serialize_sequence_value, + value_is_sequence, +) from fastapi.concurrency import ( AsyncExitStack, asynccontextmanager, @@ -31,50 +55,14 @@ from fastapi.security.base import SecurityBase from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_response_field, get_path_param_names -from pydantic import BaseModel, create_model -from pydantic.error_wrappers import ErrorWrapper -from pydantic.errors import MissingError -from pydantic.fields import ( - SHAPE_FROZENSET, - SHAPE_LIST, - SHAPE_SEQUENCE, - SHAPE_SET, - SHAPE_SINGLETON, - SHAPE_TUPLE, - SHAPE_TUPLE_ELLIPSIS, - FieldInfo, - ModelField, - Required, - Undefined, -) -from pydantic.schema import get_annotation_from_field_info -from pydantic.typing import evaluate_forwardref, get_args, get_origin -from pydantic.utils import lenient_issubclass +from pydantic.fields import FieldInfo from starlette.background import BackgroundTasks from starlette.concurrency import run_in_threadpool from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated - -sequence_shapes = { - SHAPE_LIST, - SHAPE_SET, - SHAPE_FROZENSET, - SHAPE_TUPLE, - SHAPE_SEQUENCE, - SHAPE_TUPLE_ELLIPSIS, -} -sequence_types = (list, set, tuple) -sequence_shape_to_type = { - SHAPE_LIST: list, - SHAPE_SET: set, - SHAPE_TUPLE: tuple, - SHAPE_SEQUENCE: list, - SHAPE_TUPLE_ELLIPSIS: list, -} - +from typing_extensions import Annotated, get_args, get_origin multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -216,36 +204,6 @@ def get_flat_params(dependant: Dependant) -> List[ModelField]: ) -def is_scalar_field(field: ModelField) -> bool: - field_info = field.field_info - if not ( - field.shape == SHAPE_SINGLETON - and not lenient_issubclass(field.type_, BaseModel) - and not lenient_issubclass(field.type_, sequence_types + (dict,)) - and not dataclasses.is_dataclass(field.type_) - and not isinstance(field_info, params.Body) - ): - return False - if field.sub_fields: - if not all(is_scalar_field(f) for f in field.sub_fields): - return False - return True - - -def is_scalar_sequence_field(field: ModelField) -> bool: - if (field.shape in sequence_shapes) and not lenient_issubclass( - field.type_, BaseModel - ): - if field.sub_fields is not None: - for sub_field in field.sub_fields: - if not is_scalar_field(sub_field): - return False - return True - if lenient_issubclass(field.type_, sequence_types): - return True - return False - - def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature: signature = inspect.signature(call) globalns = getattr(call, "__globals__", {}) @@ -364,12 +322,11 @@ def analyze_param( is_path_param: bool, ) -> Tuple[Any, Optional[params.Depends], Optional[ModelField]]: field_info = None - used_default_field_info = False depends = None type_annotation: Any = Any if ( annotation is not inspect.Signature.empty - and get_origin(annotation) is Annotated # type: ignore[comparison-overlap] + and get_origin(annotation) is Annotated ): annotated_args = get_args(annotation) type_annotation = annotated_args[0] @@ -384,7 +341,9 @@ def analyze_param( fastapi_annotation = next(iter(fastapi_annotations), None) if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. - field_info = copy(fastapi_annotation) + field_info = copy_field_info( + field_info=fastapi_annotation, annotation=annotation + ) assert field_info.default is Undefined or field_info.default is Required, ( f"`{field_info.__class__.__name__}` default value cannot be set in" f" `Annotated` for {param_name!r}. Set the default value with `=` instead." @@ -415,6 +374,8 @@ def analyze_param( f" together for {param_name!r}" ) field_info = value + if PYDANTIC_V2: + field_info.annotation = type_annotation if depends is not None and depends.dependency is None: depends.dependency = type_annotation @@ -433,10 +394,15 @@ def analyze_param( # We might check here that `default_value is Required`, but the fact is that the same # parameter might sometimes be a path parameter and sometimes not. See # `tests/test_infer_param_optionality.py` for an example. - field_info = params.Path() + field_info = params.Path(annotation=type_annotation) + elif is_uploadfile_or_nonable_uploadfile_annotation( + type_annotation + ) or is_uploadfile_sequence_annotation(type_annotation): + field_info = params.File(annotation=type_annotation, default=default_value) + elif not field_annotation_is_scalar(annotation=type_annotation): + field_info = params.Body(annotation=type_annotation, default=default_value) else: - field_info = params.Query(default=default_value) - used_default_field_info = True + field_info = params.Query(annotation=type_annotation, default=default_value) field = None if field_info is not None: @@ -450,8 +416,8 @@ def analyze_param( and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - annotation = get_annotation_from_field_info( - annotation if annotation is not inspect.Signature.empty else Any, + use_annotation = get_annotation_from_field_info( + type_annotation, field_info, param_name, ) @@ -459,19 +425,15 @@ def analyze_param( alias = param_name.replace("_", "-") else: alias = field_info.alias or param_name + field_info.alias = alias field = create_response_field( name=param_name, - type_=annotation, + type_=use_annotation, default=field_info.default, alias=alias, required=field_info.default in (Required, Undefined), field_info=field_info, ) - if used_default_field_info: - if lenient_issubclass(field.type_, UploadFile): - field.field_info = params.File(field_info.default) - elif not is_scalar_field(field=field): - field.field_info = params.Body(field_info.default) return type_annotation, depends, field @@ -554,13 +516,13 @@ async def solve_dependencies( dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, ) -> Tuple[ Dict[str, Any], - List[ErrorWrapper], + List[Any], Optional[BackgroundTasks], Response, Dict[Tuple[Callable[..., Any], Tuple[str]], Any], ]: values: Dict[str, Any] = {} - errors: List[ErrorWrapper] = [] + errors: List[Any] = [] if response is None: response = Response() del response.headers["content-length"] @@ -674,7 +636,7 @@ async def solve_dependencies( def request_params_to_args( required_params: Sequence[ModelField], received_params: Union[Mapping[str, Any], QueryParams, Headers], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Any]]: values = {} errors = [] for field in required_params: @@ -688,23 +650,19 @@ def request_params_to_args( assert isinstance( field_info, params.Param ), "Params must be subclasses of Param" + loc = (field_info.in_.value, field.alias) if value is None: if field.required: - errors.append( - ErrorWrapper( - MissingError(), loc=(field_info.in_.value, field.alias) - ) - ) + errors.append(get_missing_field_error(loc=loc)) else: values[field.name] = deepcopy(field.default) continue - v_, errors_ = field.validate( - value, values, loc=(field_info.in_.value, field.alias) - ) + v_, errors_ = field.validate(value, values, loc=loc) if isinstance(errors_, ErrorWrapper): errors.append(errors_) elif isinstance(errors_, list): - errors.extend(errors_) + new_errors = _regenerate_error_with_loc(errors=errors_, loc_prefix=()) + errors.extend(new_errors) else: values[field.name] = v_ return values, errors @@ -713,9 +671,9 @@ def request_params_to_args( async def request_body_to_args( required_params: List[ModelField], received_body: Optional[Union[Dict[str, Any], FormData]], -) -> Tuple[Dict[str, Any], List[ErrorWrapper]]: +) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: values = {} - errors = [] + errors: List[Dict[str, Any]] = [] if required_params: field = required_params[0] field_info = field.field_info @@ -733,9 +691,7 @@ async def request_body_to_args( value: Optional[Any] = None if received_body is not None: - if ( - field.shape in sequence_shapes or field.type_ in sequence_types - ) and isinstance(received_body, FormData): + if (is_sequence_field(field)) and isinstance(received_body, FormData): value = received_body.getlist(field.alias) else: try: @@ -748,7 +704,7 @@ async def request_body_to_args( or (isinstance(field_info, params.Form) and value == "") or ( isinstance(field_info, params.Form) - and field.shape in sequence_shapes + and is_sequence_field(field) and len(value) == 0 ) ): @@ -759,16 +715,17 @@ async def request_body_to_args( continue if ( isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) + and is_bytes_field(field) and isinstance(value, UploadFile) ): value = await value.read() elif ( - field.shape in sequence_shapes + is_bytes_sequence_field(field) and isinstance(field_info, params.File) - and lenient_issubclass(field.type_, bytes) - and isinstance(value, sequence_types) + and value_is_sequence(value) ): + # For types + assert isinstance(value, sequence_types) # type: ignore[arg-type] results: List[Union[bytes, str]] = [] async def process_fn( @@ -780,24 +737,19 @@ async def request_body_to_args( async with anyio.create_task_group() as tg: for sub_value in value: tg.start_soon(process_fn, sub_value.read) - value = sequence_shape_to_type[field.shape](results) + value = serialize_sequence_value(field=field, value=results) v_, errors_ = field.validate(value, values, loc=loc) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) else: values[field.name] = v_ return values, errors -def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper: - missing_field_error = ErrorWrapper(MissingError(), loc=loc) - return missing_field_error - - def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: flat_dependant = get_flat_dependant(dependant) if not flat_dependant.body_params: @@ -815,12 +767,16 @@ def get_body_field(*, dependant: Dependant, name: str) -> Optional[ModelField]: for param in flat_dependant.body_params: setattr(param.field_info, "embed", True) # noqa: B010 model_name = "Body_" + name - BodyModel: Type[BaseModel] = create_model(model_name) - for f in flat_dependant.body_params: - BodyModel.__fields__[f.name] = f + BodyModel = create_body_model( + fields=flat_dependant.body_params, model_name=model_name + ) required = any(True for f in flat_dependant.body_params if f.required) - - BodyFieldInfo_kwargs: Dict[str, Any] = {"default": None} + BodyFieldInfo_kwargs: Dict[str, Any] = { + "annotation": BodyModel, + "alias": "body", + } + if not required: + BodyFieldInfo_kwargs["default"] = None if any(isinstance(f.field_info, params.File) for f in flat_dependant.body_params): BodyFieldInfo: Type[params.Body] = params.File elif any(isinstance(f.field_info, params.Form) for f in flat_dependant.body_params): diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 94f41bfa1..b542749f2 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -1,15 +1,87 @@ import dataclasses +import datetime from collections import defaultdict, deque +from decimal import Decimal from enum import Enum -from pathlib import PurePath +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, +) +from pathlib import Path, PurePath +from re import Pattern from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union +from uuid import UUID +from fastapi.types import IncEx from pydantic import BaseModel -from pydantic.json import ENCODERS_BY_TYPE +from pydantic.color import Color +from pydantic.networks import NameEmail +from pydantic.types import SecretBytes, SecretStr -SetIntStr = Set[Union[int, str]] -DictIntStrAny = Dict[Union[int, str], Any] +from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump + + +# Taken from Pydantic v1 as is +def isoformat(o: Union[datetime.date, datetime.time]) -> str: + return o.isoformat() + + +# Taken from Pydantic v1 as is +# TODO: pv2 should this return strings instead? +def decimal_encoder(dec_value: Decimal) -> Union[int, float]: + """ + Encodes a Decimal as int of there's no exponent, otherwise float + + This is useful when we use ConstrainedDecimal to represent Numeric(x,0) + where a integer (but not int typed) is used. Encoding this as a float + results in failed round-tripping between encode and parse. + Our Id type is a prime example of this. + + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + """ + if dec_value.as_tuple().exponent >= 0: # type: ignore[operator] + return int(dec_value) + else: + return float(dec_value) + + +ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { + bytes: lambda o: o.decode(), + Color: str, + datetime.date: isoformat, + datetime.datetime: isoformat, + datetime.time: isoformat, + datetime.timedelta: lambda td: td.total_seconds(), + Decimal: decimal_encoder, + Enum: lambda o: o.value, + frozenset: list, + deque: list, + GeneratorType: list, + IPv4Address: str, + IPv4Interface: str, + IPv4Network: str, + IPv6Address: str, + IPv6Interface: str, + IPv6Network: str, + NameEmail: str, + Path: str, + Pattern: lambda o: o.pattern, + SecretBytes: str, + SecretStr: str, + set: list, + UUID: str, + Url: str, + MultiHostUrl: str, +} def generate_encoders_by_class_tuples( @@ -28,8 +100,8 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) def jsonable_encoder( obj: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + include: Optional[IncEx] = None, + exclude: Optional[IncEx] = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -50,10 +122,15 @@ def jsonable_encoder( if exclude is not None and not isinstance(exclude, (set, dict)): exclude = set(exclude) if isinstance(obj, BaseModel): - encoder = getattr(obj.__config__, "json_encoders", {}) - if custom_encoder: - encoder.update(custom_encoder) - obj_dict = obj.dict( + # TODO: remove when deprecating Pydantic v1 + encoders: Dict[Any, Any] = {} + if not PYDANTIC_V2: + encoders = getattr(obj.__config__, "json_encoders", {}) # type: ignore[attr-defined] + if custom_encoder: + encoders.update(custom_encoder) + obj_dict = _model_dump( + obj, + mode="json", include=include, exclude=exclude, by_alias=by_alias, @@ -67,7 +144,8 @@ def jsonable_encoder( obj_dict, exclude_none=exclude_none, exclude_defaults=exclude_defaults, - custom_encoder=encoder, + # TODO: remove when deprecating Pydantic v1 + custom_encoder=encoders, sqlalchemy_safe=sqlalchemy_safe, ) if dataclasses.is_dataclass(obj): diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index cac5330a2..c1692f396 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,7 +1,6 @@ from typing import Any, Dict, Optional, Sequence, Type -from pydantic import BaseModel, ValidationError, create_model -from pydantic.error_wrappers import ErrorList +from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 @@ -26,12 +25,25 @@ class FastAPIError(RuntimeError): """ -class RequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None: +class ValidationException(Exception): + def __init__(self, errors: Sequence[Any]) -> None: + self._errors = errors + + def errors(self) -> Sequence[Any]: + return self._errors + + +class RequestValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) self.body = body - super().__init__(errors, RequestErrorModel) -class WebSocketRequestValidationError(ValidationError): - def __init__(self, errors: Sequence[ErrorList]) -> None: - super().__init__(errors, WebSocketErrorModel) +class WebSocketRequestValidationError(ValidationException): + pass + + +class ResponseValidationError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) + self.body = body diff --git a/fastapi/openapi/constants.py b/fastapi/openapi/constants.py index 1897ad750..d724ee3cf 100644 --- a/fastapi/openapi/constants.py +++ b/fastapi/openapi/constants.py @@ -1,2 +1,3 @@ METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"} REF_PREFIX = "#/components/schemas/" +REF_TEMPLATE = "#/components/schemas/{model}" diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index a2ea53607..2268dd229 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -1,13 +1,21 @@ from enum import Enum -from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union +from fastapi._compat import ( + PYDANTIC_V2, + CoreSchema, + GetJsonSchemaHandler, + JsonSchemaValue, + _model_rebuild, + general_plain_validator_function, +) from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field from typing_extensions import Annotated, Literal from typing_extensions import deprecated as typing_deprecated try: - import email_validator # type: ignore + import email_validator assert email_validator # make autoflake ignore the unused import from pydantic import EmailStr @@ -26,14 +34,39 @@ except ImportError: # pragma: no cover ) return str(v) + @classmethod + def _validate(cls, __input_value: Any, _: Any) -> str: + logger.warning( + "email-validator not installed, email fields will be treated as str.\n" + "To install, run: pip install email-validator" + ) + return str(__input_value) + + @classmethod + def __get_pydantic_json_schema__( + cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler + ) -> JsonSchemaValue: + return {"type": "string", "format": "email"} + + @classmethod + def __get_pydantic_core_schema__( + cls, source: Type[Any], handler: Callable[[Any], CoreSchema] + ) -> CoreSchema: + return general_plain_validator_function(cls._validate) + class Contact(BaseModel): name: Optional[str] = None url: Optional[AnyUrl] = None email: Optional[EmailStr] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class License(BaseModel): @@ -41,8 +74,13 @@ class License(BaseModel): identifier: Optional[str] = None url: Optional[AnyUrl] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Info(BaseModel): @@ -54,17 +92,27 @@ class Info(BaseModel): license: Optional[License] = None version: str - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ServerVariable(BaseModel): - enum: Annotated[Optional[List[str]], Field(min_items=1)] = None + enum: Annotated[Optional[List[str]], Field(min_length=1)] = None default: str description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Server(BaseModel): @@ -72,8 +120,13 @@ class Server(BaseModel): description: Optional[str] = None variables: Optional[Dict[str, ServerVariable]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Reference(BaseModel): @@ -92,16 +145,26 @@ class XML(BaseModel): attribute: Optional[bool] = None wrapped: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ExternalDocumentation(BaseModel): description: Optional[str] = None url: AnyUrl - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Schema(BaseModel): @@ -190,8 +253,13 @@ class Schema(BaseModel): ), ] = None - class Config: - extra: str = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" # Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents @@ -205,8 +273,13 @@ class Example(BaseModel): value: Optional[Any] = None externalValue: Optional[AnyUrl] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterInType(Enum): @@ -223,8 +296,13 @@ class Encoding(BaseModel): explode: Optional[bool] = None allowReserved: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class MediaType(BaseModel): @@ -233,8 +311,13 @@ class MediaType(BaseModel): examples: Optional[Dict[str, Union[Example, Reference]]] = None encoding: Optional[Dict[str, Encoding]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class ParameterBase(BaseModel): @@ -251,8 +334,13 @@ class ParameterBase(BaseModel): # Serialization rules for more complex scenarios content: Optional[Dict[str, MediaType]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Parameter(ParameterBase): @@ -269,8 +357,13 @@ class RequestBody(BaseModel): content: Dict[str, MediaType] required: Optional[bool] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Link(BaseModel): @@ -281,8 +374,13 @@ class Link(BaseModel): description: Optional[str] = None server: Optional[Server] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Response(BaseModel): @@ -291,8 +389,13 @@ class Response(BaseModel): content: Optional[Dict[str, MediaType]] = None links: Optional[Dict[str, Union[Link, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Operation(BaseModel): @@ -310,8 +413,13 @@ class Operation(BaseModel): security: Optional[List[Dict[str, List[str]]]] = None servers: Optional[List[Server]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class PathItem(BaseModel): @@ -329,8 +437,13 @@ class PathItem(BaseModel): servers: Optional[List[Server]] = None parameters: Optional[List[Union[Parameter, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class SecuritySchemeType(Enum): @@ -344,8 +457,13 @@ class SecurityBase(BaseModel): type_: SecuritySchemeType = Field(alias="type") description: Optional[str] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class APIKeyIn(Enum): @@ -374,8 +492,13 @@ class OAuthFlow(BaseModel): refreshUrl: Optional[str] = None scopes: Dict[str, str] = {} - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuthFlowImplicit(OAuthFlow): @@ -401,8 +524,13 @@ class OAuthFlows(BaseModel): clientCredentials: Optional[OAuthFlowClientCredentials] = None authorizationCode: Optional[OAuthFlowAuthorizationCode] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OAuth2(SecurityBase): @@ -433,8 +561,13 @@ class Components(BaseModel): callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class Tag(BaseModel): @@ -442,8 +575,13 @@ class Tag(BaseModel): description: Optional[str] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" class OpenAPI(BaseModel): @@ -459,10 +597,15 @@ class OpenAPI(BaseModel): tags: Optional[List[Tag]] = None externalDocs: Optional[ExternalDocumentation] = None - class Config: - extra = "allow" + if PYDANTIC_V2: + model_config = {"extra": "allow"} + + else: + + class Config: + extra = "allow" -Schema.update_forward_refs() -Operation.update_forward_refs() -Encoding.update_forward_refs() +_model_rebuild(Schema) +_model_rebuild(Operation) +_model_rebuild(Encoding) diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 609fe4389..e295361e6 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -1,35 +1,37 @@ import http.client import inspect import warnings -from enum import Enum from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast from fastapi import routing +from fastapi._compat import ( + GenerateJsonSchema, + JsonSchemaValue, + ModelField, + Undefined, + get_compat_model_name_map, + get_definitions, + get_schema_from_model_field, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import get_flat_dependant, get_flat_params from fastapi.encoders import jsonable_encoder -from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX +from fastapi.openapi.constants import METHODS_WITH_BODY, REF_PREFIX, REF_TEMPLATE from fastapi.openapi.models import OpenAPI from fastapi.params import Body, Param from fastapi.responses import Response +from fastapi.types import ModelNameMap from fastapi.utils import ( deep_dict_update, generate_operation_id_for_path, - get_model_definitions, is_body_allowed_for_status_code, ) -from pydantic import BaseModel -from pydantic.fields import ModelField, Undefined -from pydantic.schema import ( - field_schema, - get_flat_models_from_fields, - get_model_name_map, -) -from pydantic.utils import lenient_issubclass from starlette.responses import JSONResponse from starlette.routing import BaseRoute from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY +from typing_extensions import Literal validation_error_definition = { "title": "ValidationError", @@ -88,7 +90,11 @@ def get_openapi_security_definitions( def get_openapi_operation_parameters( *, all_route_params: Sequence[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: @@ -96,13 +102,17 @@ def get_openapi_operation_parameters( field_info = cast(Param, field_info) if not field_info.include_in_schema: continue + param_schema = get_schema_from_model_field( + field=param, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, + ) parameter = { "name": param.alias, "in": field_info.in_.value, "required": param.required, - "schema": field_schema( - param, model_name_map=model_name_map, ref_prefix=REF_PREFIX - )[0], + "schema": param_schema, } if field_info.description: parameter["description"] = field_info.description @@ -117,13 +127,20 @@ def get_openapi_operation_parameters( def get_openapi_operation_request_body( *, body_field: Optional[ModelField], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> Optional[Dict[str, Any]]: if not body_field: return None assert isinstance(body_field, ModelField) - body_schema, _, _ = field_schema( - body_field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + body_schema = get_schema_from_model_field( + field=body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) field_info = cast(Body, body_field.field_info) request_media_type = field_info.media_type @@ -186,7 +203,14 @@ def get_openapi_operation_metadata( def get_openapi_path( - *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str] + *, + route: routing.APIRoute, + operation_ids: Set[str], + schema_generator: GenerateJsonSchema, + model_name_map: ModelNameMap, + field_mapping: Dict[ + Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue + ], ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: path = {} security_schemes: Dict[str, Any] = {} @@ -214,7 +238,10 @@ def get_openapi_path( security_schemes.update(security_definitions) all_route_params = get_flat_params(route.dependant) operation_parameters = get_openapi_operation_parameters( - all_route_params=all_route_params, model_name_map=model_name_map + all_route_params=all_route_params, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) parameters.extend(operation_parameters) if parameters: @@ -232,7 +259,10 @@ def get_openapi_path( operation["parameters"] = list(all_parameters.values()) if method in METHODS_WITH_BODY: request_body_oai = get_openapi_operation_request_body( - body_field=route.body_field, model_name_map=model_name_map + body_field=route.body_field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if request_body_oai: operation["requestBody"] = request_body_oai @@ -246,8 +276,10 @@ def get_openapi_path( cb_definitions, ) = get_openapi_path( route=callback, - model_name_map=model_name_map, operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) callbacks[callback.name] = {callback.path: cb_path} operation["callbacks"] = callbacks @@ -273,10 +305,11 @@ def get_openapi_path( response_schema = {"type": "string"} if lenient_issubclass(current_response_class, JSONResponse): if route.response_field: - response_schema, _, _ = field_schema( - route.response_field, + response_schema = get_schema_from_model_field( + field=route.response_field, + schema_generator=schema_generator, model_name_map=model_name_map, - ref_prefix=REF_PREFIX, + field_mapping=field_mapping, ) else: response_schema = {} @@ -305,8 +338,11 @@ def get_openapi_path( field = route.response_fields.get(additional_status_code) additional_field_schema: Optional[Dict[str, Any]] = None if field: - additional_field_schema, _, _ = field_schema( - field, model_name_map=model_name_map, ref_prefix=REF_PREFIX + additional_field_schema = get_schema_from_model_field( + field=field, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) media_type = route_response_media_type or "application/json" additional_schema = ( @@ -352,13 +388,13 @@ def get_openapi_path( return path, security_schemes, definitions -def get_flat_models_from_routes( +def get_fields_from_routes( routes: Sequence[BaseRoute], -) -> Set[Union[Type[BaseModel], Type[Enum]]]: +) -> List[ModelField]: body_fields_from_routes: List[ModelField] = [] responses_from_routes: List[ModelField] = [] request_fields_from_routes: List[ModelField] = [] - callback_flat_models: Set[Union[Type[BaseModel], Type[Enum]]] = set() + callback_flat_models: List[ModelField] = [] for route in routes: if getattr(route, "include_in_schema", None) and isinstance( route, routing.APIRoute @@ -373,13 +409,12 @@ def get_flat_models_from_routes( if route.response_fields: responses_from_routes.extend(route.response_fields.values()) if route.callbacks: - callback_flat_models |= get_flat_models_from_routes(route.callbacks) + callback_flat_models.extend(get_fields_from_routes(route.callbacks)) params = get_flat_params(route.dependant) request_fields_from_routes.extend(params) - flat_models = callback_flat_models | get_flat_models_from_fields( - body_fields_from_routes + responses_from_routes + request_fields_from_routes, - known_models=set(), + flat_models = callback_flat_models + list( + body_fields_from_routes + responses_from_routes + request_fields_from_routes ) return flat_models @@ -417,15 +452,22 @@ def get_openapi( paths: Dict[str, Dict[str, Any]] = {} webhook_paths: Dict[str, Dict[str, Any]] = {} operation_ids: Set[str] = set() - flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or [])) - model_name_map = get_model_name_map(flat_models) - definitions = get_model_definitions( - flat_models=flat_models, model_name_map=model_name_map + all_fields = get_fields_from_routes(list(routes or []) + list(webhooks or [])) + model_name_map = get_compat_model_name_map(all_fields) + schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE) + field_mapping, definitions = get_definitions( + fields=all_fields, + schema_generator=schema_generator, + model_name_map=model_name_map, ) for route in routes or []: if isinstance(route, routing.APIRoute): result = get_openapi_path( - route=route, model_name_map=model_name_map, operation_ids=operation_ids + route=route, + operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if result: path, security_schemes, path_definitions = result @@ -441,8 +483,10 @@ def get_openapi( if isinstance(webhook, routing.APIRoute): result = get_openapi_path( route=webhook, - model_name_map=model_name_map, operation_ids=operation_ids, + schema_generator=schema_generator, + model_name_map=model_name_map, + field_mapping=field_mapping, ) if result: path, security_schemes, path_definitions = result diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 2f5818c85..a43afaf31 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,14 +1,22 @@ -from typing import Any, Callable, List, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi import params -from pydantic.fields import Undefined +from fastapi._compat import Undefined from typing_extensions import Annotated, deprecated +_Unset: Any = Undefined + def Path( # noqa: N802 default: Any = ..., *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -17,7 +25,19 @@ def Path( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -25,14 +45,19 @@ def Path( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Path( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -41,11 +66,19 @@ def Path( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -53,7 +86,13 @@ def Path( # noqa: N802 def Query( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -62,7 +101,19 @@ def Query( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -70,14 +121,19 @@ def Query( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Query( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -86,11 +142,19 @@ def Query( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -98,7 +162,13 @@ def Query( # noqa: N802 def Header( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, description: Optional[str] = None, @@ -108,7 +178,19 @@ def Header( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -116,14 +198,19 @@ def Header( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Header( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, convert_underscores=convert_underscores, title=title, description=description, @@ -133,11 +220,19 @@ def Header( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -145,7 +240,13 @@ def Header( # noqa: N802 def Cookie( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -154,7 +255,19 @@ def Cookie( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -162,14 +275,19 @@ def Cookie( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Cookie( default=default, + default_factory=default_factory, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -178,11 +296,19 @@ def Cookie( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, deprecated=deprecated, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -190,9 +316,15 @@ def Cookie( # noqa: N802 def Body( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, embed: bool = False, media_type: str = "application/json", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -201,7 +333,19 @@ def Body( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -209,14 +353,21 @@ def Body( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Body( default=default, + default_factory=default_factory, embed=embed, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -225,9 +376,19 @@ def Body( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -235,8 +396,14 @@ def Body( # noqa: N802 def Form( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -245,7 +412,19 @@ def Form( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -253,13 +432,20 @@ def Form( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.Form( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -268,9 +454,19 @@ def Form( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -278,8 +474,14 @@ def Form( # noqa: N802 def File( # noqa: N802 default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, media_type: str = "multipart/form-data", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -288,7 +490,19 @@ def File( # noqa: N802 le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -296,13 +510,20 @@ def File( # noqa: N802 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ) -> Any: return params.File( default=default, + default_factory=default_factory, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -311,9 +532,19 @@ def File( # noqa: N802 le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, example=example, examples=examples, + deprecated=deprecated, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) diff --git a/fastapi/params.py b/fastapi/params.py index 4069f2cda..30af5713e 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -1,10 +1,14 @@ import warnings from enum import Enum -from typing import Any, Callable, List, Optional, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence, Union -from pydantic.fields import FieldInfo, Undefined +from pydantic.fields import FieldInfo from typing_extensions import Annotated, deprecated +from ._compat import PYDANTIC_V2, Undefined + +_Unset: Any = Undefined + class ParamTypes(Enum): query = "query" @@ -20,7 +24,14 @@ class Param(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -29,7 +40,19 @@ class Param(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -37,25 +60,24 @@ class Param(FieldInfo): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.deprecated = deprecated - if example is not Undefined: + if example is not _Unset: warnings.warn( "`example` has been depreacated, please use `examples` instead", category=DeprecationWarning, - stacklevel=1, + stacklevel=4, ) self.example = example self.include_in_schema = include_in_schema - extra_kwargs = {**extra} - if examples: - extra_kwargs["examples"] = examples - super().__init__( + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -65,9 +87,40 @@ class Param(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, - **extra_kwargs, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been depreacated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -80,7 +133,14 @@ class Path(Param): self, default: Any = ..., *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -89,7 +149,19 @@ class Path(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -97,16 +169,22 @@ class Path(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): assert default is ..., "Path parameters cannot have a default value" self.in_ = self.in_ super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -115,11 +193,19 @@ class Path(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -131,7 +217,14 @@ class Query(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -140,7 +233,19 @@ class Query(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -148,14 +253,20 @@ class Query(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -164,11 +275,19 @@ class Query(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -180,7 +299,14 @@ class Header(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, convert_underscores: bool = True, title: Optional[str] = None, description: Optional[str] = None, @@ -190,7 +316,19 @@ class Header(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -198,15 +336,21 @@ class Header(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.convert_underscores = convert_underscores super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -215,11 +359,19 @@ class Header(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -231,7 +383,14 @@ class Cookie(Param): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -240,7 +399,19 @@ class Cookie(Param): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -248,14 +419,20 @@ class Cookie(Param): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, deprecated: Optional[bool] = None, include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -264,11 +441,19 @@ class Cookie(Param): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, deprecated=deprecated, example=example, examples=examples, include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -278,9 +463,16 @@ class Body(FieldInfo): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, embed: bool = False, media_type: str = "application/json", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -289,7 +481,19 @@ class Body(FieldInfo): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -297,23 +501,26 @@ class Body(FieldInfo): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): self.embed = embed self.media_type = media_type - if example is not Undefined: + self.deprecated = deprecated + if example is not _Unset: warnings.warn( "`example` has been depreacated, please use `examples` instead", category=DeprecationWarning, - stacklevel=1, + stacklevel=4, ) self.example = example - extra_kwargs = {**extra} - if examples is not None: - extra_kwargs["examples"] = examples - super().__init__( + self.include_in_schema = include_in_schema + kwargs = dict( default=default, + default_factory=default_factory, alias=alias, title=title, description=description, @@ -323,9 +530,41 @@ class Body(FieldInfo): le=le, min_length=min_length, max_length=max_length, - regex=regex, - **extra_kwargs, + discriminator=discriminator, + multiple_of=multiple_of, + allow_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + **extra, ) + if examples is not None: + kwargs["examples"] = examples + if regex is not None: + warnings.warn( + "`regex` has been depreacated, please use `pattern` instead", + category=DeprecationWarning, + stacklevel=4, + ) + current_json_schema_extra = json_schema_extra or extra + if PYDANTIC_V2: + kwargs.update( + { + "annotation": annotation, + "alias_priority": alias_priority, + "validation_alias": validation_alias, + "serialization_alias": serialization_alias, + "strict": strict, + "json_schema_extra": current_json_schema_extra, + } + ) + kwargs["pattern"] = pattern or regex + else: + kwargs["regex"] = pattern or regex + kwargs.update(**current_json_schema_extra) + + use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset} + + super().__init__(**use_kwargs) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.default})" @@ -336,8 +575,15 @@ class Form(Body): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "application/x-www-form-urlencoded", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -346,7 +592,19 @@ class Form(Body): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -354,14 +612,22 @@ class Form(Body): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, embed=True, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -370,9 +636,19 @@ class Form(Body): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) @@ -382,8 +658,15 @@ class File(Form): self, default: Any = Undefined, *, + default_factory: Union[Callable[[], Any], None] = _Unset, + annotation: Optional[Any] = None, media_type: str = "multipart/form-data", alias: Optional[str] = None, + alias_priority: Union[int, None] = _Unset, + # TODO: update when deprecating Pydantic v1, import these types + # validation_alias: str | AliasPath | AliasChoices | None + validation_alias: Union[str, None] = None, + serialization_alias: Union[str, None] = None, title: Optional[str] = None, description: Optional[str] = None, gt: Optional[float] = None, @@ -392,7 +675,19 @@ class File(Form): le: Optional[float] = None, min_length: Optional[int] = None, max_length: Optional[int] = None, - regex: Optional[str] = None, + pattern: Optional[str] = None, + regex: Annotated[ + Optional[str], + deprecated( + "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." + ), + ] = None, + discriminator: Union[str, None] = None, + strict: Union[bool, None] = _Unset, + multiple_of: Union[float, None] = _Unset, + allow_inf_nan: Union[bool, None] = _Unset, + max_digits: Union[int, None] = _Unset, + decimal_places: Union[int, None] = _Unset, examples: Optional[List[Any]] = None, example: Annotated[ Optional[Any], @@ -400,13 +695,21 @@ class File(Form): "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, " "although still supported. Use examples instead." ), - ] = Undefined, + ] = _Unset, + deprecated: Optional[bool] = None, + include_in_schema: bool = True, + json_schema_extra: Union[Dict[str, Any], None] = None, **extra: Any, ): super().__init__( default=default, + default_factory=default_factory, + annotation=annotation, media_type=media_type, alias=alias, + alias_priority=alias_priority, + validation_alias=validation_alias, + serialization_alias=serialization_alias, title=title, description=description, gt=gt, @@ -415,9 +718,19 @@ class File(Form): le=le, min_length=min_length, max_length=max_length, + pattern=pattern, regex=regex, + discriminator=discriminator, + strict=strict, + multiple_of=multiple_of, + allow_inf_nan=allow_inf_nan, + max_digits=max_digits, + decimal_places=decimal_places, + deprecated=deprecated, example=example, examples=examples, + include_in_schema=include_in_schema, + json_schema_extra=json_schema_extra, **extra, ) diff --git a/fastapi/routing.py b/fastapi/routing.py index ec8af99b3..d8ff0579c 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -20,6 +20,14 @@ from typing import ( ) from fastapi import params +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + _model_dump, + _normalize_errors, + lenient_issubclass, +) from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.dependencies.models import Dependant from fastapi.dependencies.utils import ( @@ -29,13 +37,14 @@ from fastapi.dependencies.utils import ( get_typed_return_annotation, solve_dependencies, ) -from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder +from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( FastAPIError, RequestValidationError, + ResponseValidationError, WebSocketRequestValidationError, ) -from fastapi.types import DecoratedCallable +from fastapi.types import DecoratedCallable, IncEx from fastapi.utils import ( create_cloned_field, create_response_field, @@ -44,9 +53,6 @@ from fastapi.utils import ( is_body_allowed_for_status_code, ) from pydantic import BaseModel -from pydantic.error_wrappers import ErrorWrapper, ValidationError -from pydantic.fields import ModelField, Undefined -from pydantic.utils import lenient_issubclass from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException @@ -73,14 +79,15 @@ 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) + read_with_orm_mode = getattr(_get_model_config(res), "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( + return _model_dump( + res, by_alias=True, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, @@ -115,8 +122,8 @@ async def serialize_response( *, field: Optional[ModelField] = None, response_content: Any, - include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + include: Optional[IncEx] = None, + exclude: Optional[IncEx] = None, by_alias: bool = True, exclude_unset: bool = False, exclude_defaults: bool = False, @@ -125,24 +132,40 @@ async def serialize_response( ) -> Any: if field: errors = [] - response_content = _prepare_response_content( - response_content, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) + if not hasattr(field, "serialize"): + # pydantic v1 + response_content = _prepare_response_content( + response_content, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) if is_coroutine: value, errors_ = field.validate(response_content, {}, loc=("response",)) else: value, errors_ = await run_in_threadpool( field.validate, response_content, {}, loc=("response",) ) - if isinstance(errors_, ErrorWrapper): - errors.append(errors_) - elif isinstance(errors_, list): + if isinstance(errors_, list): errors.extend(errors_) + elif errors_: + errors.append(errors_) if errors: - raise ValidationError(errors, field.type_) + raise ResponseValidationError( + errors=_normalize_errors(errors), body=response_content + ) + + if hasattr(field, "serialize"): + return field.serialize( + value, + include=include, + exclude=exclude, + by_alias=by_alias, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + return jsonable_encoder( value, include=include, @@ -175,8 +198,8 @@ def get_request_handler( status_code: Optional[int] = None, response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse), response_field: Optional[ModelField] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -220,7 +243,16 @@ def get_request_handler( body = body_bytes except json.JSONDecodeError as e: raise RequestValidationError( - [ErrorWrapper(e, ("body", e.pos))], body=e.doc + [ + { + "type": "json_invalid", + "loc": ("body", e.pos), + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": e.msg}, + } + ], + body=e.doc, ) from e except HTTPException: raise @@ -236,7 +268,7 @@ def get_request_handler( ) values, errors, background_tasks, sub_response, _ = solved_result if errors: - raise RequestValidationError(errors, body=body) + raise RequestValidationError(_normalize_errors(errors), body=body) else: raw_response = await run_endpoint_function( dependant=dependant, values=values, is_coroutine=is_coroutine @@ -287,7 +319,7 @@ def get_websocket_app( ) values, errors, _, _2, _3 = solved_result if errors: - raise WebSocketRequestValidationError(errors) + raise WebSocketRequestValidationError(_normalize_errors(errors)) assert dependant.call is not None, "dependant.call must be a function" await dependant.call(**values) @@ -348,8 +380,8 @@ class APIRoute(routing.Route): name: Optional[str] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -414,7 +446,11 @@ class APIRoute(routing.Route): ), f"Status code {status_code} must not have a response body" response_name = "Response_" + self.unique_id self.response_field = create_response_field( - name=response_name, type_=self.response_model + name=response_name, + type_=self.response_model, + # TODO: This should actually set mode='serialization', just, that changes the schemas + # mode="serialization", + mode="validation", ) # Create a clone of the field, so that a Pydantic submodel is not returned # as is just because it's an instance of a subclass of a more limited class @@ -423,6 +459,7 @@ class APIRoute(routing.Route): # would pass the validation and be returned as is. # By being a new field, no inheritance will be passed as is. A new model # will be always created. + # TODO: remove when deprecating Pydantic v1 self.secure_cloned_response_field: Optional[ ModelField ] = create_cloned_field(self.response_field) @@ -569,8 +606,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[Union[Set[str], List[str]]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -650,8 +687,8 @@ class APIRouter(routing.Router): deprecated: Optional[bool] = None, methods: Optional[List[str]] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -877,8 +914,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -933,8 +970,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -989,8 +1026,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1045,8 +1082,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1101,8 +1138,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1157,8 +1194,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1213,8 +1250,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, @@ -1269,8 +1306,8 @@ class APIRouter(routing.Router): responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, deprecated: Optional[bool] = None, operation_id: Optional[str] = None, - response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None, - response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None, + response_model_include: Optional[IncEx] = None, + response_model_exclude: Optional[IncEx] = None, response_model_by_alias: bool = True, response_model_exclude_unset: bool = False, response_model_exclude_defaults: bool = False, diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 938dec37c..e4c4357e7 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -9,6 +9,9 @@ from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +# TODO: import from typing when deprecating Python 3.9 +from typing_extensions import Annotated + class OAuth2PasswordRequestForm: """ @@ -45,12 +48,13 @@ class OAuth2PasswordRequestForm: def __init__( self, - grant_type: str = Form(default=None, regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + *, + grant_type: Annotated[Union[str, None], Form(pattern="password")] = None, + username: Annotated[str, Form()], + password: Annotated[str, Form()], + scope: Annotated[str, Form()] = "", + client_id: Annotated[Union[str, None], Form()] = None, + client_secret: Annotated[Union[str, None], Form()] = None, ): self.grant_type = grant_type self.username = username @@ -95,12 +99,12 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): def __init__( self, - grant_type: str = Form(regex="password"), - username: str = Form(), - password: str = Form(), - scope: str = Form(default=""), - client_id: Optional[str] = Form(default=None), - client_secret: Optional[str] = Form(default=None), + grant_type: Annotated[str, Form(pattern="password")], + username: Annotated[str, Form()], + password: Annotated[str, Form()], + scope: Annotated[str, Form()] = "", + client_id: Annotated[Union[str, None], Form()] = None, + client_secret: Annotated[Union[str, None], Form()] = None, ): super().__init__( grant_type=grant_type, diff --git a/fastapi/types.py b/fastapi/types.py index e0bca4632..7adf565a7 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -1,3 +1,11 @@ -from typing import Any, Callable, TypeVar +import types +from enum import Enum +from typing import Any, Callable, Dict, Set, Type, TypeVar, Union + +from pydantic import BaseModel DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) +UnionType = getattr(types, "UnionType", Union) +NoneType = getattr(types, "UnionType", None) +ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] +IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] diff --git a/fastapi/utils.py b/fastapi/utils.py index 9b9ebcb85..267d64ce8 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -1,7 +1,6 @@ import re import warnings from dataclasses import is_dataclass -from enum import Enum from typing import ( TYPE_CHECKING, Any, @@ -16,13 +15,20 @@ from typing import ( from weakref import WeakKeyDictionary import fastapi +from fastapi._compat import ( + PYDANTIC_V2, + BaseConfig, + ModelField, + PydanticSchemaGenerationError, + Undefined, + UndefinedType, + Validator, + lenient_issubclass, +) from fastapi.datastructures import DefaultPlaceholder, DefaultType -from fastapi.openapi.constants import REF_PREFIX -from pydantic import BaseConfig, BaseModel, create_model -from pydantic.class_validators import Validator -from pydantic.fields import FieldInfo, ModelField, UndefinedType -from pydantic.schema import model_process_schema -from pydantic.utils import lenient_issubclass +from pydantic import BaseModel, create_model +from pydantic.fields import FieldInfo +from typing_extensions import Literal if TYPE_CHECKING: # pragma: nocover from .routing import APIRoute @@ -50,24 +56,6 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: return not (current_status_code < 200 or current_status_code in {204, 304}) -def get_model_definitions( - *, - flat_models: Set[Union[Type[BaseModel], Type[Enum]]], - model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str], -) -> Dict[str, Any]: - definitions: Dict[str, Dict[str, Any]] = {} - for model in flat_models: - m_schema, m_definitions, m_nested_models = model_process_schema( - model, model_name_map=model_name_map, ref_prefix=REF_PREFIX - ) - definitions.update(m_definitions) - model_name = model_name_map[model] - if "description" in m_schema: - m_schema["description"] = m_schema["description"].split("\f")[0] - definitions[model_name] = m_schema - return definitions - - def get_path_param_names(path: str) -> Set[str]: return set(re.findall("{(.*?)}", path)) @@ -76,30 +64,40 @@ def create_response_field( name: str, type_: Type[Any], class_validators: Optional[Dict[str, Validator]] = None, - default: Optional[Any] = None, - required: Union[bool, UndefinedType] = True, + default: Optional[Any] = Undefined, + required: Union[bool, UndefinedType] = Undefined, model_config: Type[BaseConfig] = BaseConfig, field_info: Optional[FieldInfo] = None, alias: Optional[str] = None, + mode: Literal["validation", "serialization"] = "validation", ) -> ModelField: """ Create a new response field. Raises if type_ is invalid. """ class_validators = class_validators or {} - field_info = field_info or FieldInfo() - - try: - return ModelField( - name=name, - type_=type_, - class_validators=class_validators, - default=default, - required=required, - model_config=model_config, - alias=alias, - field_info=field_info, + if PYDANTIC_V2: + field_info = field_info or FieldInfo( + annotation=type_, default=default, alias=alias ) - except RuntimeError: + else: + field_info = field_info or FieldInfo() + kwargs = {"name": name, "field_info": field_info} + if PYDANTIC_V2: + kwargs.update({"mode": mode}) + else: + kwargs.update( + { + "type_": type_, + "class_validators": class_validators, + "default": default, + "required": required, + "model_config": model_config, + "alias": alias, + } + ) + try: + return ModelField(**kwargs) # type: ignore[arg-type] + except (RuntimeError, PydanticSchemaGenerationError): raise fastapi.exceptions.FastAPIError( "Invalid args for response field! Hint: " f"check that {type_} is a valid Pydantic field type. " @@ -116,6 +114,8 @@ def create_cloned_field( *, cloned_types: Optional[MutableMapping[Type[BaseModel], Type[BaseModel]]] = None, ) -> ModelField: + if PYDANTIC_V2: + return field # cloned_types caches already cloned types to support recursive models and improve # performance by avoiding unecessary cloning if cloned_types is None: @@ -136,30 +136,30 @@ def create_cloned_field( f, cloned_types=cloned_types ) new_field = create_response_field(name=field.name, type_=use_type) - new_field.has_alias = field.has_alias - new_field.alias = field.alias - new_field.class_validators = field.class_validators - new_field.default = field.default - new_field.required = field.required - new_field.model_config = field.model_config + new_field.has_alias = field.has_alias # type: ignore[attr-defined] + new_field.alias = field.alias # type: ignore[misc] + new_field.class_validators = field.class_validators # type: ignore[attr-defined] + new_field.default = field.default # type: ignore[misc] + new_field.required = field.required # type: ignore[misc] + new_field.model_config = field.model_config # type: ignore[attr-defined] new_field.field_info = field.field_info - new_field.allow_none = field.allow_none - new_field.validate_always = field.validate_always - if field.sub_fields: - new_field.sub_fields = [ + new_field.allow_none = field.allow_none # type: ignore[attr-defined] + new_field.validate_always = field.validate_always # type: ignore[attr-defined] + if field.sub_fields: # type: ignore[attr-defined] + new_field.sub_fields = [ # type: ignore[attr-defined] create_cloned_field(sub_field, cloned_types=cloned_types) - for sub_field in field.sub_fields + for sub_field in field.sub_fields # type: ignore[attr-defined] ] - if field.key_field: - new_field.key_field = create_cloned_field( - field.key_field, cloned_types=cloned_types + if field.key_field: # type: ignore[attr-defined] + new_field.key_field = create_cloned_field( # type: ignore[attr-defined] + field.key_field, cloned_types=cloned_types # type: ignore[attr-defined] ) - new_field.validators = field.validators - new_field.pre_validators = field.pre_validators - new_field.post_validators = field.post_validators - new_field.parse_json = field.parse_json - new_field.shape = field.shape - new_field.populate_validators() + new_field.validators = field.validators # type: ignore[attr-defined] + new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] + new_field.post_validators = field.post_validators # type: ignore[attr-defined] + new_field.parse_json = field.parse_json # type: ignore[attr-defined] + new_field.shape = field.shape # type: ignore[attr-defined] + new_field.populate_validators() # type: ignore[attr-defined] return new_field @@ -220,3 +220,9 @@ def get_value_or_default( if not isinstance(item, DefaultPlaceholder): return item return first_item + + +def match_pydantic_error_url(error_type: str) -> Any: + from dirty_equals import IsStr + + return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}") diff --git a/pyproject.toml b/pyproject.toml index 61dbf7629..f0917578f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,8 @@ classifiers = [ ] dependencies = [ "starlette>=0.27.0,<0.28.0", - "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0", - "typing-extensions>=4.5.0" + "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0", + "typing-extensions>=4.5.0", ] dynamic = ["version"] @@ -61,8 +61,10 @@ all = [ "pyyaml >=5.3.1", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", "orjson >=3.2.1", - "email_validator >=1.1.1", + "email_validator >=2.0.0", "uvicorn[standard] >=0.12.0", + "pydantic-settings >=2.0.0", + "pydantic-extra-types >=2.0.0", ] [tool.hatch.version] @@ -85,6 +87,7 @@ check_untyped_defs = true addopts = [ "--strict-config", "--strict-markers", + "--ignore=docs_src", ] xfail_strict = true junit_family = "xunit2" @@ -142,6 +145,7 @@ ignore = [ "docs_src/custom_response/tutorial007.py" = ["B007"] "docs_src/dataclasses/tutorial003.py" = ["I001"] "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"] +"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] "docs_src/dependencies/tutorial008_an.py" = ["F821"] "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] diff --git a/requirements-tests.txt b/requirements-tests.txt index 4b34fcc2c..abefac685 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,11 +1,13 @@ -e . +pydantic-settings >=2.0.0 pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.0 ruff ==0.0.275 black == 23.3.0 httpx >=0.23.0,<0.25.0 -email_validator >=1.1.1,<2.0.0 +email_validator >=1.1.1,<3.0.0 +dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel sqlalchemy >=1.3.18,<1.4.43 diff --git a/tests/test_additional_properties_bool.py b/tests/test_additional_properties_bool.py index e35c26342..de59e48ce 100644 --- a/tests/test_additional_properties_bool.py +++ b/tests/test_additional_properties_bool.py @@ -1,13 +1,19 @@ from typing import Union +from dirty_equals import IsDict from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict class FooBaseModel(BaseModel): - class Config: - extra = "forbid" + if PYDANTIC_V2: + model_config = ConfigDict(extra="forbid") + else: + + class Config: + extra = "forbid" class Foo(FooBaseModel): @@ -52,7 +58,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Foo"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Foo"}, + {"type": "null"}, + ], + "title": "Foo", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Foo"} + ) } } }, diff --git a/tests/test_additional_responses_custom_model_in_callback.py b/tests/test_additional_responses_custom_model_in_callback.py index 397159142..2ad575455 100644 --- a/tests/test_additional_responses_custom_model_in_callback.py +++ b/tests/test_additional_responses_custom_model_in_callback.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -42,13 +43,24 @@ def test_openapi_schema(): "parameters": [ { "required": True, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "title": "Callback Url", + "minLength": 1, + "type": "string", + "format": "uri", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 5a70c4541..541f84bca 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI, Query from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from typing_extensions import Annotated app = FastAPI() @@ -30,21 +32,46 @@ client = TestClient(app) foo_is_missing = { "detail": [ - { - "loc": ["query", "foo"], - "msg": "field required", - "type": "value_error.missing", - } + IsDict( + { + "loc": ["query", "foo"], + "msg": "Field required", + "type": "missing", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "loc": ["query", "foo"], + "msg": "field required", + "type": "value_error.missing", + } + ) ] } foo_is_short = { "detail": [ - { - "ctx": {"limit_value": 1}, - "loc": ["query", "foo"], - "msg": "ensure this value has at least 1 characters", - "type": "value_error.any_str.min_length", - } + IsDict( + { + "ctx": {"min_length": 1}, + "loc": ["query", "foo"], + "msg": "String should have at least 1 characters", + "type": "string_too_short", + "input": "", + "url": match_pydantic_error_url("string_too_short"), + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict( + { + "ctx": {"limit_value": 1}, + "loc": ["query", "foo"], + "msg": "ensure this value has at least 1 characters", + "type": "value_error.any_str.min_length", + } + ) ] } diff --git a/tests/test_application.py b/tests/test_application.py index b036e67af..ea7a80128 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from .main import app @@ -266,10 +267,17 @@ def test_openapi_schema(): "operationId": "get_path_param_id_path_param__item_id__get", "parameters": [ { - "required": True, - "schema": {"title": "Item Id", "type": "string"}, "name": "item_id", "in": "path", + "required": True, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Item Id", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Item Id", "type": "string"}), } ], } @@ -969,10 +977,17 @@ def test_openapi_schema(): "operationId": "get_query_type_optional_query_int_optional_get", "parameters": [ { - "required": False, - "schema": {"title": "Query", "type": "integer"}, "name": "query", "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Query", + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Query", "type": "integer"}), } ], } diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 000000000..47160ee76 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,93 @@ +from typing import List, Union + +from fastapi import FastAPI, UploadFile +from fastapi._compat import ( + ModelField, + Undefined, + _get_model_config, + is_bytes_sequence_annotation, + is_uploadfile_sequence_annotation, +) +from fastapi.testclient import TestClient +from pydantic import BaseConfig, BaseModel, ConfigDict +from pydantic.fields import FieldInfo + +from .utils import needs_pydanticv1, needs_pydanticv2 + + +@needs_pydanticv2 +def test_model_field_default_required(): + # For coverage + field_info = FieldInfo(annotation=str) + field = ModelField(name="foo", field_info=field_info) + assert field.default is Undefined + + +@needs_pydanticv1 +def test_upload_file_dummy_general_plain_validator_function(): + # For coverage + assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} + + +@needs_pydanticv1 +def test_union_scalar_list(): + # For coverage + # TODO: there might not be a current valid code path that uses this, it would + # potentially enable query parameters defined as both a scalar and a list + # but that would require more refactors, also not sure it's really useful + from fastapi._compat import is_pv1_scalar_field + + field_info = FieldInfo() + field = ModelField( + name="foo", + field_info=field_info, + type_=Union[str, List[int]], + class_validators={}, + model_config=BaseConfig, + ) + assert not is_pv1_scalar_field(field) + + +@needs_pydanticv2 +def test_get_model_config(): + # For coverage in Pydantic v2 + class Foo(BaseModel): + model_config = ConfigDict(from_attributes=True) + + foo = Foo() + config = _get_model_config(foo) + assert config == {"from_attributes": True} + + +def test_complex(): + app = FastAPI() + + @app.post("/") + def foo(foo: Union[str, List[int]]): + return foo + + client = TestClient(app) + + response = client.post("/", json="bar") + assert response.status_code == 200, response.text + assert response.json() == "bar" + + response2 = client.post("/", json=[1, 2]) + assert response2.status_code == 200, response2.text + assert response2.json() == [1, 2] + + +def test_is_bytes_sequence_annotation_union(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of bytes + # to be read from files and other types, but I'm not even sure it's a good idea + # to support it as a first class "feature" + assert is_bytes_sequence_annotation(Union[List[str], List[bytes]]) + + +def test_is_uploadfile_sequence_annotation(): + # For coverage + # TODO: in theory this would allow declaring types that could be lists of UploadFile + # and other types, but I'm not even sure it's a good idea to support it as a first + # class "feature" + assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]]) diff --git a/tests/test_custom_schema_fields.py b/tests/test_custom_schema_fields.py index 10b02608c..ee51fc7ff 100644 --- a/tests/test_custom_schema_fields.py +++ b/tests/test_custom_schema_fields.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient from pydantic import BaseModel @@ -8,10 +9,18 @@ app = FastAPI() class Item(BaseModel): name: str - class Config: - schema_extra = { - "x-something-internal": {"level": 4}, + if PYDANTIC_V2: + model_config = { + "json_schema_extra": { + "x-something-internal": {"level": 4}, + } } + else: + + class Config: + schema_extra = { + "x-something-internal": {"level": 4}, + } @app.get("/foo", response_model=Item) diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 2e6217d34..b91467265 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -7,11 +7,17 @@ from fastapi.datastructures import Default from fastapi.testclient import TestClient +# TODO: remove when deprecating Pydantic v1 def test_upload_file_invalid(): with pytest.raises(ValueError): UploadFile.validate("not a Starlette UploadFile") +def test_upload_file_invalid_pydantic_v2(): + with pytest.raises(ValueError): + UploadFile._validate("not a Starlette UploadFile", {}) + + def test_default_placeholder_equals(): placeholder_1 = Default("a") placeholder_2 = Default("a") diff --git a/tests/test_datetime_custom_encoder.py b/tests/test_datetime_custom_encoder.py index 5c1833eb4..3aa77c0b1 100644 --- a/tests/test_datetime_custom_encoder.py +++ b/tests/test_datetime_custom_encoder.py @@ -4,31 +4,54 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel - -class ModelWithDatetimeField(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } +from .utils import needs_pydanticv1, needs_pydanticv2 -app = FastAPI() -model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer + class ModelWithDatetimeField(BaseModel): + dt_field: datetime -@app.get("/model", response_model=ModelWithDatetimeField) -def get_model(): - return model + @field_serializer("dt_field") + def serialize_datetime(self, dt_field: datetime): + return dt_field.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) -client = TestClient(app) + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model - -def test_dt(): + client = TestClient(app) + with client: + response = client.get("/model") + assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + class ModelWithDatetimeField(BaseModel): + dt_field: datetime + + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } + + app = FastAPI() + model = ModelWithDatetimeField(dt_field=datetime(2019, 1, 1, 8)) + + @app.get("/model", response_model=ModelWithDatetimeField) + def get_model(): + return model + + client = TestClient(app) with client: response = client.get("/model") assert response.json() == {"dt_field": "2019-01-01T08:00:00+00:00"} diff --git a/tests/test_dependency_duplicates.py b/tests/test_dependency_duplicates.py index 4f4f3166c..0882cc41d 100644 --- a/tests/test_dependency_duplicates.py +++ b/tests/test_dependency_duplicates.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -47,15 +49,30 @@ async def no_duplicates_sub( def test_no_duplicates_invalid(): response = client.post("/no-duplicates", json={"item": {"data": "myitem"}}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "item2"], - "msg": "field required", - "type": "value_error.missing", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item2"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item2"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_no_duplicates(): diff --git a/tests/test_dependency_overrides.py b/tests/test_dependency_overrides.py index 8bb307971..21cff998d 100644 --- a/tests/test_dependency_overrides.py +++ b/tests/test_dependency_overrides.py @@ -1,8 +1,10 @@ from typing import Optional import pytest +from dirty_equals import IsDict from fastapi import APIRouter, Depends, FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -50,99 +52,180 @@ async def overrider_dependency_with_sub(msg: dict = Depends(overrider_sub_depend return msg -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/main-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "main-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?q=foo", 200, {"in": "decorator-depends"}), - ( - "/decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "decorator-depends"}, - ), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 0, "limit": 100}}, - ), - ( - "/router-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-depends", "params": {"q": "foo", "skip": 100, "limit": 200}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "q"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?q=foo", 200, {"in": "router-decorator-depends"}), - ( - "/router-decorator-depends/?q=foo&skip=100&limit=200", - 200, - {"in": "router-decorator-depends"}, - ), - ], -) -def test_normal_app(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected +def test_main_depends(): + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_main_depends_q_foo(): + response = client.get("/main-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_main_depends_q_foo_skip_100_limit_200(): + response = client.get("/main-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "main-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_decorator_depends(): + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_decorator_depends_q_foo(): + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + + +def test_router_depends(): + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_depends_q_foo(): + response = client.get("/router-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 0, "limit": 100}, + } + + +def test_router_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == { + "in": "router-depends", + "params": {"q": "foo", "skip": 100, "limit": 200}, + } + + +def test_router_decorator_depends(): + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "q"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_router_decorator_depends_q_foo(): + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} + + +def test_router_decorator_depends_q_foo_skip_100_limit_200(): + response = client.get("/router-decorator-depends/?q=foo&skip=100&limit=200") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} @pytest.mark.parametrize( @@ -190,126 +273,281 @@ def test_override_simple(url, status_code, expected): app.dependency_overrides = {} -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/main-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/main-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/main-depends/?k=bar", 200, {"in": "main-depends", "params": {"k": "bar"}}), - ( - "/decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/decorator-depends/?k=bar", 200, {"in": "decorator-depends"}), - ( - "/router-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-depends/?k=bar", - 200, - {"in": "router-depends", "params": {"k": "bar"}}, - ), - ( - "/router-decorator-depends/", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ( - "/router-decorator-depends/?q=foo", - 422, - { - "detail": [ - { - "loc": ["query", "k"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - ), - ("/router-decorator-depends/?k=bar", 200, {"in": "router-decorator-depends"}), - ], -) -def test_override_with_sub(url, status_code, expected): +def test_override_with_sub_main_depends(): app.dependency_overrides[common_parameters] = overrider_dependency_with_sub - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected + response = client.get("/main-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub__main_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_main_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/main-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "main-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "decorator-depends"} + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-depends", "params": {"k": "bar"}} + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_q_foo(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?q=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "k"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "k"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + app.dependency_overrides = {} + + +def test_override_with_sub_router_decorator_depends_k_bar(): + app.dependency_overrides[common_parameters] = overrider_dependency_with_sub + response = client.get("/router-decorator-depends/?k=bar") + assert response.status_code == 200 + assert response.json() == {"in": "router-decorator-depends"} app.dependency_overrides = {} diff --git a/tests/test_extra_routes.py b/tests/test_extra_routes.py index fa95d061c..bd16fe925 100644 --- a/tests/test_extra_routes.py +++ b/tests/test_extra_routes.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.responses import JSONResponse from fastapi.testclient import TestClient @@ -327,7 +328,14 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + # TODO: remove when deprecating Pydantic v1 + | IsDict({"title": "Price", "type": "number"}), }, }, "ValidationError": { diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py new file mode 100644 index 000000000..657e8c5d1 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model/app_pv1.py @@ -0,0 +1,35 @@ +from typing import Optional + +from fastapi import Depends, FastAPI +from pydantic import BaseModel, validator + +app = FastAPI() + + +class ModelB(BaseModel): + username: str + + +class ModelC(ModelB): + password: str + + +class ModelA(BaseModel): + name: str + description: Optional[str] = None + model_b: ModelB + + @validator("name") + def lower_username(cls, name: str, values): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + +async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + +@app.get("/model/{name}", response_model=ModelA) +async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "model_b": model_c} diff --git a/tests/test_filter_pydantic_sub_model.py b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py similarity index 81% rename from tests/test_filter_pydantic_sub_model.py rename to tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py index 6ee928d07..48732dbf0 100644 --- a/tests/test_filter_pydantic_sub_model.py +++ b/tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py @@ -1,46 +1,20 @@ -from typing import Optional - import pytest -from fastapi import Depends, FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError, validator -app = FastAPI() +from ..utils import needs_pydanticv1 -class ModelB(BaseModel): - username: str +@pytest.fixture(name="client") +def get_client(): + from .app_pv1 import app + + client = TestClient(app) + return client -class ModelC(ModelB): - password: str - - -class ModelA(BaseModel): - name: str - description: Optional[str] = None - model_b: ModelB - - @validator("name") - def lower_username(cls, name: str, values): - if not name.endswith("A"): - raise ValueError("name must end in A") - return name - - -async def get_model_c() -> ModelC: - return ModelC(username="test-user", password="test-password") - - -@app.get("/model/{name}", response_model=ModelA) -async def get_model_a(name: str, model_c=Depends(get_model_c)): - return {"name": name, "description": "model-a-desc", "model_b": model_c} - - -client = TestClient(app) - - -def test_filter_sub_model(): +@needs_pydanticv1 +def test_filter_sub_model(client: TestClient): response = client.get("/model/modelA") assert response.status_code == 200, response.text assert response.json() == { @@ -50,8 +24,9 @@ def test_filter_sub_model(): } -def test_validator_is_cloned(): - with pytest.raises(ValidationError) as err: +@needs_pydanticv1 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: client.get("/model/modelX") assert err.value.errors() == [ { @@ -62,7 +37,8 @@ def test_validator_is_cloned(): ] -def test_openapi_schema(): +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py new file mode 100644 index 000000000..656332a01 --- /dev/null +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -0,0 +1,182 @@ +from typing import Optional + +import pytest +from dirty_equals import IsDict +from fastapi import Depends, FastAPI +from fastapi.exceptions import ResponseValidationError +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from pydantic import BaseModel, FieldValidationInfo, field_validator + + app = FastAPI() + + class ModelB(BaseModel): + username: str + + class ModelC(ModelB): + password: str + + class ModelA(BaseModel): + name: str + description: Optional[str] = None + foo: ModelB + + @field_validator("name") + def lower_username(cls, name: str, info: FieldValidationInfo): + if not name.endswith("A"): + raise ValueError("name must end in A") + return name + + async def get_model_c() -> ModelC: + return ModelC(username="test-user", password="test-password") + + @app.get("/model/{name}", response_model=ModelA) + async def get_model_a(name: str, model_c=Depends(get_model_c)): + return {"name": name, "description": "model-a-desc", "foo": model_c} + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_filter_sub_model(client: TestClient): + response = client.get("/model/modelA") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "modelA", + "description": "model-a-desc", + "foo": {"username": "test-user"}, + } + + +@needs_pydanticv2 +def test_validator_is_cloned(client: TestClient): + with pytest.raises(ResponseValidationError) as err: + client.get("/model/modelX") + assert err.value.errors() == [ + IsDict( + { + "type": "value_error", + "loc": ("response", "name"), + "msg": "Value error, name must end in A", + "input": "modelX", + "ctx": {"error": "name must end in A"}, + "url": match_pydantic_error_url("value_error"), + } + ) + | IsDict( + # TODO remove when deprecating Pydantic v1 + { + "loc": ("response", "name"), + "msg": "name must end in A", + "type": "value_error", + } + ) + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/model/{name}": { + "get": { + "summary": "Get Model A", + "operationId": "get_model_a_model__name__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Name", "type": "string"}, + "name": "name", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ModelA"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ModelA": { + "title": "ModelA", + "required": ["name", "foo"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | + # TODO remove when deprecating Pydantic v1 + IsDict({"title": "Description", "type": "string"}), + "foo": {"$ref": "#/components/schemas/ModelB"}, + }, + }, + "ModelB": { + "title": "ModelB", + "required": ["username"], + "type": "object", + "properties": {"username": {"title": "Username", "type": "string"}}, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_infer_param_optionality.py b/tests/test_infer_param_optionality.py index 5e673d9c4..e3d57bb42 100644 --- a/tests/test_infer_param_optionality.py +++ b/tests/test_infer_param_optionality.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient @@ -104,35 +105,253 @@ def test_get_users_item(): assert response.json() == {"item_id": "item01", "user_id": "abc123"} -def test_schema_1(): - """Check that the user_id is a required path parameter under /users""" +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": True, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "path", + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/users/": { + "get": { + "summary": "Get Users", + "operationId": "get_users_users__get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + }, + "/users/{user_id}": { + "get": { + "summary": "Get User", + "operationId": "get_user_users__user_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "User Id", "type": "string"}, + "name": "user_id", + "in": "path", + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items__get", + "parameters": [ + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": False, + "name": "user_id", + "in": "query", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/": { + "get": { + "summary": "Get Items", + "operationId": "get_items_users__user_id__items__get", + "parameters": [ + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/users/{user_id}/items/{item_id}": { + "get": { + "summary": "Get Item", + "operationId": "get_item_users__user_id__items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + }, + { + "required": True, + "name": "user_id", + "in": "path", + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User Id", "type": "string"} + ), + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, } - - assert d in r["paths"]["/users/{user_id}"]["get"]["parameters"] - assert d in r["paths"]["/users/{user_id}/items/"]["get"]["parameters"] - - -def test_schema_2(): - """Check that the user_id is an optional query parameter under /items""" - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - r = response.json() - - d = { - "required": False, - "schema": {"title": "User Id", "type": "string"}, - "name": "user_id", - "in": "query", - } - - assert d in r["paths"]["/items/{item_id}"]["get"]["parameters"] - assert d in r["paths"]["/items/"]["get"]["parameters"] diff --git a/tests/test_inherited_custom_class.py b/tests/test_inherited_custom_class.py index bac7eec1b..42b249211 100644 --- a/tests/test_inherited_custom_class.py +++ b/tests/test_inherited_custom_class.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -app = FastAPI() +from .utils import needs_pydanticv1, needs_pydanticv2 class MyUuid: @@ -26,40 +26,78 @@ class MyUuid: raise TypeError("vars() argument must have __dict__ attribute") -@app.get("/fast_uuid") -def return_fast_uuid(): - # I don't want to import asyncpg for this test so I made my own UUID - # Import asyncpg and uncomment the two lines below for the actual bug +@needs_pydanticv2 +def test_pydanticv2(): + from pydantic import field_serializer - # from asyncpg.pgproto import pgproto - # asyncpg_uuid = pgproto.UUID("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + app = FastAPI() - asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") - assert isinstance(asyncpg_uuid, uuid.UUID) - assert type(asyncpg_uuid) != uuid.UUID - with pytest.raises(TypeError): - vars(asyncpg_uuid) - return {"fast_uuid": asyncpg_uuid} + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + class SomeCustomClass(BaseModel): + model_config = {"arbitrary_types_allowed": True} -class SomeCustomClass(BaseModel): - class Config: - arbitrary_types_allowed = True - json_encoders = {uuid.UUID: str} + a_uuid: MyUuid - a_uuid: MyUuid + @field_serializer("a_uuid") + def serialize_a_uuid(self, v): + return str(v) + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) -@app.get("/get_custom_class") -def return_some_user(): - # Test that the fix also works for custom pydantic classes - return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + client = TestClient(app) + + with client: + response_simple = client.get("/fast_uuid") + response_pydantic = client.get("/get_custom_class") + + assert response_simple.json() == { + "fast_uuid": "a10ff360-3b1e-4984-a26f-d3ab460bdb51" + } + + assert response_pydantic.json() == { + "a_uuid": "b8799909-f914-42de-91bc-95c819218d01" + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_pydanticv1(): + app = FastAPI() + + @app.get("/fast_uuid") + def return_fast_uuid(): + asyncpg_uuid = MyUuid("a10ff360-3b1e-4984-a26f-d3ab460bdb51") + assert isinstance(asyncpg_uuid, uuid.UUID) + assert type(asyncpg_uuid) != uuid.UUID + with pytest.raises(TypeError): + vars(asyncpg_uuid) + return {"fast_uuid": asyncpg_uuid} + + class SomeCustomClass(BaseModel): + class Config: + arbitrary_types_allowed = True + json_encoders = {uuid.UUID: str} + + a_uuid: MyUuid + + @app.get("/get_custom_class") + def return_some_user(): + # Test that the fix also works for custom pydantic classes + return SomeCustomClass(a_uuid=MyUuid("b8799909-f914-42de-91bc-95c819218d01")) + + client = TestClient(app) - -client = TestClient(app) - - -def test_dt(): with client: response_simple = client.get("/fast_uuid") response_pydantic = client.get("/get_custom_class") diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index 1f43c33c7..ff3033ecd 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -1,13 +1,17 @@ from collections import deque from dataclasses import dataclass from datetime import datetime, timezone +from decimal import Decimal from enum import Enum from pathlib import PurePath, PurePosixPath, PureWindowsPath from typing import Optional import pytest +from fastapi._compat import PYDANTIC_V2 from fastapi.encoders import jsonable_encoder -from pydantic import BaseModel, Field, ValidationError, create_model +from pydantic import BaseModel, Field, ValidationError + +from .utils import needs_pydanticv1, needs_pydanticv2 class Person: @@ -46,22 +50,6 @@ class Unserializable: raise NotImplementedError() -class ModelWithCustomEncoder(BaseModel): - dt_field: datetime - - class Config: - json_encoders = { - datetime: lambda dt: dt.replace( - microsecond=0, tzinfo=timezone.utc - ).isoformat() - } - - -class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): - class Config: - pass - - class RoleEnum(Enum): admin = "admin" normal = "normal" @@ -70,8 +58,12 @@ class RoleEnum(Enum): class ModelWithConfig(BaseModel): role: Optional[RoleEnum] = None - class Config: - use_enum_values = True + if PYDANTIC_V2: + model_config = {"use_enum_values": True} + else: + + class Config: + use_enum_values = True class ModelWithAlias(BaseModel): @@ -84,23 +76,6 @@ class ModelWithDefault(BaseModel): bla: str = "bla" -class ModelWithRoot(BaseModel): - __root__: str - - -@pytest.fixture( - name="model_with_path", params=[PurePath, PurePosixPath, PureWindowsPath] -) -def fixture_model_with_path(request): - class Config: - arbitrary_types_allowed = True - - ModelWithPath = create_model( - "ModelWithPath", path=(request.param, ...), __config__=Config # type: ignore - ) - return ModelWithPath(path=request.param("/foo", "bar")) - - def test_encode_dict(): pet = {"name": "Firulais", "owner": {"name": "Foo"}} assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} @@ -154,14 +129,47 @@ def test_encode_unsupported(): jsonable_encoder(unserializable) -def test_encode_custom_json_encoders_model(): +@needs_pydanticv2 +def test_encode_custom_json_encoders_model_pydanticv2(): + from pydantic import field_serializer + + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + + @field_serializer("dt_field") + def serialize_dt_field(self, dt): + return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() + + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + pass + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} -def test_encode_custom_json_encoders_model_subclass(): - model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_encode_custom_json_encoders_model_pydanticv1(): + class ModelWithCustomEncoder(BaseModel): + dt_field: datetime + + class Config: + json_encoders = { + datetime: lambda dt: dt.replace( + microsecond=0, tzinfo=timezone.utc + ).isoformat() + } + + class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): + class Config: + pass + + model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} + subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) + assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} def test_encode_model_with_config(): @@ -197,6 +205,7 @@ def test_encode_model_with_default(): } +@needs_pydanticv1 def test_custom_encoders(): class safe_datetime(datetime): pass @@ -227,19 +236,72 @@ def test_custom_enum_encoders(): assert encoded_instance == custom_enum_encoder(instance) -def test_encode_model_with_path(model_with_path): - if isinstance(model_with_path.path, PureWindowsPath): - expected = "\\foo\\bar" - else: - expected = "/foo/bar" - assert jsonable_encoder(model_with_path) == {"path": expected} +def test_encode_model_with_pure_path(): + class ModelWithPath(BaseModel): + path: PurePath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PurePath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "/foo/bar"} +def test_encode_model_with_pure_posix_path(): + class ModelWithPath(BaseModel): + path: PurePosixPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "/foo/bar"} + + +def test_encode_model_with_pure_windows_path(): + class ModelWithPath(BaseModel): + path: PureWindowsPath + + if PYDANTIC_V2: + model_config = {"arbitrary_types_allowed": True} + else: + + class Config: + arbitrary_types_allowed = True + + obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) + assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} + + +@needs_pydanticv1 def test_encode_root(): + class ModelWithRoot(BaseModel): + __root__: str + model = ModelWithRoot(__root__="Foo") assert jsonable_encoder(model) == "Foo" +@needs_pydanticv2 +def test_decimal_encoder_float(): + data = {"value": Decimal(1.23)} + assert jsonable_encoder(data) == {"value": 1.23} + + +@needs_pydanticv2 +def test_decimal_encoder_int(): + data = {"value": Decimal(2)} + assert jsonable_encoder(data) == {"value": 2} + + def test_encode_deque_encodes_child_models(): class Model(BaseModel): test: str diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 96043aa35..aa989c612 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -1,8 +1,10 @@ from decimal import Decimal from typing import List +from dirty_equals import IsDict, IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel, condecimal app = FastAPI() @@ -21,59 +23,115 @@ def save_item_no_body(item: List[Item]): client = TestClient(app) -single_error = { - "detail": [ - { - "ctx": {"limit_value": 0.0}, - "loc": ["body", 0, "age"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - -multiple_errors = { - "detail": [ - { - "loc": ["body", 0, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 0, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - { - "loc": ["body", 1, "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", 1, "age"], - "msg": "value is not a valid decimal", - "type": "type_error.decimal", - }, - ] -} - - def test_put_correct_body(): response = client.post("/items/", json=[{"name": "Foo", "age": 5}]) assert response.status_code == 200, response.text - assert response.json() == {"item": [{"name": "Foo", "age": 5}]} + assert response.json() == { + "item": [ + { + "name": "Foo", + "age": IsOneOf( + 5, + # TODO: remove when deprecating Pydantic v1 + "5", + ), + } + ] + } def test_jsonable_encoder_requiring_error(): response = client.post("/items/", json=[{"name": "Foo", "age": -1.0}]) assert response.status_code == 422, response.text - assert response.json() == single_error + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", 0, "age"], + "msg": "Input should be greater than 0", + "input": -1.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0.0}, + "loc": ["body", 0, "age"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) def test_put_incorrect_body_multiple(): response = client.post("/items/", json=[{"age": "five"}, {"age": "six"}]) assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", 0, "name"], + "msg": "Field required", + "input": {"age": "five"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 0, "age"], + "msg": "Input should be a valid decimal", + "input": "five", + }, + { + "type": "missing", + "loc": ["body", 1, "name"], + "msg": "Field required", + "input": {"age": "six"}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "decimal_parsing", + "loc": ["body", 1, "age"], + "msg": "Input should be a valid decimal", + "input": "six", + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 0, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 0, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + { + "loc": ["body", 1, "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", 1, "age"], + "msg": "value is not a valid decimal", + "type": "type_error.decimal", + }, + ] + } + ) def test_openapi_schema(): @@ -126,11 +184,23 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "age": { - "title": "Age", - "exclusiveMinimum": 0.0, - "type": "number", - }, + "age": IsDict( + { + "title": "Age", + "anyOf": [ + {"exclusiveMinimum": 0.0, "type": "number"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Age", + "exclusiveMinimum": 0.0, + "type": "number", + } + ), }, }, "ValidationError": { diff --git a/tests/test_multi_query_errors.py b/tests/test_multi_query_errors.py index c1f0678d0..470a35808 100644 --- a/tests/test_multi_query_errors.py +++ b/tests/test_multi_query_errors.py @@ -1,7 +1,9 @@ from typing import List +from dirty_equals import IsDict from fastapi import FastAPI, Query from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url app = FastAPI() @@ -14,22 +16,6 @@ def read_items(q: List[int] = Query(default=None)): client = TestClient(app) -multiple_errors = { - "detail": [ - { - "loc": ["query", "q", 0], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "q", 1], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] -} - - def test_multi_query(): response = client.get("/items/?q=5&q=6") assert response.status_code == 200, response.text @@ -39,7 +25,42 @@ def test_multi_query(): def test_multi_query_incorrect(): response = client.get("/items/?q=five&q=six") assert response.status_code == 422, response.text - assert response.json() == multiple_errors + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "q", 0], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "five", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "q", 1], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "six", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "q", 0], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "q", 1], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) def test_openapi_schema(): diff --git a/tests/test_openapi_query_parameter_extension.py b/tests/test_openapi_query_parameter_extension.py index 6f62e6726..dc7147c71 100644 --- a/tests/test_openapi_query_parameter_extension.py +++ b/tests/test_openapi_query_parameter_extension.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient @@ -52,11 +53,21 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": { - "title": "Standard Query Param", - "type": "integer", - "default": 50, - }, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "default": 50, + "title": "Standard Query Param", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Standard Query Param", + "type": "integer", + "default": 50, + } + ), "name": "standard_query_param", "in": "query", }, diff --git a/tests/test_openapi_servers.py b/tests/test_openapi_servers.py index 11cd795ac..8697c8438 100644 --- a/tests/test_openapi_servers.py +++ b/tests/test_openapi_servers.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi import FastAPI from fastapi.testclient import TestClient @@ -35,10 +36,20 @@ def test_openapi_schema(): "servers": [ {"url": "/", "description": "Default, relative server"}, { - "url": "http://staging.localhost.tiangolo.com:8000", + "url": IsOneOf( + "http://staging.localhost.tiangolo.com:8000/", + # TODO: remove when deprecating Pydantic v1 + "http://staging.localhost.tiangolo.com:8000", + ), "description": "Staging but actually localhost still", }, - {"url": "https://prod.example.com"}, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ) + }, ], "paths": { "/foo": { diff --git a/tests/test_params_repr.py b/tests/test_params_repr.py index d8dca1ea4..bfc7bed09 100644 --- a/tests/test_params_repr.py +++ b/tests/test_params_repr.py @@ -1,6 +1,6 @@ from typing import Any, List -import pytest +from dirty_equals import IsOneOf from fastapi.params import Body, Cookie, Depends, Header, Param, Path, Query test_data: List[Any] = ["teststr", None, ..., 1, []] @@ -10,34 +10,137 @@ def get_user(): return {} # pragma: no cover -@pytest.fixture(scope="function", params=test_data) -def params(request): - return request.param +def test_param_repr_str(): + assert repr(Param("teststr")) == "Param(teststr)" -def test_param_repr(params): - assert repr(Param(params)) == "Param(" + str(params) + ")" +def test_param_repr_none(): + assert repr(Param(None)) == "Param(None)" + + +def test_param_repr_ellipsis(): + assert repr(Param(...)) == IsOneOf( + "Param(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Param(Ellipsis)", + ) + + +def test_param_repr_number(): + assert repr(Param(1)) == "Param(1)" + + +def test_param_repr_list(): + assert repr(Param([])) == "Param([])" def test_path_repr(): - assert repr(Path()) == "Path(Ellipsis)" - assert repr(Path(...)) == "Path(Ellipsis)" + assert repr(Path()) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) + assert repr(Path(...)) == IsOneOf( + "Path(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Path(Ellipsis)", + ) -def test_query_repr(params): - assert repr(Query(params)) == "Query(" + str(params) + ")" +def test_query_repr_str(): + assert repr(Query("teststr")) == "Query(teststr)" -def test_header_repr(params): - assert repr(Header(params)) == "Header(" + str(params) + ")" +def test_query_repr_none(): + assert repr(Query(None)) == "Query(None)" -def test_cookie_repr(params): - assert repr(Cookie(params)) == "Cookie(" + str(params) + ")" +def test_query_repr_ellipsis(): + assert repr(Query(...)) == IsOneOf( + "Query(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Query(Ellipsis)", + ) -def test_body_repr(params): - assert repr(Body(params)) == "Body(" + str(params) + ")" +def test_query_repr_number(): + assert repr(Query(1)) == "Query(1)" + + +def test_query_repr_list(): + assert repr(Query([])) == "Query([])" + + +def test_header_repr_str(): + assert repr(Header("teststr")) == "Header(teststr)" + + +def test_header_repr_none(): + assert repr(Header(None)) == "Header(None)" + + +def test_header_repr_ellipsis(): + assert repr(Header(...)) == IsOneOf( + "Header(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Header(Ellipsis)", + ) + + +def test_header_repr_number(): + assert repr(Header(1)) == "Header(1)" + + +def test_header_repr_list(): + assert repr(Header([])) == "Header([])" + + +def test_cookie_repr_str(): + assert repr(Cookie("teststr")) == "Cookie(teststr)" + + +def test_cookie_repr_none(): + assert repr(Cookie(None)) == "Cookie(None)" + + +def test_cookie_repr_ellipsis(): + assert repr(Cookie(...)) == IsOneOf( + "Cookie(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Cookie(Ellipsis)", + ) + + +def test_cookie_repr_number(): + assert repr(Cookie(1)) == "Cookie(1)" + + +def test_cookie_repr_list(): + assert repr(Cookie([])) == "Cookie([])" + + +def test_body_repr_str(): + assert repr(Body("teststr")) == "Body(teststr)" + + +def test_body_repr_none(): + assert repr(Body(None)) == "Body(None)" + + +def test_body_repr_ellipsis(): + assert repr(Body(...)) == IsOneOf( + "Body(PydanticUndefined)", + # TODO: remove when deprecating Pydantic v1 + "Body(Ellipsis)", + ) + + +def test_body_repr_number(): + assert repr(Body(1)) == "Body(1)" + + +def test_body_repr_list(): + assert repr(Body([])) == "Body([])" def test_depends_repr(): diff --git a/tests/test_path.py b/tests/test_path.py index 03b93623a..848b245e2 100644 --- a/tests/test_path.py +++ b/tests/test_path.py @@ -1,5 +1,6 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app @@ -18,235 +19,1259 @@ def test_nonexistent(): assert response.json() == {"detail": "Not Found"} -response_not_valid_bool = { - "detail": [ +def test_path_foobar(): + response = client.get("/path/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_foobar(): + response = client.get("/path/str/foobar") + assert response.status_code == 200 + assert response.json() == "foobar" + + +def test_path_str_42(): + response = client.get("/path/str/42") + assert response.status_code == 200 + assert response.json() == "42" + + +def test_path_str_True(): + response = client.get("/path/str/True") + assert response.status_code == 200 + assert response.json() == "True" + + +def test_path_int_foobar(): + response = client.get("/path/int/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value could not be parsed to a boolean", - "type": "type_error.bool", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foobar", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - -response_not_valid_int = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_not_valid_float = { - "detail": [ + +def test_path_int_True(): + response = client.get("/path/int/True") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "True", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - -response_at_least_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 3 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_at_least_2 = { - "detail": [ +def test_path_int_42(): + response = client.get("/path/int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_int_42_5(): + response = client.get("/path/int/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value has at least 2 characters", - "type": "value_error.any_str.min_length", - "ctx": {"limit_value": 2}, + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} - - -response_maximum_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value has at most 3 characters", - "type": "value_error.any_str.max_length", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] } - ] -} + ) -response_greater_than_3 = { - "detail": [ +def test_path_float_foobar(): + response = client.get("/path/float/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 3", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "foobar", + "url": match_pydantic_error_url("float_parsing"), + } + ] } - ] -} - - -response_greater_than_0 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 0}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) -response_greater_than_1 = { - "detail": [ +def test_path_float_True(): + response = client.get("/path/float/True") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than 1", - "type": "value_error.number.not_gt", - "ctx": {"limit_value": 1}, + "detail": [ + { + "type": "float_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "True", + "url": match_pydantic_error_url("float_parsing"), + } + ] } - ] -} - - -response_greater_than_equal_3 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is greater than or equal to 3", - "type": "value_error.number.not_ge", - "ctx": {"limit_value": 3}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] } - ] -} + ) -response_less_than_3 = { - "detail": [ +def test_path_float_42(): + response = client.get("/path/float/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_float_42_5(): + response = client.get("/path/float/42.5") + assert response.status_code == 200 + assert response.json() == 42.5 + + +def test_path_bool_foobar(): + response = client.get("/path/bool/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 3", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "foobar", + "url": match_pydantic_error_url("bool_parsing"), + } + ] } - ] -} - - -response_less_than_0 = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than 0", - "type": "value_error.number.not_lt", - "ctx": {"limit_value": 0}, + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] } - ] -} + ) -response_less_than_equal_3 = { - "detail": [ +def test_path_bool_True(): + response = client.get("/path/bool/True") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_42(): + response = client.get("/path/bool/42") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "ensure this value is less than or equal to 3", - "type": "value_error.number.not_le", - "ctx": {"limit_value": 3}, + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42", + "url": match_pydantic_error_url("bool_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/path/foobar", 200, "foobar"), - ("/path/str/foobar", 200, "foobar"), - ("/path/str/42", 200, "42"), - ("/path/str/True", 200, "True"), - ("/path/int/foobar", 422, response_not_valid_int), - ("/path/int/True", 422, response_not_valid_int), - ("/path/int/42", 200, 42), - ("/path/int/42.5", 422, response_not_valid_int), - ("/path/float/foobar", 422, response_not_valid_float), - ("/path/float/True", 422, response_not_valid_float), - ("/path/float/42", 200, 42), - ("/path/float/42.5", 200, 42.5), - ("/path/bool/foobar", 422, response_not_valid_bool), - ("/path/bool/True", 200, True), - ("/path/bool/42", 422, response_not_valid_bool), - ("/path/bool/42.5", 422, response_not_valid_bool), - ("/path/bool/1", 200, True), - ("/path/bool/0", 200, False), - ("/path/bool/true", 200, True), - ("/path/bool/False", 200, False), - ("/path/bool/false", 200, False), - ("/path/param/foo", 200, "foo"), - ("/path/param-minlength/foo", 200, "foo"), - ("/path/param-minlength/fo", 422, response_at_least_3), - ("/path/param-maxlength/foo", 200, "foo"), - ("/path/param-maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/foo", 200, "foo"), - ("/path/param-min_maxlength/foobar", 422, response_maximum_3), - ("/path/param-min_maxlength/f", 422, response_at_least_2), - ("/path/param-gt/42", 200, 42), - ("/path/param-gt/2", 422, response_greater_than_3), - ("/path/param-gt0/0.05", 200, 0.05), - ("/path/param-gt0/0", 422, response_greater_than_0), - ("/path/param-ge/42", 200, 42), - ("/path/param-ge/3", 200, 3), - ("/path/param-ge/2", 422, response_greater_than_equal_3), - ("/path/param-lt/42", 422, response_less_than_3), - ("/path/param-lt/2", 200, 2), - ("/path/param-lt0/-1", 200, -1), - ("/path/param-lt0/0", 422, response_less_than_0), - ("/path/param-le/42", 422, response_less_than_equal_3), - ("/path/param-le/3", 200, 3), - ("/path/param-le/2", 200, 2), - ("/path/param-lt-gt/2", 200, 2), - ("/path/param-lt-gt/4", 422, response_less_than_3), - ("/path/param-lt-gt/0", 422, response_greater_than_1), - ("/path/param-le-ge/2", 200, 2), - ("/path/param-le-ge/1", 200, 1), - ("/path/param-le-ge/3", 200, 3), - ("/path/param-le-ge/4", 422, response_less_than_equal_3), - ("/path/param-lt-int/2", 200, 2), - ("/path/param-lt-int/42", 422, response_less_than_3), - ("/path/param-lt-int/2.7", 422, response_not_valid_int), - ("/path/param-gt-int/42", 200, 42), - ("/path/param-gt-int/2", 422, response_greater_than_3), - ("/path/param-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-int/42", 422, response_less_than_equal_3), - ("/path/param-le-int/3", 200, 3), - ("/path/param-le-int/2", 200, 2), - ("/path/param-le-int/2.7", 422, response_not_valid_int), - ("/path/param-ge-int/42", 200, 42), - ("/path/param-ge-int/3", 200, 3), - ("/path/param-ge-int/2", 422, response_greater_than_equal_3), - ("/path/param-ge-int/2.7", 422, response_not_valid_int), - ("/path/param-lt-gt-int/2", 200, 2), - ("/path/param-lt-gt-int/4", 422, response_less_than_3), - ("/path/param-lt-gt-int/0", 422, response_greater_than_1), - ("/path/param-lt-gt-int/2.7", 422, response_not_valid_int), - ("/path/param-le-ge-int/2", 200, 2), - ("/path/param-le-ge-int/1", 200, 1), - ("/path/param-le-ge-int/3", 200, 3), - ("/path/param-le-ge-int/4", 422, response_less_than_equal_3), - ("/path/param-le-ge-int/2.7", 422, response_not_valid_int), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_path_bool_42_5(): + response = client.get("/path/bool/42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "bool_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid boolean, unable to interpret input", + "input": "42.5", + "url": match_pydantic_error_url("bool_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value could not be parsed to a boolean", + "type": "type_error.bool", + } + ] + } + ) + + +def test_path_bool_1(): + response = client.get("/path/bool/1") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_0(): + response = client.get("/path/bool/0") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_bool_true(): + response = client.get("/path/bool/true") + assert response.status_code == 200 + assert response.json() is True + + +def test_path_bool_False(): + response = client.get("/path/bool/False") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_bool_false(): + response = client.get("/path/bool/false") + assert response.status_code == 200 + assert response.json() is False + + +def test_path_param_foo(): + response = client.get("/path/param/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_foo(): + response = client.get("/path/param-minlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_minlength_fo(): + response = client.get("/path/param-minlength/fo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 3 characters", + "input": "fo", + "ctx": {"min_length": 3}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 3 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_maxlength_foo(): + response = client.get("/path/param-maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_maxlength_foobar(): + response = client.get("/path/param-maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_foo(): + response = client.get("/path/param-min_maxlength/foo") + assert response.status_code == 200 + assert response.json() == "foo" + + +def test_path_param_min_maxlength_foobar(): + response = client.get("/path/param-min_maxlength/foobar") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_long", + "loc": ["path", "item_id"], + "msg": "String should have at most 3 characters", + "input": "foobar", + "ctx": {"max_length": 3}, + "url": match_pydantic_error_url("string_too_long"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at most 3 characters", + "type": "value_error.any_str.max_length", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_min_maxlength_f(): + response = client.get("/path/param-min_maxlength/f") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_too_short", + "loc": ["path", "item_id"], + "msg": "String should have at least 2 characters", + "input": "f", + "ctx": {"min_length": 2}, + "url": match_pydantic_error_url("string_too_short"), + } + ] + } + ) | IsDict( + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value has at least 2 characters", + "type": "value_error.any_str.min_length", + "ctx": {"limit_value": 2}, + } + ] + } + ) + + +def test_path_param_gt_42(): + response = client.get("/path/param-gt/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_2(): + response = client.get("/path/param-gt/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt0_0_05(): + response = client.get("/path/param-gt0/0.05") + assert response.status_code == 200 + assert response.json() == 0.05 + + +def test_path_param_gt0_0(): + response = client.get("/path/param-gt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 0", + "input": "0", + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_ge_42(): + response = client.get("/path/param-ge/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_3(): + response = client.get("/path/param-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_2(): + response = client.get("/path/param-ge/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3.0}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_42(): + response = client.get("/path/param-lt/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_2(): + response = client.get("/path/param-lt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt0__1(): + response = client.get("/path/param-lt0/-1") + assert response.status_code == 200 + assert response.json() == -1 + + +def test_path_param_lt0_0(): + response = client.get("/path/param-lt0/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 0", + "input": "0", + "ctx": {"lt": 0.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 0", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 0}, + } + ] + } + ) + + +def test_path_param_le_42(): + response = client.get("/path/param-le/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_3(): + response = client.get("/path/param-le/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_2(): + response = client.get("/path/param-le/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_2(): + response = client.get("/path/param-lt-gt/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_4(): + response = client.get("/path/param-lt-gt/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3.0}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_0(): + response = client.get("/path/param-lt-gt/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1.0}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_le_ge_2(): + response = client.get("/path/param-le-ge/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_1(): + response = client.get("/path/param-le-ge/1") + assert response.status_code == 200 + + +def test_path_param_le_ge_3(): + response = client.get("/path/param-le-ge/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_4(): + response = client.get("/path/param-le-ge/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3.0}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2(): + response = client.get("/path/param-lt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_int_42(): + response = client.get("/path/param-lt-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "42", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_int_2_7(): + response = client.get("/path/param-lt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_gt_int_42(): + response = client.get("/path/param-gt-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_gt_int_2(): + response = client.get("/path/param-gt-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 3", + "input": "2", + "ctx": {"gt": 3}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 3", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_gt_int_2_7(): + response = client.get("/path/param-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_int_42(): + response = client.get("/path/param-le-int/42") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "42", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_int_3(): + response = client.get("/path/param-le-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_int_2(): + response = client.get("/path/param-le-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_int_2_7(): + response = client.get("/path/param-le-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_ge_int_42(): + response = client.get("/path/param-ge-int/42") + assert response.status_code == 200 + assert response.json() == 42 + + +def test_path_param_ge_int_3(): + response = client.get("/path/param-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_ge_int_2(): + response = client.get("/path/param-ge-int/2") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be greater than or equal to 3", + "input": "2", + "ctx": {"ge": 3}, + "url": match_pydantic_error_url("greater_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than or equal to 3", + "type": "value_error.number.not_ge", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_ge_int_2_7(): + response = client.get("/path/param-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_lt_gt_int_2(): + response = client.get("/path/param-lt-gt-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_lt_gt_int_4(): + response = client.get("/path/param-lt-gt-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than", + "loc": ["path", "item_id"], + "msg": "Input should be less than 3", + "input": "4", + "ctx": {"lt": 3}, + "url": match_pydantic_error_url("less_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than 3", + "type": "value_error.number.not_lt", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_0(): + response = client.get("/path/param-lt-gt-int/0") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["path", "item_id"], + "msg": "Input should be greater than 1", + "input": "0", + "ctx": {"gt": 1}, + "url": match_pydantic_error_url("greater_than"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is greater than 1", + "type": "value_error.number.not_gt", + "ctx": {"limit_value": 1}, + } + ] + } + ) + + +def test_path_param_lt_gt_int_2_7(): + response = client.get("/path/param-lt-gt-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_path_param_le_ge_int_2(): + response = client.get("/path/param-le-ge-int/2") + assert response.status_code == 200 + assert response.json() == 2 + + +def test_path_param_le_ge_int_1(): + response = client.get("/path/param-le-ge-int/1") + assert response.status_code == 200 + assert response.json() == 1 + + +def test_path_param_le_ge_int_3(): + response = client.get("/path/param-le-ge-int/3") + assert response.status_code == 200 + assert response.json() == 3 + + +def test_path_param_le_ge_int_4(): + response = client.get("/path/param-le-ge-int/4") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "less_than_equal", + "loc": ["path", "item_id"], + "msg": "Input should be less than or equal to 3", + "input": "4", + "ctx": {"le": 3}, + "url": match_pydantic_error_url("less_than_equal"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "ensure this value is less than or equal to 3", + "type": "value_error.number.not_le", + "ctx": {"limit_value": 3}, + } + ] + } + ) + + +def test_path_param_le_ge_int_2_7(): + response = client.get("/path/param-le-ge-int/2.7") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "2.7", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) diff --git a/tests/test_query.py b/tests/test_query.py index 0c73eb665..5bb9995d6 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,62 +1,410 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from .main import app client = TestClient(app) -response_missing = { - "detail": [ + +def test_query(): + response = client.get("/query") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "query"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - -response_not_valid_int = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["query", "query"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/query", 422, response_missing), - ("/query?query=baz", 200, "foo bar baz"), - ("/query?not_declared=baz", 422, response_missing), - ("/query/optional", 200, "foo bar"), - ("/query/optional?query=baz", 200, "foo bar baz"), - ("/query/optional?not_declared=baz", 200, "foo bar"), - ("/query/int", 422, response_missing), - ("/query/int?query=42", 200, "foo bar 42"), - ("/query/int?query=42.5", 422, response_not_valid_int), - ("/query/int?query=baz", 422, response_not_valid_int), - ("/query/int?not_declared=baz", 422, response_missing), - ("/query/int/optional", 200, "foo bar"), - ("/query/int/optional?query=50", 200, "foo bar 50"), - ("/query/int/optional?query=foo", 422, response_not_valid_int), - ("/query/int/default", 200, "foo bar 10"), - ("/query/int/default?query=50", 200, "foo bar 50"), - ("/query/int/default?query=foo", 422, response_not_valid_int), - ("/query/param", 200, "foo bar"), - ("/query/param?query=50", 200, "foo bar 50"), - ("/query/param-required", 422, response_missing), - ("/query/param-required?query=50", 200, "foo bar 50"), - ("/query/param-required/int", 422, response_missing), - ("/query/param-required/int?query=50", 200, "foo bar 50"), - ("/query/param-required/int?query=foo", 422, response_not_valid_int), - ("/query/frozenset/?query=1&query=1&query=2", 200, "1,2"), - ], -) -def test_get_path(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_query_baz(): + response = client.get("/query?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_not_declared_baz(): + response = client.get("/query?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_optional(): + response = client.get("/query/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_optional_query_baz(): + response = client.get("/query/optional?query=baz") + assert response.status_code == 200 + assert response.json() == "foo bar baz" + + +def test_query_optional_not_declared_baz(): + response = client.get("/query/optional?not_declared=baz") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int(): + response = client.get("/query/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_query_42(): + response = client.get("/query/int?query=42") + assert response.status_code == 200 + assert response.json() == "foo bar 42" + + +def test_query_int_query_42_5(): + response = client.get("/query/int?query=42.5") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "42.5", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_query_baz(): + response = client.get("/query/int?query=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "baz", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_not_declared_baz(): + response = client.get("/query/int?not_declared=baz") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_int_optional(): + response = client.get("/query/int/optional") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_int_optional_query_50(): + response = client.get("/query/int/optional?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_optional_query_foo(): + response = client.get("/query/int/optional?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_int_default(): + response = client.get("/query/int/default") + assert response.status_code == 200 + assert response.json() == "foo bar 10" + + +def test_query_int_default_query_50(): + response = client.get("/query/int/default?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_int_default_query_foo(): + response = client.get("/query/int/default?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_param(): + response = client.get("/query/param") + assert response.status_code == 200 + assert response.json() == "foo bar" + + +def test_query_param_query_50(): + response = client.get("/query/param?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required(): + response = client.get("/query/param-required") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_query_50(): + response = client.get("/query/param-required?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int(): + response = client.get("/query/param-required/int") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "query"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_query_param_required_int_query_50(): + response = client.get("/query/param-required/int?query=50") + assert response.status_code == 200 + assert response.json() == "foo bar 50" + + +def test_query_param_required_int_query_foo(): + response = client.get("/query/param-required/int?query=foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["query", "query"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "query"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) + + +def test_query_frozenset_query_1_query_1_query_2(): + response = client.get("/query/frozenset/?query=1&query=1&query=2") + assert response.status_code == 200 + assert response.json() == "1,2" diff --git a/tests/test_read_with_orm_mode.py b/tests/test_read_with_orm_mode.py index 360ad2503..b35987443 100644 --- a/tests/test_read_with_orm_mode.py +++ b/tests/test_read_with_orm_mode.py @@ -2,48 +2,83 @@ 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) +from pydantic import BaseModel, ConfigDict + +from .utils import needs_pydanticv1, needs_pydanticv2 +@needs_pydanticv2 def test_read_with_orm_mode() -> None: + class PersonBase(BaseModel): + name: str + lastname: str + + class Person(PersonBase): + @property + def full_name(self) -> str: + return f"{self.name} {self.lastname}" + + model_config = ConfigDict(from_attributes=True) + + class PersonCreate(PersonBase): + pass + + class PersonRead(PersonBase): + full_name: str + + model_config = {"from_attributes": True} + + app = FastAPI() + + @app.post("/people/", response_model=PersonRead) + def create_person(person: PersonCreate) -> Any: + db_person = Person.model_validate(person) + return db_person + + client = TestClient(app) + + 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"] + + +@needs_pydanticv1 +def test_read_with_orm_mode_pv1() -> None: + 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) + person_data = {"name": "Dive", "lastname": "Wilson"} response = client.post("/people/", json=person_data) data = response.json() diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py new file mode 100644 index 000000000..ca1ab514c --- /dev/null +++ b/tests/test_regex_deprecated_body.py @@ -0,0 +1,182 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Form +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.post("/items/") + async def read_items( + q: Annotated[str | None, Form(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_no_query(): + client = get_client() + response = client.post("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_q_fixedquery(): + client = get_client() + response = client.post("/items/", data={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_nonregexquery(): + client = get_client() + response = client.post("/items/", data={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["body", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Read Items", + "operationId": "read_items_items__post", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__post" + } + ) + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "Body_read_items_items__post": { + "properties": { + "q": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"type": "string", "pattern": "^fixedquery$", "title": "Q"} + ) + }, + "type": "object", + "title": "Body_read_items_items__post", + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py new file mode 100644 index 000000000..79a653353 --- /dev/null +++ b/tests/test_regex_deprecated_params.py @@ -0,0 +1,165 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI, Query +from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url +from typing_extensions import Annotated + +from .utils import needs_py310 + + +def get_client(): + app = FastAPI() + with pytest.warns(DeprecationWarning): + + @app.get("/items/") + async def read_items( + q: Annotated[str | None, Query(regex="^fixedquery$")] = None + ): + if q: + return f"Hello {q}" + else: + return "Hello World" + + client = TestClient(app) + return client + + +@needs_py310 +def test_query_params_str_validations_no_query(): + client = get_client() + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == "Hello World" + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(): + client = get_client() + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == "Hello fixedquery" + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(): + client = get_client() + response = client.get("/items/", params={"q": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "q"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "q"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) + + +@needs_py310 +def test_openapi_schema(): + client = get_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "parameters": [ + { + "name": "q", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [ + {"type": "string", "pattern": "^fixedquery$"}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "pattern": "^fixedquery$", + "title": "Q", + } + ), + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_request_body_parameters_media_type.py b/tests/test_request_body_parameters_media_type.py index 8424bf551..8c72fee54 100644 --- a/tests/test_request_body_parameters_media_type.py +++ b/tests/test_request_body_parameters_media_type.py @@ -39,7 +39,6 @@ client = TestClient(app) def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, diff --git a/tests/test_response_by_alias.py b/tests/test_response_by_alias.py index c3ff5b1d2..e162cd39b 100644 --- a/tests/test_response_by_alias.py +++ b/tests/test_response_by_alias.py @@ -1,8 +1,9 @@ from typing import List from fastapi import FastAPI +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field app = FastAPI() @@ -14,13 +15,24 @@ class Model(BaseModel): class ModelNoAlias(BaseModel): name: str - class Config: - schema_extra = { - "description": ( - "response_model_by_alias=False is basically a quick hack, to support " - "proper OpenAPI use another model with the correct field names" - ) - } + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={ + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } + ) + else: + + class Config: + schema_extra = { + "description": ( + "response_model_by_alias=False is basically a quick hack, to support " + "proper OpenAPI use another model with the correct field names" + ) + } @app.get("/dict", response_model=Model, response_model_by_alias=False) diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 7a0cf47ec..85dd450eb 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -2,10 +2,10 @@ from typing import List, Union import pytest from fastapi import FastAPI -from fastapi.exceptions import FastAPIError +from fastapi.exceptions import FastAPIError, ResponseValidationError from fastapi.responses import JSONResponse, Response from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel class BaseUser(BaseModel): @@ -277,12 +277,12 @@ def test_response_model_no_annotation_return_exact_dict(): def test_response_model_no_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model-no_annotation-return_invalid_dict") def test_response_model_no_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model-no_annotation-return_invalid_model") @@ -313,12 +313,12 @@ def test_no_response_model_annotation_return_exact_dict(): def test_no_response_model_annotation_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/no_response_model-annotation-return_invalid_dict") def test_no_response_model_annotation_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/no_response_model-annotation-return_invalid_model") @@ -395,12 +395,12 @@ def test_response_model_model1_annotation_model2_return_exact_dict(): def test_response_model_model1_annotation_model2_return_invalid_dict(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model_model1-annotation_model2-return_invalid_dict") def test_response_model_model1_annotation_model2_return_invalid_model(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/response_model_model1-annotation_model2-return_invalid_model") diff --git a/tests/test_response_model_data_filter.py b/tests/test_response_model_data_filter.py new file mode 100644 index 000000000..a3e0f95f0 --- /dev/null +++ b/tests/test_response_model_data_filter.py @@ -0,0 +1,81 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserBase(BaseModel): + email: str + + +class UserCreate(UserBase): + password: str + + +class UserDB(UserBase): + hashed_password: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: UserBase + + +@app.post("/users/", response_model=UserBase) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_response_model_data_filter_no_inheritance.py b/tests/test_response_model_data_filter_no_inheritance.py new file mode 100644 index 000000000..64003a841 --- /dev/null +++ b/tests/test_response_model_data_filter_no_inheritance.py @@ -0,0 +1,83 @@ +from typing import List + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class UserCreate(BaseModel): + email: str + password: str + + +class UserDB(BaseModel): + email: str + hashed_password: str + + +class User(BaseModel): + email: str + + +class PetDB(BaseModel): + name: str + owner: UserDB + + +class PetOut(BaseModel): + name: str + owner: User + + +@app.post("/users/", response_model=User) +async def create_user(user: UserCreate): + return user + + +@app.get("/pets/{pet_id}", response_model=PetOut) +async def read_pet(pet_id: int): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet = PetDB(name="Nibbler", owner=user) + return pet + + +@app.get("/pets/", response_model=List[PetOut]) +async def read_pets(): + user = UserDB( + email="johndoe@example.com", + hashed_password="secrethashed", + ) + pet1 = PetDB(name="Nibbler", owner=user) + pet2 = PetDB(name="Zoidberg", owner=user) + return [pet1, pet2] + + +client = TestClient(app) + + +def test_filter_top_level_model(): + response = client.post( + "/users", json={"email": "johndoe@example.com", "password": "secret"} + ) + assert response.json() == {"email": "johndoe@example.com"} + + +def test_filter_second_level_model(): + response = client.get("/pets/1") + assert response.json() == { + "name": "Nibbler", + "owner": {"email": "johndoe@example.com"}, + } + + +def test_list_of_models(): + response = client.get("/pets/") + assert response.json() == [ + {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}}, + {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}}, + ] diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index 45caa1615..a1505afe2 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -1,9 +1,11 @@ from typing import Union import pytest +from dirty_equals import IsDict from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi._compat import PYDANTIC_V2 from fastapi.testclient import TestClient -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict def create_app(): @@ -12,8 +14,14 @@ def create_app(): class Item(BaseModel): data: str - class Config: - schema_extra = {"example": {"data": "Data in schema_extra"}} + if PYDANTIC_V2: + model_config = ConfigDict( + json_schema_extra={"example": {"data": "Data in schema_extra"}} + ) + else: + + class Config: + schema_extra = {"example": {"data": "Data in schema_extra"}} @app.post("/schema_extra/") def schema_extra(item: Item): @@ -333,14 +341,28 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "Data in Body examples, example1"}, - {"data": "Data in Body examples, example2"}, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"}, + {"data": "Data in Body examples, example2"}, + ], + } + ) } }, "required": True, @@ -370,14 +392,28 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - {"data": "examples example_examples 1"}, - {"data": "examples example_examples 2"}, - ], - }, + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + } + ) + | IsDict( + # TODO: remove this when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + {"data": "examples example_examples 1"}, + {"data": "examples example_examples 2"}, + ], + }, + ), "example": {"data": "Overridden example"}, } }, @@ -508,7 +544,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Data", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "query1", "name": "data", "in": "query", @@ -539,11 +584,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), "name": "data", "in": "query", } @@ -573,11 +628,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["query1", "query2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["query1", "query2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["query1", "query2"], + } + ), "example": "query_overridden", "name": "data", "in": "query", @@ -608,7 +673,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"type": "string", "title": "Data"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "header1", "name": "data", "in": "header", @@ -639,11 +713,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "type": "string", + "title": "Data", + "examples": ["header1", "header2"], + } + ), "name": "data", "in": "header", } @@ -673,11 +757,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["header1", "header2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["header1", "header2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["header1", "header2"], + } + ), "example": "header_overridden", "name": "data", "in": "header", @@ -708,7 +802,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"type": "string", "title": "Data"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + {"title": "Data", "type": "string"} + ), "example": "cookie1", "name": "data", "in": "cookie", @@ -739,11 +842,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), "name": "data", "in": "cookie", } @@ -773,11 +886,21 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "type": "string", - "title": "Data", - "examples": ["cookie1", "cookie2"], - }, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Data", + "examples": ["cookie1", "cookie2"], + } + ) + | IsDict( + # TODO: Remove this when deprecating Pydantic v1 + { + "title": "Data", + "type": "string", + "examples": ["cookie1", "cookie2"], + } + ), "example": "cookie_overridden", "name": "data", "in": "cookie", diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 73d1b7d94..e98f80ebf 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -1,7 +1,8 @@ -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -59,76 +60,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"detail": "Not authenticated"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -199,8 +260,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional.py b/tests/test_security_oauth2_optional.py index b24c1b58f..d06c01bba 100644 --- a/tests/test_security_oauth2_optional.py +++ b/tests/test_security_oauth2_optional.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -63,76 +64,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_no_data(): + response = client.post("/login") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_data(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200 + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -203,8 +264,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_security_oauth2_optional_description.py b/tests/test_security_oauth2_optional_description.py index cda635151..9287e4366 100644 --- a/tests/test_security_oauth2_optional_description.py +++ b/tests/test_security_oauth2_optional_description.py @@ -1,9 +1,10 @@ from typing import Optional -import pytest +from dirty_equals import IsDict from fastapi import Depends, FastAPI, Security from fastapi.security import OAuth2, OAuth2PasswordRequestFormStrict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from pydantic import BaseModel app = FastAPI() @@ -64,76 +65,136 @@ def test_security_oauth2_password_bearer_no_header(): assert response.json() == {"msg": "Create an account first"} -required_params = { - "detail": [ +def test_strict_login_None(): + response = client.post("/login", data=None) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -grant_type_required = { - "detail": [ - { - "loc": ["body", "grant_type"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] } - ] -} - -grant_type_incorrect = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "grant_type"], - "msg": 'string does not match regex "password"', - "type": "value_error.str.regex", - "ctx": {"pattern": "password"}, + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] } - ] -} + ) -@pytest.mark.parametrize( - "data,expected_status,expected_response", - [ - (None, 422, required_params), - ({"username": "johndoe", "password": "secret"}, 422, grant_type_required), - ( - {"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, - 422, - grant_type_incorrect, - ), - ( - {"username": "johndoe", "password": "secret", "grant_type": "password"}, - 200, - { - "grant_type": "password", - "username": "johndoe", - "password": "secret", - "scopes": [], - "client_id": None, - "client_secret": None, - }, - ), - ], -) -def test_strict_login(data, expected_status, expected_response): - response = client.post("/login", data=data) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_strict_login_no_grant_type(): + response = client.post("/login", data={"username": "johndoe", "password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "grant_type"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_strict_login_incorrect_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "incorrect"}, + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["body", "grant_type"], + "msg": "String should match pattern 'password'", + "input": "incorrect", + "ctx": {"pattern": "password"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "grant_type"], + "msg": 'string does not match regex "password"', + "type": "value_error.str.regex", + "ctx": {"pattern": "password"}, + } + ] + } + ) + + +def test_strict_login_correct_correct_grant_type(): + response = client.post( + "/login", + data={"username": "johndoe", "password": "secret", "grant_type": "password"}, + ) + assert response.status_code == 200, response.text + assert response.json() == { + "grant_type": "password", + "username": "johndoe", + "password": "secret", + "scopes": [], + "client_id": None, + "client_secret": None, + } def test_openapi_schema(): @@ -204,8 +265,26 @@ def test_openapi_schema(): "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_skip_defaults.py b/tests/test_skip_defaults.py index 181fff612..02765291c 100644 --- a/tests/test_skip_defaults.py +++ b/tests/test_skip_defaults.py @@ -12,7 +12,7 @@ class SubModel(BaseModel): class Model(BaseModel): - x: Optional[int] + x: Optional[int] = None sub: SubModel diff --git a/tests/test_sub_callbacks.py b/tests/test_sub_callbacks.py index dce3ea5e2..ed7f4efe8 100644 --- a/tests/test_sub_callbacks.py +++ b/tests/test_sub_callbacks.py @@ -1,5 +1,6 @@ from typing import Optional +from dirty_equals import IsDict from fastapi import APIRouter, FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel, HttpUrl @@ -98,13 +99,30 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "title": "Callback Url", + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } @@ -244,7 +262,16 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tuples.py b/tests/test_tuples.py index c37a25ca6..ca33d2580 100644 --- a/tests/test_tuples.py +++ b/tests/test_tuples.py @@ -1,5 +1,6 @@ from typing import List, Tuple +from dirty_equals import IsDict from fastapi import FastAPI, Form from fastapi.testclient import TestClient from pydantic import BaseModel @@ -126,16 +127,31 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "title": "Square", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [ - {"$ref": "#/components/schemas/Coordinate"}, - {"$ref": "#/components/schemas/Coordinate"}, - ], - } + "schema": IsDict( + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Square", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [ + {"$ref": "#/components/schemas/Coordinate"}, + {"$ref": "#/components/schemas/Coordinate"}, + ], + } + ) } }, "required": True, @@ -198,13 +214,28 @@ def test_openapi_schema(): "required": ["values"], "type": "object", "properties": { - "values": { - "title": "Values", - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "integer"}, {"type": "integer"}], - } + "values": IsDict( + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "integer"}, + {"type": "integer"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Values", + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "integer"}, {"type": "integer"}], + } + ) }, }, "Coordinate": { @@ -235,12 +266,26 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": { - "maxItems": 2, - "minItems": 2, - "type": "array", - "items": [{"type": "string"}, {"type": "string"}], - }, + "items": IsDict( + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "prefixItems": [ + {"type": "string"}, + {"type": "string"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "maxItems": 2, + "minItems": 2, + "type": "array", + "items": [{"type": "string"}, {"type": "string"}], + } + ), } }, }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial002.py b/tests/test_tutorial/test_additional_responses/test_tutorial002.py index 8e084e152..588a3160a 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial002.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial002.py @@ -1,6 +1,7 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial002 import app @@ -64,7 +65,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Img", "type": "boolean"}, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_additional_responses/test_tutorial004.py b/tests/test_tutorial/test_additional_responses/test_tutorial004.py index 5fc8b81ca..55b556d8e 100644 --- a/tests/test_tutorial/test_additional_responses/test_tutorial004.py +++ b/tests/test_tutorial/test_additional_responses/test_tutorial004.py @@ -1,6 +1,7 @@ import os import shutil +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.additional_responses.tutorial004 import app @@ -67,7 +68,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Img", "type": "boolean"}, + "schema": IsDict( + { + "anyOf": [{"type": "boolean"}, {"type": "null"}], + "title": "Img", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Img", "type": "boolean"} + ), "name": "img", "in": "query", }, diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 8126cdcc6..25d6df3e9 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -2,7 +2,11 @@ from fastapi.testclient import TestClient from docs_src.async_sql_databases.tutorial001 import app +from ...utils import needs_pydanticv1 + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_read(): with TestClient(app) as client: note = {"text": "Foo bar", "completed": False} diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py index 0ae9f4f93..ec17b4179 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial003 import app @@ -11,7 +12,7 @@ def test_main(): assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -19,9 +20,20 @@ def test_openapi(): "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ {"url": "/api/v1"}, - {"url": "https://stag.example.com", "description": "Staging environment"}, { - "url": "https://prod.example.com", + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), "description": "Production environment", }, ], diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py index 576a411a4..2f8eb4699 100644 --- a/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py +++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.behind_a_proxy.tutorial004 import app @@ -11,16 +12,27 @@ def test_main(): assert response.json() == {"message": "Hello World", "root_path": "/api/v1"} -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "servers": [ - {"url": "https://stag.example.com", "description": "Staging environment"}, { - "url": "https://prod.example.com", + "url": IsOneOf( + "https://stag.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://stag.example.com", + ), + "description": "Staging environment", + }, + { + "url": IsOneOf( + "https://prod.example.com/", + # TODO: remove when deprecating Pydantic v1 + "https://prod.example.com", + ), "description": "Production environment", }, ], diff --git a/tests/test_tutorial/test_bigger_applications/test_main.py b/tests/test_tutorial/test_bigger_applications/test_main.py index 7da663435..526e265a6 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,138 +1,368 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.bigger_applications.app.main import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -no_jessica = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app.main import app + + client = TestClient(app) + return client + + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path(path, expected_status, expected_response, headers): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} -def test_put_no_header(): - response = client.put("/items/foo") - assert response.status_code == 422, response.text +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, } -def test_put_invalid_header(): +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): response = client.put("/items/foo", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -140,7 +370,7 @@ def test_put(): assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} -def test_put_forbidden(): +def test_put_forbidden(client: TestClient): response = client.put( "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -148,7 +378,7 @@ def test_put_forbidden(): assert response.json() == {"detail": "You can only update the item: plumbus"} -def test_admin(): +def test_admin(client: TestClient): response = client.post( "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -156,13 +386,13 @@ def test_admin(): assert response.json() == {"message": "Admin getting schwifty"} -def test_admin_invalid_header(): +def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an.py b/tests/test_tutorial/test_bigger_applications/test_main_an.py index 8f42d9dd1..c0b77d4a7 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an.py @@ -1,138 +1,368 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.bigger_applications.app_an.main import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -no_jessica = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.bigger_applications.app_an.main import app + + client = TestClient(app) + return client + + +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path(path, expected_status, expected_response, headers): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} -def test_put_no_header(): - response = client.put("/items/foo") - assert response.status_code == 422, response.text +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, } -def test_put_invalid_header(): +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_no_header(client: TestClient): + response = client.put("/items/foo") + assert response.status_code == 422, response.text + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_put_invalid_header(client: TestClient): response = client.put("/items/foo", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -140,7 +370,7 @@ def test_put(): assert response.json() == {"item_id": "plumbus", "name": "The great Plumbus"} -def test_put_forbidden(): +def test_put_forbidden(client: TestClient): response = client.put( "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -148,7 +378,7 @@ def test_put_forbidden(): assert response.json() == {"detail": "You can only update the item: plumbus"} -def test_admin(): +def test_admin(client: TestClient): response = client.post( "/admin/?token=jessica", headers={"X-Token": "fake-super-secret-token"} ) @@ -156,13 +386,13 @@ def test_admin(): assert response.json() == {"message": "Admin getting schwifty"} -def test_admin_invalid_header(): +def test_admin_invalid_header(client: TestClient): response = client.post("/admin/", headers={"X-Token": "invalid"}) assert response.status_code == 400, response.text assert response.json() == {"detail": "X-Token header invalid"} -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py index 44694e371..948331b5d 100644 --- a/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py +++ b/tests/test_tutorial/test_bigger_applications/test_main_an_py39.py @@ -1,18 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 -no_jessica = { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - @pytest.fixture(name="client") def get_client(): @@ -23,116 +15,366 @@ def get_client(): @needs_py39 -@pytest.mark.parametrize( - "path,expected_status,expected_response,headers", - [ - ( - "/users?token=jessica", - 200, - [{"username": "Rick"}, {"username": "Morty"}], - {}, - ), - ("/users", 422, no_jessica, {}), - ("/users/foo?token=jessica", 200, {"username": "foo"}, {}), - ("/users/foo", 422, no_jessica, {}), - ("/users/me?token=jessica", 200, {"username": "fakecurrentuser"}, {}), - ("/users/me", 422, no_jessica, {}), - ( - "/users?token=monica", - 400, - {"detail": "No Jessica token provided"}, - {}, - ), - ( - "/items?token=jessica", - 200, - {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items/plumbus?token=jessica", - 200, - {"name": "Plumbus", "item_id": "plumbus"}, - {"X-Token": "fake-super-secret-token"}, - ), - ( - "/items/bar?token=jessica", - 404, - {"detail": "Item not found"}, - {"X-Token": "fake-super-secret-token"}, - ), - ("/items/plumbus", 422, no_jessica, {"X-Token": "fake-super-secret-token"}), - ( - "/items?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items/bar?token=jessica", - 400, - {"detail": "X-Token header invalid"}, - {"X-Token": "invalid"}, - ), - ( - "/items?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ( - "/items/plumbus?token=jessica", - 422, - { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - } - ] - }, - {}, - ), - ("/?token=jessica", 200, {"message": "Hello Bigger Applications!"}, {}), - ("/", 422, no_jessica, {}), - ], -) -def test_get_path( - path, expected_status, expected_response, headers, client: TestClient -): - response = client.get(path, headers=headers) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_users_token_jessica(client: TestClient): + response = client.get("/users?token=jessica") + assert response.status_code == 200 + assert response.json() == [{"username": "Rick"}, {"username": "Morty"}] + + +@needs_py39 +def test_users_with_no_token(client: TestClient): + response = client.get("/users") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_foo_token_jessica(client: TestClient): + response = client.get("/users/foo?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "foo"} + + +@needs_py39 +def test_users_foo_with_no_token(client: TestClient): + response = client.get("/users/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_me_token_jessica(client: TestClient): + response = client.get("/users/me?token=jessica") + assert response.status_code == 200 + assert response.json() == {"username": "fakecurrentuser"} + + +@needs_py39 +def test_users_me_with_no_token(client: TestClient): + response = client.get("/users/me") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_users_token_monica_with_no_jessica(client: TestClient): + response = client.get("/users?token=monica") + assert response.status_code == 400 + assert response.json() == {"detail": "No Jessica token provided"} + + +@needs_py39 +def test_items_token_jessica(client: TestClient): + response = client.get( + "/items?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == { + "plumbus": {"name": "Plumbus"}, + "gun": {"name": "Portal Gun"}, + } + + +@needs_py39 +def test_items_with_no_token_jessica(client: TestClient): + response = client.get("/items", headers={"X-Token": "fake-super-secret-token"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_plumbus_token_jessica(client: TestClient): + response = client.get( + "/items/plumbus?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 200 + assert response.json() == {"name": "Plumbus", "item_id": "plumbus"} + + +@needs_py39 +def test_items_bar_token_jessica(client: TestClient): + response = client.get( + "/items/bar?token=jessica", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 404 + assert response.json() == {"detail": "Item not found"} + + +@needs_py39 +def test_items_plumbus_with_no_token(client: TestClient): + response = client.get( + "/items/plumbus", headers={"X-Token": "fake-super-secret-token"} + ) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_items_with_invalid_token(client: TestClient): + response = client.get("/items?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_bar_with_invalid_token(client: TestClient): + response = client.get("/items/bar?token=jessica", headers={"X-Token": "invalid"}) + assert response.status_code == 400 + assert response.json() == {"detail": "X-Token header invalid"} + + +@needs_py39 +def test_items_with_missing_x_token_header(client: TestClient): + response = client.get("/items?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_items_plumbus_with_missing_x_token_header(client: TestClient): + response = client.get("/items/plumbus?token=jessica") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_root_token_jessica(client: TestClient): + response = client.get("/?token=jessica") + assert response.status_code == 200 + assert response.json() == {"message": "Hello Bigger Applications!"} + + +@needs_py39 +def test_root_with_no_token(client: TestClient): + response = client.get("/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_put_no_header(client: TestClient): response = client.put("/items/foo") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["query", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index 469198e0f..2476b773f 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -1,134 +1,268 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -price_missing = { - "detail": [ +@pytest.fixture +def client(): + from docs_src.body.tutorial001 import app + + client = TestClient(app) + return client + + +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - -price_not_float = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} + ) -name_price_missing = { - "detail": [ + +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_broken_body(): +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +def test_post_broken_body(client: TestClient): response = client.post( "/items/", headers={"content-type": "application/json"}, content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) -def test_post_form_for_json(): +def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_explicit_content_type(): +def test_explicit_content_type(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -137,7 +271,7 @@ def test_explicit_content_type(): assert response.status_code == 200, response.text -def test_geo_json(): +def test_geo_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -146,7 +280,7 @@ def test_geo_json(): assert response.status_code == 200, response.text -def test_no_content_type_is_json(): +def test_no_content_type_is_json(client: TestClient): response = client.post( "/items/", content='{"name": "Foo", "price": 50.5}', @@ -160,43 +294,104 @@ def test_no_content_type_is_json(): } -def test_wrong_headers(): +def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url( + "model_attributes_type" + ), # "https://errors.pydantic.dev/0.38.0/v/dict_attributes_type", + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) -def test_other_exceptions(): +def test_other_exceptions(client: TestClient): with patch("json.loads", side_effect=Exception): response = client.post("/items/", json={"test": "test2"}) assert response.status_code == 400, response.text -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -243,8 +438,26 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body/test_tutorial001_py310.py b/tests/test_tutorial/test_body/test_tutorial001_py310.py index a68b4e044..b64d86005 100644 --- a/tests/test_tutorial/test_body/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body/test_tutorial001_py310.py @@ -1,7 +1,9 @@ from unittest.mock import patch import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -14,86 +16,189 @@ def client(): return client -price_missing = { - "detail": [ - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - -price_not_float = { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] -} - -name_price_missing = { - "detail": [ - { - "loc": ["body", "name"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "price"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -body_missing = { - "detail": [ - {"loc": ["body"], "msg": "field required", "type": "value_error.missing"} - ] -} +@needs_py310 +def test_body_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/", - {"name": "Foo", "price": 50.5}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5"}, - 200, - {"name": "Foo", "price": 50.5, "description": None, "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo"}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": None}, - ), - ( - "/items/", - {"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, - 200, - {"name": "Foo", "price": 50.5, "description": "Some Foo", "tax": 0.3}, - ), - ("/items/", {"name": "Foo"}, 422, price_missing), - ("/items/", {"name": "Foo", "price": "twenty"}, 422, price_not_float), - ("/items/", {}, 422, name_price_missing), - ("/items/", None, 422, body_missing), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.post(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_with_str_float(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "50.5"}) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description(client: TestClient): + response = client.post( + "/items/", json={"name": "Foo", "price": "50.5", "description": "Some Foo"} + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": None, + } + + +@needs_py310 +def test_post_with_str_float_description_tax(client: TestClient): + response = client.post( + "/items/", + json={"name": "Foo", "price": "50.5", "description": "Some Foo", "tax": 0.3}, + ) + assert response.status_code == 200 + assert response.json() == { + "name": "Foo", + "price": 50.5, + "description": "Some Foo", + "tax": 0.3, + } + + +@needs_py310 +def test_post_with_only_name(client: TestClient): + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {"name": "Foo"}, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py310 +def test_post_with_only_name_price(client: TestClient): + response = client.post("/items/", json={"name": "Foo", "price": "twenty"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "twenty", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) + + +@needs_py310 +def test_post_with_no_data(client: TestClient): + response = client.post("/items/", json={}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "name"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "price"], + "msg": "Field required", + "input": {}, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "name"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "price"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_with_none(client: TestClient): + response = client.post("/items/", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py310 @@ -104,37 +209,69 @@ def test_post_broken_body(client: TestClient): content="{some broken json}", ) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", 1], - "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", - "type": "value_error.jsondecode", - "ctx": { - "msg": "Expecting property name enclosed in double quotes", - "doc": "{some broken json}", - "pos": 1, - "lineno": 1, - "colno": 2, - }, - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "json_invalid", + "loc": ["body", 1], + "msg": "JSON decode error", + "input": {}, + "ctx": { + "error": "Expecting property name enclosed in double quotes" + }, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", 1], + "msg": "Expecting property name enclosed in double quotes: line 1 column 2 (char 1)", + "type": "value_error.jsondecode", + "ctx": { + "msg": "Expecting property name enclosed in double quotes", + "doc": "{some broken json}", + "pos": 1, + "lineno": 1, + "colno": 2, + }, + } + ] + } + ) @needs_py310 def test_post_form_for_json(client: TestClient): response = client.post("/items/", data={"name": "Foo", "price": 50.5}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": "name=Foo&price=50.5", + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -175,32 +312,91 @@ def test_no_content_type_is_json(client: TestClient): @needs_py310 def test_wrong_headers(client: TestClient): data = '{"name": "Foo", "price": 50.5}' - invalid_dict = { - "detail": [ - { - "loc": ["body"], - "msg": "value is not a valid dict", - "type": "type_error.dict", - } - ] - } - response = client.post( "/items/", content=data, headers={"Content-Type": "text/plain"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/geo+json-seq"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) response = client.post( "/items/", content=data, headers={"Content-Type": "application/not-really-json"} ) assert response.status_code == 422, response.text - assert response.json() == invalid_dict + assert response.json() == IsDict( + { + "detail": [ + { + "type": "model_attributes_type", + "loc": ["body"], + "msg": "Input should be a valid dictionary or object to extract fields from", + "input": '{"name": "Foo", "price": 50.5}', + "url": match_pydantic_error_url("model_attributes_type"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body"], + "msg": "value is not a valid dict", + "type": "type_error.dict", + } + ] + } + ) @needs_py310 @@ -258,8 +454,26 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001.py b/tests/test_tutorial/test_body_fields/test_tutorial001.py index 4999cbf6b..1ff2d9576 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001.py @@ -1,66 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_fields.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -price_not_greater = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001 import app + + client = TestClient(app) + return client + + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -116,18 +132,39 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py index 011946d07..907d6842a 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an.py @@ -1,66 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_fields.tutorial001_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -price_not_greater = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_fields.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } + + +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} - - -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -116,18 +132,39 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py index e7dcb54e9..431d2d181 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py310 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py310 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py index f1015a03b..8cef6c154 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py39 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py39 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py39 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py index 29c8ef4e9..b48cd9ec2 100644 --- a/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_fields/test_tutorial001_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,59 +14,71 @@ def get_client(): return client -price_not_greater = { - "detail": [ - { - "ctx": {"limit_value": 0}, - "loc": ["body", "item", "price"], - "msg": "ensure this value is greater than 0", - "type": "value_error.number.not_gt", - } - ] -} +@needs_py310 +def test_items_5(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": 3.0}}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - {"item": {"name": "Foo", "price": 3.0}}, - 200, - { - "item_id": 5, - "item": {"name": "Foo", "price": 3.0, "description": None, "tax": None}, - }, - ), - ( - "/items/6", - { - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": "5.4", +def test_items_6(client: TestClient): + response = client.put( + "/items/6", + json={ + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": "5.4", + } + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 6, + "item": { + "name": "Bar", + "price": 0.2, + "description": "Some bar", + "tax": 5.4, + }, + } + + +@needs_py310 +def test_invalid_price(client: TestClient): + response = client.put("/items/5", json={"item": {"name": "Foo", "price": -3.0}}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "greater_than", + "loc": ["body", "item", "price"], + "msg": "Input should be greater than 0", + "input": -3.0, + "ctx": {"gt": 0.0}, + "url": match_pydantic_error_url("greater_than"), } - }, - 200, - { - "item_id": 6, - "item": { - "name": "Bar", - "price": 0.2, - "description": "Some bar", - "tax": 5.4, - }, - }, - ), - ("/items/5", {"item": {"name": "Foo", "price": -3.0}}, 422, price_not_greater), - ], -) -def test(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"limit_value": 0}, + "loc": ["body", "item", "price"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt", + } + ] + } + ) @needs_py310 @@ -124,18 +138,39 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": { - "title": "The description of the item", - "maxLength": 300, - "type": "string", - }, + "description": IsDict( + { + "title": "The description of the item", + "anyOf": [ + {"maxLength": 300, "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "The description of the item", + "maxLength": 300, + "type": "string", + } + ), "price": { "title": "Price", "exclusiveMinimum": 0.0, "type": "number", "description": "The price must be greater than zero", }, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py index ce41a4283..e5dc13b26 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001.py @@ -1,52 +1,74 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_multiple_params.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -item_id_not_int = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -87,7 +109,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -95,7 +126,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -110,9 +153,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py index acc4cfadc..51e8e3a4e 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py @@ -1,52 +1,74 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_multiple_params.tutorial001_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -item_id_not_int = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } + + +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -87,7 +109,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -95,7 +126,19 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -110,9 +153,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py index a8dc02a6c..8ac1f7261 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py310 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py310 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py index f31fee78e..7ada42c52 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py39 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py39 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py39 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py39 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py index 0e46df253..0a832eaf6 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,45 +14,64 @@ def get_client(): return client -item_id_not_int = { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] -} +@needs_py310 +def test_post_body_q_bar_content(client: TestClient): + response = client.put("/items/5?q=bar", json={"name": "Foo", "price": 50.5}) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "q": "bar", + } @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5?q=bar", - {"name": "Foo", "price": 50.5}, - 200, - { - "item_id": 5, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, - }, - "q": "bar", - }, - ), - ("/items/5?q=bar", None, 200, {"item_id": 5, "q": "bar"}), - ("/items/5", None, 200, {"item_id": 5}), - ("/items/foo", None, 422, item_id_not_int), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_no_body_q_bar(client: TestClient): + response = client.put("/items/5?q=bar", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5, "q": "bar"} + + +@needs_py310 +def test_post_no_body(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 200 + assert response.json() == {"item_id": 5} + + +@needs_py310 +def test_post_id_foo(client: TestClient): + response = client.put("/items/foo", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py310 @@ -95,7 +116,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -103,7 +133,19 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} + "schema": IsDict( + { + "anyOf": [ + {"$ref": "#/components/schemas/Item"}, + {"type": "null"}, + ], + "title": "Item", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"$ref": "#/components/schemas/Item"} + ) } } }, @@ -118,9 +160,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py index 8555cf88c..2046579a9 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003.py @@ -1,92 +1,147 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_multiple_params.tutorial003 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -# Test required and embedded body parameters with no bodies sent -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003 import app + + client = TestClient(app) + return client + + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -142,9 +197,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -153,7 +226,16 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py index f4d300cc5..1282483e0 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py @@ -1,92 +1,147 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_multiple_params.tutorial003_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -# Test required and embedded body parameters with no bodies sent -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_multiple_params.tutorial003_an import app + + client = TestClient(app) + return client + + +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -142,9 +197,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -153,7 +226,16 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py index afe2b2c20..577c079d0 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py310 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py index 033d5892e..0ec04151c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py39 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py39 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py index 8fcc00013..9caf5fe6c 100644 --- a/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,85 +14,136 @@ def get_client(): return client -# Test required and embedded body parameters with no bodies sent @needs_py310 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/items/5", - { - "importance": 2, - "item": {"name": "Foo", "price": 50.5}, - "user": {"username": "Dave"}, - }, - 200, - { - "item_id": 5, - "importance": 2, - "item": { - "name": "Foo", - "price": 50.5, - "description": None, - "tax": None, +def test_post_body_valid(client: TestClient): + response = client.put( + "/items/5", + json={ + "importance": 2, + "item": {"name": "Foo", "price": 50.5}, + "user": {"username": "Dave"}, + }, + ) + assert response.status_code == 200 + assert response.json() == { + "item_id": 5, + "importance": 2, + "item": { + "name": "Foo", + "price": 50.5, + "description": None, + "tax": None, + }, + "user": {"username": "Dave", "full_name": None}, + } + + +@needs_py310 +def test_post_body_no_data(client: TestClient): + response = client.put("/items/5", json=None) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), }, - "user": {"username": "Dave", "full_name": None}, - }, - ), - ( - "/items/5", - None, - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ( - "/items/5", - [], - 422, - { - "detail": [ - { - "loc": ["body", "item"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "user"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "importance"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - }, - ), - ], -) -def test_post_body(path, body, expected_status, expected_response, client: TestClient): - response = client.put(path, json=body) - assert response.status_code == expected_status - assert response.json() == expected_response + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) + + +@needs_py310 +def test_post_body_empty_list(client: TestClient): + response = client.put("/items/5", json=[]) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "item"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "user"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "importance"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "item"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "user"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "importance"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py310 @@ -150,9 +203,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "User": { @@ -161,7 +232,16 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "Body_update_item_items__item_id__put": { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py index ac39cd93f..f4a76be44 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py @@ -1,33 +1,55 @@ +import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.body_nested_models.tutorial009 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -def test_post_body(): +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_nested_models.tutorial009 import app + + client = TestClient(app) + return client + + +def test_post_body(client: TestClient): data = {"2": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 200, response.text assert response.json() == data -def test_post_invalid_body(): +def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py index 0800abe29..8ab9bcac8 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -25,15 +27,30 @@ def test_post_invalid_body(client: TestClient): data = {"foo": 2.2, "3": 3.3} response = client.post("/index-weights/", json=data) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "__key__"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "foo", "[key]"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "__key__"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 151b4b917..b02f7c81c 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,11 +1,17 @@ +import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from docs_src.body_updates.tutorial001 import app -client = TestClient(app) +@pytest.fixture(name="client") +def get_client(): + from docs_src.body_updates.tutorial001 import app + + client = TestClient(app) + return client -def test_get(): +def test_get(client: TestClient): response = client.get("/items/baz") assert response.status_code == 200, response.text assert response.json() == { @@ -17,7 +23,7 @@ def test_get(): } -def test_put(): +def test_put(client: TestClient): response = client.put( "/items/bar", json={"name": "Barz", "price": 3, "description": None} ) @@ -30,7 +36,7 @@ def test_put(): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -118,9 +124,36 @@ def test_openapi_schema(): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index c4b4b9df3..4af2652a7 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 940b4b3b8..832f45388 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -128,9 +129,36 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "price": {"title": "Price", "type": "number"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": IsDict( + { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Price", "type": "number"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py index a43394ab1..b098f259c 100644 --- a/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py +++ b/tests/test_tutorial/test_conditional_openapi/test_tutorial001.py @@ -2,13 +2,23 @@ import importlib from fastapi.testclient import TestClient -from docs_src.conditional_openapi import tutorial001 +from ...utils import needs_pydanticv2 +def get_client() -> TestClient: + from docs_src.conditional_openapi import tutorial001 + + importlib.reload(tutorial001) + + client = TestClient(tutorial001.app) + return client + + +@needs_pydanticv2 def test_disable_openapi(monkeypatch): monkeypatch.setenv("OPENAPI_URL", "") - importlib.reload(tutorial001) - client = TestClient(tutorial001.app) + # Load the client after setting the env var + client = get_client() response = client.get("/openapi.json") assert response.status_code == 404, response.text response = client.get("/docs") @@ -17,16 +27,17 @@ def test_disable_openapi(monkeypatch): assert response.status_code == 404, response.text +@needs_pydanticv2 def test_root(): - client = TestClient(tutorial001.app) + client = get_client() response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "Hello World"} +@needs_pydanticv2 def test_default_openapi(): - importlib.reload(tutorial001) - client = TestClient(tutorial001.app) + client = get_client() response = client.get("/docs") assert response.status_code == 200, response.text response = client.get("/redoc") diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001.py b/tests/test_tutorial/test_cookie_params/test_tutorial001.py index 902bed843..7d0e669ab 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001 import app @@ -56,7 +57,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py index aa5807844..2505876c8 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.cookie_params.tutorial001_an import app @@ -56,7 +57,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py index ffb55d4e1..108f78b9c 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py index 9bc38effd..8126a1052 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py index bb2953ef6..6711fa581 100644 --- a/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -62,7 +63,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Ads Id", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Ads Id", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Ads Id", "type": "string"} + ), "name": "ads_id", "in": "cookie", } diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index d2d27f8a2..ad142ec88 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.custom_request_and_route.tutorial002 import app @@ -12,16 +14,33 @@ def test_endpoint_works(): def test_exception_handler_body_access(): response = client.post("/", json={"numbers": [1, 2, 3]}) - - assert response.json() == { - "detail": { - "body": '{"numbers": [1, 2, 3]}', - "errors": [ - { - "loc": ["body"], - "msg": "value is not a valid list", - "type": "type_error.list", - } - ], + assert response.json() == IsDict( + { + "detail": { + "errors": [ + { + "type": "list_type", + "loc": ["body"], + "msg": "Input should be a valid list", + "input": {"numbers": [1, 2, 3]}, + "url": match_pydantic_error_url("list_type"), + } + ], + "body": '{"numbers": [1, 2, 3]}', + } } - } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": { + "body": '{"numbers": [1, 2, 3]}', + "errors": [ + { + "loc": ["body"], + "msg": "value is not a valid list", + "type": "type_error.list", + } + ], + } + } + ) diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial001.py b/tests/test_tutorial/test_dataclasses/test_tutorial001.py index e20c0efe9..9f1200f37 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial001.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial001.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dataclasses.tutorial001 import app @@ -19,15 +21,30 @@ def test_post_item(): def test_post_invalid_item(): response = client.post("/items/", json={"name": "Foo", "price": "invalid price"}) assert response.status_code == 422 - assert response.json() == { - "detail": [ - { - "loc": ["body", "price"], - "msg": "value is not a valid float", - "type": "type_error.float", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "float_parsing", + "loc": ["body", "price"], + "msg": "Input should be a valid number, unable to parse string as a number", + "input": "invalid price", + "url": match_pydantic_error_url("float_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "price"], + "msg": "value is not a valid float", + "type": "type_error.float", + } + ] + } + ) def test_openapi_schema(): @@ -88,8 +105,26 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index e122239d8..7d88e2861 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -51,13 +52,42 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - }, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tags": IsDict( + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + } + ), + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, } } diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 204426e8b..597757e09 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial003 import app @@ -135,11 +136,22 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - }, + "items": IsDict( + { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + "default": [], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + } + ), }, }, "HTTPValidationError": { @@ -159,7 +171,16 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001.py b/tests/test_tutorial/test_dependencies/test_tutorial001.py index a8e564ebe..d1324a641 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial001 import app @@ -52,7 +53,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -102,7 +112,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py index 4e6a329f4..79c2a1e10 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial001_an import app @@ -52,7 +53,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -102,7 +112,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py index 205aee908..7db55a1c5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py index 73593ea55..68c2dedb1 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py index 10bf84fb5..381eecb63 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -60,7 +61,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, @@ -110,7 +120,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004.py b/tests/test_tutorial/test_dependencies/test_tutorial004.py index d16fd9ef7..5c5d34cfc 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial004 import app @@ -90,7 +91,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py index 46fe97fb2..c5c1a1fb8 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dependencies.tutorial004_an import app @@ -90,7 +91,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py index c6a0fc665..6fd093ddb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py index 30431cd29..fbbe84cc9 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py index 9793c8c33..845b098e7 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -98,7 +99,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Q", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Q", "type": "string"} + ), "name": "q", "in": "query", }, diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006.py b/tests/test_tutorial/test_dependencies/test_tutorial006.py index 6fac9f8eb..704e389a5 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial006 import app @@ -8,20 +10,42 @@ client = TestClient(app) def test_get_no_headers(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py index 810537e48..5034fceba 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial006_an import app @@ -8,20 +10,42 @@ client = TestClient(app) def test_get_no_headers(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py index f17cbcfc7..3fc22dd3c 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -16,20 +18,42 @@ def get_client(): def test_get_no_headers(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012.py b/tests/test_tutorial/test_dependencies/test_tutorial012.py index af1fcde55..753e62e43 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial012 import app @@ -8,39 +10,83 @@ client = TestClient(app) def test_get_no_headers_items(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_no_headers_users(): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header_items(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py index c33d51d87..4157d4612 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.dependencies.tutorial012_an import app @@ -8,39 +10,83 @@ client = TestClient(app) def test_get_no_headers_items(): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_no_headers_users(): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) def test_get_invalid_one_header_items(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py index d7bd756b5..9e46758cb 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -16,40 +18,84 @@ def get_client(): def test_get_no_headers_items(client: TestClient): response = client.get("/items/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_get_no_headers_users(client: TestClient): response = client.get("/users/") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["header", "x-token"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["header", "x-key"], - "msg": "field required", - "type": "value_error.missing", - }, - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["header", "x-token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["header", "x-key"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["header", "x-token"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["header", "x-key"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py index 39d2005ab..7710446ce 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.extra_data_types.tutorial001 import app @@ -68,9 +69,22 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -83,26 +97,74 @@ def test_openapi_schema(): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py index 3e497a291..9951b3b51 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.extra_data_types.tutorial001_an import app @@ -68,9 +69,22 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -83,26 +97,74 @@ def test_openapi_schema(): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py index b539cf3d6..7c482b8cb 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py index efd31e63d..87473867b 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py index 733d9f406..0b71d9177 100644 --- a/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Body_read_items_items__item_id__put" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_read_items_items__item_id__put" + } + ) } } }, @@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient): "title": "Body_read_items_items__item_id__put", "type": "object", "properties": { - "start_datetime": { - "title": "Start Datetime", - "type": "string", - "format": "date-time", - }, - "end_datetime": { - "title": "End Datetime", - "type": "string", - "format": "date-time", - }, - "repeat_at": { - "title": "Repeat At", - "type": "string", - "format": "time", - }, - "process_after": { - "title": "Process After", - "type": "number", - "format": "time-delta", - }, + "start_datetime": IsDict( + { + "title": "Start Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Start Datetime", + "type": "string", + "format": "date-time", + } + ), + "end_datetime": IsDict( + { + "title": "End Datetime", + "anyOf": [ + {"type": "string", "format": "date-time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "End Datetime", + "type": "string", + "format": "date-time", + } + ), + "repeat_at": IsDict( + { + "title": "Repeat At", + "anyOf": [ + {"type": "string", "format": "time"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Repeat At", + "type": "string", + "format": "time", + } + ), + "process_after": IsDict( + { + "title": "Process After", + "anyOf": [ + {"type": "string", "format": "duration"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Process After", + "type": "number", + "format": "time-delta", + } + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 0c0988c64..217159a59 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -8,12 +8,18 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - validation_error_str_lines = [ - b"1 validation error for Request", - b"path -> item_id", - b" value is not a valid integer (type=type_error.integer)", - ] - assert response.content == b"\n".join(validation_error_str_lines) + # TODO: remove when deprecating Pydantic v1 + assert ( + # TODO: remove when deprecating Pydantic v1 + "path -> item_id" in response.text + or "'loc': ('path', 'item_id')" in response.text + ) + assert ( + # TODO: remove when deprecating Pydantic v1 + "value is not a valid integer" in response.text + or "Input should be a valid integer, unable to parse string as an integer" + in response.text + ) def test_get_http_error(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py index f356178ac..494c317ca 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial005 import app @@ -8,16 +10,32 @@ client = TestClient(app) def test_post_validation_error(): response = client.post("/items/", json={"title": "towel", "size": "XL"}) assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["body", "size"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ], - "body": {"title": "towel", "size": "XL"}, - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["body", "size"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "XL", + "url": match_pydantic_error_url("int_parsing"), + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "size"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ], + "body": {"title": "towel", "size": "XL"}, + } + ) def test_post(): diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial006.py b/tests/test_tutorial/test_handling_errors/test_tutorial006.py index 4dd1adf43..cc2b496a8 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial006.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial006.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.handling_errors.tutorial006 import app @@ -8,15 +10,30 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 422, response.text - assert response.json() == { - "detail": [ - { - "loc": ["path", "item_id"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - } - ] - } + assert response.json() == IsDict( + { + "detail": [ + { + "type": "int_parsing", + "loc": ["path", "item_id"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": match_pydantic_error_url("int_parsing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["path", "item_id"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + } + ] + } + ) def test_get_http_error(): diff --git a/tests/test_tutorial/test_header_params/test_tutorial001.py b/tests/test_tutorial/test_header_params/test_tutorial001.py index 030159dcf..746fc0502 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial001 import app @@ -20,7 +21,7 @@ def test(path, headers, expected_status, expected_response): assert response.json() == expected_response -def test_openapi(): +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -50,7 +51,16 @@ def test_openapi(): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an.py b/tests/test_tutorial/test_header_params/test_tutorial001_an.py index 3755ab758..a715228aa 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial001_an import app @@ -50,7 +51,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py index 207b3b02b..caf85bc6c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py index bf51982b7..57e0a296a 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial001_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "User-Agent", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "User-Agent", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "User-Agent", "type": "string"} + ), "name": "user-agent", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002.py b/tests/test_tutorial/test_header_params/test_tutorial002.py index 545fc836b..78bac838c 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial002 import app @@ -61,7 +62,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an.py b/tests/test_tutorial/test_header_params/test_tutorial002_an.py index cfd581e33..ffda8158f 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial002_an import app @@ -61,7 +62,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py index c8d61e42e..6f332f3ba 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -69,7 +70,16 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py index 85150d4a9..8202bc671 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -72,7 +73,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py index f189d85b5..c113ed23e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial002_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial002_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -72,7 +73,16 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": {"title": "Strange Header", "type": "string"}, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Strange Header", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Strange Header", "type": "string"} + ), "name": "strange_header", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index b2fc17b8f..268df7a3e 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial003 import app @@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -36,11 +36,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an.py b/tests/test_tutorial/test_header_params/test_tutorial003_an.py index 87fa839e2..742ed41f4 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.header_params.tutorial003_an import app @@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -36,11 +36,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py index ef6c268c5..fdac4a416 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py index 6525fd50c..c50543cc8 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py @@ -1,7 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py39 @pytest.fixture(name="client") @@ -12,7 +13,7 @@ def get_client(): return client -@needs_py310 +@needs_py39 @pytest.mark.parametrize( "path,headers,expected_status,expected_response", [ @@ -28,11 +29,10 @@ def test(path, headers, expected_status, expected_response, client: TestClient): assert response.json() == expected_response -@needs_py310 +@needs_py39 def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py index b404ce5d8..3afb355e9 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient): def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 - # insert_assert(response.json()) assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "X-Token", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "title": "X-Token", + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "X-Token", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "x-token", "in": "header", } diff --git a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py index a6e898c49..73af420ae 100644 --- a/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification @@ -33,13 +34,30 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Callback Url", - "maxLength": 2083, - "minLength": 1, - "type": "string", - "format": "uri", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "format": "uri", + "minLength": 1, + "maxLength": 2083, + }, + {"type": "null"}, + ], + "title": "Callback Url", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Callback Url", + "maxLength": 2083, + "minLength": 1, + "type": "string", + "format": "uri", + } + ), "name": "callback_url", "in": "query", } @@ -132,7 +150,16 @@ def test_openapi_schema(): "type": "object", "properties": { "id": {"title": "Id", "type": "string"}, - "title": {"title": "Title", "type": "string"}, + "title": IsDict( + { + "title": "Title", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Title", "type": "string"} + ), "customer": {"title": "Customer", "type": "string"}, "total": {"title": "Total", "type": "number"}, }, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index cd9fc520e..dd123f48d 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_advanced_configuration.tutorial004 import app @@ -68,9 +69,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py index 3b88a38c2..2d2802269 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py @@ -1,11 +1,20 @@ +import pytest from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url -from docs_src.path_operation_advanced_configuration.tutorial007 import app - -client = TestClient(app) +from ...utils import needs_pydanticv2 -def test_post(): +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -21,7 +30,8 @@ def test_post(): } -def test_post_broken_yaml(): +@needs_pydanticv2 +def test_post_broken_yaml(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -34,7 +44,8 @@ def test_post_broken_yaml(): assert response.json() == {"detail": "Invalid YAML"} -def test_post_invalid(): +@needs_pydanticv2 +def test_post_invalid(client: TestClient): yaml_data = """ name: Deadpoolio tags: @@ -45,14 +56,22 @@ def test_post_invalid(): """ response = client.post("/items/", content=yaml_data) assert response.status_code == 422, response.text + # insert_assert(response.json()) assert response.json() == { "detail": [ - {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + { + "type": "string_type", + "loc": ["tags", 3], + "msg": "Input should be a valid string", + "input": {"sneaky": "object"}, + "url": match_pydantic_error_url("string_type"), + } ] } -def test_openapi_schema(): +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py new file mode 100644 index 000000000..ef012f8a6 --- /dev/null +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py @@ -0,0 +1,106 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 200, response.text + assert response.json() == { + "name": "Deadpoolio", + "tags": ["x-force", "x-men", "x-avengers"], + } + + +@needs_pydanticv1 +def test_post_broken_yaml(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + x - x-force + x - x-men + x - x-avengers + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == {"detail": "Invalid YAML"} + + +@needs_pydanticv1 +def test_post_invalid(client: TestClient): + yaml_data = """ + name: Deadpoolio + tags: + - x-force + - x-men + - x-avengers + - sneaky: object + """ + response = client.post("/items/", content=yaml_data) + assert response.status_code == 422, response.text + assert response.json() == { + "detail": [ + {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"} + ] + } + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/x-yaml": { + "schema": { + "title": "Item", + "required": ["name", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + }, + }, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + } + } + }, + } diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index 30278caf8..e7e9a982e 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_configuration.tutorial005 import app @@ -68,9 +69,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index cf59d354c..ebfeb809c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -77,9 +78,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index a93ea8807..8e79afe96 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -77,9 +78,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index b9b58c961..90fa6adaf 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -1,4 +1,4 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_params.tutorial005 import app @@ -6,47 +6,55 @@ from docs_src.path_params.tutorial005 import app client = TestClient(app) -@pytest.mark.parametrize( - "url,status_code,expected", - [ - ( - "/models/alexnet", - 200, - {"model_name": "alexnet", "message": "Deep Learning FTW!"}, - ), - ( - "/models/lenet", - 200, - {"model_name": "lenet", "message": "LeCNN all the images"}, - ), - ( - "/models/resnet", - 200, - {"model_name": "resnet", "message": "Have some residuals"}, - ), - ( - "/models/foo", - 422, - { - "detail": [ - { - "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, - "loc": ["path", "model_name"], - "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", - "type": "type_error.enum", - } - ] - }, - ), - ], -) -def test_get_enums(url, status_code, expected): - response = client.get(url) - assert response.status_code == status_code - assert response.json() == expected +def test_get_enums_alexnet(): + response = client.get("/models/alexnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "alexnet", "message": "Deep Learning FTW!"} -def test_openapi(): +def test_get_enums_lenet(): + response = client.get("/models/lenet") + assert response.status_code == 200 + assert response.json() == {"model_name": "lenet", "message": "LeCNN all the images"} + + +def test_get_enums_resnet(): + response = client.get("/models/resnet") + assert response.status_code == 200 + assert response.json() == {"model_name": "resnet", "message": "Have some residuals"} + + +def test_get_enums_invalid(): + response = client.get("/models/foo") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "enum", + "loc": ["path", "model_name"], + "msg": "Input should be 'alexnet','resnet' or 'lenet'", + "input": "foo", + "ctx": {"expected": "'alexnet','resnet' or 'lenet'"}, + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]}, + "loc": ["path", "model_name"], + "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'", + "type": "type_error.enum", + } + ] + } + ) + + +def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text data = response.json() @@ -98,12 +106,22 @@ def test_openapi(): } }, }, - "ModelName": { - "title": "ModelName", - "enum": ["alexnet", "resnet", "lenet"], - "type": "string", - "description": "An enumeration.", - }, + "ModelName": IsDict( + { + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + } + ) + | IsDict( + { + # TODO: remove when deprecating Pydantic v1 + "title": "ModelName", + "enum": ["alexnet", "resnet", "lenet"], + "type": "string", + "description": "An enumeration.", + } + ), "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_query_params/test_tutorial005.py b/tests/test_tutorial/test_query_params/test_tutorial005.py index 6c2cba7e1..921586357 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial005.py +++ b/tests/test_tutorial/test_query_params/test_tutorial005.py @@ -1,34 +1,45 @@ -import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.query_params.tutorial005 import app client = TestClient(app) -query_required = { - "detail": [ +def test_foo_needy_very(): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == {"item_id": "foo", "needy": "very"} + + +def test_foo_no_needy(): + response = client.get("/items/foo") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} - - -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}), - ("/items/foo", 422, query_required), - ("/items/foo", 422, query_required), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_openapi_schema(): diff --git a/tests/test_tutorial/test_query_params/test_tutorial006.py b/tests/test_tutorial/test_query_params/test_tutorial006.py index 626637903..e07803d6c 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006.py @@ -1,62 +1,82 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.query_params.tutorial006 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -query_required = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params.tutorial006 import app + + c = TestClient(app) + return c + + +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } + + +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -108,7 +128,16 @@ def test_openapi_schema(): }, { "required": False, - "schema": {"title": "Limit", "type": "integer"}, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py index b6fb2f39e..6c4c0b4dc 100644 --- a/tests/test_tutorial/test_query_params/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_query_params/test_tutorial006_py310.py @@ -1,18 +1,10 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 -query_required = { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - @pytest.fixture(name="client") def get_client(): @@ -23,43 +15,69 @@ def get_client(): @needs_py310 -@pytest.mark.parametrize( - "path,expected_status,expected_response", - [ - ( - "/items/foo?needy=very", - 200, - {"item_id": "foo", "needy": "very", "skip": 0, "limit": None}, - ), - ( - "/items/foo?skip=a&limit=b", - 422, - { - "detail": [ - { - "loc": ["query", "needy"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["query", "skip"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - { - "loc": ["query", "limit"], - "msg": "value is not a valid integer", - "type": "type_error.integer", - }, - ] - }, - ), - ], -) -def test(path, expected_status, expected_response, client: TestClient): - response = client.get(path) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_foo_needy_very(client: TestClient): + response = client.get("/items/foo?needy=very") + assert response.status_code == 200 + assert response.json() == { + "item_id": "foo", + "needy": "very", + "skip": 0, + "limit": None, + } + + +@needs_py310 +def test_foo_no_needy(client: TestClient): + response = client.get("/items/foo?skip=a&limit=b") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["query", "needy"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "int_parsing", + "loc": ["query", "skip"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "a", + "url": match_pydantic_error_url("int_parsing"), + }, + { + "type": "int_parsing", + "loc": ["query", "limit"], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "b", + "url": match_pydantic_error_url("int_parsing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["query", "needy"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["query", "skip"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + { + "loc": ["query", "limit"], + "msg": "value is not a valid integer", + "type": "type_error.integer", + }, + ] + } + ) @needs_py310 @@ -115,7 +133,16 @@ def test_openapi_schema(client: TestClient): }, { "required": False, - "schema": {"title": "Limit", "type": "integer"}, + "schema": IsDict( + { + "anyOf": [{"type": "integer"}, {"type": "null"}], + "title": "Limit", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Limit", "type": "integer"} + ), "name": "limit", "in": "query", }, diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index 370ae0ff0..287c2e8f8 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -1,47 +1,70 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial010 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -regex_error = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010 import app + + client = TestClient(app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations(q_name, q, expected_status, expected_response): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -73,14 +96,32 @@ def test_openapi_schema(): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py index 1f76ef314..5b0515070 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py @@ -1,47 +1,70 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.query_params_str_validations.tutorial010_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -regex_error = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.query_params_str_validations.tutorial010_an import app + + client = TestClient(app) + return client + + +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] } - ] -} + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations(q_name, q, expected_status, expected_response): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response - - -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -73,14 +96,32 @@ def test_openapi_schema(): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py index 3a06b4bc7..d22b1ce20 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py310 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py310 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py index 1e6f93093..3e7d5d3ad 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py39 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py39 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py39 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py39 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py39 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py index 63524d291..1c3a09d39 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py310 @@ -12,42 +14,60 @@ def get_client(): return client -regex_error = { - "detail": [ - { - "ctx": {"pattern": "^fixedquery$"}, - "loc": ["query", "item-query"], - "msg": 'string does not match regex "^fixedquery$"', - "type": "value_error.str.regex", - } - ] -} +@needs_py310 +def test_query_params_str_validations_no_query(client: TestClient): + response = client.get("/items/") + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} @needs_py310 -@pytest.mark.parametrize( - "q_name,q,expected_status,expected_response", - [ - (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ( - "item-query", - "fixedquery", - 200, - {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"}, - ), - ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}), - ("item-query", "nonregexquery", 422, regex_error), - ], -) -def test_query_params_str_validations( - q_name, q, expected_status, expected_response, client: TestClient -): - url = "/items/" - if q_name and q: - url = f"{url}?{q_name}={q}" - response = client.get(url) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_query_params_str_validations_item_query_fixedquery(client: TestClient): + response = client.get("/items/", params={"item-query": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == { + "items": [{"item_id": "Foo"}, {"item_id": "Bar"}], + "q": "fixedquery", + } + + +@needs_py310 +def test_query_params_str_validations_q_fixedquery(client: TestClient): + response = client.get("/items/", params={"q": "fixedquery"}) + assert response.status_code == 200 + assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} + + +@needs_py310 +def test_query_params_str_validations_item_query_nonregexquery(client: TestClient): + response = client.get("/items/", params={"item-query": "nonregexquery"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "string_pattern_mismatch", + "loc": ["query", "item-query"], + "msg": "String should match pattern '^fixedquery$'", + "input": "nonregexquery", + "ctx": {"pattern": "^fixedquery$"}, + "url": match_pydantic_error_url("string_pattern_mismatch"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "ctx": {"pattern": "^fixedquery$"}, + "loc": ["query", "item-query"], + "msg": 'string does not match regex "^fixedquery$"', + "type": "value_error.str.regex", + } + ] + } + ) @needs_py310 @@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient): "description": "Query string for the items to search in the database that have a good match", "required": False, "deprecated": True, - "schema": { - "title": "Query string", - "maxLength": 50, - "minLength": 3, - "pattern": "^fixedquery$", - "type": "string", - "description": "Query string for the items to search in the database that have a good match", - }, + "schema": IsDict( + { + "anyOf": [ + { + "type": "string", + "minLength": 3, + "maxLength": 50, + "pattern": "^fixedquery$", + }, + {"type": "null"}, + ], + "title": "Query string", + "description": "Query string for the items to search in the database that have a good match", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Query string", + "maxLength": 50, + "minLength": 3, + "pattern": "^fixedquery$", + "type": "string", + "description": "Query string for the items to search in the database that have a good match", + } + ), "name": "item-query", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py index 164ec1193..5ba39b05d 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial011 import app @@ -49,11 +50,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py index 2afaafd92..3942ea77a 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.query_params_str_validations.tutorial011_an import app @@ -49,11 +50,23 @@ def test_openapi_schema(): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py index fafd38337..f2ec38c95 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py index f3fb47528..cd7b15679 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py index 21f348f2b..bdc729516 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py index f2c2a5a33..26ac56b2f 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient): "parameters": [ { "required": False, - "schema": { - "title": "Q", - "type": "array", - "items": {"type": "string"}, - }, + "schema": IsDict( + { + "anyOf": [ + {"type": "array", "items": {"type": "string"}}, + {"type": "null"}, + ], + "title": "Q", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Q", + "type": "array", + "items": {"type": "string"}, + } + ), "name": "q", "in": "query", } diff --git a/tests/test_tutorial/test_request_files/test_tutorial001.py b/tests/test_tutorial/test_request_files/test_tutorial001.py index 84c736180..91cc2b636 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001.py @@ -1,4 +1,6 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial001 import app @@ -19,13 +21,59 @@ file_required = { def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_file(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02.py b/tests/test_tutorial/test_request_files/test_tutorial001_02.py index 8ebe4eafd..42f75442a 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.request_files.tutorial001_02 import app @@ -53,9 +54,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -84,9 +98,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -115,14 +142,38 @@ def test_openapi_schema(): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py index 5da8b320b..f63eb339c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.request_files.tutorial001_02_an import app @@ -53,9 +54,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -84,9 +98,22 @@ def test_openapi_schema(): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -115,14 +142,38 @@ def test_openapi_schema(): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py index 166f59b1a..94b6ac67e 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py index 02ea604b2..fcb39f8f1 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py index c753e14d1..a700752a3 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_file_files__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_file_files__post" + } + ) } } }, @@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" - } + "schema": IsDict( + { + "allOf": [ + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ], + "title": "Body", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post" + } + ) } } }, @@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient): "title": "Body_create_file_files__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "Body_create_upload_file_uploadfile__post": { "title": "Body_create_upload_file_uploadfile__post", "type": "object", "properties": { - "file": {"title": "File", "type": "string", "format": "binary"} + "file": IsDict( + { + "title": "File", + "anyOf": [ + {"type": "string", "format": "binary"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "File", "type": "string", "format": "binary"} + ) }, }, "HTTPValidationError": { diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_files/test_tutorial001_an.py index 6eb2d55dc..3021eb3c3 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial001_an import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_file(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py index 4e3ef6869..04f3a4693 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,29 +14,64 @@ def get_client(): return client -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_files/test_tutorial002.py b/tests/test_tutorial/test_request_files/test_tutorial002.py index 65a8a9e61..ed9680b62 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial002 import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_files(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an.py b/tests/test_tutorial/test_request_files/test_tutorial002_an.py index 52a8e1964..ea8c1216c 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an.py @@ -1,31 +1,68 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from docs_src.request_files.tutorial002_an import app client = TestClient(app) -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - def test_post_form_no_body(): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_body_json(): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) def test_post_files(tmp_path): diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py index 6594e0116..6d5877836 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -18,29 +20,64 @@ def get_client(app: FastAPI): return client -file_required = { - "detail": [ - { - "loc": ["body", "files"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py index bfe964604..2d0445421 100644 --- a/tests/test_tutorial/test_request_files/test_tutorial002_py39.py +++ b/tests/test_tutorial/test_request_files/test_tutorial002_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -33,14 +35,60 @@ file_required = { def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "files"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "files"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001.py b/tests/test_tutorial/test_request_forms/test_tutorial001.py index 4a2a7abe9..805daeb10 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001.py @@ -1,72 +1,164 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.request_forms.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -password_required = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001 import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} -username_required = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} -username_and_password_required = { - "detail": [ + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form(path, body, expected_status, expected_response): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py index 347361344..c43a0b695 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an.py @@ -1,72 +1,164 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient - -from docs_src.request_forms.tutorial001_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -password_required = { - "detail": [ +@pytest.fixture(name="client") +def get_client(): + from docs_src.request_forms.tutorial001_an import app + + client = TestClient(app) + return client + + +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} + + +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] } - ] -} -username_required = { - "detail": [ + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] } - ] -} -username_and_password_required = { - "detail": [ + ) + + +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form(path, body, expected_status, expected_response): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py index e65a8823e..078b812aa 100644 --- a/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py @@ -1,5 +1,7 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -12,68 +14,155 @@ def get_client(): return client -password_required = { - "detail": [ - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} -username_required = { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - } - ] -} -username_and_password_required = { - "detail": [ - { - "loc": ["body", "username"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "password"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@needs_py39 +def test_post_body_form(client: TestClient): + response = client.post("/login/", data={"username": "Foo", "password": "secret"}) + assert response.status_code == 200 + assert response.json() == {"username": "Foo"} @needs_py39 -@pytest.mark.parametrize( - "path,body,expected_status,expected_response", - [ - ( - "/login/", - {"username": "Foo", "password": "secret"}, - 200, - {"username": "Foo"}, - ), - ("/login/", {"username": "Foo"}, 422, password_required), - ("/login/", {"password": "secret"}, 422, username_required), - ("/login/", None, 422, username_and_password_required), - ], -) -def test_post_body_form( - path, body, expected_status, expected_response, client: TestClient -): - response = client.post(path, data=body) - assert response.status_code == expected_status - assert response.json() == expected_response +def test_post_body_form_no_password(client: TestClient): + response = client.post("/login/", data={"username": "Foo"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_username(client: TestClient): + response = client.post("/login/", data={"password": "secret"}) + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + } + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + } + ] + } + ) + + +@needs_py39 +def test_post_body_form_no_data(client: TestClient): + response = client.post("/login/") + assert response.status_code == 422 + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/login/", json={"username": "Foo", "password": "secret"}) assert response.status_code == 422, response.text - assert response.json() == username_and_password_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "username"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "password"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "username"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "password"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py index be12656d2..cac58639f 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py @@ -1,82 +1,171 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient - -from docs_src.request_forms_and_files.tutorial001 import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001 import app -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + return app -def test_post_form_no_body(): +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_form_no_file(): +def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_file_no_token(tmp_path): +def test_post_file_no_token(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") @@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_files_and_token(tmp_path): +def test_post_files_and_token(tmp_path, app: FastAPI): patha = tmp_path / "test.txt" pathb = tmp_path / "testb.txt" patha.write_text("") @@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py index a5fcb3a94..009568048 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py @@ -1,82 +1,171 @@ +import pytest +from dirty_equals import IsDict +from fastapi import FastAPI from fastapi.testclient import TestClient - -from docs_src.request_forms_and_files.tutorial001_an import app - -client = TestClient(app) +from fastapi.utils import match_pydantic_error_url -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} +@pytest.fixture(name="app") +def get_app(): + from docs_src.request_forms_and_files.tutorial001_an import app -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} + return app -def test_post_form_no_body(): +@pytest.fixture(name="client") +def get_client(app: FastAPI): + client = TestClient(app) + return client + + +def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_form_no_file(): +def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_body_json(): +def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_file_no_token(tmp_path): +def test_post_file_no_token(tmp_path, app: FastAPI): path = tmp_path / "test.txt" path.write_bytes(b"") @@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) -def test_post_files_and_token(tmp_path): +def test_post_files_and_token(tmp_path, app: FastAPI): patha = tmp_path / "test.txt" pathb = tmp_path / "testb.txt" patha.write_text("") @@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path): } -def test_openapi_schema(): +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { diff --git a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py index 6eacb2fcf..3d007e90b 100644 --- a/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py +++ b/tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py @@ -1,6 +1,8 @@ import pytest +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient +from fastapi.utils import match_pydantic_error_url from ...utils import needs_py39 @@ -18,78 +20,154 @@ def get_client(app: FastAPI): return client -file_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -token_required = { - "detail": [ - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - -# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]} - -file_and_token_required = { - "detail": [ - { - "loc": ["body", "file"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "fileb"], - "msg": "field required", - "type": "value_error.missing", - }, - { - "loc": ["body", "token"], - "msg": "field required", - "type": "value_error.missing", - }, - ] -} - - @needs_py39 def test_post_form_no_body(client: TestClient): response = client.post("/files/") assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_form_no_file(client: TestClient): response = client.post("/files/", data={"token": "foo"}) assert response.status_code == 422, response.text - assert response.json() == file_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 def test_post_body_json(client: TestClient): response = client.post("/files/", json={"file": "Foo", "token": "Bar"}) assert response.status_code == 422, response.text - assert response.json() == file_and_token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "file"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "file"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 @@ -101,7 +179,42 @@ def test_post_file_no_token(tmp_path, app: FastAPI): with path.open("rb") as file: response = client.post("/files/", files={"file": file}) assert response.status_code == 422, response.text - assert response.json() == token_required + assert response.json() == IsDict( + { + "detail": [ + { + "type": "missing", + "loc": ["body", "fileb"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + { + "type": "missing", + "loc": ["body", "token"], + "msg": "Field required", + "input": None, + "url": match_pydantic_error_url("missing"), + }, + ] + } + ) | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "detail": [ + { + "loc": ["body", "fileb"], + "msg": "field required", + "type": "value_error.missing", + }, + { + "loc": ["body", "token"], + "msg": "field required", + "type": "value_error.missing", + }, + ] + } + ) @needs_py39 diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 9cb0419a3..20221399b 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial003 import app @@ -78,7 +79,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "UserIn": { @@ -93,7 +103,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index 8b8fe514a..e8f0658f4 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial003_01 import app @@ -78,7 +79,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "HTTPValidationError": { @@ -103,7 +113,16 @@ def test_openapi_schema(): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), "password": {"title": "Password", "type": "string"}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index 01dc8e71c..a69f8cc8d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "HTTPValidationError": { @@ -112,7 +122,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), "password": {"title": "Password", "type": "string"}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 602147b13..64dcd6cbd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "UserIn": { @@ -102,7 +112,16 @@ def test_openapi_schema(client: TestClient): "type": "string", "format": "email", }, - "full_name": {"title": "Full Name", "type": "string"}, + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 07af29207..8beb847d1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial004 import app @@ -83,7 +84,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 90147fbdd..28eb88c34 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 740a49590..9e1a21f8d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index e8c8946c5..06e5d0fd1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial005 import app @@ -106,7 +107,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 388e030bd..0f1566243 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 548a3dbd8..6e6152b9f 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.response_model.tutorial006 import app @@ -106,7 +107,16 @@ def test_openapi_schema(): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 075bb8079..9a980ab5b 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient): "properties": { "name": {"title": "Name", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "tax": {"title": "Tax", "type": "number", "default": 10.5}, }, }, diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py new file mode 100644 index 000000000..98b187355 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001 import app + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py new file mode 100644 index 000000000..3520ef61d --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py @@ -0,0 +1,127 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_pv1 import app + + client = TestClient(app) + return client + + +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py new file mode 100644 index 000000000..e63e33cda --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py @@ -0,0 +1,135 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv2 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + } + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"type": "number", "title": "Price"}, + "tax": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Tax", + }, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "description": "A very nice Item", + "name": "Foo", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py new file mode 100644 index 000000000..e036d6b68 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py @@ -0,0 +1,129 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv1 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial001_py310_pv1 import app + + client = TestClient(app) + return client + + +@needs_py310 +@needs_pydanticv1 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + # insert_assert(response.json()) + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"type": "integer", "title": "Item Id"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": {"type": "string", "title": "Description"}, + "price": {"type": "number", "title": "Price"}, + "tax": {"type": "number", "title": "Tax"}, + }, + "type": "object", + "required": ["name", "price"], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + } + ], + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py index 313cd51d6..eac0d1e29 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.schema_extra_example.tutorial004 import app @@ -41,23 +42,46 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -100,9 +124,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py index 353401b78..a9cecd098 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.schema_extra_example.tutorial004_an import app @@ -41,23 +42,46 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -100,9 +124,27 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py index 79f4e1e1e..b6a735599 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py index 1ee120705..2493194a0 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py index b77368400..15f54bd5a 100644 --- a/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": { - "allOf": [{"$ref": "#/components/schemas/Item"}], - "title": "Item", - "examples": [ - { - "name": "Foo", - "description": "A very nice Item", - "price": 35.4, - "tax": 3.2, - }, - {"name": "Bar", "price": "35.4"}, - { - "name": "Baz", - "price": "thirty five point four", - }, - ], - } + "schema": IsDict( + { + "$ref": "#/components/schemas/Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + "examples": [ + { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + {"name": "Bar", "price": "35.4"}, + { + "name": "Baz", + "price": "thirty five point four", + }, + ], + } + ) } }, "required": True, @@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), "price": {"title": "Price", "type": "number"}, - "tax": {"title": "Tax", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003.py b/tests/test_tutorial/test_security/test_tutorial003.py index cb5cdaa04..18d4680f6 100644 --- a/tests/test_tutorial/test_security/test_tutorial003.py +++ b/tests/test_tutorial/test_security/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial003 import app @@ -126,16 +127,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an.py b/tests/test_tutorial/test_security/test_tutorial003_an.py index 26e68a029..a8f64d0c6 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial003_an import app @@ -126,16 +127,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py index 1250d4afb..7cbbcee2f 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py index b74cfdc54..7b21fbcc9 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial003_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial003_py310.py b/tests/test_tutorial/test_security/test_tutorial003_py310.py index 8a75d2321..512504534 100644 --- a/tests/test_tutorial/test_security/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 4e4b6afe8..22ae76f42 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial005 import ( @@ -270,9 +271,36 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -289,16 +317,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index 51cc8329a..07239cc89 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -1,3 +1,4 @@ +from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.security.tutorial005_an import ( @@ -270,9 +271,36 @@ def test_openapi_schema(): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -289,16 +317,46 @@ def test_openapi_schema(): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index b0d0fed12..1ab836639 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index 26deaaf3c..6aabbe04a 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index e93f34c3b..c21884df8 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 737a8548f..170c5d60b 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, - "email": {"title": "Email", "type": "string"}, - "full_name": {"title": "Full Name", "type": "string"}, - "disabled": {"title": "Disabled", "type": "boolean"}, + "email": IsDict( + { + "title": "Email", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Email", "type": "string"} + ), + "full_name": IsDict( + { + "title": "Full Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Full Name", "type": "string"} + ), + "disabled": IsDict( + { + "title": "Disabled", + "anyOf": [{"type": "boolean"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Disabled", "type": "boolean"} + ), }, }, "Token": { @@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient): "required": ["username", "password"], "type": "object", "properties": { - "grant_type": { - "title": "Grant Type", - "pattern": "password", - "type": "string", - }, + "grant_type": IsDict( + { + "title": "Grant Type", + "anyOf": [ + {"pattern": "password", "type": "string"}, + {"type": "null"}, + ], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "title": "Grant Type", + "pattern": "password", + "type": "string", + } + ), "username": {"title": "Username", "type": "string"}, "password": {"title": "Password", "type": "string"}, "scope": {"title": "Scope", "type": "string", "default": ""}, - "client_id": {"title": "Client Id", "type": "string"}, - "client_secret": {"title": "Client Secret", "type": "string"}, + "client_id": IsDict( + { + "title": "Client Id", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Id", "type": "string"} + ), + "client_secret": IsDict( + { + "title": "Client Secret", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Client Secret", "type": "string"} + ), }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_settings/test_app02.py b/tests/test_tutorial/test_settings/test_app02.py index fd32b8766..eced88c04 100644 --- a/tests/test_tutorial/test_settings/test_app02.py +++ b/tests/test_tutorial/test_settings/test_app02.py @@ -1,17 +1,20 @@ -from fastapi.testclient import TestClient from pytest import MonkeyPatch -from docs_src.settings.app02 import main, test_main - -client = TestClient(main.app) +from ...utils import needs_pydanticv2 +@needs_pydanticv2 def test_settings(monkeypatch: MonkeyPatch): + from docs_src.settings.app02 import main + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") settings = main.get_settings() assert settings.app_name == "Awesome API" assert settings.items_per_user == 50 +@needs_pydanticv2 def test_override_settings(): + from docs_src.settings.app02 import test_main + test_main.test_app() diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py new file mode 100644 index 000000000..eb30dbcee --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_settings/test_tutorial001_pv1.py b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py new file mode 100644 index 000000000..e4659de66 --- /dev/null +++ b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py @@ -0,0 +1,19 @@ +from fastapi.testclient import TestClient +from pytest import MonkeyPatch + +from ...utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_settings(monkeypatch: MonkeyPatch): + monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com") + from docs_src.settings.tutorial001_pv1 import app + + client = TestClient(app) + response = client.get("/info") + assert response.status_code == 200, response.text + assert response.json() == { + "app_name": "Awesome API", + "admin_email": "admin@example.com", + "items_per_user": 50, + } diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases.py b/tests/test_tutorial/test_sql_databases/test_sql_databases.py index d927940da..03e747433 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases.py @@ -3,8 +3,11 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(tmp_path_factory: pytest.TempPathFactory): @@ -26,6 +29,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): os.chdir(cwd) +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -37,6 +42,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -45,11 +52,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -58,6 +69,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -75,6 +88,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -85,7 +100,9 @@ def test_read_items(client): assert "description" in first_item -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -313,7 +330,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -322,7 +348,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py index 08d7b3533..a503ef2a6 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py @@ -2,8 +2,11 @@ import importlib from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(): @@ -22,6 +25,8 @@ def client(): test_db.unlink() +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -33,6 +38,8 @@ def test_create_user(client): assert response.status_code == 400, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -41,11 +48,15 @@ def test_get_user(client): assert "id" in data +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -54,6 +65,8 @@ def test_get_users(client): assert "id" in data[0] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -77,6 +90,8 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -87,7 +102,9 @@ def test_read_items(client): assert "description" in first_item -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -315,7 +332,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -324,7 +350,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py index 493fb3b6b..d54cc6552 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module") @@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -42,6 +45,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -51,12 +56,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -66,6 +75,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -90,6 +101,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -101,7 +114,9 @@ def test_read_items(client): @needs_py310 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -329,7 +344,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -338,7 +362,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py index 7b56685bc..4e43995e6 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module") @@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -42,6 +45,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -51,12 +56,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -66,6 +75,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -90,6 +101,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -101,7 +114,9 @@ def test_read_items(client): @needs_py39 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -329,7 +344,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -338,7 +362,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py index 43c2b272f..b89b8b031 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -41,6 +44,8 @@ def test_create_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -50,12 +55,16 @@ def test_get_user(client): @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -65,6 +74,8 @@ def test_get_users(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -89,6 +100,8 @@ def test_create_item(client): @needs_py310 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -100,7 +113,9 @@ def test_read_items(client): @needs_py310 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -328,7 +343,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -337,7 +361,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py index fd33517db..13351bc81 100644 --- a/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py @@ -3,9 +3,10 @@ import os from pathlib import Path import pytest +from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @pytest.fixture(scope="module", name="client") @@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -41,6 +44,8 @@ def test_create_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -50,12 +55,16 @@ def test_get_user(client): @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -65,6 +74,8 @@ def test_get_users(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -89,6 +100,8 @@ def test_create_item(client): @needs_py39 +# TODO: pv2 add Pydantic v2 version +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -100,7 +113,9 @@ def test_read_items(client): @needs_py39 -def test_openapi_schema(client): +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 +def test_openapi_schema(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -328,7 +343,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), }, }, "Item": { @@ -337,7 +361,16 @@ def test_openapi_schema(client): "type": "object", "properties": { "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"}, + ), "id": {"title": "Id", "type": "integer"}, "owner_id": {"title": "Owner Id", "type": "integer"}, }, diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases.py b/tests/test_tutorial/test_sql_databases/test_testing_databases.py index 6f667dea0..ce6ce230c 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases.py @@ -4,7 +4,11 @@ from pathlib import Path import pytest +from ...utils import needs_pydanticv1 + +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py index 9e6b3f3e2..545d63c2a 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1 @needs_py310 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py index 0b27adf44..99bfd3fa8 100644 --- a/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py +++ b/tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py @@ -4,10 +4,12 @@ from pathlib import Path import pytest -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1 @needs_py39 +# TODO: pv2 add version with Pydantic v2 +@needs_pydanticv1 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory): tmp_path = tmp_path_factory.mktemp("data") cwd = os.getcwd() diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py index ac6c427ca..4350567d1 100644 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1 + @pytest.fixture(scope="module") def client(): @@ -17,6 +19,7 @@ def client(): test_db.unlink() +@needs_pydanticv1 def test_create_user(client): test_user = {"email": "johndoe@example.com", "password": "secret"} response = client.post("/users/", json=test_user) @@ -28,6 +31,7 @@ def test_create_user(client): assert response.status_code == 400, response.text +@needs_pydanticv1 def test_get_user(client): response = client.get("/users/1") assert response.status_code == 200, response.text @@ -36,11 +40,13 @@ def test_get_user(client): assert "id" in data +@needs_pydanticv1 def test_inexistent_user(client): response = client.get("/users/999") assert response.status_code == 404, response.text +@needs_pydanticv1 def test_get_users(client): response = client.get("/users/") assert response.status_code == 200, response.text @@ -52,6 +58,7 @@ def test_get_users(client): time.sleep = MagicMock() +@needs_pydanticv1 def test_get_slowusers(client): response = client.get("/slowusers/") assert response.status_code == 200, response.text @@ -60,6 +67,7 @@ def test_get_slowusers(client): assert "id" in data[0] +@needs_pydanticv1 def test_create_item(client): item = {"title": "Foo", "description": "Something that fights"} response = client.post("/users/1/items/", json=item) @@ -83,6 +91,7 @@ def test_create_item(client): assert item_to_check["description"] == item["description"] +@needs_pydanticv1 def test_read_items(client): response = client.get("/items/") assert response.status_code == 200, response.text @@ -93,6 +102,7 @@ def test_read_items(client): assert "description" in first_item +@needs_pydanticv1 def test_openapi_schema(client): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_union_body.py b/tests/test_union_body.py index 57a14b574..c15acacd1 100644 --- a/tests/test_union_body.py +++ b/tests/test_union_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -90,7 +91,18 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, + "properties": IsDict( + { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"name": {"title": "Name", "type": "string"}} + ), }, "ValidationError": { "title": "ValidationError", diff --git a/tests/test_union_inherited_body.py b/tests/test_union_inherited_body.py index c2a37d3dd..ef75d459e 100644 --- a/tests/test_union_inherited_body.py +++ b/tests/test_union_inherited_body.py @@ -1,5 +1,6 @@ from typing import Optional, Union +from dirty_equals import IsDict from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel @@ -84,14 +85,34 @@ def test_openapi_schema(): "Item": { "title": "Item", "type": "object", - "properties": {"name": {"title": "Name", "type": "string"}}, + "properties": { + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ) + }, }, "ExtendedItem": { "title": "ExtendedItem", "required": ["age"], "type": "object", "properties": { - "name": {"title": "Name", "type": "string"}, + "name": IsDict( + { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Name", "type": "string"} + ), "age": {"title": "Age", "type": "integer"}, }, }, diff --git a/tests/test_validate_response.py b/tests/test_validate_response.py index 62f51c960..cd97007a4 100644 --- a/tests/test_validate_response.py +++ b/tests/test_validate_response.py @@ -2,8 +2,9 @@ from typing import List, Optional, Union import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel app = FastAPI() @@ -50,12 +51,12 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_invalid_none(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidnone") @@ -74,10 +75,10 @@ def test_valid_none_none(): def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/tests/test_validate_response_dataclass.py b/tests/test_validate_response_dataclass.py index f2cfa7a11..0415988a0 100644 --- a/tests/test_validate_response_dataclass.py +++ b/tests/test_validate_response_dataclass.py @@ -2,8 +2,8 @@ from typing import List, Optional import pytest from fastapi import FastAPI +from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient -from pydantic import ValidationError from pydantic.dataclasses import dataclass app = FastAPI() @@ -39,15 +39,15 @@ client = TestClient(app) def test_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalid") def test_double_invalid(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/innerinvalid") def test_invalid_list(): - with pytest.raises(ValidationError): + with pytest.raises(ResponseValidationError): client.get("/items/invalidlist") diff --git a/tests/test_validate_response_recursive/__init__.py b/tests/test_validate_response_recursive/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_validate_response_recursive.py b/tests/test_validate_response_recursive/app_pv1.py similarity index 58% rename from tests/test_validate_response_recursive.py rename to tests/test_validate_response_recursive/app_pv1.py index 3a4b10e0c..4cfc4b3ee 100644 --- a/tests/test_validate_response_recursive.py +++ b/tests/test_validate_response_recursive/app_pv1.py @@ -1,7 +1,6 @@ from typing import List from fastapi import FastAPI -from fastapi.testclient import TestClient from pydantic import BaseModel app = FastAPI() @@ -49,32 +48,3 @@ def get_recursive_submodel(): } ], } - - -client = TestClient(app) - - -def test_recursive(): - response = client.get("/items/recursive") - assert response.status_code == 200, response.text - assert response.json() == { - "sub_items": [{"name": "subitem", "sub_items": []}], - "name": "item", - } - - response = client.get("/items/recursive-submodel") - assert response.status_code == 200, response.text - assert response.json() == { - "name": "item", - "sub_items1": [ - { - "name": "subitem", - "sub_items2": [ - { - "name": "subsubitem", - "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], - } - ], - } - ], - } diff --git a/tests/test_validate_response_recursive/app_pv2.py b/tests/test_validate_response_recursive/app_pv2.py new file mode 100644 index 000000000..8c93a8349 --- /dev/null +++ b/tests/test_validate_response_recursive/app_pv2.py @@ -0,0 +1,51 @@ +from typing import List + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class RecursiveItem(BaseModel): + sub_items: List["RecursiveItem"] = [] + name: str + + +RecursiveItem.model_rebuild() + + +class RecursiveSubitemInSubmodel(BaseModel): + sub_items2: List["RecursiveItemViaSubmodel"] = [] + name: str + + +class RecursiveItemViaSubmodel(BaseModel): + sub_items1: List[RecursiveSubitemInSubmodel] = [] + name: str + + +RecursiveSubitemInSubmodel.model_rebuild() +RecursiveItemViaSubmodel.model_rebuild() + + +@app.get("/items/recursive", response_model=RecursiveItem) +def get_recursive(): + return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]} + + +@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel) +def get_recursive_submodel(): + return { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py new file mode 100644 index 000000000..de578ae03 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv1 + + +@needs_pydanticv1 +def test_recursive(): + from .app_pv1 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py new file mode 100644 index 000000000..7d45e7fe4 --- /dev/null +++ b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py @@ -0,0 +1,33 @@ +from fastapi.testclient import TestClient + +from ..utils import needs_pydanticv2 + + +@needs_pydanticv2 +def test_recursive(): + from .app_pv2 import app + + client = TestClient(app) + response = client.get("/items/recursive") + assert response.status_code == 200, response.text + assert response.json() == { + "sub_items": [{"name": "subitem", "sub_items": []}], + "name": "item", + } + + response = client.get("/items/recursive-submodel") + assert response.status_code == 200, response.text + assert response.json() == { + "name": "item", + "sub_items1": [ + { + "name": "subitem", + "sub_items2": [ + { + "name": "subsubitem", + "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}], + } + ], + } + ], + } diff --git a/tests/utils.py b/tests/utils.py index 5305424c4..460c028f7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,8 +1,11 @@ import sys import pytest +from fastapi._compat import PYDANTIC_V2 needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+") needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") +needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") From bb7e5b7261b7aebaf47fdc0ee9e9e8763d69fd23 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jul 2023 17:12:58 +0000 Subject: [PATCH 1149/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f4ce74404..57595e3d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for Pydantic v2. PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo). ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: From 179e409159c4b3683cb133d2e4bd0179f8f624d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:14:54 +0200 Subject: [PATCH 1150/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 57595e3d1..946c38e10 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,6 @@ ## Latest Changes -* ✨ Add support for Pydantic v2. PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo). ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: @@ -77,6 +76,8 @@ There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept a * Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly. * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/). +* PR [#9816](https://github.com/tiangolo/fastapi/pull/9816) by [@tiangolo](https://github.com/tiangolo), included all the work done (in multiple PRs) on the beta branch (`main-pv2`). + ## 0.99.1 ### Fixes From f8356d9fffcc728062cc4dbfc905882969bd5123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 19:25:59 +0200 Subject: [PATCH 1151/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?100.0?= 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 946c38e10..3f5b076ff 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.100.0 + ✨ Support for **Pydantic v2** ✨ Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example: diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 5eb3c4de2..e9c3abe01 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.100.0-beta3" +__version__ = "0.100.0" from starlette import status as status From 5f85e2cf58f4b145141718a32ce613e3fbee2862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 7 Jul 2023 20:15:08 +0200 Subject: [PATCH 1152/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20links=20for?= =?UTF-8?q?=20self-hosted=20Swagger=20UI,=20point=20to=20v5,=20for=20OpenA?= =?UTF-8?q?PI=2031.0=20(#9834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 3.1.0 --- docs/en/docs/advanced/extending-openapi.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md index c47f939af..bec184dee 100644 --- a/docs/en/docs/advanced/extending-openapi.md +++ b/docs/en/docs/advanced/extending-openapi.md @@ -136,8 +136,8 @@ You can probably right-click each link and select an option similar to `Save lin **Swagger UI** uses the files: -* `swagger-ui-bundle.js` -* `swagger-ui.css` +* `swagger-ui-bundle.js` +* `swagger-ui.css` And **ReDoc** uses the file: From c165be380f87b7b079f98c5134cccef0c9d67e4d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 7 Jul 2023 18:15:42 +0000 Subject: [PATCH 1153/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3f5b076ff..4e176d366 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). ## 0.100.0 From 69df2fa1e591f21acad8ab22c8a4389d2c50175c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 16:34:45 +0200 Subject: [PATCH 1154/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20token=20for?= =?UTF-8?q?=20latest=20changes=20(#9842)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index f11a63848..1f7ac7b28 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v3 with: # To allow latest-changes to commit to master - token: ${{ secrets.ACTIONS_TOKEN }} + token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 @@ -32,7 +32,7 @@ jobs: limit-access-to-actor: true - uses: docker://tiangolo/latest-changes:0.0.3 with: - token: ${{ secrets.FASTAPI_LATEST_CHANGES }} + token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md latest_changes_header: '## Latest Changes\n\n' debug_logs: true From eaa14e18d3179ca4e60012aeec84bc67ee992352 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 14:37:16 +0000 Subject: [PATCH 1155/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4e176d366..105a0b75a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). ## 0.100.0 From 9213b72115870b25f86fedbdb7126b72831be077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:39:42 +0200 Subject: [PATCH 1156/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20MkDocs=20Mate?= =?UTF-8?q?rial=20token=20(#9843)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a155ecfec..19009447b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -51,7 +51,7 @@ jobs: # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Export Language Codes id: show-langs run: | @@ -86,7 +86,7 @@ jobs: run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.ACTIONS_TOKEN }}@github.com/squidfunk/mkdocs-material-insiders.git + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v3 From ea92dcaa01dc85ea8a87ddde466727e26b731c01 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:40:19 +0000 Subject: [PATCH 1157/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 105a0b75a..33a95adc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). From 73c39745d8c08158bb8d84343d1a8009d44eef09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:44:21 +0200 Subject: [PATCH 1158/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#9775)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 81 +++++++++------ docs/en/data/people.yml | 166 +++++++++++++++++-------------- 2 files changed, 139 insertions(+), 108 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 71afb66b1..56a886c68 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: nanram22 + avatarUrl: https://avatars.githubusercontent.com/u/116367316?v=4 + url: https://github.com/nanram22 - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -41,24 +44,30 @@ sponsors: - login: marvin-robot avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4 url: https://github.com/marvin-robot + - login: Flint-company + avatarUrl: https://avatars.githubusercontent.com/u/48908872?u=355cd3d8992d4be8173058e7000728757c55ad49&v=4 + url: https://github.com/Flint-company - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP - - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore + - login: petebachant + avatarUrl: https://avatars.githubusercontent.com/u/4604869?u=b17a5a4ac82f77b7efff864d439e8068d2a36593&v=4 + url: https://github.com/petebachant - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie -- - login: JonasKs - avatarUrl: https://avatars.githubusercontent.com/u/5310116?u=98a049f3e1491bffb91e1feb7e93def6881a9389&v=4 - url: https://github.com/JonasKs - - login: moellenbeck avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 url: https://github.com/moellenbeck - login: birkjernstrom avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 url: https://github.com/birkjernstrom + - login: yasyf + avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 + url: https://github.com/yasyf - login: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign @@ -92,9 +101,9 @@ sponsors: - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 url: https://github.com/jefftriplett - - login: medecau - avatarUrl: https://avatars.githubusercontent.com/u/59870?u=f9341c95adaba780828162fd4c7442357ecfcefa&v=4 - url: https://github.com/medecau + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden - login: kamalgill avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 url: https://github.com/kamalgill @@ -119,9 +128,6 @@ sponsors: - login: jqueguiner avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 url: https://github.com/jqueguiner - - login: iobruno - avatarUrl: https://avatars.githubusercontent.com/u/901651?u=460bc34ac298dca9870aafe3a1560a2ae789bc4a&v=4 - url: https://github.com/iobruno - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith @@ -173,6 +179,9 @@ sponsors: - login: iwpnd avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 url: https://github.com/iwpnd + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=ab6108a843a2fb9df0934f482375d2907609f3ff&v=4 + url: https://github.com/FernandoCelmer - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 url: https://github.com/simw @@ -191,6 +200,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow + - login: joeds13 + avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 + url: https://github.com/joeds13 - login: dannywade avatarUrl: https://avatars.githubusercontent.com/u/13680237?u=418ee985bd41577b20fde81417fb2d901e875e8a&v=4 url: https://github.com/dannywade @@ -209,12 +221,6 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: shuheng-liu - avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 - url: https://github.com/shuheng-liu - - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota - login: LarryGF avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4 url: https://github.com/LarryGF @@ -245,6 +251,9 @@ sponsors: - login: thenickben avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 url: https://github.com/thenickben + - login: adtalos + avatarUrl: https://avatars.githubusercontent.com/u/40748353?v=4 + url: https://github.com/adtalos - login: ybressler avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 url: https://github.com/ybressler @@ -257,9 +266,6 @@ sponsors: - login: thisistheplace avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 url: https://github.com/thisistheplace - - login: A-Edge - avatarUrl: https://avatars.githubusercontent.com/u/59514131?v=4 - url: https://github.com/A-Edge - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -275,6 +281,9 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare + - login: khoadaniel + avatarUrl: https://avatars.githubusercontent.com/u/84840546?v=4 + url: https://github.com/khoadaniel - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki @@ -284,9 +293,9 @@ sponsors: - login: Dagmaara avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 url: https://github.com/Dagmaara -- - login: Yarden-zamir - avatarUrl: https://avatars.githubusercontent.com/u/8178413?u=ee177a8b0f87ea56747f4d96f34cd4e9604a8217&v=4 - url: https://github.com/Yarden-zamir +- - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy @@ -323,9 +332,15 @@ sponsors: - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan + - login: NateShoffner + avatarUrl: https://avatars.githubusercontent.com/u/1712163?u=b43cc2fa3fd8bec54b7706e4b98b72543c7bfea8&v=4 + url: https://github.com/NateShoffner - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 + - login: leobiscassi + avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 + url: https://github.com/leobiscassi - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz @@ -338,9 +353,6 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: jonathanhle - avatarUrl: https://avatars.githubusercontent.com/u/3851599?u=76b9c5d2fecd6c3a16e7645231878c4507380d4d&v=4 - url: https://github.com/jonathanhle - login: nikeee avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=bbe73151f2b409c120160d032dc9aa6875ef0c4b&v=4 url: https://github.com/nikeee @@ -413,15 +425,18 @@ sponsors: - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia + - login: shuheng-liu + avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 + url: https://github.com/shuheng-liu - login: ghandic avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - - login: kadekillary + - login: kxzk avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 - url: https://github.com/kadekillary + url: https://github.com/kxzk - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -441,8 +456,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=9fbf76b9bf7786275e2900efa51d1394bcf1f06a&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=527044d90b5ebb7f8dad517db5da1f45253b774b&v=4 url: https://github.com/bnkc + - login: devbruce + avatarUrl: https://avatars.githubusercontent.com/u/35563380?u=ca4e811ac7f7b3eb1600fa63285119fcdee01188&v=4 + url: https://github.com/devbruce - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon @@ -470,15 +488,15 @@ sponsors: - login: leo-jp-edwards avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 url: https://github.com/leo-jp-edwards - - login: tamtam-fitness - avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 - url: https://github.com/tamtam-fitness - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea + - login: tomast1337 + avatarUrl: https://avatars.githubusercontent.com/u/15125899?u=2c2f2907012d820499e2c43632389184923513fe&v=4 + url: https://github.com/tomast1337 - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu @@ -491,6 +509,3 @@ sponsors: - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: xNykram - avatarUrl: https://avatars.githubusercontent.com/u/55030025?u=2c1ba313fd79d29273b5ff7c9c5cf4edfb271b29&v=4 - url: https://github.com/xNykram diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2da1c968b..dd2dbe5ea 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,17 +1,17 @@ maintainers: - login: tiangolo - answers: 1839 - prs: 398 + answers: 1844 + prs: 430 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 410 + count: 434 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu count: 237 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: Mause count: 220 @@ -29,14 +29,14 @@ experts: count: 152 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: phy25 - count: 126 - avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 - url: https://github.com/phy25 - login: jgould22 - count: 124 + count: 139 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: phy25 + count: 126 + avatarUrl: https://avatars.githubusercontent.com/u/331403?u=191cd73f0c936497c8d1931a217bb3039d050265&v=4 + url: https://github.com/phy25 - login: iudeen count: 118 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 @@ -61,34 +61,34 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: adriangb + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: yinziyan1206 count: 45 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: acidjunk count: 45 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb +- login: odiseo0 count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=1e2c2c9b39f5c9b780fb933d8995cf08ec235a47&v=4 - url: https://github.com/adriangb + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 + url: https://github.com/odiseo0 - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 -- login: odiseo0 - count: 42 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 - url: https://github.com/odiseo0 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -97,14 +97,14 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: krishnardt - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 - url: https://github.com/krishnardt - login: chbndrhnns count: 35 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 url: https://github.com/chbndrhnns +- login: krishnardt + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 + url: https://github.com/krishnardt - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -133,30 +133,30 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf -- login: nsidnev - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt -- login: zoliknemet +- login: nsidnev + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 + url: https://github.com/nsidnev +- login: n8sty count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 - url: https://github.com/zoliknemet + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: retnikt count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt +- login: zoliknemet + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 url: https://github.com/Hultner -- login: n8sty - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: harunyasar count: 17 avatarUrl: https://avatars.githubusercontent.com/u/1765494?u=5b1ab7c582db4b4016fa31affe977d10af108ad4&v=4 @@ -177,6 +177,14 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: abhint + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 + url: https://github.com/abhint +- login: nymous + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: ghost count: 15 avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 @@ -189,39 +197,39 @@ experts: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 url: https://github.com/jorgerpo -- login: ebottos94 - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 -- login: hellocoldworld - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/47581948?u=3d2186796434c507a6cb6de35189ab0ad27c356f&v=4 - url: https://github.com/hellocoldworld last_month_active: - login: jgould22 - count: 13 + count: 15 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: Kludex - count: 7 + count: 13 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: abhint count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=5b9f9f6192c83ca86a411eafd4be46d9e5828585&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 url: https://github.com/abhint - login: chrisK824 count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 -- login: djimontyp - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/53098395?u=583bade70950b277c322d35f1be2b75c7b0f189c&v=4 - url: https://github.com/djimontyp -- login: JavierSanchezCastro +- login: arjwilliams count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro + avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 + url: https://github.com/arjwilliams +- login: wu-clan + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/52145145?u=f8c9e5c8c259d248e1683fedf5027b4ee08a0967&v=4 + url: https://github.com/wu-clan +- login: Ahmed-Abdou14 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d1e1c064d57c3ad5b6481716928da840f6d5a492&v=4 + url: https://github.com/Ahmed-Abdou14 +- login: esrefzeki + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/54935247?u=193cf5a169ca05fc54995a4dceabc82c7dc6e5ea&v=4 + url: https://github.com/esrefzeki top_contributors: - login: waynerv count: 25 @@ -232,7 +240,7 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: Kludex - count: 17 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: jaystone776 @@ -241,20 +249,20 @@ top_contributors: url: https://github.com/jaystone776 - login: dmontagu count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: Xewus + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl -- login: Xewus - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: Smlep count: 10 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 @@ -275,6 +283,10 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 url: https://github.com/hard-coders +- login: Alexandrhub + count: 7 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: batlopes count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 @@ -331,17 +343,21 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 url: https://github.com/axel584 +- login: ivan-abc + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc top_reviewers: - login: Kludex - count: 117 + count: 122 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 75 + count: 79 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 74 + count: 77 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -368,6 +384,10 @@ top_reviewers: count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay +- login: Xewus + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: JarroVGIT count: 34 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 @@ -376,10 +396,6 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda -- login: Xewus - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 @@ -402,7 +418,7 @@ top_reviewers: url: https://github.com/Ryandaydev - login: dmontagu count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=58ed2a45798a4339700e2f62b2e12e6e54bf0396&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: LorhanSohaky count: 23 @@ -452,6 +468,10 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: Alexandrhub + count: 15 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -472,6 +492,10 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 url: https://github.com/axel584 +- login: ivan-abc + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 + url: https://github.com/ivan-abc - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 @@ -496,10 +520,6 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: Alexandrhub - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -524,7 +544,3 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 url: https://github.com/oandersonmagalhaes -- login: NinaHwang - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 - url: https://github.com/NinaHwang From fe91def5153683bffcf58ab9513e952d75d842be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 9 Jul 2023 17:44:40 +0200 Subject: [PATCH 1159/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20token=20(#9844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/action.yml | 5 +---- .github/actions/people/app/main.py | 3 +-- .github/workflows/people.yml | 3 +-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/actions/people/action.yml b/.github/actions/people/action.yml index 16bc8cdcb..71745b874 100644 --- a/.github/actions/people/action.yml +++ b/.github/actions/people/action.yml @@ -3,10 +3,7 @@ description: "Generate the data for the FastAPI People page" author: "Sebastián Ramírez " inputs: token: - description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.ACTION_TOKEN }}' - required: true - standard_token: - description: 'Default GitHub Action token, used for the PR. Can be passed in using {{ secrets.GITHUB_TOKEN }}' + description: 'User token, to read the GitHub API. Can be passed in using {{ secrets.FASTAPI_PEOPLE }}' required: true runs: using: 'docker' diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index 2bf59f25e..b11e3456d 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -352,7 +352,6 @@ class SponsorsResponse(BaseModel): class Settings(BaseSettings): input_token: SecretStr - input_standard_token: SecretStr github_repository: str httpx_timeout: int = 30 @@ -609,7 +608,7 @@ if __name__ == "__main__": logging.basicConfig(level=logging.INFO) settings = Settings() logging.info(f"Using config: {settings.json()}") - g = Github(settings.input_standard_token.get_secret_value()) + g = Github(settings.input_token.get_secret_value()) repo = g.get_repo(settings.github_repository) question_commentors, question_last_month_commentors, question_authors = get_experts( settings=settings diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 15ea464a1..dac526a6c 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -27,5 +27,4 @@ jobs: limit-access-to-actor: true - uses: ./.github/actions/people with: - token: ${{ secrets.ACTIONS_TOKEN }} - standard_token: ${{ secrets.FASTAPI_PEOPLE }} + token: ${{ secrets.FASTAPI_PEOPLE }} From 2d69531509fbcf875d804c3e1bed75326d86279c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:44:58 +0000 Subject: [PATCH 1160/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 33a95adc1..836d056a7 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 [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). * 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). From f7e3559bd5997f831fb9b02bef9c767a50facbc3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 9 Jul 2023 15:45:55 +0000 Subject: [PATCH 1161/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 836d056a7..72fa346d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). From 6c99e90a6b808a1d819ecc45bcf25751dd30b22b Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 27 Jul 2023 19:22:23 +0100 Subject: [PATCH 1162/1881] =?UTF-8?q?=F0=9F=90=9B=20Replace=20`MultHostUrl?= =?UTF-8?q?`=20to=20`AnyUrl`=20for=20compatibility=20with=20older=20versio?= =?UTF-8?q?ns=20of=20Pydantic=20v1=20(#9852)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/_compat.py | 4 ---- fastapi/encoders.py | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 2233fe33c..9ffcaf409 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -56,7 +56,6 @@ if PYDANTIC_V2: from pydantic.json_schema import GenerateJsonSchema as GenerateJsonSchema from pydantic.json_schema import JsonSchemaValue as JsonSchemaValue from pydantic_core import CoreSchema as CoreSchema - from pydantic_core import MultiHostUrl as MultiHostUrl from pydantic_core import PydanticUndefined, PydanticUndefinedType from pydantic_core import Url as Url from pydantic_core.core_schema import ( @@ -294,9 +293,6 @@ else: from pydantic.fields import ( # type: ignore[no-redef, attr-defined] UndefinedType as UndefinedType, # noqa: F401 ) - from pydantic.networks import ( # type: ignore[no-redef] - MultiHostDsn as MultiHostUrl, # noqa: F401 - ) from pydantic.schema import ( field_schema, get_flat_models_from_fields, diff --git a/fastapi/encoders.py b/fastapi/encoders.py index b542749f2..30493697e 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -20,10 +20,10 @@ from uuid import UUID from fastapi.types import IncEx from pydantic import BaseModel from pydantic.color import Color -from pydantic.networks import NameEmail +from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr -from ._compat import PYDANTIC_V2, MultiHostUrl, Url, _model_dump +from ._compat import PYDANTIC_V2, Url, _model_dump # Taken from Pydantic v1 as is @@ -80,7 +80,7 @@ ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = { set: list, UUID: str, Url: str, - MultiHostUrl: str, + AnyUrl: str, } From 7cdea41431a6b2abcb07cf8507592cb39f5eade1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:23:13 +0000 Subject: [PATCH 1163/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 72fa346d1..84e3c4855 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). * 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). From 703a1f200aa2c7dbba13e52407338ec1d691c1b5 Mon Sep 17 00:00:00 2001 From: Creat55 <64245796+Creat55@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:42:48 +0800 Subject: [PATCH 1164/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/zh/docs/tutorial/handling-errors.md`=20(?= =?UTF-8?q?#9485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/zh/docs/tutorial/handling-errors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/docs/tutorial/handling-errors.md b/docs/zh/docs/tutorial/handling-errors.md index 9b066bc2c..a0d66e557 100644 --- a/docs/zh/docs/tutorial/handling-errors.md +++ b/docs/zh/docs/tutorial/handling-errors.md @@ -145,7 +145,7 @@ ``` -访问 `/items/foo`,可以看到以下内容替换了默认 JSON 错误信息: +访问 `/items/foo`,可以看到默认的 JSON 错误信息: ```JSON { @@ -163,7 +163,7 @@ ``` -以下是文本格式的错误信息: +被替换为了以下文本格式的错误信息: ``` 1 validation error From 39318a39f40bec0d4919e09b397cfb44f0f623a0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:43:30 +0000 Subject: [PATCH 1165/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 84e3c4855..7219156b8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). * 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). * 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). From 608cc4fea31130dca45806da06dd50e4d0f007dd Mon Sep 17 00:00:00 2001 From: dedkot Date: Thu, 27 Jul 2023 21:47:42 +0300 Subject: [PATCH 1166/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/tutorial/request-forms.md`=20(#9841)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/request-forms.md | 92 ++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/ru/docs/tutorial/request-forms.md diff --git a/docs/ru/docs/tutorial/request-forms.md b/docs/ru/docs/tutorial/request-forms.md new file mode 100644 index 000000000..a20cf78e0 --- /dev/null +++ b/docs/ru/docs/tutorial/request-forms.md @@ -0,0 +1,92 @@ +# Данные формы + +Когда вам нужно получить поля формы вместо JSON, вы можете использовать `Form`. + +!!! info "Дополнительная информация" + Чтобы использовать формы, сначала установите `python-multipart`. + + Например, выполните команду `pip install python-multipart`. + +## Импорт `Form` + +Импортируйте `Form` из `fastapi`: + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` + +## Определение параметров `Form` + +Создайте параметры формы так же, как это делается для `Body` или `Query`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_forms/tutorial001.py!} + ``` + +Например, в одном из способов использования спецификации OAuth2 (называемом "потоком пароля") требуется отправить `username` и `password` в виде полей формы. + +Данный способ требует отправку данных для авторизации посредством формы (а не JSON) и обязательного наличия в форме строго именованных полей `username` и `password`. + +Вы можете настроить `Form` точно так же, как настраиваете и `Body` ( `Query`, `Path`, `Cookie`), включая валидации, примеры, псевдонимы (например, `user-name` вместо `username`) и т.д. + +!!! info "Дополнительная информация" + `Form` - это класс, который наследуется непосредственно от `Body`. + +!!! tip "Подсказка" + Вам необходимо явно указывать параметр `Form` при объявлении каждого поля, иначе поля будут интерпретироваться как параметры запроса или параметры тела (JSON). + +## О "полях формы" + +Обычно способ, которым HTML-формы (`
`) отправляют данные на сервер, использует "специальное" кодирование для этих данных, отличное от JSON. + +**FastAPI** гарантирует правильное чтение этих данных из соответствующего места, а не из JSON. + +!!! note "Технические детали" + Данные из форм обычно кодируются с использованием "типа медиа" `application/x-www-form-urlencoded`. + + Но когда форма содержит файлы, она кодируется как `multipart/form-data`. Вы узнаете о работе с файлами в следующей главе. + + Если вы хотите узнать больше про кодировки и поля формы, ознакомьтесь с документацией MDN для `POST` на веб-сайте. + +!!! warning "Предупреждение" + Вы можете объявлять несколько параметров `Form` в *операции пути*, но вы не можете одновременно с этим объявлять поля `Body`, которые вы ожидаете получить в виде JSON, так как запрос будет иметь тело, закодированное с использованием `application/x-www-form-urlencoded`, а не `application/json`. + + Это не ограничение **FastAPI**, это часть протокола HTTP. + +## Резюме + +Используйте `Form` для объявления входных параметров данных формы. From 6a95a3a8e74a9373f830246570d23f8d7d593da9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:48:28 +0000 Subject: [PATCH 1167/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7219156b8..ad01f3c5b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). * 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). * 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). From 943baa387f52084f80a3db2c7417093325b19917 Mon Sep 17 00:00:00 2001 From: mahone3297 <329730566@qq.com> Date: Fri, 28 Jul 2023 02:49:03 +0800 Subject: [PATCH 1168/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslations=20with=20new=20source=20files=20(#9738)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: mkdir700 --- docs/zh/docs/tutorial/body-fields.md | 76 +++++++- docs/zh/docs/tutorial/body-multiple-params.md | 166 ++++++++++++++-- docs/zh/docs/tutorial/body-nested-models.md | 184 +++++++++++++++--- docs/zh/docs/tutorial/body.md | 84 ++++++-- docs/zh/docs/tutorial/cookie-params.md | 76 +++++++- docs/zh/docs/tutorial/extra-data-types.md | 76 +++++++- docs/zh/docs/tutorial/extra-models.md | 70 +++++-- docs/zh/docs/tutorial/header-params.md | 161 +++++++++++++-- .../path-params-numeric-validations.md | 87 ++++++++- .../tutorial/query-params-str-validations.md | 14 +- docs/zh/docs/tutorial/response-model.md | 62 ++++-- docs/zh/docs/tutorial/schema-extra-example.md | 66 ++++++- docs/zh/docs/tutorial/security/first-steps.md | 23 ++- 13 files changed, 1000 insertions(+), 145 deletions(-) diff --git a/docs/zh/docs/tutorial/body-fields.md b/docs/zh/docs/tutorial/body-fields.md index 053cae71c..c153784dc 100644 --- a/docs/zh/docs/tutorial/body-fields.md +++ b/docs/zh/docs/tutorial/body-fields.md @@ -6,9 +6,41 @@ 首先,你必须导入它: -```Python hl_lines="2" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` !!! warning 注意,`Field` 是直接从 `pydantic` 导入的,而不是像其他的(`Query`,`Path`,`Body` 等)都从 `fastapi` 导入。 @@ -17,9 +49,41 @@ 然后,你可以对模型属性使用 `Field`: -```Python hl_lines="9-10" -{!../../../docs_src/body_fields/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` `Field` 的工作方式和 `Query`、`Path` 和 `Body` 相同,包括它们的参数等等也完全相同。 diff --git a/docs/zh/docs/tutorial/body-multiple-params.md b/docs/zh/docs/tutorial/body-multiple-params.md index 34fa5b638..ee2cba6df 100644 --- a/docs/zh/docs/tutorial/body-multiple-params.md +++ b/docs/zh/docs/tutorial/body-multiple-params.md @@ -8,9 +8,41 @@ 你还可以通过将默认值设置为 `None` 来将请求体参数声明为可选参数: -```Python hl_lines="17-19" -{!../../../docs_src/body_multiple_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` !!! note 请注意,在这种情况下,将从请求体获取的 `item` 是可选的。因为它的默认值为 `None`。 @@ -30,9 +62,17 @@ 但是你也可以声明多个请求体参数,例如 `item` 和 `user`: -```Python hl_lines="20" -{!../../../docs_src/body_multiple_params/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` 在这种情况下,**FastAPI** 将注意到该函数中有多个请求体参数(两个 Pydantic 模型参数)。 @@ -72,9 +112,41 @@ 但是你可以使用 `Body` 指示 **FastAPI** 将其作为请求体的另一个键进行处理。 -```Python hl_lines="22" -{!../../../docs_src/body_multiple_params/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` 在这种情况下,**FastAPI** 将期望像这样的请求体: @@ -109,9 +181,41 @@ q: str = None 比如: -```Python hl_lines="25" -{!../../../docs_src/body_multiple_params/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` !!! info `Body` 同样具有与 `Query`、`Path` 以及其他后面将看到的类完全相同的额外校验和元数据参数。 @@ -131,9 +235,41 @@ item: Item = Body(embed=True) 比如: -```Python hl_lines="15" -{!../../../docs_src/body_multiple_params/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` 在这种情况下,**FastAPI** 将期望像这样的请求体: diff --git a/docs/zh/docs/tutorial/body-nested-models.md b/docs/zh/docs/tutorial/body-nested-models.md index 7649ee6fe..7704d2624 100644 --- a/docs/zh/docs/tutorial/body-nested-models.md +++ b/docs/zh/docs/tutorial/body-nested-models.md @@ -6,9 +6,17 @@ 你可以将一个属性定义为拥有子元素的类型。例如 Python `list`: -```Python hl_lines="12" -{!../../../docs_src/body_nested_models/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` 这将使 `tags` 成为一个由元素组成的列表。不过它没有声明每个元素的类型。 @@ -21,7 +29,7 @@ 首先,从 Python 的标准库 `typing` 模块中导入 `List`: ```Python hl_lines="1" -{!../../../docs_src/body_nested_models/tutorial002.py!} +{!> ../../../docs_src/body_nested_models/tutorial002.py!} ``` ### 声明具有子类型的 List @@ -43,9 +51,23 @@ my_list: List[str] 因此,在我们的示例中,我们可以将 `tags` 明确地指定为一个「字符串列表」: -```Python hl_lines="14" -{!../../../docs_src/body_nested_models/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` ## Set 类型 @@ -55,9 +77,23 @@ Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `se 然后我们可以导入 `Set` 并将 `tag` 声明为一个由 `str` 组成的 `set`: -```Python hl_lines="1 14" -{!../../../docs_src/body_nested_models/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` 这样,即使你收到带有重复数据的请求,这些数据也会被转换为一组唯一项。 @@ -79,17 +115,45 @@ Pydantic 模型的每个属性都具有类型。 例如,我们可以定义一个 `Image` 模型: -```Python hl_lines="9 10 11" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` ### 将子模型用作类型 然后我们可以将其用作一个属性的类型: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` 这意味着 **FastAPI** 将期望类似于以下内容的请求体: @@ -122,9 +186,23 @@ Pydantic 模型的每个属性都具有类型。 例如,在 `Image` 模型中我们有一个 `url` 字段,我们可以把它声明为 Pydantic 的 `HttpUrl`,而不是 `str`: -```Python hl_lines="4 10" -{!../../../docs_src/body_nested_models/tutorial005.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` 该字符串将被检查是否为有效的 URL,并在 JSON Schema / OpenAPI 文档中进行记录。 @@ -132,9 +210,23 @@ Pydantic 模型的每个属性都具有类型。 你还可以将 Pydantic 模型用作 `list`、`set` 等的子类型: -```Python hl_lines="20" -{!../../../docs_src/body_nested_models/tutorial006.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` 这将期望(转换,校验,记录文档等)下面这样的 JSON 请求体: @@ -169,9 +261,23 @@ Pydantic 模型的每个属性都具有类型。 你可以定义任意深度的嵌套模型: -```Python hl_lines="9 14 20 23 27" -{!../../../docs_src/body_nested_models/tutorial007.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` !!! info 请注意 `Offer` 拥有一组 `Item` 而反过来 `Item` 又是一个可选的 `Image` 列表是如何发生的。 @@ -186,9 +292,17 @@ images: List[Image] 例如: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial008.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` ## 无处不在的编辑器支持 @@ -218,9 +332,17 @@ images: List[Image] 在下面的例子中,你将接受任意键为 `int` 类型并且值为 `float` 类型的 `dict`: -```Python hl_lines="15" -{!../../../docs_src/body_nested_models/tutorial009.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` !!! tip 请记住 JSON 仅支持将 `str` 作为键。 diff --git a/docs/zh/docs/tutorial/body.md b/docs/zh/docs/tutorial/body.md index f80ab5bf5..d00c96dc3 100644 --- a/docs/zh/docs/tutorial/body.md +++ b/docs/zh/docs/tutorial/body.md @@ -17,9 +17,17 @@ 首先,你需要从 `pydantic` 中导入 `BaseModel`: -```Python hl_lines="2" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` ## 创建数据模型 @@ -27,9 +35,17 @@ 使用标准的 Python 类型来声明所有属性: -```Python hl_lines="5-9" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` 和声明查询参数时一样,当一个模型属性具有默认值时,它不是必需的。否则它是一个必需属性。将默认值设为 `None` 可使其成为可选属性。 @@ -57,9 +73,17 @@ 使用与声明路径和查询参数的相同方式声明请求体,即可将其添加到「路径操作」中: -```Python hl_lines="16" -{!../../../docs_src/body/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` ...并且将它的类型声明为你创建的 `Item` 模型。 @@ -112,9 +136,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 在函数内部,你可以直接访问模型对象的所有属性: -```Python hl_lines="19" -{!../../../docs_src/body/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` ## 请求体 + 路径参数 @@ -122,9 +154,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 **FastAPI** 将识别出与路径参数匹配的函数参数应**从路径中获取**,而声明为 Pydantic 模型的函数参数应**从请求体中获取**。 -```Python hl_lines="15-16" -{!../../../docs_src/body/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` ## 请求体 + 路径参数 + 查询参数 @@ -132,9 +172,17 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 **FastAPI** 会识别它们中的每一个,并从正确的位置获取数据。 -```Python hl_lines="16" -{!../../../docs_src/body/tutorial004.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` 函数参数将依次按如下规则进行识别: diff --git a/docs/zh/docs/tutorial/cookie-params.md b/docs/zh/docs/tutorial/cookie-params.md index d67daf0f9..470fd8e82 100644 --- a/docs/zh/docs/tutorial/cookie-params.md +++ b/docs/zh/docs/tutorial/cookie-params.md @@ -6,9 +6,41 @@ 首先,导入 `Cookie`: -```Python hl_lines="3" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` ## 声明 `Cookie` 参数 @@ -17,9 +49,41 @@ 第一个值是参数的默认值,同时也可以传递所有验证参数或注释参数,来校验参数: -```Python hl_lines="9" -{!../../../docs_src/cookie_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` !!! note "技术细节" `Cookie` 、`Path` 、`Query`是兄弟类,它们都继承自公共的 `Param` 类 diff --git a/docs/zh/docs/tutorial/extra-data-types.md b/docs/zh/docs/tutorial/extra-data-types.md index ac3e07654..76d606903 100644 --- a/docs/zh/docs/tutorial/extra-data-types.md +++ b/docs/zh/docs/tutorial/extra-data-types.md @@ -55,12 +55,76 @@ 下面是一个*路径操作*的示例,其中的参数使用了上面的一些类型。 -```Python hl_lines="1 3 12-16" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` 注意,函数内的参数有原生的数据类型,你可以,例如,执行正常的日期操作,如: -```Python hl_lines="18-19" -{!../../../docs_src/extra_data_types/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` diff --git a/docs/zh/docs/tutorial/extra-models.md b/docs/zh/docs/tutorial/extra-models.md index 1fbe77be8..32f8f9df1 100644 --- a/docs/zh/docs/tutorial/extra-models.md +++ b/docs/zh/docs/tutorial/extra-models.md @@ -17,9 +17,17 @@ 下面是应该如何根据它们的密码字段以及使用位置去定义模型的大概思路: -```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" -{!../../../docs_src/extra_models/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 9 14 20 22 27-28 31-33 38-39" + {!> ../../../docs_src/extra_models/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" + {!> ../../../docs_src/extra_models/tutorial001.py!} + ``` ### 关于 `**user_in.dict()` @@ -150,9 +158,17 @@ UserInDB( 这样,我们可以仅声明模型之间的差异部分(具有明文的 `password`、具有 `hashed_password` 以及不包括密码)。 -```Python hl_lines="9 15-16 19-20 23-24" -{!../../../docs_src/extra_models/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7 13-14 17-18 21-22" + {!> ../../../docs_src/extra_models/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 15-16 19-20 23-24" + {!> ../../../docs_src/extra_models/tutorial002.py!} + ``` ## `Union` 或者 `anyOf` @@ -166,9 +182,17 @@ UserInDB( !!! note 定义一个 `Union` 类型时,首先包括最详细的类型,然后是不太详细的类型。在下面的示例中,更详细的 `PlaneItem` 位于 `Union[PlaneItem,CarItem]` 中的 `CarItem` 之前。 -```Python hl_lines="1 14-15 18-20 33" -{!../../../docs_src/extra_models/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 14-15 18-20 33" + {!> ../../../docs_src/extra_models/tutorial003.py!} + ``` ## 模型列表 @@ -176,9 +200,17 @@ UserInDB( 为此,请使用标准的 Python `typing.List`: -```Python hl_lines="1 20" -{!../../../docs_src/extra_models/tutorial004.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="18" + {!> ../../../docs_src/extra_models/tutorial004_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 20" + {!> ../../../docs_src/extra_models/tutorial004.py!} + ``` ## 任意 `dict` 构成的响应 @@ -188,9 +220,17 @@ UserInDB( 在这种情况下,你可以使用 `typing.Dict`: -```Python hl_lines="1 8" -{!../../../docs_src/extra_models/tutorial005.py!} -``` +=== "Python 3.9+" + + ```Python hl_lines="6" + {!> ../../../docs_src/extra_models/tutorial005_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 8" + {!> ../../../docs_src/extra_models/tutorial005.py!} + ``` ## 总结 diff --git a/docs/zh/docs/tutorial/header-params.md b/docs/zh/docs/tutorial/header-params.md index c4b1c38ce..22ff6dc27 100644 --- a/docs/zh/docs/tutorial/header-params.md +++ b/docs/zh/docs/tutorial/header-params.md @@ -6,9 +6,41 @@ 首先导入 `Header`: -```Python hl_lines="3" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` ## 声明 `Header` 参数 @@ -16,9 +48,41 @@ 第一个值是默认值,你可以传递所有的额外验证或注释参数: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` !!! note "技术细节" `Header` 是 `Path`, `Query` 和 `Cookie` 的兄弟类型。它也继承自通用的 `Param` 类. @@ -44,9 +108,41 @@ 如果出于某些原因,你需要禁用下划线到连字符的自动转换,设置`Header`的参数 `convert_underscores` 为 `False`: -```Python hl_lines="10" -{!../../../docs_src/header_params/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` !!! warning 在设置 `convert_underscores` 为 `False` 之前,请记住,一些HTTP代理和服务器不允许使用带有下划线的headers。 @@ -62,9 +158,50 @@ 比如, 为了声明一个 `X-Token` header 可以出现多次,你可以这样写: -```Python hl_lines="9" -{!../../../docs_src/header_params/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +=== "Python 3.9+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` 如果你与*路径操作*通信时发送两个HTTP headers,就像: diff --git a/docs/zh/docs/tutorial/path-params-numeric-validations.md b/docs/zh/docs/tutorial/path-params-numeric-validations.md index 13512a08e..78fa922b4 100644 --- a/docs/zh/docs/tutorial/path-params-numeric-validations.md +++ b/docs/zh/docs/tutorial/path-params-numeric-validations.md @@ -6,9 +6,41 @@ 首先,从 `fastapi` 导入 `Path`: -```Python hl_lines="1" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3-4" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="1" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="3" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` ## 声明元数据 @@ -16,9 +48,41 @@ 例如,要声明路径参数 `item_id`的 `title` 元数据值,你可以输入: -```Python hl_lines="8" -{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="8" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="10" + {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} + ``` !!! note 路径参数总是必需的,因为它必须是路径的一部分。 @@ -43,9 +107,14 @@ 因此,你可以将函数声明为: -```Python hl_lines="7" -{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} -``` +=== "Python 3.6 non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="7" + {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} + ``` ## 按需对参数排序的技巧 diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 070074839..7244aeade 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -4,9 +4,17 @@ 让我们以下面的应用程序为例: -```Python hl_lines="7" -{!../../../docs_src/query_params_str_validations/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} + ``` 查询参数 `q` 的类型为 `str`,默认值为 `None`,因此它是可选的。 diff --git a/docs/zh/docs/tutorial/response-model.md b/docs/zh/docs/tutorial/response-model.md index ea3d0666d..f529cb0d8 100644 --- a/docs/zh/docs/tutorial/response-model.md +++ b/docs/zh/docs/tutorial/response-model.md @@ -8,9 +8,23 @@ * `@app.delete()` * 等等。 -```Python hl_lines="17" -{!../../../docs_src/response_model/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="17 22 24-27" + {!> ../../../docs_src/response_model/tutorial001.py!} + ``` !!! note 注意,`response_model`是「装饰器」方法(`get`,`post` 等)的一个参数。不像之前的所有参数和请求体,它不属于*路径操作函数*。 @@ -58,21 +72,45 @@ FastAPI 将使用此 `response_model` 来: 相反,我们可以创建一个有明文密码的输入模型和一个没有明文密码的输出模型: -```Python hl_lines="9 11 16" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9 11 16" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` 这样,即便我们的*路径操作函数*将会返回包含密码的相同输入用户: -```Python hl_lines="24" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` ...我们已经将 `response_model` 声明为了不包含密码的 `UserOut` 模型: -```Python hl_lines="22" -{!../../../docs_src/response_model/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="22" + {!> ../../../docs_src/response_model/tutorial003.py!} + ``` 因此,**FastAPI** 将会负责过滤掉未在输出模型中声明的所有数据(使用 Pydantic)。 diff --git a/docs/zh/docs/tutorial/schema-extra-example.md b/docs/zh/docs/tutorial/schema-extra-example.md index 8f5fbfe70..816e8f68e 100644 --- a/docs/zh/docs/tutorial/schema-extra-example.md +++ b/docs/zh/docs/tutorial/schema-extra-example.md @@ -10,9 +10,17 @@ 您可以使用 `Config` 和 `schema_extra` 为Pydantic模型声明一个示例,如Pydantic 文档:定制 Schema 中所述: -```Python hl_lines="15-23" -{!../../../docs_src/schema_extra_example/tutorial001.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="13-21" + {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="15-23" + {!> ../../../docs_src/schema_extra_example/tutorial001.py!} + ``` 这些额外的信息将按原样添加到输出的JSON模式中。 @@ -20,9 +28,17 @@ 在 `Field`, `Path`, `Query`, `Body` 和其他你之后将会看到的工厂函数,你可以为JSON 模式声明额外信息,你也可以通过给工厂函数传递其他的任意参数来给JSON 模式声明额外信息,比如增加 `example`: -```Python hl_lines="4 10-13" -{!../../../docs_src/schema_extra_example/tutorial002.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="2 8-11" + {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="4 10-13" + {!> ../../../docs_src/schema_extra_example/tutorial002.py!} + ``` !!! warning 请记住,传递的那些额外参数不会添加任何验证,只会添加注释,用于文档的目的。 @@ -33,9 +49,41 @@ 比如,你可以将请求体的一个 `example` 传递给 `Body`: -```Python hl_lines="20-25" -{!../../../docs_src/schema_extra_example/tutorial003.py!} -``` +=== "Python 3.10+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="22-27" + {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="23-28" + {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="18-23" + {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="20-25" + {!> ../../../docs_src/schema_extra_example/tutorial003.py!} + ``` ## 文档 UI 中的例子 diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md index 86c3320ce..7b1052e12 100644 --- a/docs/zh/docs/tutorial/security/first-steps.md +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -20,9 +20,26 @@ 把下面的示例代码复制到 `main.py`: -```Python -{!../../../docs_src/security/tutorial001.py!} -``` +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` ## 运行 From 3ffebbcf0165faf79fe8105c7a6c878f4ba2d9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Sauvage?= Date: Thu, 27 Jul 2023 20:49:56 +0200 Subject: [PATCH 1169/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/benchmarks.md`=20(#2155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sam Courtemanche Co-authored-by: Ruidy --- docs/fr/docs/benchmarks.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/fr/docs/benchmarks.md diff --git a/docs/fr/docs/benchmarks.md b/docs/fr/docs/benchmarks.md new file mode 100644 index 000000000..d33c263a2 --- /dev/null +++ b/docs/fr/docs/benchmarks.md @@ -0,0 +1,34 @@ +# Test de performance + +Les tests de performance de TechEmpower montrent que les applications **FastAPI** tournant sous Uvicorn comme étant l'un des frameworks Python les plus rapides disponibles, seulement inférieur à Starlette et Uvicorn (tous deux utilisés au cœur de FastAPI). (*) + +Mais en prêtant attention aux tests de performance et aux comparaisons, il faut tenir compte de ce qu'il suit. + +## Tests de performance et rapidité + +Lorsque vous vérifiez les tests de performance, il est commun de voir plusieurs outils de différents types comparés comme équivalents. + +En particulier, on voit Uvicorn, Starlette et FastAPI comparés (parmi de nombreux autres outils). + +Plus le problème résolu par un outil est simple, mieux seront les performances obtenues. Et la plupart des tests de performance ne prennent pas en compte les fonctionnalités additionnelles fournies par les outils. + +La hiérarchie est la suivante : + +* **Uvicorn** : un serveur ASGI + * **Starlette** : (utilise Uvicorn) un micro-framework web + * **FastAPI**: (utilise Starlette) un micro-framework pour API disposant de fonctionnalités additionnelles pour la création d'API, avec la validation des données, etc. + +* **Uvicorn** : + * A les meilleures performances, étant donné qu'il n'a pas beaucoup de code mis-à-part le serveur en lui-même. + * On n'écrit pas une application avec uniquement Uvicorn. Cela signifie que le code devrait inclure plus ou moins, au minimum, tout le code offert par Starlette (ou **FastAPI**). Et si on fait cela, l'application finale apportera les mêmes complications que si on avait utilisé un framework et que l'on avait minimisé la quantité de code et de bugs. + * Si on compare Uvicorn, il faut le comparer à d'autre applications de serveurs comme Daphne, Hypercorn, uWSGI, etc. +* **Starlette** : + * A les seconde meilleures performances après Uvicorn. Starlette utilise en réalité Uvicorn. De ce fait, il ne peut qu’être plus "lent" qu'Uvicorn car il requiert l'exécution de plus de code. + * Cependant il nous apporte les outils pour construire une application web simple, avec un routage basé sur des chemins, etc. + * Si on compare Starlette, il faut le comparer à d'autres frameworks web (ou micorframework) comme Sanic, Flask, Django, etc. +* **FastAPI** : + * Comme Starlette, FastAPI utilise Uvicorn et ne peut donc pas être plus rapide que ce dernier. + * FastAPI apporte des fonctionnalités supplémentaires à Starlette. Des fonctionnalités qui sont nécessaires presque systématiquement lors de la création d'une API, comme la validation des données, la sérialisation. En utilisant FastAPI, on obtient une documentation automatiquement (qui ne requiert aucune manipulation pour être mise en place). + * Si on n'utilisait pas FastAPI mais directement Starlette (ou un outil équivalent comme Sanic, Flask, Responder, etc) il faudrait implémenter la validation des données et la sérialisation par nous-même. Le résultat serait donc le même dans les deux cas mais du travail supplémentaire serait à réaliser avec Starlette, surtout en considérant que la validation des données et la sérialisation représentent la plus grande quantité de code à écrire dans une application. + * De ce fait, en utilisant FastAPI on minimise le temps de développement, les bugs, le nombre de lignes de code, et on obtient les mêmes performances (si ce n'est de meilleurs performances) que l'on aurait pu avoir sans ce framework (en ayant à implémenter de nombreuses fonctionnalités importantes par nous-mêmes). + * Si on compare FastAPI, il faut le comparer à d'autres frameworks web (ou ensemble d'outils) qui fournissent la validation des données, la sérialisation et la documentation, comme Flask-apispec, NestJS, Molten, etc. From d7c6894b8bd1584cbf29cc11fb6148c40d03a52d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:50:16 +0000 Subject: [PATCH 1170/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ad01f3c5b..b203afe33 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). * 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). From 2dcf78f29526eec2d96f5312a13ea9956e62fc56 Mon Sep 17 00:00:00 2001 From: Julian Maurin Date: Thu, 27 Jul 2023 20:51:07 +0200 Subject: [PATCH 1171/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/contributing.md`=20(#2132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruidy Co-authored-by: Sebastián Ramírez --- docs/fr/docs/contributing.md | 501 +++++++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 docs/fr/docs/contributing.md diff --git a/docs/fr/docs/contributing.md b/docs/fr/docs/contributing.md new file mode 100644 index 000000000..8292f14bb --- /dev/null +++ b/docs/fr/docs/contributing.md @@ -0,0 +1,501 @@ +# Développement - Contribuer + +Tout d'abord, vous voudrez peut-être voir les moyens de base pour [aider FastAPI et obtenir de l'aide](help-fastapi.md){.internal-link target=_blank}. + +## Développement + +Si vous avez déjà cloné le dépôt et que vous savez que vous devez vous plonger dans le code, voici quelques directives pour mettre en place votre environnement. + +### Environnement virtuel avec `venv` + +Vous pouvez créer un environnement virtuel dans un répertoire en utilisant le module `venv` de Python : + +
+ +```console +$ python -m venv env +``` + +
+ +Cela va créer un répertoire `./env/` avec les binaires Python et vous pourrez alors installer des paquets pour cet environnement isolé. + +### Activer l'environnement + +Activez le nouvel environnement avec : + +=== "Linux, macOS" + +
+ + ```console + $ source ./env/bin/activate + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ .\env\Scripts\Activate.ps1 + ``` + +
+ +=== "Windows Bash" + + Ou si vous utilisez Bash pour Windows (par exemple Git Bash): + +
+ + ```console + $ source ./env/Scripts/activate + ``` + +
+ +Pour vérifier que cela a fonctionné, utilisez : + +=== "Linux, macOS, Windows Bash" + +
+ + ```console + $ which pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +=== "Windows PowerShell" + +
+ + ```console + $ Get-Command pip + + some/directory/fastapi/env/bin/pip + ``` + +
+ +Si celui-ci montre le binaire `pip` à `env/bin/pip`, alors ça a fonctionné. 🎉 + + + +!!! tip + Chaque fois que vous installez un nouveau paquet avec `pip` sous cet environnement, activez à nouveau l'environnement. + + Cela permet de s'assurer que si vous utilisez un programme terminal installé par ce paquet (comme `flit`), vous utilisez celui de votre environnement local et pas un autre qui pourrait être installé globalement. + +### Flit + +**FastAPI** utilise Flit pour build, packager et publier le projet. + +Après avoir activé l'environnement comme décrit ci-dessus, installez `flit` : + +
+ +```console +$ pip install flit + +---> 100% +``` + +
+ +Réactivez maintenant l'environnement pour vous assurer que vous utilisez le "flit" que vous venez d'installer (et non un environnement global). + +Et maintenant, utilisez `flit` pour installer les dépendances de développement : + +=== "Linux, macOS" + +
+ + ```console + $ flit install --deps develop --symlink + + ---> 100% + ``` + +
+ +=== "Windows" + + Si vous êtes sous Windows, utilisez `--pth-file` au lieu de `--symlink` : + +
+ + ```console + $ flit install --deps develop --pth-file + + ---> 100% + ``` + +
+ +Il installera toutes les dépendances et votre FastAPI local dans votre environnement local. + +#### Utiliser votre FastAPI local + +Si vous créez un fichier Python qui importe et utilise FastAPI, et que vous l'exécutez avec le Python de votre environnement local, il utilisera votre code source FastAPI local. + +Et si vous mettez à jour le code source local de FastAPI, tel qu'il est installé avec `--symlink` (ou `--pth-file` sous Windows), lorsque vous exécutez à nouveau ce fichier Python, il utilisera la nouvelle version de FastAPI que vous venez d'éditer. + +De cette façon, vous n'avez pas à "installer" votre version locale pour pouvoir tester chaque changement. + +### Formatage + +Il existe un script que vous pouvez exécuter qui formatera et nettoiera tout votre code : + +
+ +```console +$ bash scripts/format.sh +``` + +
+ +Il effectuera également un tri automatique de touts vos imports. + +Pour qu'il puisse les trier correctement, vous devez avoir FastAPI installé localement dans votre environnement, avec la commande dans la section ci-dessus en utilisant `--symlink` (ou `--pth-file` sous Windows). + +### Formatage des imports + +Il existe un autre script qui permet de formater touts les imports et de s'assurer que vous n'avez pas d'imports inutilisés : + +
+ +```console +$ bash scripts/format-imports.sh +``` + +
+ +Comme il exécute une commande après l'autre et modifie et inverse de nombreux fichiers, il prend un peu plus de temps à s'exécuter, il pourrait donc être plus facile d'utiliser fréquemment `scripts/format.sh` et `scripts/format-imports.sh` seulement avant de commit. + +## Documentation + +Tout d'abord, assurez-vous que vous configurez votre environnement comme décrit ci-dessus, qui installera toutes les exigences. + +La documentation utilise MkDocs. + +Et il y a des outils/scripts supplémentaires en place pour gérer les traductions dans `./scripts/docs.py`. + +!!! tip + Vous n'avez pas besoin de voir le code dans `./scripts/docs.py`, vous l'utilisez simplement dans la ligne de commande. + +Toute la documentation est au format Markdown dans le répertoire `./docs/fr/`. + +De nombreux tutoriels comportent des blocs de code. + +Dans la plupart des cas, ces blocs de code sont de véritables applications complètes qui peuvent être exécutées telles quelles. + +En fait, ces blocs de code ne sont pas écrits à l'intérieur du Markdown, ce sont des fichiers Python dans le répertoire `./docs_src/`. + +Et ces fichiers Python sont inclus/injectés dans la documentation lors de la génération du site. + +### Documentation pour les tests + +La plupart des tests sont en fait effectués par rapport aux exemples de fichiers sources dans la documentation. + +Cela permet de s'assurer que : + +* La documentation est à jour. +* Les exemples de documentation peuvent être exécutés tels quels. +* La plupart des fonctionnalités sont couvertes par la documentation, assurées par la couverture des tests. + +Au cours du développement local, un script build le site et vérifie les changements éventuels, puis il est rechargé en direct : + +
+ +```console +$ python ./scripts/docs.py live + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Il servira la documentation sur `http://127.0.0.1:8008`. + +De cette façon, vous pouvez modifier la documentation/les fichiers sources et voir les changements en direct. + +#### Typer CLI (facultatif) + +Les instructions ici vous montrent comment utiliser le script à `./scripts/docs.py` avec le programme `python` directement. + +Mais vous pouvez également utiliser Typer CLI, et vous obtiendrez l'auto-complétion dans votre terminal pour les commandes après l'achèvement de l'installation. + +Si vous installez Typer CLI, vous pouvez installer la complétion avec : + +
+ +```console +$ typer --install-completion + +zsh completion installed in /home/user/.bashrc. +Completion will take effect once you restart the terminal. +``` + +
+ +### Apps et documentation en même temps + +Si vous exécutez les exemples avec, par exemple : + +
+ +```console +$ uvicorn tutorial001:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Comme Uvicorn utilisera par défaut le port `8000`, la documentation sur le port `8008` n'entrera pas en conflit. + +### Traductions + +L'aide aux traductions est TRÈS appréciée ! Et cela ne peut se faire sans l'aide de la communauté. 🌎 🚀 + +Voici les étapes à suivre pour aider à la traduction. + +#### Conseils et lignes directrices + +* Vérifiez les pull requests existantes pour votre langue et ajouter des reviews demandant des changements ou les approuvant. + +!!! tip + Vous pouvez ajouter des commentaires avec des suggestions de changement aux pull requests existantes. + + Consultez les documents concernant l'ajout d'un review de pull request pour l'approuver ou demander des modifications. + +* Vérifiez dans issues pour voir s'il y a une personne qui coordonne les traductions pour votre langue. + +* Ajoutez une seule pull request par page traduite. Il sera ainsi beaucoup plus facile pour les autres de l'examiner. + +Pour les langues que je ne parle pas, je vais attendre plusieurs autres reviews de la traduction avant de merge. + +* Vous pouvez également vérifier s'il existe des traductions pour votre langue et y ajouter une review, ce qui m'aidera à savoir si la traduction est correcte et je pourrai la fusionner. + +* Utilisez les mêmes exemples en Python et ne traduisez que le texte des documents. Vous n'avez pas besoin de changer quoi que ce soit pour que cela fonctionne. + +* Utilisez les mêmes images, noms de fichiers et liens. Vous n'avez pas besoin de changer quoi que ce soit pour que cela fonctionne. + +* Pour vérifier le code à 2 lettres de la langue que vous souhaitez traduire, vous pouvez utiliser le tableau Liste des codes ISO 639-1. + +#### Langue existante + +Disons que vous voulez traduire une page pour une langue qui a déjà des traductions pour certaines pages, comme l'espagnol. + +Dans le cas de l'espagnol, le code à deux lettres est `es`. Ainsi, le répertoire des traductions espagnoles se trouve à l'adresse `docs/es/`. + +!!! tip + La langue principale ("officielle") est l'anglais, qui se trouve à l'adresse "docs/en/". + +Maintenant, lancez le serveur en live pour les documents en espagnol : + +
+ +```console +// Use the command "live" and pass the language code as a CLI argument +$ python ./scripts/docs.py live es + +[INFO] Serving on http://127.0.0.1:8008 +[INFO] Start watching changes +[INFO] Start detecting changes +``` + +
+ +Vous pouvez maintenant aller sur http://127.0.0.1:8008 et voir vos changements en direct. + +Si vous regardez le site web FastAPI docs, vous verrez que chaque langue a toutes les pages. Mais certaines pages ne sont pas traduites et sont accompagnées d'une notification concernant la traduction manquante. + +Mais si vous le gérez localement de cette manière, vous ne verrez que les pages déjà traduites. + +Disons maintenant que vous voulez ajouter une traduction pour la section [Features](features.md){.internal-link target=_blank}. + +* Copiez le fichier à : + +``` +docs/en/docs/features.md +``` + +* Collez-le exactement au même endroit mais pour la langue que vous voulez traduire, par exemple : + +``` +docs/es/docs/features.md +``` + +!!! tip + Notez que le seul changement dans le chemin et le nom du fichier est le code de langue, qui passe de `en` à `es`. + +* Ouvrez maintenant le fichier de configuration de MkDocs pour l'anglais à + +``` +docs/en/docs/mkdocs.yml +``` + +* Trouvez l'endroit où cette `docs/features.md` se trouve dans le fichier de configuration. Quelque part comme : + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +* Ouvrez le fichier de configuration MkDocs pour la langue que vous éditez, par exemple : + +``` +docs/es/docs/mkdocs.yml +``` + +* Ajoutez-le à l'endroit exact où il se trouvait pour l'anglais, par exemple : + +```YAML hl_lines="8" +site_name: FastAPI +# More stuff +nav: +- FastAPI: index.md +- Languages: + - en: / + - es: /es/ +- features.md +``` + +Assurez-vous que s'il y a d'autres entrées, la nouvelle entrée avec votre traduction est exactement dans le même ordre que dans la version anglaise. + +Si vous allez sur votre navigateur, vous verrez que maintenant les documents montrent votre nouvelle section. 🎉 + +Vous pouvez maintenant tout traduire et voir à quoi cela ressemble au fur et à mesure que vous enregistrez le fichier. + +#### Nouvelle langue + +Disons que vous voulez ajouter des traductions pour une langue qui n'est pas encore traduite, pas même quelques pages. + +Disons que vous voulez ajouter des traductions pour le Créole, et que ce n'est pas encore dans les documents. + +En vérifiant le lien ci-dessus, le code pour "Créole" est `ht`. + +L'étape suivante consiste à exécuter le script pour générer un nouveau répertoire de traduction : + +
+ +```console +// Use the command new-lang, pass the language code as a CLI argument +$ python ./scripts/docs.py new-lang ht + +Successfully initialized: docs/ht +Updating ht +Updating en +``` + +
+ +Vous pouvez maintenant vérifier dans votre éditeur de code le répertoire nouvellement créé `docs/ht/`. + +!!! tip + Créez une première demande d'extraction à l'aide de cette fonction, afin de configurer la nouvelle langue avant d'ajouter des traductions. + + Ainsi, d'autres personnes peuvent vous aider à rédiger d'autres pages pendant que vous travaillez sur la première. 🚀 + +Commencez par traduire la page principale, `docs/ht/index.md`. + +Vous pouvez ensuite continuer avec les instructions précédentes, pour une "langue existante". + +##### Nouvelle langue non prise en charge + +Si, lors de l'exécution du script du serveur en direct, vous obtenez une erreur indiquant que la langue n'est pas prise en charge, quelque chose comme : + +``` + raise TemplateNotFound(template) +jinja2.exceptions.TemplateNotFound: partials/language/xx.html +``` + +Cela signifie que le thème ne supporte pas cette langue (dans ce cas, avec un faux code de 2 lettres de `xx`). + +Mais ne vous inquiétez pas, vous pouvez définir la langue du thème en anglais et ensuite traduire le contenu des documents. + +Si vous avez besoin de faire cela, modifiez le fichier `mkdocs.yml` pour votre nouvelle langue, il aura quelque chose comme : + +```YAML hl_lines="5" +site_name: FastAPI +# More stuff +theme: + # More stuff + language: xx +``` + +Changez cette langue de `xx` (de votre code de langue) à `fr`. + +Vous pouvez ensuite relancer le serveur live. + +#### Prévisualisez le résultat + +Lorsque vous utilisez le script à `./scripts/docs.py` avec la commande `live`, il n'affiche que les fichiers et les traductions disponibles pour la langue courante. + +Mais une fois que vous avez terminé, vous pouvez tester le tout comme il le ferait en ligne. + +Pour ce faire, il faut d'abord construire tous les documents : + +
+ +```console +// Use the command "build-all", this will take a bit +$ python ./scripts/docs.py build-all + +Updating es +Updating en +Building docs for: en +Building docs for: es +Successfully built docs for: es +Copying en index.md to README.md +``` + +
+ +Cela génère tous les documents à `./docs_build/` pour chaque langue. Cela inclut l'ajout de tout fichier dont la traduction est manquante, avec une note disant que "ce fichier n'a pas encore de traduction". Mais vous n'avez rien à faire avec ce répertoire. + +Ensuite, il construit tous ces sites MkDocs indépendants pour chaque langue, les combine, et génère le résultat final à `./site/`. + +Ensuite, vous pouvez servir cela avec le commandement `serve`: + +
+ +```console +// Use the command "serve" after running "build-all" +$ python ./scripts/docs.py serve + +Warning: this is a very simple server. For development, use mkdocs serve instead. +This is here only to preview a site with translations already built. +Make sure you run the build-all command first. +Serving at: http://127.0.0.1:8008 +``` + +
+ +## Tests + +Il existe un script que vous pouvez exécuter localement pour tester tout le code et générer des rapports de couverture en HTML : + +
+ +```console +$ bash scripts/test-cov-html.sh +``` + +
+ +Cette commande génère un répertoire `./htmlcov/`, si vous ouvrez le fichier `./htmlcov/index.html` dans votre navigateur, vous pouvez explorer interactivement les régions de code qui sont couvertes par les tests, et remarquer s'il y a une région manquante. From e79dc9697ceefcc88c7ac06fe564bfec9afb008b Mon Sep 17 00:00:00 2001 From: Julian Maurin Date: Thu, 27 Jul 2023 20:51:55 +0200 Subject: [PATCH 1172/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/tutorial/index.md`=20(#2234)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ruidy Co-authored-by: Sebastián Ramírez --- docs/fr/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/fr/docs/tutorial/index.md diff --git a/docs/fr/docs/tutorial/index.md b/docs/fr/docs/tutorial/index.md new file mode 100644 index 000000000..4dc202b33 --- /dev/null +++ b/docs/fr/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutoriel - Guide utilisateur - Introduction + +Ce tutoriel vous montre comment utiliser **FastAPI** avec la plupart de ses fonctionnalités, étape par étape. + +Chaque section s'appuie progressivement sur les précédentes, mais elle est structurée de manière à séparer les sujets, afin que vous puissiez aller directement à l'un d'entre eux pour résoudre vos besoins spécifiques en matière d'API. + +Il est également conçu pour fonctionner comme une référence future. + +Vous pouvez donc revenir et voir exactement ce dont vous avez besoin. + +## Exécuter le code + +Tous les blocs de code peuvent être copiés et utilisés directement (il s'agit en fait de fichiers Python testés). + +Pour exécuter l'un de ces exemples, copiez le code dans un fichier `main.py`, et commencez `uvicorn` avec : + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +Il est **FORTEMENT encouragé** que vous écriviez ou copiez le code, l'éditiez et l'exécutiez localement. + +L'utiliser dans votre éditeur est ce qui vous montre vraiment les avantages de FastAPI, en voyant le peu de code que vous avez à écrire, toutes les vérifications de type, l'autocomplétion, etc. + +--- + +## Installer FastAPI + +La première étape consiste à installer FastAPI. + +Pour le tutoriel, vous voudrez peut-être l'installer avec toutes les dépendances et fonctionnalités optionnelles : + +
+ +```console +$ pip install fastapi[all] + +---> 100% +``` + +
+ +... qui comprend également `uvicorn`, que vous pouvez utiliser comme serveur pour exécuter votre code. + +!!! note + Vous pouvez également l'installer pièce par pièce. + + C'est ce que vous feriez probablement une fois que vous voudrez déployer votre application en production : + + ``` + pip install fastapi + ``` + + Installez également `uvicorn` pour qu'il fonctionne comme serveur : + + ``` + pip install uvicorn + ``` + + Et la même chose pour chacune des dépendances facultatives que vous voulez utiliser. + +## Guide utilisateur avancé + +Il existe également un **Guide d'utilisation avancé** que vous pouvez lire plus tard après ce **Tutoriel - Guide d'utilisation**. + +Le **Guide d'utilisation avancé**, qui s'appuie sur cette base, utilise les mêmes concepts et vous apprend quelques fonctionnalités supplémentaires. + +Mais vous devez d'abord lire le **Tutoriel - Guide d'utilisation** (ce que vous êtes en train de lire en ce moment). + +Il est conçu pour que vous puissiez construire une application complète avec seulement le **Tutoriel - Guide d'utilisation**, puis l'étendre de différentes manières, en fonction de vos besoins, en utilisant certaines des idées supplémentaires du **Guide d'utilisation avancé**. From 35707a1b2913e3f3d2f478e2752178d71357563a Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:51:59 +0000 Subject: [PATCH 1173/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b203afe33..fda474978 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/benchmarks.md`. PR [#2155](https://github.com/tiangolo/fastapi/pull/2155) by [@clemsau](https://github.com/clemsau). * 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). From 02ed00cc471862588479ffc364c1c908ef0b35f5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:53:03 +0000 Subject: [PATCH 1174/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fda474978..f8837f2d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/benchmarks.md`. PR [#2155](https://github.com/tiangolo/fastapi/pull/2155) by [@clemsau](https://github.com/clemsau). * 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). From 04b9a67cbb12553b0011f240c4d3ef39913ab0df Mon Sep 17 00:00:00 2001 From: Sam Courtemanche Date: Thu, 27 Jul 2023 20:53:21 +0200 Subject: [PATCH 1175/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20French=20transla?= =?UTF-8?q?tion=20for=20`docs/fr/docs/tutorial/query-params-str-validation?= =?UTF-8?q?s.md`=20(#4075)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Julian Maurin Co-authored-by: Sebastián Ramírez --- .../tutorial/query-params-str-validations.md | 305 ++++++++++++++++++ 1 file changed, 305 insertions(+) create mode 100644 docs/fr/docs/tutorial/query-params-str-validations.md diff --git a/docs/fr/docs/tutorial/query-params-str-validations.md b/docs/fr/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..f5248fe8b --- /dev/null +++ b/docs/fr/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,305 @@ +# Paramètres de requête et validations de chaînes de caractères + +**FastAPI** vous permet de déclarer des informations et des validateurs additionnels pour vos paramètres de requêtes. + +Commençons avec cette application pour exemple : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial001.py!} +``` + +Le paramètre de requête `q` a pour type `Union[str, None]` (ou `str | None` en Python 3.10), signifiant qu'il est de type `str` mais pourrait aussi être égal à `None`, et bien sûr, la valeur par défaut est `None`, donc **FastAPI** saura qu'il n'est pas requis. + +!!! note + **FastAPI** saura que la valeur de `q` n'est pas requise grâce à la valeur par défaut `= None`. + + Le `Union` dans `Union[str, None]` permettra à votre éditeur de vous offrir un meilleur support et de détecter les erreurs. + +## Validation additionnelle + +Nous allons imposer que bien que `q` soit un paramètre optionnel, dès qu'il est fourni, **sa longueur n'excède pas 50 caractères**. + +## Importer `Query` + +Pour cela, importez d'abord `Query` depuis `fastapi` : + +```Python hl_lines="3" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +## Utiliser `Query` comme valeur par défaut + +Construisez ensuite la valeur par défaut de votre paramètre avec `Query`, en choisissant 50 comme `max_length` : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +Comme nous devons remplacer la valeur par défaut `None` dans la fonction par `Query()`, nous pouvons maintenant définir la valeur par défaut avec le paramètre `Query(default=None)`, il sert le même objectif qui est de définir cette valeur par défaut. + +Donc : + +```Python +q: Union[str, None] = Query(default=None) +``` + +... rend le paramètre optionnel, et est donc équivalent à : + +```Python +q: Union[str, None] = None +``` + +Mais déclare explicitement `q` comme étant un paramètre de requête. + +!!! info + Gardez à l'esprit que la partie la plus importante pour rendre un paramètre optionnel est : + + ```Python + = None + ``` + + ou : + + ```Python + = Query(None) + ``` + + et utilisera ce `None` pour détecter que ce paramètre de requête **n'est pas requis**. + + Le `Union[str, None]` est uniquement là pour permettre à votre éditeur un meilleur support. + +Ensuite, nous pouvons passer d'autres paramètres à `Query`. Dans cet exemple, le paramètre `max_length` qui s'applique aux chaînes de caractères : + +```Python +q: Union[str, None] = Query(default=None, max_length=50) +``` + +Cela va valider les données, montrer une erreur claire si ces dernières ne sont pas valides, et documenter le paramètre dans le schéma `OpenAPI` de cette *path operation*. + +## Rajouter plus de validation + +Vous pouvez aussi rajouter un second paramètre `min_length` : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial003.py!} +``` + +## Ajouter des validations par expressions régulières + +On peut définir une expression régulière à laquelle le paramètre doit correspondre : + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial004.py!} +``` + +Cette expression régulière vérifie que la valeur passée comme paramètre : + +* `^` : commence avec les caractères qui suivent, avec aucun caractère avant ceux-là. +* `fixedquery` : a pour valeur exacte `fixedquery`. +* `$` : se termine directement ensuite, n'a pas d'autres caractères après `fixedquery`. + +Si vous vous sentez perdu avec le concept d'**expression régulière**, pas d'inquiétudes. Il s'agit d'une notion difficile pour beaucoup, et l'on peut déjà réussir à faire beaucoup sans jamais avoir à les manipuler. + +Mais si vous décidez d'apprendre à les utiliser, sachez qu'ensuite vous pouvez les utiliser directement dans **FastAPI**. + +## Valeurs par défaut + +De la même façon que vous pouvez passer `None` comme premier argument pour l'utiliser comme valeur par défaut, vous pouvez passer d'autres valeurs. + +Disons que vous déclarez le paramètre `q` comme ayant une longueur minimale de `3`, et une valeur par défaut étant `"fixedquery"` : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note "Rappel" + Avoir une valeur par défaut rend le paramètre optionnel. + +## Rendre ce paramètre requis + +Quand on ne déclare ni validation, ni métadonnée, on peut rendre le paramètre `q` requis en ne lui déclarant juste aucune valeur par défaut : + +```Python +q: str +``` + +à la place de : + +```Python +q: Union[str, None] = None +``` + +Mais maintenant, on déclare `q` avec `Query`, comme ceci : + +```Python +q: Union[str, None] = Query(default=None, min_length=3) +``` + +Donc pour déclarer une valeur comme requise tout en utilisant `Query`, il faut utiliser `...` comme premier argument : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +!!! info + Si vous n'avez jamais vu ce `...` auparavant : c'est une des constantes natives de Python appelée "Ellipsis". + +Cela indiquera à **FastAPI** que la présence de ce paramètre est obligatoire. + +## Liste de paramètres / valeurs multiples via Query + +Quand on définit un paramètre de requête explicitement avec `Query` on peut aussi déclarer qu'il reçoit une liste de valeur, ou des "valeurs multiples". + +Par exemple, pour déclarer un paramètre de requête `q` qui peut apparaître plusieurs fois dans une URL, on écrit : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial011.py!} +``` + +Ce qui fait qu'avec une URL comme : + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +vous recevriez les valeurs des multiples paramètres de requête `q` (`foo` et `bar`) dans une `list` Python au sein de votre fonction de **path operation**, dans le paramètre de fonction `q`. + +Donc la réponse de cette URL serait : + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "Astuce" + Pour déclarer un paramètre de requête de type `list`, comme dans l'exemple ci-dessus, il faut explicitement utiliser `Query`, sinon cela sera interprété comme faisant partie du corps de la requête. + +La documentation sera donc mise à jour automatiquement pour autoriser plusieurs valeurs : + + + +### Combiner liste de paramètres et valeurs par défaut + +Et l'on peut aussi définir une liste de valeurs par défaut si aucune n'est fournie : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial012.py!} +``` + +Si vous allez à : + +``` +http://localhost:8000/items/ +``` + +la valeur par défaut de `q` sera : `["foo", "bar"]` + +et la réponse sera : + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### Utiliser `list` + +Il est aussi possible d'utiliser directement `list` plutôt que `List[str]` : + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note + Dans ce cas-là, **FastAPI** ne vérifiera pas le contenu de la liste. + + Par exemple, `List[int]` vérifiera (et documentera) que la liste est bien entièrement composée d'entiers. Alors qu'un simple `list` ne ferait pas cette vérification. + +## Déclarer des métadonnées supplémentaires + +On peut aussi ajouter plus d'informations sur le paramètre. + +Ces informations seront incluses dans le schéma `OpenAPI` généré et utilisées par la documentation interactive ou les outils externes utilisés. + +!!! note + Gardez en tête que les outils externes utilisés ne supportent pas forcément tous parfaitement OpenAPI. + + Il se peut donc que certains d'entre eux n'utilisent pas toutes les métadonnées que vous avez déclarées pour le moment, bien que dans la plupart des cas, les fonctionnalités manquantes ont prévu d'être implémentées. + +Vous pouvez ajouter un `title` : + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial007.py!} +``` + +Et une `description` : + +```Python hl_lines="13" +{!../../../docs_src/query_params_str_validations/tutorial008.py!} +``` + +## Alias de paramètres + +Imaginez que vous vouliez que votre paramètre se nomme `item-query`. + +Comme dans la requête : + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +Mais `item-query` n'est pas un nom de variable valide en Python. + +Le nom le plus proche serait `item_query`. + +Mais vous avez vraiment envie que ce soit exactement `item-query`... + +Pour cela vous pouvez déclarer un `alias`, et cet alias est ce qui sera utilisé pour trouver la valeur du paramètre : + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial009.py!} +``` + +## Déprécier des paramètres + +Disons que vous ne vouliez plus utiliser ce paramètre désormais. + +Il faut qu'il continue à exister pendant un certain temps car vos clients l'utilisent, mais vous voulez que la documentation mentionne clairement que ce paramètre est déprécié. + +On utilise alors l'argument `deprecated=True` de `Query` : + +```Python hl_lines="18" +{!../../../docs_src/query_params_str_validations/tutorial010.py!} +``` + +La documentation le présentera comme il suit : + + + +## Pour résumer + +Il est possible d'ajouter des validateurs et métadonnées pour vos paramètres. + +Validateurs et métadonnées génériques: + +* `alias` +* `title` +* `description` +* `deprecated` + +Validateurs spécifiques aux chaînes de caractères : + +* `min_length` +* `max_length` +* `regex` + +Parmi ces exemples, vous avez pu voir comment déclarer des validateurs pour les chaînes de caractères. + +Dans les prochains chapitres, vous verrez comment déclarer des validateurs pour d'autres types, comme les nombres. From 570ca011f91346fa0d79bf3d9ae5ba629165f25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 27 Jul 2023 20:53:51 +0200 Subject: [PATCH 1176/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Fern=20(#9956)?= 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/docs/img/sponsors/fern-banner.png | Bin 0 -> 8801 bytes docs/en/docs/img/sponsors/fern.png | Bin 0 -> 10924 bytes docs/en/overrides/main.html | 6 ++++++ 5 files changed, 10 insertions(+) create mode 100644 docs/en/docs/img/sponsors/fern-banner.png create mode 100644 docs/en/docs/img/sponsors/fern.png diff --git a/README.md b/README.md index 36c71081e..f0e76c4b6 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 1b5240b5e..33d57c873 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,6 +5,9 @@ gold: - url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023 title: "Build, run and scale your apps on a modern, reliable, and secure PaaS." img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png + - url: https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge + title: Fern | SDKs and API docs + img: https://fastapi.tiangolo.com/img/sponsors/fern.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/fern-banner.png b/docs/en/docs/img/sponsors/fern-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..1b70ab96d9548145028a8667013843cc810a5ee4 GIT binary patch literal 8801 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 literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 4608880f2..fcd1704b9 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -34,6 +34,12 @@
+
{% endblock %} From a52875c6563ccf17596287170d42d27676947891 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:54:32 +0000 Subject: [PATCH 1177/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f8837f2d0..2f9aada99 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/benchmarks.md`. PR [#2155](https://github.com/tiangolo/fastapi/pull/2155) by [@clemsau](https://github.com/clemsau). * 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). From e081145c7dc1218b4fee08f61295f76549aa0156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=AE=9A=E7=84=95?= <108172295+wdh99@users.noreply.github.com> Date: Fri, 28 Jul 2023 02:55:40 +0800 Subject: [PATCH 1178/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/background-tasks.md`=20(#9?= =?UTF-8?q?812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/zh/docs/tutorial/background-tasks.md | 126 ++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/zh/docs/tutorial/background-tasks.md diff --git a/docs/zh/docs/tutorial/background-tasks.md b/docs/zh/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..c8568298b --- /dev/null +++ b/docs/zh/docs/tutorial/background-tasks.md @@ -0,0 +1,126 @@ +# 后台任务 + +你可以定义在返回响应后运行的后台任务。 + +这对需要在请求之后执行的操作很有用,但客户端不必在接收响应之前等待操作完成。 + +包括这些例子: + +* 执行操作后发送的电子邮件通知: + * 由于连接到电子邮件服务器并发送电子邮件往往很“慢”(几秒钟),您可以立即返回响应并在后台发送电子邮件通知。 +* 处理数据: + * 例如,假设您收到的文件必须经过一个缓慢的过程,您可以返回一个"Accepted"(HTTP 202)响应并在后台处理它。 + +## 使用 `BackgroundTasks` + +首先导入 `BackgroundTasks` 并在 *路径操作函数* 中使用类型声明 `BackgroundTasks` 定义一个参数: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** 会创建一个 `BackgroundTasks` 类型的对象并作为该参数传入。 + +## 创建一个任务函数 + +创建要作为后台任务运行的函数。 + +它只是一个可以接收参数的标准函数。 + +它可以是 `async def` 或普通的 `def` 函数,**FastAPI** 知道如何正确处理。 + +在这种情况下,任务函数将写入一个文件(模拟发送电子邮件)。 + +由于写操作不使用 `async` 和 `await`,我们用普通的 `def` 定义函数: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## 添加后台任务 + +在你的 *路径操作函数* 里,用 `.add_task()` 方法将任务函数传到 *后台任务* 对象中: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` 接收以下参数: + +* 在后台运行的任务函数(`write_notification`)。 +* 应按顺序传递给任务函数的任意参数序列(`email`)。 +* 应传递给任务函数的任意关键字参数(`message="some notification"`)。 + +## 依赖注入 + +使用 `BackgroundTasks` 也适用于依赖注入系统,你可以在多个级别声明 `BackgroundTasks` 类型的参数:在 *路径操作函数* 里,在依赖中(可依赖),在子依赖中,等等。 + +**FastAPI** 知道在每种情况下该做什么以及如何复用同一对象,因此所有后台任务被合并在一起并且随后在后台运行: + +=== "Python 3.10+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ 没Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ 没Annotated" + + !!! tip + 尽可能选择使用 `Annotated` 的版本。 + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +该示例中,信息会在响应发出 *之后* 被写到 `log.txt` 文件。 + +如果请求中有查询,它将在后台任务中写入日志。 + +然后另一个在 *路径操作函数* 生成的后台任务会使用路径参数 `email` 写入一条信息。 + +## 技术细节 + +`BackgroundTasks` 类直接来自 `starlette.background`。 + +它被直接导入/包含到FastAPI以便你可以从 `fastapi` 导入,并避免意外从 `starlette.background` 导入备用的 `BackgroundTask` (后面没有 `s`)。 + +通过仅使用 `BackgroundTasks` (而不是 `BackgroundTask`),使得能将它作为 *路径操作函数* 的参数 ,并让**FastAPI**为您处理其余部分, 就像直接使用 `Request` 对象。 + +在FastAPI中仍然可以单独使用 `BackgroundTask`,但您必须在代码中创建对象,并返回包含它的Starlette `Response`。 + +更多细节查看 Starlette's official docs for Background Tasks. + +## 告诫 + +如果您需要执行繁重的后台计算,并且不一定需要由同一进程运行(例如,您不需要共享内存、变量等),那么使用其他更大的工具(如 Celery)可能更好。 + +它们往往需要更复杂的配置,即消息/作业队列管理器,如RabbitMQ或Redis,但它们允许您在多个进程中运行后台任务,甚至是在多个服务器中。 + +要查看示例,查阅 [Project Generators](../project-generation.md){.internal-link target=_blank},它们都包括已经配置的Celery。 + +但是,如果您需要从同一个**FastAPI**应用程序访问变量和对象,或者您需要执行小型后台任务(如发送电子邮件通知),您只需使用 `BackgroundTasks` 即可。 + +## 回顾 + +导入并使用 `BackgroundTasks` 通过 *路径操作函数* 中的参数和依赖项来添加后台任务。 From 5d3f51c8bc7b902204b1b55ea7044ca5f10a7256 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:56:19 +0000 Subject: [PATCH 1179/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2f9aada99..d295826fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/benchmarks.md`. PR [#2155](https://github.com/tiangolo/fastapi/pull/2155) by [@clemsau](https://github.com/clemsau). From e334065d1055e27773271c00ba8830c7176f975d Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:57:37 +0000 Subject: [PATCH 1180/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d295826fc..f6097bedb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). From 55871036dbd85531f042f1e6b87a14fe8556ccd0 Mon Sep 17 00:00:00 2001 From: Nina Hwang <79563565+NinaHwang@users.noreply.github.com> Date: Fri, 28 Jul 2023 03:59:18 +0900 Subject: [PATCH 1181/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/async.md`=20(#4179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ko/docs/async.md | 404 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 docs/ko/docs/async.md diff --git a/docs/ko/docs/async.md b/docs/ko/docs/async.md new file mode 100644 index 000000000..47dbaa1b0 --- /dev/null +++ b/docs/ko/docs/async.md @@ -0,0 +1,404 @@ +# 동시성과 async / await + +*경로 작동 함수*에서의 `async def` 문법에 대한 세부사항과 비동기 코드, 동시성 및 병렬성에 대한 배경 + +## 바쁘신 경우 + +요약 + +다음과 같이 `await`를 사용해 호출하는 제3의 라이브러리를 사용하는 경우: + +```Python +results = await some_library() +``` + +다음처럼 *경로 작동 함수*를 `async def`를 사용해 선언하십시오: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note "참고" + `async def`로 생성된 함수 내부에서만 `await`를 사용할 수 있습니다. + +--- + +데이터베이스, API, 파일시스템 등과 의사소통하는 제3의 라이브러리를 사용하고, 그것이 `await`를 지원하지 않는 경우(현재 거의 모든 데이터베이스 라이브러리가 그러합니다), *경로 작동 함수*를 일반적인 `def`를 사용해 선언하십시오: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +만약 당신의 응용프로그램이 (어째서인지) 다른 무엇과 의사소통하고 그것이 응답하기를 기다릴 필요가 없다면 `async def`를 사용하십시오. + +--- + +모르겠다면, 그냥 `def`를 사용하십시오. + +--- + +**참고**: *경로 작동 함수*에서 필요한만큼 `def`와 `async def`를 혼용할 수 있고, 가장 알맞은 것을 선택해서 정의할 수 있습니다. FastAPI가 자체적으로 알맞은 작업을 수행할 것입니다. + +어찌되었든, 상기 어떠한 경우라도, FastAPI는 여전히 비동기적으로 작동하고 매우 빠릅니다. + +그러나 상기 작업을 수행함으로써 어느 정도의 성능 최적화가 가능합니다. + +## 기술적 세부사항 + +최신 파이썬 버전은 `async`와 `await` 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. + +아래 섹션들에서 해당 문장을 부분별로 살펴보겠습니다: + +* **비동기 코드** +* **`async`와 `await`** +* **코루틴** + +## 비동기 코드 + +비동기 코드란 언어 💬 가 코드의 어느 한 부분에서, 컴퓨터 / 프로그램🤖에게 *다른 무언가*가 어딘가에서 끝날 때까지 기다려야한다고 말하는 방식입니다. *다른 무언가*가 “느린-파일" 📝 이라고 불린다고 가정해봅시다. + +따라서 “느린-파일” 📝이 끝날때까지 컴퓨터는 다른 작업을 수행할 수 있습니다. + +그 다음 컴퓨터 / 프로그램 🤖 은 다시 기다리고 있기 때문에 기회가 있을 때마다 다시 돌아오거나, 혹은 당시에 수행해야하는 작업들이 완료될 때마다 다시 돌아옵니다. 그리고 그것 🤖 은 기다리고 있던 작업 중 어느 것이 이미 완료되었는지, 그것 🤖 이 해야하는 모든 작업을 수행하면서 확인합니다. + +다음으로, 그것 🤖 은 완료할 첫번째 작업에 착수하고(우리의 "느린-파일" 📝 이라고 가정합시다) 그에 대해 수행해야하는 작업을 계속합니다. + +"다른 무언가를 기다리는 것"은 일반적으로 비교적 "느린" (프로세서와 RAM 메모리 속도에 비해) I/O 작업을 의미합니다. 예를 들면 다음의 것들을 기다리는 것입니다: + +* 네트워크를 통해 클라이언트로부터 전송되는 데이터 +* 네트워크를 통해 클라이언트가 수신할, 당신의 프로그램으로부터 전송되는 데이터 +* 시스템이 읽고 프로그램에 전달할 디스크 내의 파일 내용 +* 당신의 프로그램이 시스템에 전달하는, 디스크에 작성될 내용 +* 원격 API 작업 +* 완료될 데이터베이스 작업 +* 결과를 반환하는 데이터베이스 쿼리 +* 기타 + +수행 시간의 대부분이 I/O 작업을 기다리는데에 소요되기 때문에, "I/O에 묶인" 작업이라고 불립니다. + +이것은 "비동기"라고 불리는데 컴퓨터 / 프로그램이 작업 결과를 가지고 일을 수행할 수 있도록, 느린 작업에 "동기화"되어 아무것도 하지 않으면서 작업이 완료될 정확한 시점만을 기다릴 필요가 없기 때문입니다. + +이 대신에, "비동기" 시스템에서는, 작업은 일단 완료되면, 컴퓨터 / 프로그램이 수행하고 있는 일을 완료하고 이후 다시 돌아와서 그것의 결과를 받아 이를 사용해 작업을 지속할 때까지 잠시 (몇 마이크로초) 대기할 수 있습니다. + +"동기"("비동기"의 반대)는 컴퓨터 / 프로그램이 상이한 작업들간 전환을 하기 전에 그것이 대기를 동반하게 될지라도 모든 순서를 따르기 때문에 "순차"라는 용어로도 흔히 불립니다. + +### 동시성과 버거 + +위에서 설명한 **비동기** 코드에 대한 개념은 종종 **"동시성"**이라고도 불립니다. 이것은 **"병렬성"**과는 다릅니다. + +**동시성**과 **병렬성**은 모두 "동시에 일어나는 서로 다른 일들"과 관련이 있습니다. + +하지만 *동시성*과 *병렬성*의 세부적인 개념에는 꽤 차이가 있습니다. + +차이를 확인하기 위해, 다음의 버거에 대한 이야기를 상상해보십시오: + +### 동시 버거 + +당신은 짝사랑 상대 😍 와 패스트푸드 🍔 를 먹으러 갔습니다. 당신은 점원 💁 이 당신 앞에 있는 사람들의 주문을 받을 동안 줄을 서서 기다리고 있습니다. + +이제 당신의 순서가 되어서, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. + +당신이 돈을 냅니다 💸. + +점원 💁 은 주방 👨‍🍳 에 요리를 하라고 전달하고, 따라서 그들은 당신의 버거 🍔 를 준비해야한다는 사실을 알게됩니다(그들이 지금은 당신 앞 고객들의 주문을 준비하고 있을지라도 말입니다). + +점원 💁 은 당신의 순서가 적힌 번호표를 줍니다. + +기다리는 동안, 당신은 짝사랑 상대 😍 와 함께 테이블을 고르고, 자리에 앉아 오랫동안 (당신이 주문한 버거는 꽤나 고급스럽기 때문에 준비하는데 시간이 조금 걸립니다 ✨🍔✨) 대화를 나눕니다. + +짝사랑 상대 😍 와 테이블에 앉아서 버거 🍔 를 기다리는 동안, 그 사람 😍 이 얼마나 멋지고, 사랑스럽고, 똑똑한지 감탄하며 시간을 보냅니다 ✨😍✨. + +짝사랑 상대 😍 와 기다리면서 얘기하는 동안, 때때로, 당신은 당신의 차례가 되었는지 보기 위해 카운터의 번호를 확인합니다. + +그러다 어느 순간, 당신의 차례가 됩니다. 카운터에 가서, 버거 🍔 를 받고, 테이블로 다시 돌아옵니다. + +당신과 짝사랑 상대 😍 는 버거 🍔 를 먹으며 좋은 시간을 보냅니다 ✨. + +--- + +당신이 이 이야기에서 컴퓨터 / 프로그램 🤖 이라고 상상해보십시오. + +줄을 서서 기다리는 동안, 당신은 아무것도 하지 않고 😴 당신의 차례를 기다리며, 어떠한 "생산적인" 일도 하지 않습니다. 하지만 점원 💁 이 (음식을 준비하지는 않고) 주문을 받기만 하기 때문에 줄이 빨리 줄어들어서 괜찮습니다. + +그다음, 당신이 차례가 오면, 당신은 실제로 "생산적인" 일 🤓 을 합니다. 당신은 메뉴를 보고, 무엇을 먹을지 결정하고, 짝사랑 상대 😍 의 선택을 묻고, 돈을 내고 💸 , 맞는 카드를 냈는지 확인하고, 비용이 제대로 지불되었는지 확인하고, 주문이 제대로 들어갔는지 확인을 하는 작업 등등을 수행합니다. + +하지만 이후에는, 버거 🍔 를 아직 받지 못했음에도, 버거가 준비될 때까지 기다려야 🕙 하기 때문에 점원 💁 과의 작업은 "일시정지" ⏸ 상태입니다. + +하지만 번호표를 받고 카운터에서 나와 테이블에 앉으면, 당신은 짝사랑 상대 😍 와 그 "작업" ⏯ 🤓 에 번갈아가며 🔀 집중합니다. 그러면 당신은 다시 짝사랑 상대 😍 에게 작업을 거는 매우 "생산적인" 일 🤓 을 합니다. + +점원 💁 이 카운터 화면에 당신의 번호를 표시함으로써 "버거 🍔 가 준비되었습니다"라고 해도, 당신은 즉시 뛰쳐나가지는 않을 것입니다. 당신은 당신의 번호를 갖고있고, 다른 사람들은 그들의 번호를 갖고있기 때문에, 아무도 당신의 버거 🍔 를 훔쳐가지 않는다는 사실을 알기 때문입니다. + +그래서 당신은 짝사랑 상대 😍 가 이야기를 끝낼 때까지 기다린 후 (현재 작업 완료 ⏯ / 진행 중인 작업 처리 🤓 ), 정중하게 미소짓고 버거를 가지러 가겠다고 말합니다 ⏸. + +그다음 당신은 카운터에 가서 🔀 , 초기 작업을 이제 완료하고 ⏯ , 버거 🍔 를 받고, 감사하다고 말하고 테이블로 가져옵니다. 이로써 카운터와의 상호작용 단계 / 작업이 종료됩니다 ⏹. + +이전 작업인 "버거 받기"가 종료되면 ⏹ "버거 먹기"라는 새로운 작업이 생성됩니다 🔀 ⏯. + +### 병렬 버거 + +이제 "동시 버거"가 아닌 "병렬 버거"를 상상해보십시오. + +당신은 짝사랑 상대 😍 와 함께 병렬 패스트푸드 🍔 를 먹으러 갔습니다. + +당신은 여러명(8명이라고 가정합니다)의 점원이 당신 앞 사람들의 주문을 받으며 동시에 요리 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 도 하는 동안 줄을 서서 기다립니다. + +당신 앞 모든 사람들이 버거가 준비될 때까지 카운터에서 떠나지 않고 기다립니다 🕙 . 왜냐하면 8명의 직원들이 다음 주문을 받기 전에 버거를 준비하러 가기 때문입니다. + +마침내 당신의 차례가 왔고, 당신은 당신과 짝사랑 상대 😍 를 위한 두 개의 고급스러운 버거 🍔 를 주문합니다. + +당신이 비용을 지불합니다 💸 . + +점원이 주방에 갑니다 👨‍🍳 . + +당신은 번호표가 없기 때문에 누구도 당신의 버거 🍔 를 대신 가져갈 수 없도록 카운터에 서서 기다립니다 🕙 . + +당신과 짝사랑 상대 😍 는 다른 사람이 새치기해서 버거를 가져가지 못하게 하느라 바쁘기 때문에 🕙 , 짝사랑 상대에게 주의를 기울일 수 없습니다 😞 . + +이것은 "동기" 작업이고, 당신은 점원/요리사 👨‍🍳 와 "동기화" 되었습니다. 당신은 기다리고 🕙 , 점원/요리사 👨‍🍳 가 버거 🍔 준비를 완료한 후 당신에게 주거나, 누군가가 그것을 가져가는 그 순간에 그 곳에 있어야합니다. + +카운터 앞에서 오랫동안 기다린 후에 🕙 , 점원/요리사 👨‍🍳 가 당신의 버거 🍔 를 가지고 돌아옵니다. + +당신은 버거를 받고 짝사랑 상대와 함께 테이블로 돌아옵니다. + +단지 먹기만 하다가, 다 먹었습니다 🍔 ⏹. + +카운터 앞에서 기다리면서 🕙 너무 많은 시간을 허비했기 때문에 대화를 하거나 작업을 걸 시간이 거의 없었습니다 😞 . + +--- + +이 병렬 버거 시나리오에서, 당신은 기다리고 🕙 , 오랜 시간동안 "카운터에서 기다리는" 🕙 데에 주의를 기울이는 ⏯ 두 개의 프로세서(당신과 짝사랑 상대😍)를 가진 컴퓨터 / 프로그램 🤖 입니다. + +패스트푸드점에는 8개의 프로세서(점원/요리사) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 가 있습니다. 동시 버거는 단 두 개(한 명의 직원과 한 명의 요리사) 💁 👨‍🍳 만을 가지고 있었습니다. + +하지만 여전히, 병렬 버거 예시가 최선은 아닙니다 😞 . + +--- + +이 예시는 버거🍔 이야기와 결이 같습니다. + +더 "현실적인" 예시로, 은행을 상상해보십시오. + +최근까지, 대다수의 은행에는 다수의 은행원들 👨‍💼👨‍💼👨‍💼👨‍💼 과 긴 줄 🕙🕙🕙🕙🕙🕙🕙🕙 이 있습니다. + +모든 은행원들은 한 명 한 명의 고객들을 차례로 상대합니다 👨‍💼⏯ . + +그리고 당신은 오랫동안 줄에서 기다려야하고 🕙 , 그렇지 않으면 당신의 차례를 잃게 됩니다. + +아마 당신은 은행 🏦 심부름에 짝사랑 상대 😍 를 데려가고 싶지는 않을 것입니다. + +### 버거 예시의 결론 + +"짝사랑 상대와의 패스트푸드점 버거" 시나리오에서, 오랜 기다림 🕙 이 있기 때문에 동시 시스템 ⏸🔀⏯ 을 사용하는 것이 더 합리적입니다. + +대다수의 웹 응용프로그램의 경우가 그러합니다. + +매우 많은 수의 유저가 있지만, 서버는 그들의 요청을 전송하기 위해 그닥-좋지-않은 연결을 기다려야 합니다 🕙 . + +그리고 응답이 돌아올 때까지 다시 기다려야 합니다 🕙 . + +이 "기다림" 🕙 은 마이크로초 단위이지만, 모두 더해지면, 결국에는 매우 긴 대기시간이 됩니다. + +따라서 웹 API를 위해 비동기 ⏸🔀⏯ 코드를 사용하는 것이 합리적입니다. + +대부분의 존재하는 유명한 파이썬 프레임워크 (Flask와 Django 등)은 새로운 비동기 기능들이 파이썬에 존재하기 전에 만들어졌습니다. 그래서, 그들의 배포 방식은 병렬 실행과 새로운 기능만큼 강력하지는 않은 예전 버전의 비동기 실행을 지원합니다. + +비동기 웹 파이썬(ASGI)에 대한 주요 명세가 웹소켓을 지원하기 위해 Django에서 개발 되었음에도 그렇습니다. + +이러한 종류의 비동기성은 (NodeJS는 병렬적이지 않음에도) NodeJS가 사랑받는 이유이고, 프로그래밍 언어로서의 Go의 강점입니다. + +그리고 **FastAPI**를 사용함으로써 동일한 성능을 낼 수 있습니다. + +또한 병렬성과 비동기성을 동시에 사용할 수 있기 때문에, 대부분의 테스트가 완료된 NodeJS 프레임워크보다 더 높은 성능을 얻고 C에 더 가까운 컴파일 언어인 Go와 동등한 성능을 얻을 수 있습니다(모두 Starlette 덕분입니다). + +### 동시성이 병렬성보다 더 나은가? + +그렇지 않습니다! 그것이 이야기의 교훈은 아닙니다. + +동시성은 병렬성과 다릅니다. 그리고 그것은 많은 대기를 필요로하는 **특정한** 시나리오에서는 더 낫습니다. 이로 인해, 웹 응용프로그램 개발에서 동시성이 병렬성보다 일반적으로 훨씬 낫습니다. 하지만 모든 경우에 그런 것은 아닙니다. + +따라서, 균형을 맞추기 위해, 다음의 짧은 이야기를 상상해보십시오: + +> 당신은 크고, 더러운 집을 청소해야합니다. + +*네, 이게 전부입니다*. + +--- + +어디에도 대기 🕙 는 없고, 집안 곳곳에서 해야하는 많은 작업들만 있습니다. + +버거 예시처럼 처음에는 거실, 그 다음은 부엌과 같은 식으로 순서를 정할 수도 있으나, 무엇도 기다리지 🕙 않고 계속해서 청소 작업만 수행하기 때문에, 순서는 아무런 영향을 미치지 않습니다. + +순서가 있든 없든 동일한 시간이 소요될 것이고(동시성) 동일한 양의 작업을 하게 될 것입니다. + +하지만 이 경우에서, 8명의 전(前)-점원/요리사이면서-현(現)-청소부 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 를 고용할 수 있고, 그들 각자(그리고 당신)가 집의 한 부분씩 맡아 청소를 한다면, 당신은 **병렬적**으로 작업을 수행할 수 있고, 조금의 도움이 있다면, 훨씬 더 빨리 끝낼 수 있습니다. + +이 시나리오에서, (당신을 포함한) 각각의 청소부들은 프로세서가 될 것이고, 각자의 역할을 수행합니다. + +실행 시간의 대부분이 대기가 아닌 실제 작업에 소요되고, 컴퓨터에서 작업은 CPU에서 이루어지므로, 이러한 문제를 "CPU에 묶였"다고 합니다. + +--- + +CPU에 묶인 연산에 관한 흔한 예시는 복잡한 수학 처리를 필요로 하는 경우입니다. + +예를 들어: + +* **오디오** 또는 **이미지** 처리. +* **컴퓨터 비전**: 하나의 이미지는 수백개의 픽셀로 구성되어있고, 각 픽셀은 3개의 값 / 색을 갖고 있으며, 일반적으로 해당 픽셀들에 대해 동시에 무언가를 계산해야하는 처리. +* **머신러닝**: 일반적으로 많은 "행렬"과 "벡터" 곱셈이 필요합니다. 거대한 스프레드 시트에 수들이 있고 그 수들을 동시에 곱해야 한다고 생각해보십시오. +* **딥러닝**: 머신러닝의 하위영역으로, 동일한 예시가 적용됩니다. 단지 이 경우에는 하나의 스프레드 시트에 곱해야할 수들이 있는 것이 아니라, 거대한 세트의 스프레드 시트들이 있고, 많은 경우에, 이 모델들을 만들고 사용하기 위해 특수한 프로세서를 사용합니다. + +### 동시성 + 병렬성: 웹 + 머신러닝 + +**FastAPI**를 사용하면 웹 개발에서는 매우 흔한 동시성의 이점을 (NodeJS의 주된 매력만큼) 얻을 수 있습니다. + +뿐만 아니라 머신러닝 시스템과 같이 **CPU에 묶인** 작업을 위해 병렬성과 멀티프로세싱(다수의 프로세스를 병렬적으로 동작시키는 것)을 이용하는 것도 가능합니다. + +파이썬이 **데이터 사이언스**, 머신러닝과 특히 딥러닝에 의 주된 언어라는 간단한 사실에 더해서, 이것은 FastAPI를 데이터 사이언스 / 머신러닝 웹 API와 응용프로그램에 (다른 것들보다) 좋은 선택지가 되게 합니다. + +배포시 병렬을 어떻게 가능하게 하는지 알고싶다면, [배포](/ko/deployment){.internal-link target=_blank}문서를 참고하십시오. + +## `async`와 `await` + +최신 파이썬 버전에는 비동기 코드를 정의하는 매우 직관적인 방법이 있습니다. 이는 이것을 평범한 "순차적" 코드로 보이게 하고, 적절한 순간에 당신을 위해 "대기"합니다. + +연산이 결과를 전달하기 전에 대기를 해야하고 새로운 파이썬 기능들을 지원한다면, 이렇게 코드를 작성할 수 있습니다: + +```Python +burgers = await get_burgers(2) +``` + +여기서 핵심은 `await`입니다. 이것은 파이썬에게 `burgers` 결과를 저장하기 이전에 `get_burgers(2)`의 작업이 완료되기를 🕙 기다리라고 ⏸ 말합니다. 이로 인해, 파이썬은 그동안 (다른 요청을 받는 것과 같은) 다른 작업을 수행해도 된다는 것을 🔀 ⏯ 알게될 것입니다. + +`await`가 동작하기 위해, 이것은 비동기를 지원하는 함수 내부에 있어야 합니다. 이를 위해서 함수를 `async def`를 사용해 정의하기만 하면 됩니다: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...`def`를 사용하는 대신: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +`async def`를 사용하면, 파이썬은 해당 함수 내에서 `await` 표현에 주의해야한다는 사실과, 해당 함수의 실행을 "일시정지"⏸하고 다시 돌아오기 전까지 다른 작업을 수행🔀할 수 있다는 것을 알게됩니다. + +`async def`f 함수를 호출하고자 할 때, "대기"해야합니다. 따라서, 아래는 동작하지 않습니다. + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +따라서, `await`f를 사용해서 호출할 수 있는 라이브러리를 사용한다면, 다음과 같이 `async def`를 사용하는 *경로 작동 함수*를 생성해야 합니다: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 더 세부적인 기술적 사항 + +`await`가 `async def`를 사용하는 함수 내부에서만 사용이 가능하다는 것을 눈치채셨을 것입니다. + +하지만 동시에, `async def`로 정의된 함수들은 "대기"되어야만 합니다. 따라서, `async def`를 사용한 함수들은 역시 `async def`를 사용한 함수 내부에서만 호출될 수 있습니다. + +그렇다면 닭이 먼저냐, 달걀이 먼저냐, 첫 `async` 함수를 어떻게 호출할 수 있겠습니까? + +**FastAPI**를 사용해 작업한다면 이것을 걱정하지 않아도 됩니다. 왜냐하면 그 "첫" 함수는 당신의 *경로 작동 함수*가 될 것이고, FastAPI는 어떻게 올바르게 처리할지 알고있기 때문입니다. + +하지만 FastAPI를 사용하지 않고 `async` / `await`를 사용하고 싶다면, 이 역시 가능합니다. + +### 당신만의 비동기 코드 작성하기 + +Starlette(그리고 FastAPI)는 AnyIO를 기반으로 하고있고, 따라서 파이썬 표준 라이브러리인 asyncioTrio와 호환됩니다. + +특히, 코드에서 고급 패턴이 필요한 고급 동시성을 사용하는 경우 직접적으로 AnyIO를 사용할 수 있습니다. + +FastAPI를 사용하지 않더라도, 높은 호환성 및 AnyIO의 이점(예: *구조화된 동시성*)을 취하기 위해 AnyIO를 사용해 비동기 응용프로그램을 작성할 수 있습니다. + +### 비동기 코드의 다른 형태 + +파이썬에서 `async`와 `await`를 사용하게 된 것은 비교적 최근의 일입니다. + +하지만 이로 인해 비동기 코드 작업이 훨씬 간단해졌습니다. + +같은 (또는 거의 유사한) 문법은 최신 버전의 자바스크립트(브라우저와 NodeJS)에도 추가되었습니다. + +하지만 그 이전에, 비동기 코드를 처리하는 것은 꽤 복잡하고 어려운 일이었습니다. + +파이썬의 예전 버전이라면, 스레드 또는 Gevent를 사용할 수 있을 것입니다. 하지만 코드를 이해하고, 디버깅하고, 이에 대해 생각하는게 훨씬 복잡합니다. + +예전 버전의 NodeJS / 브라우저 자바스크립트라면, "콜백 함수"를 사용했을 것입니다. 그리고 이로 인해 콜백 지옥에 빠지게 될 수 있습니다. + +## 코루틴 + +**코루틴**은 `async def` 함수가 반환하는 것을 칭하는 매우 고급스러운 용어일 뿐입니다. 파이썬은 그것이 시작되고 어느 시점에서 완료되지만 내부에 `await`가 있을 때마다 내부적으로 일시정지⏸될 수도 있는 함수와 유사한 것이라는 사실을 알고있습니다. + +그러나 `async` 및 `await`와 함께 비동기 코드를 사용하는 이 모든 기능들은 "코루틴"으로 간단히 요약됩니다. 이것은 Go의 주된 핵심 기능인 "고루틴"에 견줄 수 있습니다. + +## 결론 + +상기 문장을 다시 한 번 봅시다: + +> 최신 파이썬 버전은 **`async` 및 `await`** 문법과 함께 **“코루틴”**이라고 하는 것을 사용하는 **“비동기 코드”**를 지원합니다. + +이제 이 말을 조금 더 이해할 수 있을 것입니다. ✨ + +이것이 (Starlette을 통해) FastAPI를 강하게 하면서 그것이 인상적인 성능을 낼 수 있게 합니다. + +## 매우 세부적인 기술적 사항 + +!!! warning "경고" + 이 부분은 넘어가도 됩니다. + + 이것들은 **FastAPI**가 내부적으로 어떻게 동작하는지에 대한 매우 세부적인 기술사항입니다. + + 만약 기술적 지식(코루틴, 스레드, 블록킹 등)이 있고 FastAPI가 어떻게 `async def` vs `def`를 다루는지 궁금하다면, 계속하십시오. + +### 경로 작동 함수 + +경로 작동 함수를 `async def` 대신 일반적인 `def`로 선언하는 경우, (서버를 차단하는 것처럼) 그것을 직접 호출하는 대신 대기중인 외부 스레드풀에서 실행됩니다. + +만약 상기에 묘사된대로 동작하지 않는 비동기 프로그램을 사용해왔고 약간의 성능 향상 (약 100 나노초)을 위해 `def`를 사용해서 계산만을 위한 사소한 *경로 작동 함수*를 정의해왔다면, **FastAPI**는 이와는 반대라는 것에 주의하십시오. 이러한 경우에, *경로 작동 함수*가 블로킹 I/O를 수행하는 코드를 사용하지 않는 한 `async def`를 사용하는 편이 더 낫습니다. + +하지만 두 경우 모두, FastAPI가 당신이 전에 사용하던 프레임워크보다 [더 빠를](/#performance){.internal-link target=_blank} (최소한 비견될) 확률이 높습니다. + +### 의존성 + +의존성에도 동일하게 적용됩니다. 의존성이 `async def`가 아닌 표준 `def` 함수라면, 외부 스레드풀에서 실행됩니다. + +### 하위-의존성 + +함수 정의시 매개변수로 서로를 필요로하는 다수의 의존성과 하위-의존성을 가질 수 있고, 그 중 일부는 `async def`로, 다른 일부는 일반적인 `def`로 생성되었을 수 있습니다. 이것은 여전히 잘 동작하고, 일반적인 `def`로 생성된 것들은 "대기"되는 대신에 (스레드풀로부터) 외부 스레드에서 호출됩니다. + +### 다른 유틸리티 함수 + +직접 호출되는 다른 모든 유틸리티 함수는 일반적인 `def`나 `async def`로 생성될 수 있고 FastAPI는 이를 호출하는 방식에 영향을 미치지 않습니다. + +이것은 FastAPI가 당신을 위해 호출하는 함수와는 반대입니다: *경로 작동 함수*와 의존성 + +만약 당신의 유틸리티 함수가 `def`를 사용한 일반적인 함수라면, 스레드풀에서가 아니라 직접 호출(당신이 코드에 작성한 대로)될 것이고, `async def`로 생성된 함수라면 코드에서 호출할 때 그 함수를 `await` 해야 합니다. + +--- + +다시 말하지만, 이것은 당신이 이것에 대해 찾고있던 경우에 한해 유용할 매우 세부적인 기술사항입니다. + +그렇지 않은 경우, 상기의 가이드라인만으로도 충분할 것입니다: [바쁘신 경우](#in-a-hurry). From 77cfb3c822c6e58b68a8694f4b01bb1bb9c87255 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 18:59:56 +0000 Subject: [PATCH 1182/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f6097bedb..ce28cb029 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). * 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). From 1d088eaf18b096b0ded50c80d33aa5e603add49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Kh=E1=BA=AFc=20Th=C3=A0nh?= Date: Fri, 28 Jul 2023 02:01:57 +0700 Subject: [PATCH 1183/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20tra?= =?UTF-8?q?nslation=20for=20`docs/vi/docs/features.md`=20and=20`docs/vi/do?= =?UTF-8?q?cs/index.md`=20(#3006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nguyen Khac Thanh Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/vi/docs/features.md | 197 ++++++++++++++++ docs/vi/docs/index.md | 476 +++++++++++++++++++++++++++++++++++++++ docs/vi/mkdocs.yml | 1 + 3 files changed, 674 insertions(+) create mode 100644 docs/vi/docs/features.md create mode 100644 docs/vi/docs/index.md create mode 100644 docs/vi/mkdocs.yml diff --git a/docs/vi/docs/features.md b/docs/vi/docs/features.md new file mode 100644 index 000000000..0599530e8 --- /dev/null +++ b/docs/vi/docs/features.md @@ -0,0 +1,197 @@ +# Tính năng + +## Tính năng của FastAPI + +**FastAPI** cho bạn những tính năng sau: + +### Dựa trên những tiêu chuẩn mở + +* OpenAPI cho việc tạo API, bao gồm những khai báo về đường dẫn các toán tử, tham số, body requests, cơ chế bảo mật, etc. +* Tự động tài liệu hóa data model theo JSON Schema (OpenAPI bản thân nó được dựa trên JSON Schema). +* Được thiết kế xung quanh các tiêu chuẩn này sau khi nghiên cứu tỉ mỉ thay vì chỉ suy nghĩ đơn giản và sơ xài. +* Điều này cho phép tự động hóa **trình sinh code client** cho nhiều ngôn ngữ lập trình khác nhau. + +### Tự động hóa tài liệu + + +Tài liệu tương tác API và web giao diện người dùng. Là một framework được dựa trên OpenAPI do đó có nhiều tùy chọn giao diện cho tài liệu API, 2 giao diện bên dưới là mặc định. + +* Swagger UI, với giao diện khám phá, gọi và kiểm thử API trực tiếp từ trình duyệt. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Thay thế với tài liệu API với ReDoc. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Chỉ cần phiên bản Python hiện đại + +Tất cả được dựa trên khai báo kiểu dữ liệu chuẩn của **Python 3.6** (cảm ơn Pydantic). Bạn không cần học cú pháp mới, chỉ cần biết chuẩn Python hiện đại. + +Nếu bạn cần 2 phút để làm mới lại cách sử dụng các kiểu dữ liệu mới của Python (thậm chí nếu bạn không sử dụng FastAPI), xem hướng dẫn ngắn: [Kiểu dữ liệu Python](python-types.md){.internal-link target=_blank}. + +Bạn viết chuẩn Python với kiểu dữ liệu như sau: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +Sau đó có thể được sử dụng: + +```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` nghĩa là: + + Truyền các khóa và giá trị của dict `second_user_data` trực tiếp như các tham số kiểu key-value, tương đương với: `User(id=4, name="Mary", joined="2018-11-30")` + +### Được hỗ trợ từ các trình soạn thảo + + +Toàn bộ framework được thiết kế để sử dụng dễ dàng và trực quan, toàn bộ quyết định đã được kiểm thử trên nhiều trình soạn thảo thậm chí trước khi bắt đầu quá trình phát triển, để chắc chắn trải nghiệm phát triển là tốt nhất. + +Trong lần khảo sát cuối cùng dành cho các lập trình viên Python, đã rõ ràng rằng đa số các lập trình viên sử dụng tính năng "autocompletion". + +Toàn bộ framework "FastAPI" phải đảm bảo rằng: autocompletion hoạt động ở mọi nơi. Bạn sẽ hiếm khi cần quay lại để đọc tài liệu. + +Đây là các trình soạn thảo có thể giúp bạn: + +* trong Visual Studio Code: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* trong PyCharm: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +Bạn sẽ có được auto-completion trong code, thậm chí trước đó là không thể. Như trong ví dụ, khóa `price` bên trong một JSON (đó có thể được lồng nhau) đến từ một request. + +Không còn nhập sai tên khóa, quay đi quay lại giữa các tài liệu hoặc cuộn lên cuộn xuống để tìm xem cuối cùng bạn đã sử dụng `username` hay `user_name`. + +### Ngắn gọn + +FastAPI có các giá trị mặc định hợp lý cho mọi thứ, với các cấu hình tùy chọn ở mọi nơi. Tất cả các tham số có thể được tinh chỉnh để thực hiện những gì bạn cần và để định nghĩa API bạn cần. + +Nhưng mặc định, tất cả **đều hoạt động**. + +### Validation + +* Validation cho đa số (hoặc tất cả?) **các kiểu dữ liệu** Python, bao gồm: + * JSON objects (`dict`). + * Mảng JSON (`list`) định nghĩa kiểu dữ liệu từng phần tử. + * Xâu (`str`), định nghĩa độ dài lớn nhất, nhỏ nhất. + * Số (`int`, `float`) với các giá trị lớn nhất, nhỏ nhất, etc. + +* Validation cho nhiều kiểu dữ liệu bên ngoài như: + * URL. + * Email. + * UUID. + * ...và nhiều cái khác. + +Tất cả validation được xử lí bằng những thiết lập tốt và mạnh mẽ của **Pydantic**. + +### Bảo mật và xác thực + +Bảo mật và xác thực đã tích hợp mà không làm tổn hại tới cơ sở dữ liệu hoặc data models. + +Tất cả cơ chế bảo mật định nghĩa trong OpenAPI, bao gồm: + +* HTTP Basic. +* **OAuth2** (với **JWT tokens**). Xem hướng dẫn [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. +* API keys in: + * Headers. + * Các tham số trong query string. + * Cookies, etc. + +Cộng với tất cả các tính năng bảo mật từ Starlette (bao gồm **session cookies**). + +Tất cả được xây dựng dưới dạng các công cụ và thành phần có thể tái sử dụng, dễ dàng tích hợp với hệ thống, kho lưu trữ dữ liệu, cơ sở dữ liệu quan hệ và NoSQL của bạn,... + +### Dependency Injection + +FastAPI bao gồm một hệ thống Dependency Injection vô cùng dễ sử dụng nhưng vô cùng mạnh mẽ. + +* Thậm chí, các dependency có thể có các dependency khác, tạo thành một phân cấp hoặc **"một đồ thị" của các dependency**. +* Tất cả **được xử lí tự động** bởi framework. +* Tất cả các dependency có thể yêu cầu dữ liệu từ request và **tăng cường các ràng buộc từ đường dẫn** và tự động tài liệu hóa. +* **Tự động hóa validation**, thậm chí với các tham số *đường dẫn* định nghĩa trong các dependency. +* Hỗ trợ hệ thống xác thực người dùng phức tạp, **các kết nối cơ sở dữ liệu**,... +* **Không làm tổn hại** cơ sở dữ liệu, frontends,... Nhưng dễ dàng tích hợp với tất cả chúng. + +### Không giới hạn "plug-ins" + +Hoặc theo một cách nào khác, không cần chúng, import và sử dụng code bạn cần. + +Bất kì tích hợp nào được thiết kế để sử dụng đơn giản (với các dependency), đến nỗi bạn có thể tạo một "plug-in" cho ứng dụng của mình trong 2 dòng code bằng cách sử dụng cùng một cấu trúc và cú pháp được sử dụng cho *path operations* của bạn. + +### Đã được kiểm thử + +* 100% test coverage. +* 100% type annotated code base. +* Được sử dụng cho các ứng dụng sản phẩm. + +## Tính năng của Starlette + +`FastAPI` is thực sự là một sub-class của `Starlette`. Do đó, nếu bạn đã biết hoặc đã sử dụng Starlette, đa số các chức năng sẽ làm việc giống như vậy. + +Với **FastAPI**, bạn có được tất cả những tính năng của **Starlette**: + +* Hiệu năng thực sự ấn tượng. Nó là một trong nhưng framework Python nhanh nhất, khi so sánh với **NodeJS** và **Go**. +* Hỗ trợ **WebSocket**. +* In-process background tasks. +* Startup and shutdown events. +* Client cho kiểm thử xây dựng trên HTTPX. +* **CORS**, GZip, Static Files, Streaming responses. +* Hỗ trợ **Session and Cookie**. +* 100% test coverage. +* 100% type annotated codebase. + +## Tính năng của Pydantic + +**FastAPI** tương thích đầy đủ với (và dựa trên) Pydantic. Do đó, bất kì code Pydantic nào bạn thêm vào cũng sẽ hoạt động. + +Bao gồm các thư viện bên ngoài cũng dựa trên Pydantic, như ORMs, ODMs cho cơ sở dữ liệu. + +Nó cũng có nghĩa là trong nhiều trường hợp, bạn có thể truyền cùng object bạn có từ một request **trực tiếp cho cơ sở dữ liệu**, vì mọi thứ được validate tự động. + +Điều tương tự áp dụng cho các cách khác nhau, trong nhiều trường hợp, bạn có thể chỉ truyền object từ cơ sở dữ liêu **trực tiếp tới client**. + +Với **FastAPI**, bạn có tất cả những tính năng của **Pydantic** (FastAPI dựa trên Pydantic cho tất cả những xử lí về dữ liệu): + +* **Không gây rối não**: + * Không cần học ngôn ngữ mô tả cấu trúc mới. + * Nếu bạn biết kiểu dữ liệu Python, bạn biết cách sử dụng Pydantic. +* Sử dụng tốt với **IDE/linter/não của bạn**: + + * Bởi vì các cấu trúc dữ liệu của Pydantic chỉ là các instances của class bạn định nghĩa; auto-completion, linting, mypy và trực giác của bạn nên làm việc riêng biệt với những dữ liệu mà bạn đã validate. +* Validate **các cấu trúc phức tạp**: + * Sử dụng các models Pydantic phân tầng, `List` và `Dict` của Python `typing`,... + * Và các validators cho phép các cấu trúc dữ liệu phức tạp trở nên rõ ràng và dễ dàng để định nghĩa, kiểm tra và tài liệu hóa thành JSON Schema. + * Bạn có thể có các object **JSON lồng nhau** và tất cả chúng đã validate và annotated. +* **Có khả năng mở rộng**: + * Pydantic cho phép bạn tùy chỉnh kiểu dữ liệu bằng việc định nghĩa hoặc bạn có thể mở rộng validation với các decorator trong model. +* 100% test coverage. diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md new file mode 100644 index 000000000..ba5d68161 --- /dev/null +++ b/docs/vi/docs/index.md @@ -0,0 +1,476 @@ + +{!../../../docs/missing-translation.md!} + + +

+ FastAPI +

+

+ FastAPI framework, hiệu năng cao, dễ học, dễ code, sẵn sàng để tạo ra sản phẩm +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Tài liệu**: https://fastapi.tiangolo.com + +**Mã nguồn**: https://github.com/tiangolo/fastapi + +--- + +FastAPI là một web framework hiện đại, hiệu năng cao để xây dựng web APIs với Python 3.7+ dựa trên tiêu chuẩn Python type hints. + +Những tính năng như: + +* **Nhanh**: Hiệu năng rất cao khi so sánh với **NodeJS** và **Go** (cảm ơn Starlette và Pydantic). [Một trong những Python framework nhanh nhất](#performance). +* **Code nhanh**: Tăng tốc độ phát triển tính năng từ 200% tới 300%. * +* **Ít lỗi hơn**: Giảm khoảng 40% những lỗi phát sinh bởi con người (nhà phát triển). * +* **Trực giác tốt hơn**: Được các trình soạn thảo hỗ tuyệt vời. Completion mọi nơi. Ít thời gian gỡ lỗi. +* **Dễ dàng**: Được thiết kế để dễ dàng học và sử dụng. Ít thời gian đọc tài liệu. +* **Ngắn**: Tối thiểu code bị trùng lặp. Nhiều tính năng được tích hợp khi định nghĩa tham số. Ít lỗi hơn. +* **Tăng tốc**: Có được sản phẩm cùng với tài liệu (được tự động tạo) có thể tương tác. +* **Được dựa trên các tiêu chuẩn**: Dựa trên (và hoàn toàn tương thích với) các tiêu chuẩn mở cho APIs : OpenAPI (trước đó được biết đến là Swagger) và JSON Schema. + +* ước tính được dựa trên những kiểm chứng trong nhóm phát triển nội bộ, xây dựng các ứng dụng sản phẩm. + +## Nhà tài trợ + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Những nhà tài trợ khác + +## Ý kiến đánh giá + +"_[...] Tôi đang sử dụng **FastAPI** vô cùng nhiều vào những ngày này. [...] Tôi thực sự đang lên kế hoạch sử dụng nó cho tất cả các nhóm **dịch vụ ML tại Microsoft**. Một vài trong số đó đang tích hợp vào sản phẩm lõi của **Window** và một vài sản phẩm cho **Office**._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_Chúng tôi tích hợp thư viện **FastAPI** để sinh ra một **REST** server, nó có thể được truy vấn để thu được những **dự đoán**._ [bởi Ludwid] " + +
Piero Molino, Yaroslav Dudin, và Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** vui mừng thông báo việc phát hành framework mã nguồn mở của chúng tôi cho *quản lí khủng hoảng* tập trung: **Dispatch**! [xây dựng với **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Tôi vô cùng hào hứng về **FastAPI**. Nó rất thú vị_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Thành thật, những gì bạn đã xây dựng nhìn siêu chắc chắn và bóng bẩy. Theo nhiều cách, nó là những gì tôi đã muốn Hug trở thành - thật sự truyền cảm hứng để thấy ai đó xây dựng nó._" + +
Timothy Crosley - người tạo ra Hug (ref)
+ +--- + +"_Nếu bạn đang tìm kiếm một **framework hiện đại** để xây dựng một REST APIs, thử xem xét **FastAPI** [...] Nó nhanh, dễ dùng và dễ học [...]_" + +"_Chúng tôi đã chuyển qua **FastAPI cho **APIs** của chúng tôi [...] Tôi nghĩ bạn sẽ thích nó [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+
Ines Montani - Matthew Honnibal - nhà sáng lập Explosion AI - người tạo ra spaCy (ref) - (ref)
+ +--- + +"_Nếu ai đó đang tìm cách xây dựng sản phẩm API bằng Python, tôi sẽ đề xuất **FastAPI**. Nó **được thiết kế đẹp đẽ**, **sử dụng đơn giản** và **có khả năng mở rộng cao**, nó đã trở thành một **thành phần quan trọng** trong chiến lược phát triển API của chúng tôi và đang thúc đẩy nhiều dịch vụ và mảng tự động hóa như Kỹ sư TAC ảo của chúng tôi._" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, giao diện dòng lệnh của FastAPI + + + +Nếu bạn đang xây dựng một CLI - ứng dụng được sử dụng trong giao diện dòng lệnh, xem về **Typer**. + +**Typer** là một người anh em của FastAPI. Và nó được dự định trở thành **giao diện dòng lệnh cho FastAPI**. ⌨️ 🚀 + +## Yêu cầu + +Python 3.7+ + +FastAPI đứng trên vai những người khổng lồ: + +* Starlette cho phần web. +* Pydantic cho phần data. + +## Cài đặt + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+ +Bạn cũng sẽ cần một ASGI server cho production như Uvicorn hoặc Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Ví dụ + +### Khởi tạo + +* Tạo một tệp tin `main.py` như sau: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Hoặc sử dụng async def... + +Nếu code của bạn sử dụng `async` / `await`, hãy sử dụng `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Lưu ý**: + +Nếu bạn không biết, xem phần _"In a hurry?"_ về `async` và `await` trong tài liệu này. + +
+ +### Chạy ứng dụng + +Chạy server như sau: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+Về lệnh uvicorn main:app --reload... + +Lệnh `uvicorn main:app` tham chiếu tới những thành phần sau: + +* `main`: tệp tin `main.py` (một Python "module"). +* `app`: object được tạo trong tệp tin `main.py` tại dòng `app = FastAPI()`. +* `--reload`: chạy lại server sau khi code thay đổi. Chỉ sử dụng trong quá trình phát triển. + +
+ +### Kiểm tra + +Mở trình duyệt của bạn tại http://127.0.0.1:8000/items/5?q=somequery. + +Bạn sẽ thấy một JSON response: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Bạn đã sẵn sàng để tạo một API như sau: + +* Nhận HTTP request với _đường dẫn_ `/` và `/items/{item_id}`. +* Cả hai _đường dẫn_ sử dụng toán tử `GET` (cũng đươc biết đến là _phương thức_ HTTP). +* _Đường dẫn_ `/items/{item_id}` có một _tham số đường dẫn_ `item_id`, nó là một tham số kiểu `int`. +* _Đường dẫn_ `/items/{item_id}` có một _tham số query string_ `q`, nó là một tham số tùy chọn kiểu `str`. + +### Tài liệu tương tác API + +Truy cập http://127.0.0.1:8000/docs. + +Bạn sẽ thấy tài liệu tương tác API được tạo tự động (cung cấp bởi Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Tài liệu API thay thế + +Và bây giờ, hãy truy cập tới http://127.0.0.1:8000/redoc. + +Bạn sẽ thấy tài liệu được thay thế (cung cấp bởi ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Nâng cấp ví dụ + +Bây giờ sửa tệp tin `main.py` để nhận body từ một request `PUT`. + +Định nghĩa của body sử dụng kiểu dữ liệu chuẩn của Python, cảm ơn Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Server nên tự động chạy lại (bởi vì bạn đã thêm `--reload` trong lệnh `uvicorn` ở trên). + +### Nâng cấp tài liệu API + +Bây giờ truy cập tới http://127.0.0.1:8000/docs. + +* Tài liệu API sẽ được tự động cập nhật, bao gồm body mới: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Click vào nút "Try it out", nó cho phép bạn điền những tham số và tương tác trực tiếp với API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Sau khi click vào nút "Execute", giao diện người dùng sẽ giao tiếp với API của bạn bao gồm: gửi các tham số, lấy kết quả và hiển thị chúng trên màn hình: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Nâng cấp tài liệu API thay thế + +Và bây giờ truy cập tới http://127.0.0.1:8000/redoc. + +* Tài liệu thay thế cũng sẽ phản ánh tham số và body mới: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Tóm lại + +Bạn khai báo **một lần** kiểu dữ liệu của các tham số, body, etc là các tham số của hàm số. + +Bạn định nghĩa bằng cách sử dụng các kiểu dữ liệu chuẩn của Python. + +Bạn không phải học một cú pháp mới, các phương thức và class của một thư viện cụ thể nào. + +Chỉ cần sử dụng các chuẩn của **Python 3.7+**. + +Ví dụ, với một tham số kiểu `int`: + +```Python +item_id: int +``` + +hoặc với một model `Item` phức tạp hơn: + +```Python +item: Item +``` + +...và với định nghĩa đơn giản đó, bạn có được: + +* Sự hỗ trợ từ các trình soạn thảo, bao gồm: + * Completion. + * Kiểm tra kiểu dữ liệu. +* Kiểm tra kiểu dữ liệu: + * Tự động sinh lỗi rõ ràng khi dữ liệu không hợp lệ . + * Kiểm tra JSON lồng nhau . +* Chuyển đổi dữ liệu đầu vào: tới từ network sang dữ liệu kiểu Python. Đọc từ: + * JSON. + * Các tham số trong đường dẫn. + * Các tham số trong query string. + * Cookies. + * Headers. + * Forms. + * Files. +* Chuyển đổi dữ liệu đầu ra: chuyển đổi từ kiểu dữ liệu Python sang dữ liệu network (như JSON): + * Chuyển đổi kiểu dữ liệu Python (`str`, `int`, `float`, `bool`, `list`,...). + * `datetime` objects. + * `UUID` objects. + * Database models. + * ...và nhiều hơn thế. +* Tự động tạo tài liệu tương tác API, bao gồm 2 giao diện người dùng: + * Swagger UI. + * ReDoc. + +--- + +Quay trở lại ví dụ trước, **FastAPI** sẽ thực hiện: + +* Kiểm tra xem có một `item_id` trong đường dẫn với các request `GET` và `PUT` không? +* Kiểm tra xem `item_id` có phải là kiểu `int` trong các request `GET` và `PUT` không? + * Nếu không, client sẽ thấy một lỗi rõ ràng và hữu ích. +* Kiểm tra xem nếu có một tham số `q` trong query string (ví dụ như `http://127.0.0.1:8000/items/foo?q=somequery`) cho request `GET`. + * Tham số `q` được định nghĩa `= None`, nó là tùy chọn. + * Nếu không phải `None`, nó là bắt buộc (như body trong trường hợp của `PUT`). +* Với request `PUT` tới `/items/{item_id}`, đọc body như JSON: + * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `str` là `name` không? + * Kiểm tra xem nó có một thuộc tính bắt buộc kiểu `float` là `price` không? + * Kiểm tra xem nó có một thuộc tính tùy chọn là `is_offer` không? Nếu có, nó phải có kiểu `bool`. + * Tất cả những kiểm tra này cũng được áp dụng với các JSON lồng nhau. +* Chuyển đổi tự động các JSON object đến và JSON object đi. +* Tài liệu hóa mọi thứ với OpenAPI, tài liệu đó có thể được sử dụng bởi: + + * Các hệ thống tài liệu có thể tương tác. + * Hệ thống sinh code tự động, cho nhiều ngôn ngữ lập trình. +* Cung cấp trực tiếp 2 giao diện web cho tài liệu tương tác + +--- + +Chúng tôi chỉ trình bày những thứ cơ bản bên ngoài, nhưng bạn đã hiểu cách thức hoạt động của nó. + +Thử thay đổi dòng này: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...từ: + +```Python + ... "item_name": item.name ... +``` + +...sang: + +```Python + ... "item_price": item.price ... +``` + +...và thấy trình soạn thảo của bạn nhận biết kiểu dữ liệu và gợi ý hoàn thiện các thuộc tính. + +![trình soạn thảo hỗ trợ](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Ví dụ hoàn chỉnh bao gồm nhiều tính năng hơn, xem Tutorial - User Guide. + + +**Cảnh báo tiết lỗ**: Tutorial - User Guide: + +* Định nghĩa **tham số** từ các nguồn khác nhau như: **headers**, **cookies**, **form fields** và **files**. +* Cách thiết lập **các ràng buộc cho validation** như `maximum_length` hoặc `regex`. +* Một hệ thống **Dependency Injection vô cùng mạnh mẽ và dễ dàng sử dụng. +* Bảo mật và xác thực, hỗ trợ **OAuth2**(với **JWT tokens**) và **HTTP Basic**. +* Những kĩ thuật nâng cao hơn (nhưng tương đối dễ) để định nghĩa **JSON models lồng nhau** (cảm ơn Pydantic). +* Tích hợp **GraphQL** với Strawberry và các thư viện khác. +* Nhiều tính năng mở rộng (cảm ơn Starlette) như: + * **WebSockets** + * kiểm thử vô cùng dễ dàng dựa trên HTTPX và `pytest` + * **CORS** + * **Cookie Sessions** + * ...và nhiều hơn thế. + +## Hiệu năng + +Independent TechEmpower benchmarks cho thấy các ứng dụng **FastAPI** chạy dưới Uvicorn là một trong những Python framework nhanh nhất, chỉ đứng sau Starlette và Uvicorn (được sử dụng bên trong FastAPI). (*) + +Để hiểu rõ hơn, xem phần Benchmarks. + +## Các dependency tùy chọn + +Sử dụng bởi Pydantic: + +* ujson - "Parse" JSON nhanh hơn. +* email_validator - cho email validation. + +Sử dụng Starlette: + +* httpx - Bắt buộc nếu bạn muốn sử dụng `TestClient`. +* jinja2 - Bắt buộc nếu bạn muốn sử dụng cấu hình template engine mặc định. +* python-multipart - Bắt buộc nếu bạn muốn hỗ trợ "parsing", form với `request.form()`. +* itsdangerous - Bắt buộc để hỗ trợ `SessionMiddleware`. +* pyyaml - Bắt buộc để hỗ trợ `SchemaGenerator` cho Starlette (bạn có thể không cần nó trong FastAPI). +* ujson - Bắt buộc nếu bạn muốn sử dụng `UJSONResponse`. + +Sử dụng bởi FastAPI / Starlette: + +* uvicorn - Server để chạy ứng dụng của bạn. +* orjson - Bắt buộc nếu bạn muốn sử dụng `ORJSONResponse`. + +Bạn có thể cài đặt tất cả những dependency trên với `pip install "fastapi[all]"`. + +## Giấy phép + +Dự án này được cấp phép dưới những điều lệ của giấy phép MIT. diff --git a/docs/vi/mkdocs.yml b/docs/vi/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/vi/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From c52c94006650dbd53f6d4bd75ef0a7459ad7e3d8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 19:04:24 +0000 Subject: [PATCH 1184/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ce28cb029..b69b6d594 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/ko/docs/async.md`. PR [#4179](https://github.com/tiangolo/fastapi/pull/4179) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). * 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). From 643d8e41c498d7575e0e5947ca5bef99e0f2804e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 19:08:23 +0000 Subject: [PATCH 1185/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b69b6d594..537e57fc7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Vietnamese translation for `docs/vi/docs/features.md` and `docs/vi/docs/index.md`. PR [#3006](https://github.com/tiangolo/fastapi/pull/3006) by [@magiskboy](https://github.com/magiskboy). * 🌐 Add Korean translation for `docs/ko/docs/async.md`. PR [#4179](https://github.com/tiangolo/fastapi/pull/4179) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). * 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). From 7b3d770d65eeaef9720b5140c18f994d82e0bbbc Mon Sep 17 00:00:00 2001 From: Orest Furda <56111536+ss-o-furda@users.noreply.github.com> Date: Thu, 27 Jul 2023 22:09:34 +0300 Subject: [PATCH 1186/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/tutorial/body.md`=20(#4574)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/uk/docs/tutorial/body.md | 213 ++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/uk/docs/tutorial/body.md diff --git a/docs/uk/docs/tutorial/body.md b/docs/uk/docs/tutorial/body.md new file mode 100644 index 000000000..e78c5de0e --- /dev/null +++ b/docs/uk/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# Тіло запиту + +Коли вам потрібно надіслати дані з клієнта (скажімо, браузера) до вашого API, ви надсилаєте їх як **тіло запиту**. + +Тіло **запиту** — це дані, надіслані клієнтом до вашого API. Тіло **відповіді** — це дані, які ваш API надсилає клієнту. + +Ваш API майже завжди має надсилати тіло **відповіді**. Але клієнтам не обов’язково потрібно постійно надсилати тіла **запитів**. + +Щоб оголосити тіло **запиту**, ви використовуєте Pydantic моделі з усією їх потужністю та перевагами. + +!!! info + Щоб надіслати дані, ви повинні використовувати один із: `POST` (більш поширений), `PUT`, `DELETE` або `PATCH`. + + Надсилання тіла із запитом `GET` має невизначену поведінку в специфікаціях, проте воно підтримується FastAPI лише для дуже складних/екстремальних випадків використання. + + Оскільки це не рекомендується, інтерактивна документація з Swagger UI не відображатиме документацію для тіла запиту під час використання `GET`, і проксі-сервери в середині можуть не підтримувати її. + +## Імпортуйте `BaseModel` від Pydantic + +Спочатку вам потрібно імпортувати `BaseModel` з `pydantic`: + +=== "Python 3.6 і вище" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +## Створіть свою модель даних + +Потім ви оголошуєте свою модель даних як клас, який успадковується від `BaseModel`. + +Використовуйте стандартні типи Python для всіх атрибутів: + +=== "Python 3.6 і вище" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +Так само, як і при оголошенні параметрів запиту, коли атрибут моделі має значення за замовчуванням, він не є обов’язковим. В іншому випадку це потрібно. Використовуйте `None`, щоб зробити його необов'язковим. + +Наприклад, ця модель вище оголошує JSON "`об'єкт`" (або Python `dict`), як: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +...оскільки `description` і `tax` є необов'язковими (зі значенням за замовчуванням `None`), цей JSON "`об'єкт`" також буде дійсним: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Оголоси її як параметр + +Щоб додати модель даних до вашої *операції шляху*, оголосіть її так само, як ви оголосили параметри шляху та запиту: + +=== "Python 3.6 і вище" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +...і вкажіть її тип як модель, яку ви створили, `Item`. + +## Результати + +Лише з цим оголошенням типу Python **FastAPI** буде: + +* Читати тіло запиту як JSON. +* Перетворювати відповідні типи (якщо потрібно). +* Валідувати дані. + * Якщо дані недійсні, він поверне гарну та чітку помилку, вказуючи, де саме і які дані були неправильними. +* Надавати отримані дані у параметрі `item`. + * Оскільки ви оголосили його у функції як тип `Item`, ви також матимете всю підтримку редактора (автозаповнення, тощо) для всіх атрибутів та їх типів. +* Генерувати JSON Schema визначення для вашої моделі, ви також можете використовувати їх де завгодно, якщо це має сенс для вашого проекту. +* Ці схеми будуть частиною згенерованої схеми OpenAPI і використовуватимуться автоматичною документацією інтерфейсу користувача. + +## Автоматична документація + +Схеми JSON ваших моделей будуть частиною вашої схеми, згенерованої OpenAPI, і будуть показані в інтерактивній API документації: + + + +А також використовуватимуться в API документації всередині кожної *операції шляху*, якій вони потрібні: + + + +## Підтримка редактора + +У вашому редакторі, всередині вашої функції, ви будете отримувати підказки типу та завершення скрізь (це б не сталося, якби ви отримали `dict` замість моделі Pydantic): + + + +Ви також отримуєте перевірку помилок на наявність операцій з неправильним типом: + + + +Це не випадково, весь каркас був побудований навколо цього дизайну. + +І він був ретельно перевірений на етапі проектування, перед будь-яким впровадженням, щоб переконатися, що він працюватиме з усіма редакторами. + +Були навіть деякі зміни в самому Pydantic, щоб підтримати це. + +Попередні скріншоти були зроблені у Visual Studio Code. + +Але ви отримаєте ту саму підтримку редактора у PyCharm та більшість інших редакторів Python: + + + +!!! tip + Якщо ви використовуєте PyCharm як ваш редактор, ви можете використати Pydantic PyCharm Plugin. + + Він покращує підтримку редакторів для моделей Pydantic за допомогою: + + * автозаповнення + * перевірки типу + * рефакторингу + * пошуку + * інспекції + +## Використовуйте модель + +Усередині функції ви можете отримати прямий доступ до всіх атрибутів об’єкта моделі: + +=== "Python 3.6 і вище" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +## Тіло запиту + параметри шляху + +Ви можете одночасно оголошувати параметри шляху та тіло запиту. + +**FastAPI** розпізнає, що параметри функції, які відповідають параметрам шляху, мають бути **взяті з шляху**, а параметри функції, які оголошуються як моделі Pydantic, **взяті з тіла запиту**. + +=== "Python 3.6 і вище" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +## Тіло запиту + шлях + параметри запиту + +Ви також можете оголосити параметри **тіло**, **шлях** і **запит** одночасно. + +**FastAPI** розпізнає кожен з них і візьме дані з потрібного місця. + +=== "Python 3.6 і вище" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +Параметри функції будуть розпізнаватися наступним чином: + +* Якщо параметр також оголошено в **шляху**, він використовуватиметься як параметр шляху. +* Якщо параметр має **сингулярний тип** (наприклад, `int`, `float`, `str`, `bool` тощо), він буде інтерпретуватися як параметр **запиту**. +* Якщо параметр оголошується як тип **Pydantic моделі**, він інтерпретується як **тіло** запиту. + +!!! note + FastAPI буде знати, що значення "q" не є обов'язковим через значення за замовчуванням "= None". + + `Optional` у `Optional[str]` не використовується FastAPI, але дозволить вашому редактору надати вам кращу підтримку та виявляти помилки. + +## Без Pydantic + +Якщо ви не хочете використовувати моделі Pydantic, ви також можете використовувати параметри **Body**. Перегляньте документацію для [Тіло – Кілька параметрів: сингулярні значення в тілі](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank}. From bec5530ac849f9c52d35d5282628333a278b6a26 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 27 Jul 2023 19:14:48 +0000 Subject: [PATCH 1187/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 537e57fc7..42d240f28 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body.md`. PR [#4574](https://github.com/tiangolo/fastapi/pull/4574) by [@ss-o-furda](https://github.com/ss-o-furda). * 🌐 Add Vietnamese translation for `docs/vi/docs/features.md` and `docs/vi/docs/index.md`. PR [#3006](https://github.com/tiangolo/fastapi/pull/3006) by [@magiskboy](https://github.com/magiskboy). * 🌐 Add Korean translation for `docs/ko/docs/async.md`. PR [#4179](https://github.com/tiangolo/fastapi/pull/4179) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). From effa578b8df27b7d4afa627a4508413811fcf72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 27 Jul 2023 21:15:16 +0200 Subject: [PATCH 1188/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 42d240f28..7211cc3fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,11 +2,20 @@ ## Latest Changes +### Fixes + +* 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). + +### Docs + +* 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body.md`. PR [#4574](https://github.com/tiangolo/fastapi/pull/4574) by [@ss-o-furda](https://github.com/ss-o-furda). * 🌐 Add Vietnamese translation for `docs/vi/docs/features.md` and `docs/vi/docs/index.md`. PR [#3006](https://github.com/tiangolo/fastapi/pull/3006) by [@magiskboy](https://github.com/magiskboy). * 🌐 Add Korean translation for `docs/ko/docs/async.md`. PR [#4179](https://github.com/tiangolo/fastapi/pull/4179) by [@NinaHwang](https://github.com/NinaHwang). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/background-tasks.md`. PR [#9812](https://github.com/tiangolo/fastapi/pull/9812) by [@wdh99](https://github.com/wdh99). -* 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add French translation for `docs/fr/docs/tutorial/query-params-str-validations.md`. PR [#4075](https://github.com/tiangolo/fastapi/pull/4075) by [@Smlep](https://github.com/Smlep). * 🌐 Add French translation for `docs/fr/docs/tutorial/index.md`. PR [#2234](https://github.com/tiangolo/fastapi/pull/2234) by [@JulianMaurin](https://github.com/JulianMaurin). * 🌐 Add French translation for `docs/fr/docs/contributing.md`. PR [#2132](https://github.com/tiangolo/fastapi/pull/2132) by [@JulianMaurin](https://github.com/JulianMaurin). @@ -14,12 +23,14 @@ * 🌐 Update Chinese translations with new source files. PR [#9738](https://github.com/tiangolo/fastapi/pull/9738) by [@mahone3297](https://github.com/mahone3297). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms.md`. PR [#9841](https://github.com/tiangolo/fastapi/pull/9841) by [@dedkot01](https://github.com/dedkot01). * 🌐 Update Chinese translation for `docs/zh/docs/tutorial/handling-errors.md`. PR [#9485](https://github.com/tiangolo/fastapi/pull/9485) by [@Creat55](https://github.com/Creat55). -* 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). + +### Internal + +* 🔧 Update sponsors, add Fern. PR [#9956](https://github.com/tiangolo/fastapi/pull/9956) by [@tiangolo](https://github.com/tiangolo). * 👷 Update FastAPI People token. PR [#9844](https://github.com/tiangolo/fastapi/pull/9844) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9775](https://github.com/tiangolo/fastapi/pull/9775) by [@tiangolo](https://github.com/tiangolo). * 👷 Update MkDocs Material token. PR [#9843](https://github.com/tiangolo/fastapi/pull/9843) by [@tiangolo](https://github.com/tiangolo). * 👷 Update token for latest changes. PR [#9842](https://github.com/tiangolo/fastapi/pull/9842) by [@tiangolo](https://github.com/tiangolo). -* 📝 Update links for self-hosted Swagger UI, point to v5, for OpenAPI 31.0. PR [#9834](https://github.com/tiangolo/fastapi/pull/9834) by [@tiangolo](https://github.com/tiangolo). ## 0.100.0 From 8d2723664801c32e6174cd87812fb92e850cfa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 27 Jul 2023 21:16:01 +0200 Subject: [PATCH 1189/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?100.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 7211cc3fc..b56941ec7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.100.1 + ### Fixes * 🐛 Replace `MultHostUrl` to `AnyUrl` for compatibility with older versions of Pydantic v1. PR [#9852](https://github.com/tiangolo/fastapi/pull/9852) by [@Kludex](https://github.com/Kludex). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index e9c3abe01..7dfeca0d4 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.100.0" +__version__ = "0.100.1" from starlette import status as status From 076bdea6716a5fbfc84d2227066dea6bd91ffb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 28 Jul 2023 14:15:29 +0200 Subject: [PATCH 1190/1881] =?UTF-8?q?=F0=9F=8C=90=20Remove=20Vietnamese=20?= =?UTF-8?q?note=20about=20missing=20translation=20(#9957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 6 ++++++ docs/uk/mkdocs.yml | 1 + docs/vi/docs/index.md | 4 ---- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 docs/uk/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 030bbe5d3..a18613185 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -59,6 +59,8 @@ nav: - pt: /pt/ - ru: /ru/ - tr: /tr/ + - uk: /uk/ + - vi: /vi/ - zh: /zh/ - features.md - fastapi-people.md @@ -236,6 +238,10 @@ extra: name: ru - русский язык - link: /tr/ name: tr - Türkçe + - link: /uk/ + name: uk + - link: /vi/ + name: vi - link: /zh/ name: zh - 汉语 extra_css: diff --git a/docs/uk/mkdocs.yml b/docs/uk/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/uk/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md index ba5d68161..0e773a011 100644 --- a/docs/vi/docs/index.md +++ b/docs/vi/docs/index.md @@ -1,7 +1,3 @@ - -{!../../../docs/missing-translation.md!} - -

FastAPI

From cd6d75e451cd998cb511c20a72055110035b63ed Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 28 Jul 2023 12:16:16 +0000 Subject: [PATCH 1191/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b56941ec7..4addb83cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). ## 0.100.1 From a0b987224aa0f80689568b3d38e211b23a7d7e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Jul 2023 21:54:07 +0200 Subject: [PATCH 1192/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20CI=20debug=20?= =?UTF-8?q?mode=20with=20Tmate=20(#9977)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 6 +++--- .github/workflows/notify-translations.yml | 11 ++++++++++- .github/workflows/people.yml | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 1f7ac7b28..0461f3dd3 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -14,7 +14,7 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: latest-changes: @@ -22,12 +22,12 @@ jobs: steps: - uses: actions/checkout@v3 with: - # To allow latest-changes to commit to master + # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: docker://tiangolo/latest-changes:0.0.3 diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 0926486e9..cd7affbc3 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -5,6 +5,15 @@ on: types: - labeled - closed + workflow_dispatch: + inputs: + number: + description: PR number + required: true + debug_enabled: + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: 'false' jobs: notify-translations: @@ -14,7 +23,7 @@ jobs: # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: ./.github/actions/notify-translations diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index dac526a6c..aa7f34464 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -8,7 +8,7 @@ on: debug_enabled: description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' required: false - default: false + default: 'false' jobs: fastapi-people: @@ -22,7 +22,7 @@ jobs: # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 - if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - uses: ./.github/actions/people From d38e86ef203f5a1c710452737b32ad63466f23d7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 19:54:46 +0000 Subject: [PATCH 1193/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4addb83cc..14ce41374 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). * 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). ## 0.100.1 From 1da0a7afbd53c66eefddc2455501159bf50f55ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 31 Jul 2023 23:49:19 +0200 Subject: [PATCH 1194/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsor=20Fer?= =?UTF-8?q?n=20(#9979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 2 +- docs/en/docs/advanced/generate-clients.md | 5 +++++ docs/en/docs/alternatives.md | 2 ++ docs/en/docs/img/sponsors/fern-banner.svg | 1 + docs/en/docs/img/sponsors/fern.svg | 1 + docs/en/overrides/main.html | 2 +- 7 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/sponsors/fern-banner.svg create mode 100644 docs/en/docs/img/sponsors/fern.svg diff --git a/README.md b/README.md index f0e76c4b6..50f80ded6 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 33d57c873..53cdb9bad 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -7,7 +7,7 @@ gold: img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png - url: https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge title: Fern | SDKs and API docs - img: https://fastapi.tiangolo.com/img/sponsors/fern.png + img: https://fastapi.tiangolo.com/img/sponsors/fern.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index f62c0b57c..3fed48b0b 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -12,6 +12,11 @@ A common tool is openapi-typescript-codegen. +Another option you could consider for several languages is Fern. + +!!! info + Fern is also a FastAPI sponsor. 😎🎉 + ## Generate a TypeScript Frontend Client Let's start with a simple FastAPI application: diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 0f074ccf3..a777ddb98 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -119,6 +119,8 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**). + For example, you could try Fern which is also a FastAPI sponsor. 😎🎉 + ### Flask REST frameworks There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit. diff --git a/docs/en/docs/img/sponsors/fern-banner.svg b/docs/en/docs/img/sponsors/fern-banner.svg new file mode 100644 index 000000000..bb3a389f3 --- /dev/null +++ b/docs/en/docs/img/sponsors/fern-banner.svg @@ -0,0 +1 @@ + diff --git a/docs/en/docs/img/sponsors/fern.svg b/docs/en/docs/img/sponsors/fern.svg new file mode 100644 index 000000000..ad3842fe0 --- /dev/null +++ b/docs/en/docs/img/sponsors/fern.svg @@ -0,0 +1 @@ + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index fcd1704b9..7e6c0f763 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -37,7 +37,7 @@
From 74de15d0df975f7da4b29e9ad49142d7da172b6c Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 31 Jul 2023 21:49:56 +0000 Subject: [PATCH 1195/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 14ce41374..ead095d8d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). * 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). From d2169fbad9eb1b2537292996ce8d26e7584e9e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 1 Aug 2023 11:19:44 +0200 Subject: [PATCH 1196/1881] =?UTF-8?q?=F0=9F=91=B7=20Deploy=20docs=20to=20C?= =?UTF-8?q?loudflare=20Pages=20(#9978)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-docs.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 312d835af..dcd6d7107 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,21 +29,20 @@ jobs: run_id: ${{ github.event.workflow_run.id }} name: docs-site path: ./site/ - - name: Deploy to Netlify + - name: Deploy to Cloudflare Pages if: steps.download.outputs.found_artifact == 'true' - id: netlify - uses: nwtgck/actions-netlify@v2.0.0 + id: deploy + uses: cloudflare/pages-action@v1 with: - publish-dir: './site' - production-deploy: ${{ github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' }} - github-token: ${{ secrets.FASTAPI_PREVIEW_DOCS_NETLIFY }} - enable-commit-comment: false - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: fastapitiangolo + directory: './site' + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ ( github.event.workflow_run.head_repository.full_name == github.repository && github.event.workflow_run.head_branch == 'master' && 'main' ) || ( github.event.workflow_run.head_sha ) }} - name: Comment Deploy - if: steps.netlify.outputs.deploy-url != '' + if: steps.deploy.outputs.url != '' uses: ./.github/actions/comment-docs-preview-in-pr with: token: ${{ secrets.FASTAPI_PREVIEW_DOCS_COMMENT_DEPLOY }} - deploy_url: "${{ steps.netlify.outputs.deploy-url }}" + deploy_url: "${{ steps.deploy.outputs.url }}" From 6c8c3b788bb7c3338f1105493833c9815ba243a4 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 1 Aug 2023 09:20:23 +0000 Subject: [PATCH 1197/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ead095d8d..2aed9859a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). * 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). From c2a33f1087b5843054de839b47e7fe7200a62504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 1 Aug 2023 23:39:22 +0200 Subject: [PATCH 1198/1881] =?UTF-8?q?=F0=9F=8D=B1=20Update=20sponsors,=20F?= =?UTF-8?q?ern=20badge=20(#9982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/img/sponsors/fern-banner.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/img/sponsors/fern-banner.svg b/docs/en/docs/img/sponsors/fern-banner.svg index bb3a389f3..e05ccc3a4 100644 --- a/docs/en/docs/img/sponsors/fern-banner.svg +++ b/docs/en/docs/img/sponsors/fern-banner.svg @@ -1 +1 @@ - + From 01f91fdb57e55b4540363ef5749480dc3f3e8bf6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 1 Aug 2023 21:40:00 +0000 Subject: [PATCH 1199/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2aed9859a..895926cab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). * 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). From 88d96799b15e530c068f749626595c739bce3fcf Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 2 Aug 2023 18:14:19 +0300 Subject: [PATCH 1200/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/security/index.md`=20(#996?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dedkot Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/security/index.md | 101 ++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/ru/docs/tutorial/security/index.md diff --git a/docs/ru/docs/tutorial/security/index.md b/docs/ru/docs/tutorial/security/index.md new file mode 100644 index 000000000..d5fe4e76f --- /dev/null +++ b/docs/ru/docs/tutorial/security/index.md @@ -0,0 +1,101 @@ +# Настройка авторизации + +Существует множество способов обеспечения безопасности, аутентификации и авторизации. + +Обычно эта тема является достаточно сложной и трудной. + +Во многих фреймворках и системах только работа с определением доступов к приложению и аутентификацией требует значительных затрат усилий и написания множества кода (во многих случаях его объём может составлять более 50% от всего написанного кода). + +**FastAPI** предоставляет несколько инструментов, которые помогут вам настроить **Авторизацию** легко, быстро, стандартным способом, без необходимости изучать все её тонкости. + +Но сначала давайте рассмотрим некоторые небольшие концепции. + +## Куда-то торопишься? + +Если вам не нужна информация о каких-либо из следующих терминов и вам просто нужно добавить защиту с аутентификацией на основе логина и пароля *прямо сейчас*, переходите к следующим главам. + +## OAuth2 + +OAuth2 - это протокол, который определяет несколько способов обработки аутентификации и авторизации. + +Он довольно обширен и охватывает несколько сложных вариантов использования. + +OAuth2 включает в себя способы аутентификации с использованием "третьей стороны". + +Это то, что используют под собой все кнопки "вход с помощью Facebook, Google, Twitter, GitHub" на страницах авторизации. + +### OAuth 1 + +Ранее использовался протокол OAuth 1, который сильно отличается от OAuth2 и является более сложным, поскольку он включал прямые описания того, как шифровать сообщение. + +В настоящее время он не очень популярен и не используется. + +OAuth2 не указывает, как шифровать сообщение, он ожидает, что ваше приложение будет обслуживаться по протоколу HTTPS. + +!!! tip "Подсказка" + В разделе **Развертывание** вы увидите [как настроить протокол HTTPS бесплатно, используя Traefik и Let's Encrypt.](https://fastapi.tiangolo.com/ru/deployment/https/) + + +## OpenID Connect + +OpenID Connect - это еще один протокол, основанный на **OAuth2**. + +Он просто расширяет OAuth2, уточняя некоторые вещи, не имеющие однозначного определения в OAuth2, в попытке сделать его более совместимым. + +Например, для входа в Google используется OpenID Connect (который под собой использует OAuth2). + +Но вход в Facebook не поддерживает OpenID Connect. У него есть собственная вариация OAuth2. + +### OpenID (не "OpenID Connect") + +Также ранее использовался стандарт "OpenID", который пытался решить ту же проблему, что и **OpenID Connect**, но не был основан на OAuth2. + +Таким образом, это была полноценная дополнительная система. + +В настоящее время не очень популярен и не используется. + +## OpenAPI + +OpenAPI (ранее известный как Swagger) - это открытая спецификация для создания API (в настоящее время является частью Linux Foundation). + +**FastAPI** основан на **OpenAPI**. + +Это то, что делает возможным наличие множества автоматических интерактивных интерфейсов документирования, сгенерированного кода и т.д. + +В OpenAPI есть способ использовать несколько "схем" безопасности. + +Таким образом, вы можете воспользоваться преимуществами Всех этих стандартных инструментов, включая интерактивные системы документирования. + +OpenAPI может использовать следующие схемы авторизации: + +* `apiKey`: уникальный идентификатор для приложения, который может быть получен из: + * Параметров запроса. + * Заголовка. + * Cookies. +* `http`: стандартные системы аутентификации по протоколу HTTP, включая: + * `bearer`: заголовок `Authorization` со значением `Bearer {уникальный токен}`. Это унаследовано от OAuth2. + * Базовая аутентификация по протоколу HTTP. + * HTTP Digest и т.д. +* `oauth2`: все способы обеспечения безопасности OAuth2 называемые "потоки" (англ. "flows"). + * Некоторые из этих "потоков" подходят для реализации аутентификации через сторонний сервис использующий OAuth 2.0 (например, Google, Facebook, Twitter, GitHub и т.д.): + * `implicit` + * `clientCredentials` + * `authorizationCode` + * Но есть один конкретный "поток", который может быть идеально использован для обработки аутентификации непосредственно в том же приложении: + * `password`: в некоторых следующих главах будут рассмотрены примеры этого. +* `openIdConnect`: способ определить, как автоматически обнаруживать данные аутентификации OAuth2. + * Это автоматическое обнаружение определено в спецификации OpenID Connect. + + +!!! tip "Подсказка" + Интеграция сторонних сервисов для аутентификации/авторизации таких как Google, Facebook, Twitter, GitHub и т.д. осуществляется достаточно легко. + + Самой сложной проблемой является создание такого провайдера аутентификации/авторизации, но **FastAPI** предоставляет вам инструменты, позволяющие легко это сделать, выполняя при этом всю тяжелую работу за вас. + +## Преимущества **FastAPI** + +Fast API предоставляет несколько инструментов для каждой из этих схем безопасности в модуле `fastapi.security`, которые упрощают использование этих механизмов безопасности. + +В следующих главах вы увидите, как обезопасить свой API, используя инструменты, предоставляемые **FastAPI**. + +И вы также увидите, как он автоматически интегрируется в систему интерактивной документации. From 2d8a776836e1363e021b2a1233a72584f57b5d7a Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 2 Aug 2023 15:15:10 +0000 Subject: [PATCH 1201/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 895926cab..fde398be5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). * 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). * 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). From 37818f553ddb21b7adaaf16d18b95f9710065ad4 Mon Sep 17 00:00:00 2001 From: Irfanuddin Shafi Ahmed Date: Wed, 2 Aug 2023 20:58:34 +0530 Subject: [PATCH 1202/1881] =?UTF-8?q?=E2=9C=85=20Fix=20test=20error=20in?= =?UTF-8?q?=20Windows=20for=20`jsonable=5Fencoder`=20(#9840)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcelo Trylesinski --- tests/test_jsonable_encoder.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_jsonable_encoder.py b/tests/test_jsonable_encoder.py index ff3033ecd..7c8338ff3 100644 --- a/tests/test_jsonable_encoder.py +++ b/tests/test_jsonable_encoder.py @@ -247,8 +247,9 @@ def test_encode_model_with_pure_path(): class Config: arbitrary_types_allowed = True - obj = ModelWithPath(path=PurePath("/foo", "bar")) - assert jsonable_encoder(obj) == {"path": "/foo/bar"} + test_path = PurePath("/foo", "bar") + obj = ModelWithPath(path=test_path) + assert jsonable_encoder(obj) == {"path": str(test_path)} def test_encode_model_with_pure_posix_path(): From b473cdd88d878b6657f0c80edb384b71971841a8 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 2 Aug 2023 15:29:13 +0000 Subject: [PATCH 1203/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fde398be5..5065295e6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). * 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). * 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). From 1e6bfa1f3931b841f3c2e173a5e673e854130a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 2 Aug 2023 17:57:20 +0200 Subject: [PATCH 1204/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20FastAPI?= =?UTF-8?q?=20People=20logic=20with=20new=20Pydantic=20(#9985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/Dockerfile | 2 +- .github/actions/people/app/main.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile index fa4197e6a..6d65f1c2b 100644 --- a/.github/actions/people/Dockerfile +++ b/.github/actions/people/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.7 -RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" +RUN pip install httpx PyGithub "pydantic==2.0.2" "pyyaml>=5.3.1,<6.0.0" COPY ./app /app diff --git a/.github/actions/people/app/main.py b/.github/actions/people/app/main.py index b11e3456d..cb6b229e8 100644 --- a/.github/actions/people/app/main.py +++ b/.github/actions/people/app/main.py @@ -9,7 +9,8 @@ from typing import Any, Container, DefaultDict, Dict, List, Set, Union import httpx import yaml from github import Github -from pydantic import BaseModel, BaseSettings, SecretStr +from pydantic import BaseModel, SecretStr +from pydantic_settings import BaseSettings github_graphql_url = "https://api.github.com/graphql" questions_category_id = "MDE4OkRpc2N1c3Npb25DYXRlZ29yeTMyMDAxNDM0" @@ -382,6 +383,7 @@ def get_graphql_response( data = response.json() if "errors" in data: logging.error(f"Errors in response, after: {after}, category_id: {category_id}") + logging.error(data["errors"]) logging.error(response.text) raise RuntimeError(response.text) return data @@ -389,7 +391,7 @@ def get_graphql_response( def get_graphql_issue_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=issues_query, after=after) - graphql_response = IssuesResponse.parse_obj(data) + graphql_response = IssuesResponse.model_validate(data) return graphql_response.data.repository.issues.edges @@ -404,19 +406,19 @@ def get_graphql_question_discussion_edges( after=after, category_id=questions_category_id, ) - graphql_response = DiscussionsResponse.parse_obj(data) + graphql_response = DiscussionsResponse.model_validate(data) return graphql_response.data.repository.discussions.edges def get_graphql_pr_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=prs_query, after=after) - graphql_response = PRsResponse.parse_obj(data) + graphql_response = PRsResponse.model_validate(data) return graphql_response.data.repository.pullRequests.edges def get_graphql_sponsor_edges(*, settings: Settings, after: Union[str, None] = None): data = get_graphql_response(settings=settings, query=sponsors_query, after=after) - graphql_response = SponsorsResponse.parse_obj(data) + graphql_response = SponsorsResponse.model_validate(data) return graphql_response.data.user.sponsorshipsAsMaintainer.edges @@ -607,7 +609,7 @@ def get_top_users( if __name__ == "__main__": logging.basicConfig(level=logging.INFO) settings = Settings() - logging.info(f"Using config: {settings.json()}") + logging.info(f"Using config: {settings.model_dump_json()}") g = Github(settings.input_token.get_secret_value()) repo = g.get_repo(settings.github_repository) question_commentors, question_last_month_commentors, question_authors = get_experts( From 165f29fe5ec6be1d42b82cdef8b3d144300005f9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 2 Aug 2023 15:57:57 +0000 Subject: [PATCH 1205/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5065295e6..d0b7c6a93 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). * 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). From 53220b983227ef6147a43dfdc0528ff4e6c31f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 2 Aug 2023 20:57:48 +0200 Subject: [PATCH 1206/1881] =?UTF-8?q?=E2=9E=95=20Add=20pydantic-settings?= =?UTF-8?q?=20to=20FastAPI=20People=20dependencies=20(#9988)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile index 6d65f1c2b..f0f389c64 100644 --- a/.github/actions/people/Dockerfile +++ b/.github/actions/people/Dockerfile @@ -1,6 +1,6 @@ -FROM python:3.7 +FROM python:3.11 -RUN pip install httpx PyGithub "pydantic==2.0.2" "pyyaml>=5.3.1,<6.0.0" +RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" COPY ./app /app From 38291292451481ca9004a5031464fac036de0161 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 2 Aug 2023 18:58:29 +0000 Subject: [PATCH 1207/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d0b7c6a93..920886e5d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). From 89537a0497ef3ccacbe2f4959c3b4f31414319ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Aug 2023 16:12:28 +0200 Subject: [PATCH 1208/1881] =?UTF-8?q?=F0=9F=90=B3=20Update=20Dockerfile=20?= =?UTF-8?q?with=20compatibility=20versions,=20to=20upgrade=20later=20(#999?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/people/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/people/Dockerfile b/.github/actions/people/Dockerfile index f0f389c64..1455106bd 100644 --- a/.github/actions/people/Dockerfile +++ b/.github/actions/people/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11 +FROM python:3.9 RUN pip install httpx PyGithub "pydantic==2.0.2" pydantic-settings "pyyaml>=5.3.1,<6.0.0" From ad1d7f539ea53c3b75e9c23051d66663eac6146f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Aug 2023 14:13:59 +0000 Subject: [PATCH 1209/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 920886e5d..91745453c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). * ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). From 3fa6cfbcc5d37cecfc6a936c42c832978ecbfba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Aug 2023 16:25:11 +0200 Subject: [PATCH 1210/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#9999)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 93 +++++++-------- docs/en/data/people.yml | 190 +++++++++++++++---------------- 2 files changed, 143 insertions(+), 140 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 56a886c68..3a68ba62b 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: fern-api + avatarUrl: https://avatars.githubusercontent.com/u/102944815?v=4 + url: https://github.com/fern-api - login: nanram22 avatarUrl: https://avatars.githubusercontent.com/u/116367316?v=4 url: https://github.com/nanram22 @@ -29,15 +32,18 @@ sponsors: - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes +- - login: arcticfly + avatarUrl: https://avatars.githubusercontent.com/u/41524992?u=03c88529a86cf51f7a380e890d84d84c71468848&v=4 + url: https://github.com/arcticfly - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry -- - login: takashi-yoneya +- - login: acsone + avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 + url: https://github.com/acsone + - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya - - login: mercedes-benz - avatarUrl: https://avatars.githubusercontent.com/u/34240465?v=4 - url: https://github.com/mercedes-benz - login: xoflare avatarUrl: https://avatars.githubusercontent.com/u/74335107?v=4 url: https://github.com/xoflare @@ -50,12 +56,12 @@ sponsors: - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP + - login: jina-ai + avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 + url: https://github.com/jina-ai - - login: HiredScore avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 url: https://github.com/HiredScore - - login: petebachant - avatarUrl: https://avatars.githubusercontent.com/u/4604869?u=b17a5a4ac82f77b7efff864d439e8068d2a36593&v=4 - url: https://github.com/petebachant - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie @@ -74,9 +80,6 @@ sponsors: - login: RodneyU215 avatarUrl: https://avatars.githubusercontent.com/u/3329665?u=ec6a9adf8e7e8e306eed7d49687c398608d1604f&v=4 url: https://github.com/RodneyU215 - - login: tizz98 - avatarUrl: https://avatars.githubusercontent.com/u/5739698?u=f095a3659e3a8e7c69ccd822696990b521ea25f9&v=4 - url: https://github.com/tizz98 - login: americanair avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 url: https://github.com/americanair @@ -92,11 +95,17 @@ sponsors: - - login: indeedeng avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 url: https://github.com/indeedeng + - login: iguit0 + avatarUrl: https://avatars.githubusercontent.com/u/12905770?u=63a1a96d1e6c27d85c4f946b84836599de047f65&v=4 + url: https://github.com/iguit0 + - login: JacobKochems + avatarUrl: https://avatars.githubusercontent.com/u/41692189?u=a75f62ddc0d060ee6233a91e19c433d2687b8eb6&v=4 + url: https://github.com/JacobKochems - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: samuelcolvin - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 @@ -104,9 +113,6 @@ sponsors: - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden - - login: kamalgill - avatarUrl: https://avatars.githubusercontent.com/u/133923?u=0df9181d97436ce330e9acf90ab8a54b7022efe7&v=4 - url: https://github.com/kamalgill - login: dekoza avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 url: https://github.com/dekoza @@ -149,6 +155,9 @@ sponsors: - login: zsinx6 avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 url: https://github.com/zsinx6 + - login: kennywakeland + avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 + url: https://github.com/kennywakeland - login: aacayaco avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 url: https://github.com/aacayaco @@ -194,9 +203,6 @@ sponsors: - login: Shackelford-Arden avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 url: https://github.com/Shackelford-Arden - - login: savannahostrowski - avatarUrl: https://avatars.githubusercontent.com/u/8949415?u=c3177aa099fb2b8c36aeba349278b77f9a8df211&v=4 - url: https://github.com/savannahostrowski - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow @@ -236,9 +242,6 @@ sponsors: - login: ygorpontelo avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 url: https://github.com/ygorpontelo - - login: AlrasheedA - avatarUrl: https://avatars.githubusercontent.com/u/33544979?u=7fe66bf62b47682612b222e3e8f4795ef3be769b&v=4 - url: https://github.com/AlrasheedA - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure @@ -281,9 +284,6 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: khoadaniel - avatarUrl: https://avatars.githubusercontent.com/u/84840546?v=4 - url: https://github.com/khoadaniel - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki @@ -327,14 +327,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - login: janfilips - avatarUrl: https://avatars.githubusercontent.com/u/870699?u=96df18ad355e58b9397accc55f4eeb7a86e959b0&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/870699?u=80702ec63f14e675cd4cdcc6ce3821d2ed207fd7&v=4 url: https://github.com/janfilips - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan - - login: NateShoffner - avatarUrl: https://avatars.githubusercontent.com/u/1712163?u=b43cc2fa3fd8bec54b7706e4b98b72543c7bfea8&v=4 - url: https://github.com/NateShoffner - login: my3 avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 url: https://github.com/my3 @@ -344,12 +341,6 @@ sponsors: - login: cbonoz avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 url: https://github.com/cbonoz - - login: Patechoc - avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 - url: https://github.com/Patechoc - - login: larsvik - avatarUrl: https://avatars.githubusercontent.com/u/3442226?v=4 - url: https://github.com/larsvik - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti @@ -359,6 +350,9 @@ sponsors: - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa + - login: piotrgredowski + avatarUrl: https://avatars.githubusercontent.com/u/4294480?v=4 + url: https://github.com/piotrgredowski - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood @@ -383,6 +377,9 @@ sponsors: - login: mattwelke avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=80f02a799323b1472b389b836d95957c93a6d856&v=4 url: https://github.com/mattwelke + - login: harsh183 + avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 + url: https://github.com/harsh183 - login: hcristea avatarUrl: https://avatars.githubusercontent.com/u/7814406?u=61d7a4fcf846983a4606788eac25e1c6c1209ba8&v=4 url: https://github.com/hcristea @@ -428,9 +425,6 @@ sponsors: - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu - - login: ghandic - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 @@ -458,9 +452,6 @@ sponsors: - login: bnkc avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=527044d90b5ebb7f8dad517db5da1f45253b774b&v=4 url: https://github.com/bnkc - - login: devbruce - avatarUrl: https://avatars.githubusercontent.com/u/35563380?u=ca4e811ac7f7b3eb1600fa63285119fcdee01188&v=4 - url: https://github.com/devbruce - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon @@ -476,6 +467,9 @@ sponsors: - login: ArtyomVancyan avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 url: https://github.com/ArtyomVancyan + - login: josehenriqueroveda + avatarUrl: https://avatars.githubusercontent.com/u/46685746?u=2e672057a7dbe1dba47e57c378fc0cac336022eb&v=4 + url: https://github.com/josehenriqueroveda - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby @@ -485,27 +479,36 @@ sponsors: - login: conservative-dude avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 url: https://github.com/conservative-dude - - login: leo-jp-edwards - avatarUrl: https://avatars.githubusercontent.com/u/58213433?u=2c128e8b0794b7a66211cd7d8ebe05db20b7e9c0&v=4 - url: https://github.com/leo-jp-edwards - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea - - login: tomast1337 - avatarUrl: https://avatars.githubusercontent.com/u/15125899?u=2c2f2907012d820499e2c43632389184923513fe&v=4 - url: https://github.com/tomast1337 + - login: Patechoc + avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 + url: https://github.com/Patechoc + - login: LanceMoe + avatarUrl: https://avatars.githubusercontent.com/u/18505474?u=7fd3ead4364bdf215b6d75cb122b3811c391ef6b&v=4 + url: https://github.com/LanceMoe - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu - login: ruizdiazever avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 url: https://github.com/ruizdiazever + - login: samnimoh + avatarUrl: https://avatars.githubusercontent.com/u/33413170?u=147bc516be6cb647b28d7e3b3fea3a018a331145&v=4 + url: https://github.com/samnimoh - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline + - login: iharshgor + avatarUrl: https://avatars.githubusercontent.com/u/35490011?u=2dea054476e752d9e92c9d71a9a7cc919b1c2f8e&v=4 + url: https://github.com/iharshgor - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd + - login: ThomasPalma1 + avatarUrl: https://avatars.githubusercontent.com/u/66331874?u=5763f7402d784ba189b60d704ff5849b4d0a63fb&v=4 + url: https://github.com/ThomasPalma1 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index dd2dbe5ea..89d189564 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,16 +1,16 @@ maintainers: - login: tiangolo - answers: 1844 - prs: 430 + answers: 1849 + prs: 466 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 434 + count: 463 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 237 + count: 239 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: Mause @@ -25,34 +25,34 @@ experts: count: 193 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT -- login: euri10 - count: 152 - avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 - url: https://github.com/euri10 - login: jgould22 - count: 139 + count: 157 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 +- login: euri10 + count: 153 + avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 + url: https://github.com/euri10 - login: phy25 count: 126 - avatarUrl: https://avatars.githubusercontent.com/u/331403?u=191cd73f0c936497c8d1931a217bb3039d050265&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 118 + count: 121 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: raphaelauv count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: ghandic - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 - url: https://github.com/ghandic - login: ArcLightSlavik count: 71 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 url: https://github.com/ArcLightSlavik +- login: ghandic + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 + url: https://github.com/ghandic - login: falkben count: 57 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 @@ -61,10 +61,10 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: adriangb +- login: insomnes count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: yinziyan1206 count: 45 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 @@ -73,22 +73,22 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: odiseo0 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 - url: https://github.com/odiseo0 +- login: adriangb + count: 44 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 url: https://github.com/frankie567 +- login: odiseo0 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 + url: https://github.com/odiseo0 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -97,14 +97,14 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: chbndrhnns - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: chbndrhnns + count: 35 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -121,38 +121,38 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes +- login: acnebs + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 + url: https://github.com/acnebs - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak -- login: acnebs - count: 22 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?u=c27e50269f1ef8ea950cc6f0268c8ec5cebbe9c9&v=4 - url: https://github.com/acnebs - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 url: https://github.com/rafsaf -- login: chris-allnutt +- login: n8sty count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 - url: https://github.com/chris-allnutt + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: nsidnev count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: n8sty - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty -- login: retnikt - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 - url: https://github.com/retnikt +- login: chris-allnutt + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 + url: https://github.com/chris-allnutt - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 url: https://github.com/zoliknemet +- login: retnikt + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 + url: https://github.com/retnikt - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -198,38 +198,30 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 url: https://github.com/jorgerpo last_month_active: -- login: jgould22 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: Kludex - count: 13 + count: 24 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: abhint - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint -- login: chrisK824 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 +- login: jgould22 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: arjwilliams - count: 3 + count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 url: https://github.com/arjwilliams -- login: wu-clan - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/52145145?u=f8c9e5c8c259d248e1683fedf5027b4ee08a0967&v=4 - url: https://github.com/wu-clan - login: Ahmed-Abdou14 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=d1e1c064d57c3ad5b6481716928da840f6d5a492&v=4 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 url: https://github.com/Ahmed-Abdou14 -- login: esrefzeki +- login: iudeen count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/54935247?u=193cf5a169ca05fc54995a4dceabc82c7dc6e5ea&v=4 - url: https://github.com/esrefzeki + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen +- login: mikeedjones + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/4087139?u=cc4a242896ac2fcf88a53acfaf190d0fe0a1f0c9&v=4 + url: https://github.com/mikeedjones top_contributors: - login: waynerv count: 25 @@ -240,7 +232,7 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: Kludex - count: 20 + count: 21 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: jaystone776 @@ -264,7 +256,7 @@ top_contributors: avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 url: https://github.com/mariacamilagl - login: Smlep - count: 10 + count: 11 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep - login: Serrones @@ -287,6 +279,10 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 url: https://github.com/Alexandrhub +- login: NinaHwang + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 + url: https://github.com/NinaHwang - login: batlopes count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 @@ -297,7 +293,7 @@ top_contributors: url: https://github.com/wshayes - login: samuelcolvin count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=807390ba9cfe23906c3bf8a0d56aaca3cf2bfa0d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - login: SwftAlpc count: 5 @@ -311,10 +307,6 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp -- login: NinaHwang - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/79563565?u=eee6bfe9224c71193025ab7477f4f96ceaa05c62&v=4 - url: https://github.com/NinaHwang - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -335,10 +327,18 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/3360631?u=5fa1f475ad784d64eb9666bdd43cc4d285dcc773&v=4 url: https://github.com/hitrust +- login: JulianMaurin + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/63545168?u=b7d15ac865268cbefc2d739e2f23d9aeeac1a622&v=4 + url: https://github.com/JulianMaurin - login: lsglucas count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: iudeen + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: axel584 count: 4 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 @@ -349,7 +349,7 @@ top_contributors: url: https://github.com/ivan-abc top_reviewers: - login: Kludex - count: 122 + count: 136 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan @@ -357,7 +357,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 77 + count: 78 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 - login: tokusumi @@ -372,20 +372,20 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 +- login: iudeen + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd -- login: iudeen - count: 44 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay - login: Xewus - count: 35 + count: 38 avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 url: https://github.com/Xewus - login: JarroVGIT @@ -412,6 +412,10 @@ top_reviewers: count: 26 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: LorhanSohaky + count: 24 + avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 + url: https://github.com/LorhanSohaky - login: Ryandaydev count: 24 avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 @@ -420,10 +424,6 @@ top_reviewers: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -- login: LorhanSohaky - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 - url: https://github.com/LorhanSohaky - login: rjNemo count: 21 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 @@ -434,7 +434,7 @@ top_reviewers: url: https://github.com/hard-coders - login: odiseo0 count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=2da05dab6cc8e1ade557801634760a56e4101796&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 - login: 0417taehyun count: 19 @@ -456,6 +456,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +- login: axel584 + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 + url: https://github.com/axel584 - login: DevDae count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 @@ -488,10 +492,6 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu -- login: axel584 - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 - url: https://github.com/axel584 - login: ivan-abc count: 12 avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 @@ -500,6 +500,10 @@ top_reviewers: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 url: https://github.com/solomein-sv +- login: wdh99 + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 + url: https://github.com/wdh99 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -540,7 +544,3 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 url: https://github.com/bezaca -- login: oandersonmagalhaes - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/83456692?v=4 - url: https://github.com/oandersonmagalhaes From a73cdaed35d6d9a120ba4831c512b3a7cb25b697 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Aug 2023 14:25:48 +0000 Subject: [PATCH 1211/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 91745453c..ab8c6e68c 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 [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). * 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). * ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). From 4ab0363ad794fd60e1ebead224f57a1d01e6bd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Aug 2023 17:24:31 +0200 Subject: [PATCH 1212/1881] =?UTF-8?q?=E2=9E=96=20Remove=20direct=20depende?= =?UTF-8?q?ncy=20on=20MkDocs,=20Material=20for=20MkDocs=20defines=20its=20?= =?UTF-8?q?own=20dependency=20(#9986)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 8 ++++---- requirements-docs.txt | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 19009447b..eb816b72f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -44,14 +44,14 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v06 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git@9.1.21-insiders-4.38.0 - name: Export Language Codes id: show-langs run: | @@ -80,13 +80,13 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v05 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v06 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git@9.1.21-insiders-4.38.0 - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v3 diff --git a/requirements-docs.txt b/requirements-docs.txt index df60ca4df..7152ebf7b 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,4 @@ -e . -mkdocs==1.4.3 mkdocs-material==9.1.17 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 From 94c48cfc8cb5987113c6a8558a7a428d27888cce Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Aug 2023 15:25:10 +0000 Subject: [PATCH 1213/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ab8c6e68c..caf74c353 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). * 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). * ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). From 25694f5ae100741c9f7ee0d409e298609e71366e Mon Sep 17 00:00:00 2001 From: David Montague <35119617+dmontagu@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:46:57 +0100 Subject: [PATCH 1214/1881] =?UTF-8?q?=E2=9C=85=20Fix=20tests=20for=20compa?= =?UTF-8?q?tibility=20with=20pydantic=202.1.1=20(#9943)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- .github/workflows/test.yml | 4 +-- tests/test_filter_pydantic_sub_model_pv2.py | 4 +-- tests/test_multi_body_errors.py | 34 +++++++++++++++++++-- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b95358d01..fbaf759c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v04 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -54,7 +54,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v04 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index 656332a01..ae12179bd 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,7 +1,7 @@ from typing import Optional import pytest -from dirty_equals import IsDict +from dirty_equals import HasRepr, IsDict from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient @@ -66,7 +66,7 @@ def test_validator_is_cloned(client: TestClient): "loc": ("response", "name"), "msg": "Value error, name must end in A", "input": "modelX", - "ctx": {"error": "name must end in A"}, + "ctx": {"error": HasRepr("ValueError('name must end in A')")}, "url": match_pydantic_error_url("value_error"), } ) diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index aa989c612..931f08fc1 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -51,7 +51,7 @@ def test_jsonable_encoder_requiring_error(): "loc": ["body", 0, "age"], "msg": "Input should be greater than 0", "input": -1.0, - "ctx": {"gt": 0.0}, + "ctx": {"gt": "0"}, "url": match_pydantic_error_url("greater_than"), } ] @@ -84,9 +84,23 @@ def test_put_incorrect_body_multiple(): "input": {"age": "five"}, "url": match_pydantic_error_url("missing"), }, + { + "ctx": {"class": "Decimal"}, + "input": "five", + "loc": ["body", 0, "age", "is-instance[Decimal]"], + "msg": "Input should be an instance of Decimal", + "type": "is_instance_of", + "url": match_pydantic_error_url("is_instance_of"), + }, { "type": "decimal_parsing", - "loc": ["body", 0, "age"], + "loc": [ + "body", + 0, + "age", + "function-after[to_decimal(), " + "union[int,constrained-str,function-plain[str()]]]", + ], "msg": "Input should be a valid decimal", "input": "five", }, @@ -97,9 +111,23 @@ def test_put_incorrect_body_multiple(): "input": {"age": "six"}, "url": match_pydantic_error_url("missing"), }, + { + "ctx": {"class": "Decimal"}, + "input": "six", + "loc": ["body", 1, "age", "is-instance[Decimal]"], + "msg": "Input should be an instance of Decimal", + "type": "is_instance_of", + "url": match_pydantic_error_url("is_instance_of"), + }, { "type": "decimal_parsing", - "loc": ["body", 1, "age"], + "loc": [ + "body", + 1, + "age", + "function-after[to_decimal(), " + "union[int,constrained-str,function-plain[str()]]]", + ], "msg": "Input should be a valid decimal", "input": "six", }, From 10b4c31f063d9a098e69dfc48a3a62028fab1261 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Aug 2023 15:47:35 +0000 Subject: [PATCH 1215/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 caf74c353..91574addd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). * 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). * 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). From 059fb128926ae06a52af472bd91b216ce45a5997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 3 Aug 2023 17:59:41 +0200 Subject: [PATCH 1216/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20the=20Questio?= =?UTF-8?q?n=20template=20to=20ask=20for=20the=20Pydantic=20version=20(#10?= =?UTF-8?q?000)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/DISCUSSION_TEMPLATE/questions.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/DISCUSSION_TEMPLATE/questions.yml b/.github/DISCUSSION_TEMPLATE/questions.yml index 3726b7d18..98424a341 100644 --- a/.github/DISCUSSION_TEMPLATE/questions.yml +++ b/.github/DISCUSSION_TEMPLATE/questions.yml @@ -123,6 +123,20 @@ body: ``` validations: required: true + - type: input + id: pydantic-version + attributes: + label: Pydantic Version + description: | + What Pydantic version are you using? + + You can find the Pydantic version with: + + ```bash + python -c "import pydantic; print(pydantic.version.VERSION)" + ``` + validations: + required: true - type: input id: python-version attributes: From 3af7265a435ebb1016dfa190c196afb045451d4f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 3 Aug 2023 16:00:19 +0000 Subject: [PATCH 1217/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 91574addd..c85ef0ad2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). * 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). From 86e4e9f8f9266e471192aefaac7ef55cee957a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 19:47:42 +0200 Subject: [PATCH 1218/1881] =?UTF-8?q?=F0=9F=94=A7=20Restore=20MkDocs=20Mat?= =?UTF-8?q?erial=20pin=20after=20the=20fix=20(#10001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index eb816b72f..dedf23fb9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -51,7 +51,7 @@ jobs: # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git@9.1.21-insiders-4.38.0 + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Export Language Codes id: show-langs run: | @@ -86,7 +86,7 @@ jobs: run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git@9.1.21-insiders-4.38.0 + run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v3 From b3a1f910048ea60a108a8f40ee5643778047b344 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Aug 2023 17:48:24 +0000 Subject: [PATCH 1219/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c85ef0ad2..dc61f76f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Restore MkDocs Material pin after the fix. PR [#10001](https://github.com/tiangolo/fastapi/pull/10001) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). * 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). From ebdf952545cbd95a16d21d48421fea7aa07e3b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 20:18:38 +0200 Subject: [PATCH 1220/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20GitHub=20Actions?= =?UTF-8?q?=20step=20dump=20context=20to=20debug=20external=20failures=20(?= =?UTF-8?q?#10008)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 4 ++++ .github/workflows/label-approved.yml | 4 ++++ .github/workflows/latest-changes.yml | 4 ++++ .github/workflows/notify-translations.yml | 4 ++++ .github/workflows/people.yml | 4 ++++ .github/workflows/smokeshow.yml | 4 ++++ .github/workflows/test.yml | 16 ++++++++++++++++ 7 files changed, 40 insertions(+) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index 324623103..bb967fa11 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -19,6 +19,10 @@ jobs: if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: tiangolo/issue-manager@0.4.0 with: token: ${{ secrets.FASTAPI_ISSUE_MANAGER }} diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 976d29f74..2113c468a 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -9,6 +9,10 @@ jobs: if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: docker://tiangolo/label-approved:0.0.2 with: token: ${{ secrets.FASTAPI_LABEL_APPROVED }} diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index 0461f3dd3..e38870f46 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -20,6 +20,10 @@ jobs: latest-changes: runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 with: # To allow latest-changes to commit to the main branch diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index cd7affbc3..44ee83ec0 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -19,6 +19,10 @@ jobs: notify-translations: runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 # Allow debugging with tmate - name: Setup tmate session diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index aa7f34464..4480a1427 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -15,6 +15,10 @@ jobs: if: github.repository_owner == 'tiangolo' runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 # Ref: https://github.com/actions/runner/issues/2033 - name: Fix git safe.directory in container diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index c6d894d9f..4e689d95c 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -14,6 +14,10 @@ jobs: runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/setup-python@v4 with: python-version: '3.9' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbaf759c3..6a512a019 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,10 @@ jobs: lint: runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 @@ -42,6 +46,10 @@ jobs: pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 @@ -80,6 +88,10 @@ jobs: needs: [test] runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: @@ -110,6 +122,10 @@ jobs: - coverage-combine runs-on: ubuntu-latest steps: + - name: Dump GitHub context + env: + GITHUB_CONTEXT: ${{ toJson(github) }} + run: echo "$GITHUB_CONTEXT" - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: From d943e02232081a795874b00e399dcb0a0b179daf Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Aug 2023 18:19:22 +0000 Subject: [PATCH 1221/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 dc61f76f2..e45373f7b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 👷 Add GitHub Actions step dump context to debug external failures. PR [#10008](https://github.com/tiangolo/fastapi/pull/10008) by [@tiangolo](https://github.com/tiangolo). * 🔧 Restore MkDocs Material pin after the fix. PR [#10001](https://github.com/tiangolo/fastapi/pull/10001) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). * ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). From 19a2c3bb54ecef5fab936b45e4c262b283e100f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 22:47:07 +0200 Subject: [PATCH 1222/1881] =?UTF-8?q?=E2=9C=A8=20Enable=20Pydantic's=20ser?= =?UTF-8?q?ialization=20mode=20for=20responses,=20add=20support=20for=20Py?= =?UTF-8?q?dantic's=20`computed=5Ffield`,=20better=20OpenAPI=20for=20respo?= =?UTF-8?q?nse=20models,=20proper=20required=20attributes,=20better=20gene?= =?UTF-8?q?rated=20clients=20(#10011)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Enable Pydantic's serialization mode for responses * ✅ Update tests with new Pydantic v2 serialization mode * ✅ Add a test for Pydantic v2's computed_field --- fastapi/routing.py | 4 +- tests/test_computed_fields.py | 77 +++++++ tests/test_filter_pydantic_sub_model_pv2.py | 8 +- .../test_body_updates/test_tutorial001.py | 210 ++++++++++++++--- .../test_tutorial001_py310.py | 211 +++++++++++++++--- .../test_tutorial001_py39.py | 211 +++++++++++++++--- .../test_dataclasses/test_tutorial002.py | 12 +- .../test_dataclasses/test_tutorial003.py | 185 ++++++++++++--- .../test_extra_models/test_tutorial003.py | 13 +- .../test_tutorial003_py310.py | 13 +- .../test_tutorial004.py | 155 +++++++++++-- .../test_tutorial005.py | 155 +++++++++++-- .../test_tutorial005_py310.py | 156 +++++++++++-- .../test_tutorial005_py39.py | 156 +++++++++++-- .../test_response_model/test_tutorial003.py | 8 +- .../test_tutorial003_01.py | 8 +- .../test_tutorial003_01_py310.py | 8 +- .../test_tutorial003_py310.py | 8 +- .../test_response_model/test_tutorial004.py | 8 +- .../test_tutorial004_py310.py | 8 +- .../test_tutorial004_py39.py | 8 +- .../test_response_model/test_tutorial005.py | 8 +- .../test_tutorial005_py310.py | 8 +- .../test_response_model/test_tutorial006.py | 8 +- .../test_tutorial006_py310.py | 8 +- .../test_security/test_tutorial005.py | 8 +- .../test_security/test_tutorial005_an.py | 8 +- .../test_tutorial005_an_py310.py | 8 +- .../test_security/test_tutorial005_an_py39.py | 8 +- .../test_security/test_tutorial005_py310.py | 8 +- .../test_security/test_tutorial005_py39.py | 8 +- 31 files changed, 1446 insertions(+), 256 deletions(-) create mode 100644 tests/test_computed_fields.py diff --git a/fastapi/routing.py b/fastapi/routing.py index d8ff0579c..6efd40ff3 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -448,9 +448,7 @@ class APIRoute(routing.Route): self.response_field = create_response_field( name=response_name, type_=self.response_model, - # TODO: This should actually set mode='serialization', just, that changes the schemas - # mode="serialization", - mode="validation", + mode="serialization", ) # Create a clone of the field, so that a Pydantic submodel is not returned # as is just because it's an instance of a subclass of a more limited class diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py new file mode 100644 index 000000000..5286507b2 --- /dev/null +++ b/tests/test_computed_fields.py @@ -0,0 +1,77 @@ +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client(): + app = FastAPI() + + from pydantic import BaseModel, computed_field + + class Rectangle(BaseModel): + width: int + length: int + + @computed_field + @property + def area(self) -> int: + return self.width * self.length + + @app.get("/") + def read_root() -> Rectangle: + return Rectangle(width=3, length=4) + + client = TestClient(app) + return client + + +@needs_pydanticv2 +def test_get(client: TestClient): + response = client.get("/") + assert response.status_code == 200, response.text + assert response.json() == {"width": 3, "length": 4, "area": 12} + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/": { + "get": { + "summary": "Read Root", + "operationId": "read_root__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Rectangle"} + } + }, + } + }, + } + } + }, + "components": { + "schemas": { + "Rectangle": { + "properties": { + "width": {"type": "integer", "title": "Width"}, + "length": {"type": "integer", "title": "Length"}, + "area": {"type": "integer", "title": "Area", "readOnly": True}, + }, + "type": "object", + "required": ["width", "length", "area"], + "title": "Rectangle", + } + } + }, + } diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index ae12179bd..9f5e6b08f 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -1,7 +1,7 @@ from typing import Optional import pytest -from dirty_equals import HasRepr, IsDict +from dirty_equals import HasRepr, IsDict, IsOneOf from fastapi import Depends, FastAPI from fastapi.exceptions import ResponseValidationError from fastapi.testclient import TestClient @@ -139,7 +139,11 @@ def test_openapi_schema(client: TestClient): }, "ModelA": { "title": "ModelA", - "required": ["name", "foo"], + "required": IsOneOf( + ["name", "description", "foo"], + # TODO remove when deprecating Pydantic v1 + ["name", "foo"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index b02f7c81c..f1a46210a 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -1,7 +1,8 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient +from ...utils import needs_pydanticv1, needs_pydanticv2 + @pytest.fixture(name="client") def get_client(): @@ -36,7 +37,181 @@ def test_put(client: TestClient): } +@needs_pydanticv2 def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -124,36 +299,9 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index 4af2652a7..ab696e4c8 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -41,7 +40,182 @@ def test_put(client: TestClient): @needs_py310 +@needs_pydanticv2 def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -129,36 +303,9 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 832f45388..2ee6a5cb4 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -41,7 +40,182 @@ def test_put(client: TestClient): @needs_py39 +@needs_pydanticv2 def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Read Item", + "operationId": "read_item_items__item_id__get", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + }, + "put": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "string"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + }, + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "type": "object", + "properties": { + "name": { + "title": "Name", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": { + "title": "Price", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "type": "object", + "required": ["name", "description", "price", "tax", "tags"], + "properties": { + "name": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Name", + }, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": { + "anyOf": [{"type": "number"}, {"type": "null"}], + "title": "Price", + }, + "tax": {"title": "Tax", "type": "number", "default": 10.5}, + "tags": { + "title": "Tags", + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -129,36 +303,9 @@ def test_openapi_schema(client: TestClient): "title": "Item", "type": "object", "properties": { - "name": IsDict( - { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Name", "type": "string"} - ), - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), - "price": IsDict( - { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Price", "type": "number"} - ), + "name": {"title": "Name", "type": "string"}, + "description": {"title": "Description", "type": "string"}, + "price": {"title": "Price", "type": "number"}, "tax": {"title": "Tax", "type": "number", "default": 10.5}, "tags": { "title": "Tags", diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial002.py b/tests/test_tutorial/test_dataclasses/test_tutorial002.py index 7d88e2861..4146f4cd6 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial002.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial002.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial002 import app @@ -21,8 +21,7 @@ def test_get_item(): def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200 - data = response.json() - assert data == { + assert response.json() == { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { @@ -47,7 +46,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "price", "tags", "description", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, @@ -57,7 +60,6 @@ def test_openapi_schema(): "title": "Tags", "type": "array", "items": {"type": "string"}, - "default": [], } ) | IsDict( diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 597757e09..2e5809914 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.dataclasses.tutorial003 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -52,7 +53,157 @@ def test_get_authors(): ] +@needs_pydanticv2 def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200 + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/authors/{author_id}/items/": { + "post": { + "summary": "Create Author Items", + "operationId": "create_author_items_authors__author_id__items__post", + "parameters": [ + { + "required": True, + "schema": {"title": "Author Id", "type": "string"}, + "name": "author_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/ItemInput"}, + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Author"} + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/authors/": { + "get": { + "summary": "Get Authors", + "operationId": "get_authors_authors__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "title": "Response Get Authors Authors Get", + "type": "array", + "items": { + "$ref": "#/components/schemas/Author" + }, + } + } + }, + } + }, + } + }, + }, + "components": { + "schemas": { + "Author": { + "title": "Author", + "required": ["name", "items"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/ItemOutput"}, + }, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "ItemInput": { + "title": "Item", + "required": ["name"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): response = client.get("/openapi.json") assert response.status_code == 200 assert response.json() == { @@ -136,22 +287,11 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "items": IsDict( - { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - ), + "items": { + "title": "Items", + "type": "array", + "items": {"$ref": "#/components/schemas/Item"}, + }, }, }, "HTTPValidationError": { @@ -171,16 +311,7 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": {"title": "Description", "type": "string"}, }, }, "ValidationError": { diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003.py b/tests/test_tutorial/test_extra_models/test_tutorial003.py index 21192b7db..0ccb99948 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003.py @@ -1,3 +1,4 @@ +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from docs_src.extra_models.tutorial003 import app @@ -76,7 +77,11 @@ def test_openapi_schema(): "schemas": { "PlaneItem": { "title": "PlaneItem", - "required": ["description", "size"], + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, @@ -86,7 +91,11 @@ def test_openapi_schema(): }, "CarItem": { "title": "CarItem", - "required": ["description"], + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, diff --git a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py index c17ddbbe1..b2fe65fd9 100644 --- a/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_extra_models/test_tutorial003_py310.py @@ -1,4 +1,5 @@ import pytest +from dirty_equals import IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -86,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "PlaneItem": { "title": "PlaneItem", - "required": ["description", "size"], + "required": IsOneOf( + ["description", "type", "size"], + # TODO: remove when deprecating Pydantic v1 + ["description", "size"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, @@ -96,7 +101,11 @@ def test_openapi_schema(client: TestClient): }, "CarItem": { "title": "CarItem", - "required": ["description"], + "required": IsOneOf( + ["description", "type"], + # TODO: remove when deprecating Pydantic v1 + ["description"], + ), "type": "object", "properties": { "description": {"title": "Description", "type": "string"}, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index dd123f48d..3ffc0bca7 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_advanced_configuration.tutorial004 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -18,7 +19,137 @@ def test_query_params_str_validations(): } +@needs_pydanticv2 def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -69,27 +200,9 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": {"title": "Description", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": {"title": "Tax", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index e7e9a982e..ff98295a6 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -1,8 +1,9 @@ -from dirty_equals import IsDict from fastapi.testclient import TestClient from docs_src.path_operation_configuration.tutorial005 import app +from ...utils import needs_pydanticv1, needs_pydanticv2 + client = TestClient(app) @@ -18,7 +19,137 @@ def test_query_params_str_validations(): } +@needs_pydanticv2 def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_pydanticv1 +def test_openapi_schema_pv1(): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -69,27 +200,9 @@ def test_openapi_schema(): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": {"title": "Description", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": {"title": "Tax", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index ebfeb809c..ad1c09eae 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py310 +from ...utils import needs_py310, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -27,7 +26,138 @@ def test_query_params_str_validations(client: TestClient): @needs_py310 +@needs_pydanticv2 def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py310 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -78,27 +208,9 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": {"title": "Description", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": {"title": "Tax", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 8e79afe96..045d1d402 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -1,8 +1,7 @@ import pytest -from dirty_equals import IsDict from fastapi.testclient import TestClient -from ...utils import needs_py39 +from ...utils import needs_py39, needs_pydanticv1, needs_pydanticv2 @pytest.fixture(name="client") @@ -27,7 +26,138 @@ def test_query_params_str_validations(client: TestClient): @needs_py39 +@needs_pydanticv2 def test_openapi_schema(client: TestClient): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "post": { + "responses": { + "200": { + "description": "The created item", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ItemOutput" + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + "summary": "Create an item", + "description": "Create an item with all the information:\n\n- **name**: each item must have a name\n- **description**: a long description\n- **price**: required\n- **tax**: if the item doesn't have tax, you can omit this\n- **tags**: a set of unique tag strings for this item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ItemInput"} + } + }, + "required": True, + }, + } + } + }, + "components": { + "schemas": { + "ItemInput": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ItemOutput": { + "title": "Item", + "required": ["name", "description", "price", "tax", "tags"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "price": {"title": "Price", "type": "number"}, + "tax": { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + }, + "tags": { + "title": "Tags", + "uniqueItems": True, + "type": "array", + "items": {"type": "string"}, + "default": [], + }, + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + } + }, + } + + +# TODO: remove when deprecating Pydantic v1 +@needs_py39 +@needs_pydanticv1 +def test_openapi_schema_pv1(client: TestClient): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { @@ -78,27 +208,9 @@ def test_openapi_schema(client: TestClient): "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, - "description": IsDict( - { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Description", "type": "string"} - ), + "description": {"title": "Description", "type": "string"}, "price": {"title": "Price", "type": "number"}, - "tax": IsDict( - { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - } - ) - | IsDict( - # TODO: remove when deprecating Pydantic v1 - {"title": "Tax", "type": "number"} - ), + "tax": {"title": "Tax", "type": "number"}, "tags": { "title": "Tags", "uniqueItems": True, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003.py b/tests/test_tutorial/test_response_model/test_tutorial003.py index 20221399b..384c8e0f1 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial003 import app @@ -70,7 +70,11 @@ def test_openapi_schema(): "schemas": { "UserOut": { "title": "UserOut", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01.py b/tests/test_tutorial/test_response_model/test_tutorial003_01.py index e8f0658f4..3a6a0b20d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial003_01 import app @@ -70,7 +70,11 @@ def test_openapi_schema(): "schemas": { "BaseUser": { "title": "BaseUser", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py index a69f8cc8d..6985b9de6 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -79,7 +79,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "BaseUser": { "title": "BaseUser", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py index 64dcd6cbd..3a3aee38a 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial003_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial003_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -79,7 +79,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "UserOut": { "title": "UserOut", - "required": ["username", "email"], + "required": IsOneOf( + ["username", "email", "full_name"], + # TODO: remove when deprecating Pydantic v1 + ["username", "email"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial004.py b/tests/test_tutorial/test_response_model/test_tutorial004.py index 8beb847d1..e9bde18dd 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial004 import app @@ -79,7 +79,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py index 28eb88c34..6f8a3cbea 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -87,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py index 9e1a21f8d..cfaa1eba2 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial004_py39.py +++ b/tests/test_tutorial/test_response_model/test_tutorial004_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -87,7 +87,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax", "tags"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005.py b/tests/test_tutorial/test_response_model/test_tutorial005.py index 06e5d0fd1..b20864c07 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial005 import app @@ -102,7 +102,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py index 0f1566243..de552c8f2 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial005_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -112,7 +112,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006.py b/tests/test_tutorial/test_response_model/test_tutorial006.py index 6e6152b9f..1e47e2ead 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.response_model.tutorial006 import app @@ -102,7 +102,11 @@ def test_openapi_schema(): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py index 9a980ab5b..40058b12d 100644 --- a/tests/test_tutorial/test_response_model/test_tutorial006_py310.py +++ b/tests/test_tutorial/test_response_model/test_tutorial006_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -112,7 +112,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "Item": { "title": "Item", - "required": ["name", "price"], + "required": IsOneOf( + ["name", "description", "price", "tax"], + # TODO: remove when deprecating Pydantic v1 + ["name", "price"], + ), "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005.py b/tests/test_tutorial/test_security/test_tutorial005.py index 22ae76f42..c669c306d 100644 --- a/tests/test_tutorial/test_security/test_tutorial005.py +++ b/tests/test_tutorial/test_security/test_tutorial005.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.security.tutorial005 import ( @@ -267,7 +267,11 @@ def test_openapi_schema(): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an.py b/tests/test_tutorial/test_security/test_tutorial005_an.py index 07239cc89..aaab04f78 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an.py @@ -1,4 +1,4 @@ -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from docs_src.security.tutorial005_an import ( @@ -267,7 +267,11 @@ def test_openapi_schema(): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py index 1ab836639..243d0773c 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py index 6aabbe04a..17a3f9aa2 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_an_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_an_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_py310.py b/tests/test_tutorial/test_security/test_tutorial005_py310.py index c21884df8..06455cd63 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py310.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py310 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, diff --git a/tests/test_tutorial/test_security/test_tutorial005_py39.py b/tests/test_tutorial/test_security/test_tutorial005_py39.py index 170c5d60b..9455bfb4e 100644 --- a/tests/test_tutorial/test_security/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_security/test_tutorial005_py39.py @@ -1,5 +1,5 @@ import pytest -from dirty_equals import IsDict +from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient from ...utils import needs_py39 @@ -295,7 +295,11 @@ def test_openapi_schema(client: TestClient): "schemas": { "User": { "title": "User", - "required": ["username"], + "required": IsOneOf( + ["username", "email", "full_name", "disabled"], + # TODO: remove when deprecating Pydantic v1 + ["username"], + ), "type": "object", "properties": { "username": {"title": "Username", "type": "string"}, From 1c2051473865fcc76284eac177104fc342b51aeb Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Aug 2023 20:47:42 +0000 Subject: [PATCH 1223/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e45373f7b..32224ffc0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). * 👷 Add GitHub Actions step dump context to debug external failures. PR [#10008](https://github.com/tiangolo/fastapi/pull/10008) by [@tiangolo](https://github.com/tiangolo). * 🔧 Restore MkDocs Material pin after the fix. PR [#10001](https://github.com/tiangolo/fastapi/pull/10001) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). From 944c59180354e81988a7ed67454f3fdb879f5b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 22:50:34 +0200 Subject: [PATCH 1224/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 32224ffc0..c3756ebd3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,23 +2,34 @@ ## Latest Changes +### Features + * ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). +* ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). + +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). +* 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 👷 Add GitHub Actions step dump context to debug external failures. PR [#10008](https://github.com/tiangolo/fastapi/pull/10008) by [@tiangolo](https://github.com/tiangolo). * 🔧 Restore MkDocs Material pin after the fix. PR [#10001](https://github.com/tiangolo/fastapi/pull/10001) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update the Question template to ask for the Pydantic version. PR [#10000](https://github.com/tiangolo/fastapi/pull/10000) by [@tiangolo](https://github.com/tiangolo). -* ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). * 📍 Update MkDocs Material dependencies. PR [#9986](https://github.com/tiangolo/fastapi/pull/9986) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#9999](https://github.com/tiangolo/fastapi/pull/9999) by [@tiangolo](https://github.com/tiangolo). * 🐳 Update Dockerfile with compatibility versions, to upgrade later. PR [#9998](https://github.com/tiangolo/fastapi/pull/9998) by [@tiangolo](https://github.com/tiangolo). * ➕ Add pydantic-settings to FastAPI People dependencies. PR [#9988](https://github.com/tiangolo/fastapi/pull/9988) by [@tiangolo](https://github.com/tiangolo). * ♻️ Update FastAPI People logic with new Pydantic. PR [#9985](https://github.com/tiangolo/fastapi/pull/9985) by [@tiangolo](https://github.com/tiangolo). -* ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). -* 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). * 🍱 Update sponsors, Fern badge. PR [#9982](https://github.com/tiangolo/fastapi/pull/9982) by [@tiangolo](https://github.com/tiangolo). * 👷 Deploy docs to Cloudflare Pages. PR [#9978](https://github.com/tiangolo/fastapi/pull/9978) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsor Fern. PR [#9979](https://github.com/tiangolo/fastapi/pull/9979) by [@tiangolo](https://github.com/tiangolo). * 👷 Update CI debug mode with Tmate. PR [#9977](https://github.com/tiangolo/fastapi/pull/9977) by [@tiangolo](https://github.com/tiangolo). -* 🌐 Remove Vietnamese note about missing translation. PR [#9957](https://github.com/tiangolo/fastapi/pull/9957) by [@tiangolo](https://github.com/tiangolo). ## 0.100.1 From 77d1f69b1f2dced4fc7e2e0e934ffc259a4b4a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 22:57:30 +0200 Subject: [PATCH 1225/1881] =?UTF-8?q?=F0=9F=93=8C=20Do=20not=20allow=20Pyd?= =?UTF-8?q?antic=202.1.0=20that=20breaks=20(require=202.1.1)=20(#10012)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f0917578f..9b7cca9c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ ] dependencies = [ "starlette>=0.27.0,<0.28.0", - "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0", + "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.5.0", ] dynamic = ["version"] From 89a7cea56157837db092c8047d4c028193c30429 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 4 Aug 2023 20:58:08 +0000 Subject: [PATCH 1226/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c3756ebd3..383be69ff 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📌 Do not allow Pydantic 2.1.0 that breaks (require 2.1.1). PR [#10012](https://github.com/tiangolo/fastapi/pull/10012) by [@tiangolo](https://github.com/tiangolo). ### Features * ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). From 4b5277744ad57a46fa28ea287012c83ba0a7f4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 22:59:44 +0200 Subject: [PATCH 1227/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 383be69ff..a7a5a424e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,6 @@ ## Latest Changes -* 📌 Do not allow Pydantic 2.1.0 that breaks (require 2.1.1). PR [#10012](https://github.com/tiangolo/fastapi/pull/10012) by [@tiangolo](https://github.com/tiangolo). ### Features * ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). @@ -12,6 +11,10 @@ * ✅ Fix tests for compatibility with pydantic 2.1.1. PR [#9943](https://github.com/tiangolo/fastapi/pull/9943) by [@dmontagu](https://github.com/dmontagu). * ✅ Fix test error in Windows for `jsonable_encoder`. PR [#9840](https://github.com/tiangolo/fastapi/pull/9840) by [@iudeen](https://github.com/iudeen). +### Upgrades + +* 📌 Do not allow Pydantic 2.1.0 that breaks (require 2.1.1). PR [#10012](https://github.com/tiangolo/fastapi/pull/10012) by [@tiangolo](https://github.com/tiangolo). + ### Translations * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/index.md`. PR [#9963](https://github.com/tiangolo/fastapi/pull/9963) by [@eVery1337](https://github.com/eVery1337). From 8adbafc0760c9fd0d97da748545b3a5f92dbb0ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 4 Aug 2023 23:00:17 +0200 Subject: [PATCH 1228/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?101.0?= 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 a7a5a424e..5957a73c0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.101.0 + ### Features * ✨ Enable Pydantic's serialization mode for responses, add support for Pydantic's `computed_field`, better OpenAPI for response models, proper required attributes, better generated clients. PR [#10011](https://github.com/tiangolo/fastapi/pull/10011) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 7dfeca0d4..c113ac1fd 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.100.1" +__version__ = "0.101.0" from starlette import status as status From d48a184dd8bf7063265af2b0d44ba101884ed1a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 10:22:39 +0200 Subject: [PATCH 1229/1881] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20?= =?UTF-8?q?from=209.1.17=20to=209.1.21=20(#9960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.17 to 9.1.21. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.17...9.1.21) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 7152ebf7b..220d1ec3a 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ -e . -mkdocs-material==9.1.17 +mkdocs-material==9.1.21 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 typer-cli >=0.0.13,<0.0.14 From 8f316be088d63b9557d85fe25e9c3b26937fa9b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 10:22:58 +0200 Subject: [PATCH 1230/1881] =?UTF-8?q?=E2=AC=86=20Bump=20mypy=20from=201.4.?= =?UTF-8?q?0=20to=201.4.1=20(#9756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mypy](https://github.com/python/mypy) from 1.4.0 to 1.4.1. - [Commits](https://github.com/python/mypy/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-tests.txt b/requirements-tests.txt index abefac685..0113b6f7a 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,7 +2,7 @@ pydantic-settings >=2.0.0 pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 -mypy ==1.4.0 +mypy ==1.4.1 ruff ==0.0.275 black == 23.3.0 httpx >=0.23.0,<0.25.0 From 0148c9508c9c42988503a16c1b909d1d268760dd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:23:14 +0000 Subject: [PATCH 1231/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5957a73c0..63e27fc98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.101.0 From abfcb59fd065843e669e4e3a19bf5540e789c1ab Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:23:39 +0000 Subject: [PATCH 1232/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 63e27fc98..621f82d55 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). ## 0.101.0 From 5891be5ff1156e28a7a3128248ce3268ef97118e Mon Sep 17 00:00:00 2001 From: Ahsan Sheraz Date: Sat, 5 Aug 2023 13:24:21 +0500 Subject: [PATCH 1233/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Urdu=20translati?= =?UTF-8?q?on=20for=20`docs/ur/docs/benchmarks.md`=20(#9974)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ur/docs/benchmarks.md | 52 ++++++++++++++++++++++++++++++++++++++ docs/ur/mkdocs.yml | 1 + 2 files changed, 53 insertions(+) create mode 100644 docs/ur/docs/benchmarks.md create mode 100644 docs/ur/mkdocs.yml diff --git a/docs/ur/docs/benchmarks.md b/docs/ur/docs/benchmarks.md new file mode 100644 index 000000000..9fc793e6f --- /dev/null +++ b/docs/ur/docs/benchmarks.md @@ -0,0 +1,52 @@ +# بینچ مارکس + +انڈیپنڈنٹ ٹیک امپور بینچ مارک **FASTAPI** Uvicorn کے تحت چلنے والی ایپلی کیشنز کو ایک تیز رفتار Python فریم ورک میں سے ایک ، صرف Starlette اور Uvicorn کے نیچے ( FASTAPI کے ذریعہ اندرونی طور پر استعمال کیا جاتا ہے ) (*) + +لیکن جب بینچ مارک اور موازنہ کی جانچ پڑتال کرتے ہو تو آپ کو مندرجہ ذیل بات ذہن میں رکھنی چاہئے. + +## بینچ مارک اور رفتار + +جب آپ بینچ مارک کی جانچ کرتے ہیں تو ، مساوی کے مقابلے میں مختلف اقسام کے متعدد اوزار دیکھنا عام ہے. + +خاص طور پر ، Uvicorn, Starlette اور FastAPI کو دیکھنے کے لئے ( بہت سے دوسرے ٹولز ) کے ساتھ موازنہ کیا گیا. + +ٹول کے ذریعہ حل ہونے والا آسان مسئلہ ، اس کی بہتر کارکردگی ہوگی. اور زیادہ تر بینچ مارک ٹول کے ذریعہ فراہم کردہ اضافی خصوصیات کی جانچ نہیں کرتے ہیں. + +درجہ بندی کی طرح ہے: + +
    +
  • ASGI :Uvicorn سرور
  • +
      +
    • Starlette: (Uvicorn استعمال کرتا ہے) ایک ویب مائیکرو فریم ورک
    • +
        +
      • FastAPI: (Starlette کا استعمال کرتا ہے) ایک API مائکرو فریم ورک جس میں APIs بنانے کے لیے کئی اضافی خصوصیات ہیں، ڈیٹا کی توثیق وغیرہ کے ساتھ۔
      • +
      +
    +
+ +
    +
  • Uvicorn:
  • +
      +
    • بہترین کارکردگی ہوگی، کیونکہ اس میں سرور کے علاوہ زیادہ اضافی کوڈ نہیں ہے۔
    • +
    • آپ براہ راست Uvicorn میں درخواست نہیں لکھیں گے۔ اس کا مطلب یہ ہوگا کہ آپ کے کوڈ میں کم و بیش، کم از کم، Starlette (یا FastAPI) کی طرف سے فراہم کردہ تمام کوڈ شامل کرنا ہوں گے۔ اور اگر آپ نے ایسا کیا تو، آپ کی حتمی ایپلیکیشن کا وہی اوور ہیڈ ہوگا جیسا کہ ایک فریم ورک استعمال کرنے اور آپ کے ایپ کوڈ اور کیڑے کو کم سے کم کرنا۔
    • +
    • اگر آپ Uvicorn کا موازنہ کر رہے ہیں تو اس کا موازنہ Daphne، Hypercorn، uWSGI وغیرہ ایپلیکیشن سرورز سے کریں۔
    • +
    +
+
    +
  • Starlette:
  • +
      +
    • Uvicorn کے بعد اگلی بہترین کارکردگی ہوگی۔ درحقیقت، Starlette چلانے کے لیے Uvicorn کا استعمال کرتی ہے۔ لہذا، یہ شاید زیادہ کوڈ پر عمل درآمد کرکے Uvicorn سے "سست" ہوسکتا ہے۔
    • +
    • لیکن یہ آپ کو آسان ویب ایپلیکیشنز بنانے کے لیے ٹولز فراہم کرتا ہے، راستوں پر مبنی روٹنگ کے ساتھ، وغیرہ۔
    • +
    • اگر آپ سٹارلیٹ کا موازنہ کر رہے ہیں تو اس کا موازنہ Sanic، Flask، Django وغیرہ سے کریں۔ ویب فریم ورکس (یا مائیکرو فریم ورکس)
    • +
    +
+
    +
  • FastAPI:
  • +
      +
    • جس طرح سے Uvicorn Starlette کا استعمال کرتا ہے اور اس سے تیز نہیں ہو سکتا، Starlette FastAPI کا استعمال کرتا ہے، اس لیے یہ اس سے تیز نہیں ہو سکتا۔
    • +
    • Starlette FastAPI کے اوپری حصے میں مزید خصوصیات فراہم کرتا ہے۔ وہ خصوصیات جن کی آپ کو APIs بناتے وقت تقریباً ہمیشہ ضرورت ہوتی ہے، جیسے ڈیٹا کی توثیق اور سیریلائزیشن۔ اور اسے استعمال کرنے سے، آپ کو خودکار دستاویزات مفت میں مل جاتی ہیں (خودکار دستاویزات چلنے والی ایپلی کیشنز میں اوور ہیڈ کو بھی شامل نہیں کرتی ہیں، یہ اسٹارٹ اپ پر تیار ہوتی ہیں)۔
    • +
    • اگر آپ نے FastAPI کا استعمال نہیں کیا ہے اور Starlette کو براہ راست استعمال کیا ہے (یا کوئی دوسرا ٹول، جیسے Sanic، Flask، Responder، وغیرہ) آپ کو تمام ڈیٹا کی توثیق اور سیریلائزیشن کو خود نافذ کرنا ہوگا۔ لہذا، آپ کی حتمی ایپلیکیشن اب بھی وہی اوور ہیڈ ہوگی جیسا کہ اسے FastAPI کا استعمال کرتے ہوئے بنایا گیا تھا۔ اور بہت سے معاملات میں، یہ ڈیٹا کی توثیق اور سیریلائزیشن ایپلی کیشنز میں لکھے گئے کوڈ کی سب سے بڑی مقدار ہے۔
    • +
    • لہذا، FastAPI کا استعمال کرکے آپ ترقیاتی وقت، Bugs، کوڈ کی لائنوں کی بچت کر رہے ہیں، اور شاید آپ کو وہی کارکردگی (یا بہتر) ملے گی اگر آپ اسے استعمال نہیں کرتے (جیسا کہ آپ کو یہ سب اپنے کوڈ میں لاگو کرنا ہوگا۔ )
    • +
    • اگر آپ FastAPI کا موازنہ کر رہے ہیں، تو اس کا موازنہ ویب ایپلیکیشن فریم ورک (یا ٹولز کے سیٹ) سے کریں جو ڈیٹا کی توثیق، سیریلائزیشن اور دستاویزات فراہم کرتا ہے، جیسے Flask-apispec، NestJS، Molten، وغیرہ۔ مربوط خودکار ڈیٹا کی توثیق، سیریلائزیشن اور دستاویزات کے ساتھ فریم ورک۔
    • +
    +
diff --git a/docs/ur/mkdocs.yml b/docs/ur/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/ur/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From 1c919dee3ce73a5b3d134dbb335c0a38af8438b5 Mon Sep 17 00:00:00 2001 From: Aleksandr Pavlov Date: Sat, 5 Aug 2023 12:26:03 +0400 Subject: [PATCH 1234/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/dependencies/global-depend?= =?UTF-8?q?encies.md`=20(#9970)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: dedkot Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- .../dependencies/global-dependencies.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/ru/docs/tutorial/dependencies/global-dependencies.md diff --git a/docs/ru/docs/tutorial/dependencies/global-dependencies.md b/docs/ru/docs/tutorial/dependencies/global-dependencies.md new file mode 100644 index 000000000..870d42cf5 --- /dev/null +++ b/docs/ru/docs/tutorial/dependencies/global-dependencies.md @@ -0,0 +1,34 @@ +# Глобальные зависимости + +Для некоторых типов приложений может потребоваться добавить зависимости ко всему приложению. + +Подобно тому, как вы можете [добавлять зависимости через параметр `dependencies` в *декораторах операций пути*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank}, вы можете добавлять зависимости сразу ко всему `FastAPI` приложению. + +В этом случае они будут применяться ко всем *операциям пути* в приложении: + +=== "Python 3.9+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="16" + {!> ../../../docs_src/dependencies/tutorial012_an.py!} + ``` + +=== "Python 3.6 non-Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать 'Annotated' версию, если это возможно. + + ```Python hl_lines="15" + {!> ../../../docs_src/dependencies/tutorial012.py!} + ``` + +Все способы [добавления зависимостей в *декораторах операций пути*](dependencies-in-path-operation-decorators.md){.internal-link target=_blank} по-прежнему применимы, но в данном случае зависимости применяются ко всем *операциям пути* приложения. + +## Зависимости для групп *операций пути* + +Позднее, читая о том, как структурировать более крупные [приложения, содержащие много файлов](../../tutorial/bigger-applications.md){.internal-link target=_blank}, вы узнаете, как объявить один параметр dependencies для целой группы *операций пути*. From d86a695db931d623b4225992817d1f9ed8eb259b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:26:40 +0000 Subject: [PATCH 1235/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 621f82d55..d656813cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). From f2e80fae093e8d219e101715a8f92a15edf0ff31 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:28:26 +0000 Subject: [PATCH 1236/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d656813cb..82d256b73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). * 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). From b76112f1a5230a8724994b9fa9fcf5bb417b1e09 Mon Sep 17 00:00:00 2001 From: Reza Rohani Date: Sat, 5 Aug 2023 12:03:08 +0330 Subject: [PATCH 1237/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20code=20highlight?= =?UTF-8?q?ing=20in=20`docs/en/docs/tutorial/bigger-applications.md`=20(#9?= =?UTF-8?q?806)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update bigger-applications.md --- docs/en/docs/tutorial/bigger-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index daa7353a2..26d26475f 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -377,7 +377,7 @@ The `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: -```Python hl_lines="4" +```Python hl_lines="5" {!../../../docs_src/bigger_applications/app/main.py!} ``` From 0b496ea1f8b75943617c3b69421e2b6bfef46006 Mon Sep 17 00:00:00 2001 From: Vicente Merino <47841749+VicenteMerino@users.noreply.github.com> Date: Sat, 5 Aug 2023 04:34:07 -0400 Subject: [PATCH 1238/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20typo=20in=20`doc?= =?UTF-8?q?s/en/docs/contributing.md`=20(#9878)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vicente Merino --- docs/en/docs/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index f968489ae..cfdb607d7 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -126,7 +126,7 @@ And if you update that local FastAPI source code when you run that Python file a That way, you don't have to "install" your local version to be able to test every change. !!! note "Technical Details" - This only happens when you install using this included `requiements.txt` instead of installing `pip install fastapi` directly. + This only happens when you install using this included `requirements.txt` instead of installing `pip install fastapi` directly. That is because inside of the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. From 51f5497f3f91efcaf23ee2cf408b8fa61178ed69 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:35:55 +0000 Subject: [PATCH 1239/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 82d256b73..92061066f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). * 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). From 33e77b6e257eb263cfc82bf0572b453cb1272eb6 Mon Sep 17 00:00:00 2001 From: Adejumo Ridwan Suleiman Date: Sat, 5 Aug 2023 09:36:05 +0100 Subject: [PATCH 1240/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20external=20artic?= =?UTF-8?q?le:=20Build=20an=20SMS=20Spam=20Classifier=20Serverless=20Datab?= =?UTF-8?q?ase=20with=20FaunaDB=20and=20FastAPI=20(#9847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index ad738df35..a7f766d16 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ articles: english: + - author: Adejumo Ridwan Suleiman + author_link: https://www.linkedin.com/in/adejumoridwan/ + link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b + title: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI - author: Raf Rasenberg author_link: https://rafrasenberg.com/about/ link: https://rafrasenberg.com/fastapi-lambda/ From 87e126be2e3633a46163ae28f0b418903fdfd515 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:36:27 +0000 Subject: [PATCH 1241/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 92061066f..3b52700ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). * 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). * 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). From bb7bbafb5fa496166037b04a9fbb108aed3b84c9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:38:32 +0000 Subject: [PATCH 1242/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3b52700ef..f8138e2a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). * 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). * 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). From 5e59acd35bd00018462b6401bf78e52c91b48f70 Mon Sep 17 00:00:00 2001 From: ElliottLarsen <86161304+ElliottLarsen@users.noreply.github.com> Date: Sat, 5 Aug 2023 02:39:38 -0600 Subject: [PATCH 1243/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20comments=20on=20internal=20code=20in=20`fastapi/concurrency.?= =?UTF-8?q?py`=20and=20`fastapi/routing.py`=20(#9590)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcelo Trylesinski --- fastapi/concurrency.py | 2 +- fastapi/routing.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index 31b878d5d..754061c86 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -19,7 +19,7 @@ async def contextmanager_in_threadpool( ) -> AsyncGenerator[_T, None]: # blocking __exit__ from running waiting on a free thread # can create race conditions/deadlocks if the context manager itself - # has it's own internal pool (e.g. a database connection pool) + # has its own internal pool (e.g. a database connection pool) # to avoid this we let __exit__ run without a capacity limit # since we're creating a new limiter for each call, any non-zero limit # works (1 is arbitrary) diff --git a/fastapi/routing.py b/fastapi/routing.py index 6efd40ff3..1e3dfb4d5 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -83,7 +83,7 @@ def _prepare_response_content( 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 + # Otherwise, there's no way to extract lazy data that requires attribute # access instead of dict iteration, e.g. lazy relationships. return res return _model_dump( @@ -456,7 +456,7 @@ class APIRoute(routing.Route): # that doesn't have the hashed_password. But because it's a subclass, it # would pass the validation and be returned as is. # By being a new field, no inheritance will be passed as is. A new model - # will be always created. + # will always be created. # TODO: remove when deprecating Pydantic v1 self.secure_cloned_response_field: Optional[ ModelField From 69d5ebf34d088019b7dfa15729dabe3603e2d16e Mon Sep 17 00:00:00 2001 From: Francis Bergin Date: Sat, 5 Aug 2023 04:40:24 -0400 Subject: [PATCH 1244/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20release=20notes=20(#9835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f8138e2a8..fa8fd9d28 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -107,7 +107,7 @@ A command line tool that will **process your code** and update most of the thing ### Pydantic v1 -**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while. +**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, it will still be supported for a while. This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI. From bdd991244d4ff1393725996ac15425857e643c6f Mon Sep 17 00:00:00 2001 From: Russ Biggs Date: Sat, 5 Aug 2023 02:41:21 -0600 Subject: [PATCH 1245/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20deprecation=20warnings=20in=20`fastapi/params.py`=20(#9854)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix typo for deprecation warnings depreacated -> deprecated --- fastapi/params.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fastapi/params.py b/fastapi/params.py index 30af5713e..2d8100650 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -69,7 +69,7 @@ class Param(FieldInfo): self.deprecated = deprecated if example is not _Unset: warnings.warn( - "`example` has been depreacated, please use `examples` instead", + "`example` has been deprecated, please use `examples` instead", category=DeprecationWarning, stacklevel=4, ) @@ -98,7 +98,7 @@ class Param(FieldInfo): kwargs["examples"] = examples if regex is not None: warnings.warn( - "`regex` has been depreacated, please use `pattern` instead", + "`regex` has been deprecated, please use `pattern` instead", category=DeprecationWarning, stacklevel=4, ) @@ -512,7 +512,7 @@ class Body(FieldInfo): self.deprecated = deprecated if example is not _Unset: warnings.warn( - "`example` has been depreacated, please use `examples` instead", + "`example` has been deprecated, please use `examples` instead", category=DeprecationWarning, stacklevel=4, ) From 0f4a962c201023333b653ea1412011d02e87dd65 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:43:01 +0000 Subject: [PATCH 1246/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fa8fd9d28..ac8a67633 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). * 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). * 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). * 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). From 6df10c9753fa4a0f2d82506da63adf8985caca2b Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:44:36 +0000 Subject: [PATCH 1247/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ac8a67633..b195a7bb1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). * ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). * 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). * 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). From 942ee69d857710ee4f0dffce50b6d4d4db10b540 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 5 Aug 2023 08:46:58 +0000 Subject: [PATCH 1248/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b195a7bb1..723f338a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). * ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). * ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). * 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). From 14c96ef31bc2069dbc7dad82a501de10d83c4cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 9 Aug 2023 15:26:33 +0200 Subject: [PATCH 1249/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Jina=20back=20as=20bronze=20sponsor=20(#10050)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 53cdb9bad..6cfd5b556 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -37,3 +37,6 @@ bronze: - url: https://www.flint.sh title: IT expertise, consulting and development by passionate people img: https://fastapi.tiangolo.com/img/sponsors/flint.png + - url: https://bit.ly/3JJ7y5C + title: Build cross-modal and multimodal applications on the cloud + img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg From 01383a57cbdf57cf1ba9b39381e6ab37c8d30792 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 9 Aug 2023 13:27:14 +0000 Subject: [PATCH 1250/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 723f338a9..486b57071 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). * ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). * ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). From 87398723f91efb24834bdded970bc5065049d50d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 9 Aug 2023 19:04:49 +0200 Subject: [PATCH 1251/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20sponsor=20Porter?= =?UTF-8?q?=20(#10051)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 2 ++ docs/en/docs/img/sponsors/porter-banner.png | Bin 0 -> 17260 bytes docs/en/docs/img/sponsors/porter.png | Bin 0 -> 23992 bytes docs/en/overrides/main.html | 6 ++++++ 5 files changed, 11 insertions(+) create mode 100755 docs/en/docs/img/sponsors/porter-banner.png create mode 100755 docs/en/docs/img/sponsors/porter.png diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 6cfd5b556..6d9119520 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -8,6 +8,9 @@ gold: - url: https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge title: Fern | SDKs and API docs img: https://fastapi.tiangolo.com/img/sponsors/fern.svg + - url: https://www.porter.run + title: Deploy FastAPI on AWS with a few clicks + img: https://fastapi.tiangolo.com/img/sponsors/porter.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index b3cb06327..7c3bb2f47 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -17,3 +17,5 @@ logins: - databento-bot - nanram22 - Flint-company + - porter-dev + - fern-api diff --git a/docs/en/docs/img/sponsors/porter-banner.png b/docs/en/docs/img/sponsors/porter-banner.png new file mode 100755 index 0000000000000000000000000000000000000000..fa2e741c0cbb326b271e3e3f545a4dbc55f72519 GIT binary patch literal 17260 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 LNkvXXu0mjfJEqlj literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/porter.png b/docs/en/docs/img/sponsors/porter.png new file mode 100755 index 0000000000000000000000000000000000000000..582a15156470bd8af7ebf8e9c05beddea1a7de11 GIT binary patch literal 23992 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�S;@abbD|Vr_Aj3d(~E-cl*Fup6_8=vF_W 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
+
{% endblock %} From 82fafcc7ea2d8e2808bf5750a25be4255ba2f683 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 9 Aug 2023 17:05:31 +0000 Subject: [PATCH 1252/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 486b57071..39e04ca98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). * ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). From f0ab797de47d7eb4c80394339039ffaf4a734fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Kh=E1=BA=AFc=20Th=C3=A0nh?= Date: Thu, 10 Aug 2023 22:52:25 +0700 Subject: [PATCH 1253/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20tra?= =?UTF-8?q?nslation=20for=20`docs/vi/docs/python-types.md`=20(#10047)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/en/mkdocs.yml | 2 +- docs/vi/docs/python-types.md | 545 +++++++++++++++++++++++++++++++++++ 2 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 docs/vi/docs/python-types.md diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a18613185..a66b6c147 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -241,7 +241,7 @@ extra: - link: /uk/ name: uk - link: /vi/ - name: vi + name: vi - Tiếng Việt - link: /zh/ name: zh - 汉语 extra_css: diff --git a/docs/vi/docs/python-types.md b/docs/vi/docs/python-types.md new file mode 100644 index 000000000..7f4f51131 --- /dev/null +++ b/docs/vi/docs/python-types.md @@ -0,0 +1,545 @@ +# Giới thiệu kiểu dữ liệu Python + +Python hỗ trợ tùy chọn "type hints" (còn được gọi là "type annotations"). + +Những **"type hints"** hay chú thích là một cú pháp đặc biệt cho phép khai báo kiểu dữ liệu của một biến. + +Bằng việc khai báo kiểu dữ liệu cho các biến của bạn, các trình soạn thảo và các công cụ có thể hỗ trợ bạn tốt hơn. + +Đây chỉ là một **hướng dẫn nhanh** về gợi ý kiểu dữ liệu trong Python. Nó chỉ bao gồm những điều cần thiết tối thiểu để sử dụng chúng với **FastAPI**... đó thực sự là rất ít. + +**FastAPI** hoàn toàn được dựa trên những gợi ý kiểu dữ liệu, chúng mang đến nhiều ưu điểm và lợi ích. + +Nhưng thậm chí nếu bạn không bao giờ sử dụng **FastAPI**, bạn sẽ được lợi từ việc học một ít về chúng. + +!!! note + Nếu bạn là một chuyên gia về Python, và bạn đã biết mọi thứ về gợi ý kiểu dữ liệu, bỏ qua và đi tới chương tiếp theo. + +## Động lực + +Hãy bắt đầu với một ví dụ đơn giản: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Kết quả khi gọi chương trình này: + +``` +John Doe +``` + +Hàm thực hiện như sau: + +* Lấy một `first_name` và `last_name`. +* Chuyển đổi kí tự đầu tiên của mỗi biến sang kiểu chữ hoa với `title()`. +* Nối chúng lại với nhau bằng một kí tự trắng ở giữa. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Sửa đổi + +Nó là một chương trình rất đơn giản. + +Nhưng bây giờ hình dung rằng bạn đang viết nó từ đầu. + +Tại một vài thời điểm, bạn sẽ bắt đầu định nghĩa hàm, bạn có các tham số... + +Nhưng sau đó bạn phải gọi "phương thức chuyển đổi kí tự đầu tiên sang kiểu chữ hoa". + +Có phải là `upper`? Có phải là `uppercase`? `first_uppercase`? `capitalize`? + +Sau đó, bạn thử hỏi người bạn cũ của mình, autocompletion của trình soạn thảo. + +Bạn gõ tham số đầu tiên của hàm, `first_name`, sau đó một dấu chấm (`.`) và sau đó ấn `Ctrl+Space` để kích hoạt bộ hoàn thành. + +Nhưng đáng buồn, bạn không nhận được điều gì hữu ích cả: + + + +### Thêm kiểu dữ liệu + +Hãy sửa một dòng từ phiên bản trước. + +Chúng ta sẽ thay đổi chính xác đoạn này, tham số của hàm, từ: + +```Python + first_name, last_name +``` + +sang: + +```Python + first_name: str, last_name: str +``` + +Chính là nó. + +Những thứ đó là "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Đó không giống như khai báo những giá trị mặc định giống như: + +```Python + first_name="john", last_name="doe" +``` + +Nó là một thứ khác. + +Chúng ta sử dụng dấu hai chấm (`:`), không phải dấu bằng (`=`). + +Và việc thêm gợi ý kiểu dữ liệu không làm thay đổi những gì xảy ra so với khi chưa thêm chúng. + +But now, imagine you are again in the middle of creating that function, but with type hints. + +Tại cùng một điểm, bạn thử kích hoạt autocomplete với `Ctrl+Space` và bạn thấy: + + + +Với cái đó, bạn có thể cuộn, nhìn thấy các lựa chọn, cho đến khi bạn tìm thấy một "tiếng chuông": + + + +## Động lực nhiều hơn + +Kiểm tra hàm này, nó đã có gợi ý kiểu dữ liệu: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Bởi vì trình soạn thảo biết kiểu dữ liệu của các biến, bạn không chỉ có được completion, bạn cũng được kiểm tra lỗi: + + + +Bây giờ bạn biết rằng bạn phải sửa nó, chuyển `age` sang một xâu với `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Khai báo các kiểu dữ liệu + +Bạn mới chỉ nhìn thấy những nơi chủ yếu để đặt khai báo kiểu dữ liệu. Như là các tham số của hàm. + +Đây cũng là nơi chủ yếu để bạn sử dụng chúng với **FastAPI**. + +### Kiểu dữ liệu đơn giản + +Bạn có thể khai báo tất cả các kiểu dữ liệu chuẩn của Python, không chỉ là `str`. + +Bạn có thể sử dụng, ví dụ: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Các kiểu dữ liệu tổng quát với tham số kiểu dữ liệu + +Có một vài cấu trúc dữ liệu có thể chứa các giá trị khác nhau như `dict`, `list`, `set` và `tuple`. Và những giá trị nội tại cũng có thể có kiểu dữ liệu của chúng. + +Những kiểu dữ liệu nội bộ này được gọi là những kiểu dữ liệu "**tổng quát**". Và có khả năng khai báo chúng, thậm chí với các kiểu dữ liệu nội bộ của chúng. + +Để khai báo những kiểu dữ liệu và những kiểu dữ liệu nội bộ đó, bạn có thể sử dụng mô đun chuẩn của Python là `typing`. Nó có hỗ trợ những gợi ý kiểu dữ liệu này. + +#### Những phiên bản mới hơn của Python + +Cú pháp sử dụng `typing` **tương thích** với tất cả các phiên bản, từ Python 3.6 tới những phiên bản cuối cùng, bao gồm Python 3.9, Python 3.10,... + +As Python advances, **những phiên bản mới** mang tới sự hỗ trợ được cải tiến cho những chú thích kiểu dữ liệu và trong nhiều trường hợp bạn thậm chí sẽ không cần import và sử dụng mô đun `typing` để khai báo chú thích kiểu dữ liệu. + +Nếu bạn có thể chọn một phiên bản Python gần đây hơn cho dự án của bạn, ban sẽ có được những ưu điểm của những cải tiến đơn giản đó. + +Trong tất cả các tài liệu tồn tại những ví dụ tương thích với mỗi phiên bản Python (khi có một sự khác nhau). + +Cho ví dụ "**Python 3.6+**" có nghĩa là nó tương thích với Python 3.7 hoặc lớn hơn (bao gồm 3.7, 3.8, 3.9, 3.10,...). và "**Python 3.9+**" nghĩa là nó tương thích với Python 3.9 trở lên (bao gồm 3.10,...). + +Nếu bạn có thể sử dụng **phiên bản cuối cùng của Python**, sử dụng những ví dụ cho phiên bản cuối, những cái đó sẽ có **cú pháp đơn giản và tốt nhât**, ví dụ, "**Python 3.10+**". + +#### List + +Ví dụ, hãy định nghĩa một biến là `list` các `str`. + +=== "Python 3.9+" + + Khai báo biến với cùng dấu hai chấm (`:`). + + Tương tự kiểu dữ liệu `list`. + + Như danh sách là một kiểu dữ liệu chứa một vài kiểu dữ liệu có sẵn, bạn đặt chúng trong các dấu ngoặc vuông: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +=== "Python 3.6+" + + Từ `typing`, import `List` (với chữ cái `L` viết hoa): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + Khai báo biến với cùng dấu hai chấm (`:`). + + Tương tự như kiểu dữ liệu, `List` bạn import từ `typing`. + + Như danh sách là một kiểu dữ liệu chứa các kiểu dữ liệu có sẵn, bạn đặt chúng bên trong dấu ngoặc vuông: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +!!! info + Các kiểu dữ liệu có sẵn bên trong dấu ngoặc vuông được gọi là "tham số kiểu dữ liệu". + + Trong trường hợp này, `str` là tham số kiểu dữ liệu được truyền tới `List` (hoặc `list` trong Python 3.9 trở lên). + +Có nghĩa là: "biến `items` là một `list`, và mỗi phần tử trong danh sách này là một `str`". + +!!! tip + Nếu bạn sử dụng Python 3.9 hoặc lớn hơn, bạn không phải import `List` từ `typing`, bạn có thể sử dụng `list` để thay thế. + +Bằng cách này, trình soạn thảo của bạn có thể hỗ trợ trong khi xử lí các phần tử trong danh sách: + + + +Đa phần đều không thể đạt được nếu không có các kiểu dữ liệu. + +Chú ý rằng, biến `item` là một trong các phần tử trong danh sách `items`. + +Và do vậy, trình soạn thảo biết nó là một `str`, và cung cấp sự hỗ trợ cho nó. + +#### Tuple and Set + +Bạn sẽ làm điều tương tự để khai báo các `tuple` và các `set`: + +=== "Python 3.9+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +Điều này có nghĩa là: + +* Biến `items_t` là một `tuple` với 3 phần tử, một `int`, một `int` nữa, và một `str`. +* Biến `items_s` là một `set`, và mỗi phần tử của nó có kiểu `bytes`. + +#### Dict + +Để định nghĩa một `dict`, bạn truyền 2 tham số kiểu dữ liệu, phân cách bởi dấu phẩy. + +Tham số kiểu dữ liệu đầu tiên dành cho khóa của `dict`. + +Tham số kiểu dữ liệu thứ hai dành cho giá trị của `dict`. + +=== "Python 3.9+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +Điều này có nghĩa là: + +* Biến `prices` là một `dict`: + * Khóa của `dict` này là kiểu `str` (đó là tên của mỗi vật phẩm). + * Giá trị của `dict` này là kiểu `float` (đó là giá của mỗi vật phẩm). + +#### Union + +Bạn có thể khai báo rằng một biến có thể là **một vài kiểu dữ liệu" bất kì, ví dụ, một `int` hoặc một `str`. + +Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể sử dụng kiểu `Union` từ `typing` và đặt trong dấu ngoặc vuông những giá trị được chấp nhận. + +In Python 3.10 there's also a **new syntax** where you can put the possible types separated by a vertical bar (`|`). + +Trong Python 3.10 cũng có một **cú pháp mới** mà bạn có thể đặt những kiểu giá trị khả thi phân cách bởi một dấu sổ dọc (`|`). + + +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +Trong cả hai trường hợp có nghĩa là `item` có thể là một `int` hoặc `str`. + +#### Khả năng `None` + +Bạn có thể khai báo một giá trị có thể có một kiểu dữ liệu, giống như `str`, nhưng nó cũng có thể là `None`. + +Trong Python 3.6 hoặc lớn hơn (bao gồm Python 3.10) bạn có thể khai báo nó bằng các import và sử dụng `Optional` từ mô đun `typing`. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Sử dụng `Optional[str]` thay cho `str` sẽ cho phép trình soạn thảo giúp bạn phát hiện các lỗi mà bạn có thể gặp như một giá trị luôn là một `str`, trong khi thực tế nó rất có thể là `None`. + +`Optional[Something]` là một cách viết ngắn gọn của `Union[Something, None]`, chúng là tương đương nhau. + +Điều này cũng có nghĩa là trong Python 3.10, bạn có thể sử dụng `Something | None`: + +=== "Python 3.10+" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.6+ alternative" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +#### Sử dụng `Union` hay `Optional` + +If you are using a Python version below 3.10, here's a tip from my very **subjective** point of view: + +Nếu bạn đang sử dụng phiên bản Python dưới 3.10, đây là một mẹo từ ý kiến rất "chủ quan" của tôi: + +* 🚨 Tránh sử dụng `Optional[SomeType]` +* Thay vào đó ✨ **sử dụng `Union[SomeType, None]`** ✨. + +Cả hai là tương đương và bên dưới chúng giống nhau, nhưng tôi sẽ đễ xuất `Union` thay cho `Optional` vì từ "**tùy chọn**" có vẻ ngầm định giá trị là tùy chọn, và nó thực sự có nghĩa rằng "nó có thể là `None`", do đó nó không phải là tùy chọn và nó vẫn được yêu cầu. + +Tôi nghĩ `Union[SomeType, None]` là rõ ràng hơn về ý nghĩa của nó. + +Nó chỉ là về các từ và tên. Nhưng những từ đó có thể ảnh hưởng cách bạn và những đồng đội của bạn suy nghĩ về code. + +Cho một ví dụ, hãy để ý hàm này: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c.py!} +``` + +Tham số `name` được định nghĩa là `Optional[str]`, nhưng nó **không phải là tùy chọn**, bạn không thể gọi hàm mà không có tham số: + +```Python +say_hi() # Oh, no, this throws an error! 😱 +``` + +Tham số `name` **vẫn được yêu cầu** (không phải là *tùy chọn*) vì nó không có giá trị mặc định. Trong khi đó, `name` chấp nhận `None` như là giá trị: + +```Python +say_hi(name=None) # This works, None is valid 🎉 +``` + +Tin tốt là, khi bạn sử dụng Python 3.10, bạn sẽ không phải lo lắng về điều đó, bạn sẽ có thể sử dụng `|` để định nghĩa hợp của các kiểu dữ liệu một cách đơn giản: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009c_py310.py!} +``` + +Và sau đó, bạn sẽ không phải lo rằng những cái tên như `Optional` và `Union`. 😎 + + +#### Những kiểu dữ liệu tổng quát + +Những kiểu dữ liệu này lấy tham số kiểu dữ liệu trong dấu ngoặc vuông được gọi là **Kiểu dữ liệu tổng quát**, cho ví dụ: + +=== "Python 3.10+" + + Bạn có thể sử dụng các kiểu dữ liệu có sẵn như là kiểu dữ liệu tổng quát (với ngoặc vuông và kiểu dữ liệu bên trong): + + * `list` + * `tuple` + * `set` + * `dict` + + Và tương tự với Python 3.6, từ mô đun `typing`: + + * `Union` + * `Optional` (tương tự như Python 3.6) + * ...và các kiểu dữ liệu khác. + + Trong Python 3.10, thay vì sử dụng `Union` và `Optional`, bạn có thể sử dụng sổ dọc ('|') để khai báo hợp của các kiểu dữ liệu, điều đó tốt hơn và đơn giản hơn nhiều. + +=== "Python 3.9+" + + Bạn có thể sử dụng các kiểu dữ liệu có sẵn tương tự như (với ngoặc vuông và kiểu dữ liệu bên trong): + + * `list` + * `tuple` + * `set` + * `dict` + + Và tương tự với Python 3.6, từ mô đun `typing`: + + * `Union` + * `Optional` + * ...and others. + +=== "Python 3.6+" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...và các kiểu khác. + +### Lớp như kiểu dữ liệu + +Bạn cũng có thể khai báo một lớp như là kiểu dữ liệu của một biến. + +Hãy nói rằng bạn muốn có một lớp `Person` với một tên: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Sau đó bạn có thể khai báo một biến có kiểu là `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Và lại một lần nữa, bạn có được tất cả sự hỗ trợ từ trình soạn thảo: + + + +Lưu ý rằng, điều này có nghĩa rằng "`one_person`" là một **thực thể** của lớp `Person`. + +Nó không có nghĩa "`one_person`" là một **lớp** gọi là `Person`. + +## Pydantic models + +Pydantic là một thư viện Python để validate dữ liệu hiệu năng cao. + +Bạn có thể khai báo "hình dạng" của dữa liệu như là các lớp với các thuộc tính. + +Và mỗi thuộc tính có một kiểu dữ liệu. + +Sau đó bạn tạo một thực thể của lớp đó với một vài giá trị và nó sẽ validate các giá trị, chuyển đổi chúng sang kiểu dữ liệu phù hợp (nếu đó là trường hợp) và cho bạn một object với toàn bộ dữ liệu. + +Và bạn nhận được tất cả sự hỗ trợ của trình soạn thảo với object kết quả đó. + +Một ví dụ từ tài liệu chính thức của Pydantic: + +=== "Python 3.10+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +!!! info + Để học nhiều hơn về Pydantic, tham khảo tài liệu của nó. + +**FastAPI** được dựa hoàn toàn trên Pydantic. + +Bạn sẽ thấy nhiều ví dụ thực tế hơn trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. + +!!! tip + Pydantic có một hành vi đặc biệt khi bạn sử dụng `Optional` hoặc `Union[Something, None]` mà không có giá trị mặc dịnh, bạn có thể đọc nhiều hơn về nó trong tài liệu của Pydantic về Required Optional fields. + + +## Type Hints với Metadata Annotations + +Python cũng có một tính năng cho phép đặt **metadata bổ sung** trong những gợi ý kiểu dữ liệu này bằng cách sử dụng `Annotated`. + +=== "Python 3.9+" + + Trong Python 3.9, `Annotated` là một phần của thư viện chuẩn, do đó bạn có thể import nó từ `typing`. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013_py39.py!} + ``` + +=== "Python 3.6+" + + Ở phiên bản dưới Python 3.9, bạn import `Annotated` từ `typing_extensions`. + + Nó đã được cài đặt sẵng cùng với **FastAPI**. + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial013.py!} + ``` + +Python bản thân nó không làm bất kì điều gì với `Annotated`. Với các trình soạn thảo và các công cụ khác, kiểu dữ liệu vẫn là `str`. + +Nhưng bạn có thể sử dụng `Annotated` để cung cấp cho **FastAPI** metadata bổ sung về cách mà bạn muốn ứng dụng của bạn xử lí. + +Điều quan trọng cần nhớ là ***tham số kiểu dữ liệu* đầu tiên** bạn truyền tới `Annotated` là **kiểu giá trị thực sự**. Phần còn lại chỉ là metadata cho các công cụ khác. + +Bây giờ, bạn chỉ cần biết rằng `Annotated` tồn tại, và nó là tiêu chuẩn của Python. 😎 + + +Sau đó, bạn sẽ thấy sự **mạnh mẽ** mà nó có thể làm. + +!!! tip + Thực tế, cái này là **tiêu chuẩn của Python**, nghĩa là bạn vẫn sẽ có được **trải nghiệm phát triển tốt nhất có thể** với trình soạn thảo của bạn, với các công cụ bạn sử dụng để phân tích và tái cấu trúc code của bạn, etc. ✨ + + Và code của bạn sẽ tương thích với nhiều công cụ và thư viện khác của Python. 🚀 + +## Các gợi ý kiểu dữ liệu trong **FastAPI** + +**FastAPI** lấy các ưu điểm của các gợi ý kiểu dữ liệu để thực hiện một số thứ. + +Với **FastAPI**, bạn khai báo các tham số với gợi ý kiểu và bạn có được: + +* **Sự hỗ trợ từ các trình soạn thảo**. +* **Kiểm tra kiểu dữ liệu (type checking)**. + +...và **FastAPI** sử dụng các khia báo để: + +* **Định nghĩa các yêu cầu**: từ tham số đường dẫn của request, tham số query, headers, bodies, các phụ thuộc (dependencies),... +* **Chuyển dổi dữ liệu*: từ request sang kiểu dữ liệu được yêu cầu. +* **Kiểm tra tính đúng đắn của dữ liệu**: tới từ mỗi request: + * Sinh **lỗi tự động** để trả về máy khác khi dữ liệu không hợp lệ. +* **Tài liệu hóa** API sử dụng OpenAPI: + * cái mà sau được được sử dụng bởi tài liệu tương tác người dùng. + +Điều này có thể nghe trừu tượng. Đừng lo lắng. Bạn sẽ thấy tất cả chúng trong [Hướng dẫn sử dụng](tutorial/index.md){.internal-link target=_blank}. + +Điều quan trọng là bằng việc sử dụng các kiểu dữ liệu chuẩn của Python (thay vì thêm các lớp, decorators,...), **FastAPI** sẽ thực hiện nhiều công việc cho bạn. + +!!! info + Nếu bạn đã đi qua toàn bộ các hướng dẫn và quay trở lại để tìm hiểu nhiều hơn về các kiểu dữ liệu, một tài nguyên tốt như "cheat sheet" từ `mypy`. From 1f0d9086b35096f2b508dea8544bb2bd86074eef Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Aug 2023 15:53:06 +0000 Subject: [PATCH 1254/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 39e04ca98..8ee9d1af5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). * 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). From fe3eaf63e643ba178c7266aeb78a7751b8ca4171 Mon Sep 17 00:00:00 2001 From: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:53:26 +0300 Subject: [PATCH 1255/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/deployment/docker.md`=20(#9971)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Yois4101 <119609381+Yois4101@users.noreply.github.com> --- docs/ru/docs/deployment/docker.md | 700 ++++++++++++++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 docs/ru/docs/deployment/docker.md diff --git a/docs/ru/docs/deployment/docker.md b/docs/ru/docs/deployment/docker.md new file mode 100644 index 000000000..f045ca944 --- /dev/null +++ b/docs/ru/docs/deployment/docker.md @@ -0,0 +1,700 @@ +# FastAPI и Docker-контейнеры + +При развёртывании приложений FastAPI, часто начинают с создания **образа контейнера на основе Linux**. Обычно для этого используют **Docker**. Затем можно развернуть такой контейнер на сервере одним из нескольких способов. + +Использование контейнеров на основе Linux имеет ряд преимуществ, включая **безопасность**, **воспроизводимость**, **простоту** и прочие. + +!!! tip "Подсказка" + Торопитесь или уже знакомы с этой технологией? Перепрыгните на раздел [Создать Docker-образ для FastAPI 👇](#docker-fastapi) + +
+Развернуть Dockerfile 👀 + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] + +# Если используете прокси-сервер, такой как Nginx или Traefik, добавьте --proxy-headers +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"] +``` + +
+ +## Что такое "контейнер" + +Контейнеризация - это **легковесный** способ упаковать приложение, включая все его зависимости и необходимые файлы, чтобы изолировать его от других контейнеров (других приложений и компонентов) работающих на этой же системе. + +Контейнеры, основанные на Linux, запускаются используя ядро Linux хоста (машины, виртуальной машины, облачного сервера и т.п.). Это значит, что они очень легковесные (по сравнению с полноценными виртуальными машинами, полностью эмулирующими работу операционной системы). + +Благодаря этому, контейнеры потребляют **малое количество ресурсов**, сравнимое с процессом запущенным напрямую (виртуальная машина потребует гораздо больше ресурсов). + +Контейнеры также имеют собственные запущенные **изолированные** процессы (но часто только один процесс), файловую систему и сеть, что упрощает развёртывание, разработку, управление доступом и т.п. + +## Что такое "образ контейнера" + +Для запуска **контейнера** нужен **образ контейнера**. + +Образ контейнера - это **замороженная** версия всех файлов, переменных окружения, программ и команд по умолчанию, необходимых для работы приложения. **Замороженный** - означает, что **образ** не запущен и не выполняется, это всего лишь упакованные вместе файлы и метаданные. + +В отличие от **образа контейнера**, хранящего неизменное содержимое, под термином **контейнер** подразумевают запущенный образ, то есть объёкт, который **исполняется**. + +Когда **контейнер** запущен (на основании **образа**), он может создавать и изменять файлы, переменные окружения и т.д. Эти изменения будут существовать только внутри контейнера, но не будут сохраняться в образе контейнера (не будут сохранены на диск). + +Образ контейнера можно сравнить с файлом, содержащем **программу**, например, как файл `main.py`. + +И **контейнер** (в отличие от **образа**) - это на самом деле выполняемый экземпляр образа, примерно как **процесс**. По факту, контейнер запущен только когда запущены его процессы (чаще, всего один процесс) и остановлен, когда запущенных процессов нет. + +## Образы контейнеров + +Docker является одним оз основных инструментов для создания **образов** и **контейнеров** и управления ими. + +Существует общедоступный Docker Hub с подготовленными **официальными образами** многих инструментов, окружений, баз данных и приложений. + +К примеру, есть официальный образ Python. + +Также там представлены и другие полезные образы, такие как базы данных: + +* PostgreSQL +* MySQL +* MongoDB +* Redis + +и т.п. + +Использование подготовленных образов значительно упрощает **комбинирование** и использование разных инструментов. Например, Вы можете попытаться использовать новую базу данных. В большинстве случаев можно использовать **официальный образ** и всего лишь указать переменные окружения. + +Таким образом, Вы можете изучить, что такое контейнеризация и Docker, и использовать полученные знания с разными инструментами и компонентами. + +Так, Вы можете запустить одновременно **множество контейнеров** с базой данных, Python-приложением, веб-сервером, React-приложением и соединить их вместе через внутреннюю сеть. + +Все системы управления контейнерами (такие, как Docker или Kubernetes) имеют встроенные возможности для организации такого сетевого взаимодействия. + +## Контейнеры и процессы + +Обычно **образ контейнера** содержит метаданные предустановленной программы или команду, которую следует выполнить при запуске **контейнера**. Также он может содержать параметры, передаваемые предустановленной программе. Похоже на то, как если бы Вы запускали такую программу через терминал. + +Когда **контейнер** запущен, он будет выполнять прописанные в нём команды и программы. Но Вы можете изменить его так, чтоб он выполнял другие команды и программы. + +Контейнер буде работать до тех пор, пока выполняется его **главный процесс** (команда или программа). + +В контейнере обычно выполняется **только один процесс**, но от его имени можно запустить другие процессы, тогда в этом же в контейнере будет выполняться **множество процессов**. + +Контейнер не считается запущенным, если в нём **не выполняется хотя бы один процесс**. Если главный процесс остановлен, значит и контейнер остановлен. + +## Создать Docker-образ для FastAPI + +Что ж, давайте ужё создадим что-нибудь! 🚀 + +Я покажу Вам, как собирать **Docker-образ** для FastAPI **с нуля**, основываясь на **официальном образе Python**. + +Такой подход сгодится для **большинства случаев**, например: + +* Использование с **Kubernetes** или аналогичным инструментом +* Запуск в **Raspberry Pi** +* Использование в облачных сервисах, запускающих образы контейнеров для Вас и т.п. + +### Установить зависимости + +Обычно Вашему приложению необходимы **дополнительные библиотеки**, список которых находится в отдельном файле. + +На название и содержание такого файла влияет выбранный Вами инструмент **установки** этих библиотек (зависимостей). + +Чаще всего это простой файл `requirements.txt` с построчным перечислением библиотек и их версий. + +При этом Вы, для выбора версий, будете использовать те же идеи, что упомянуты на странице [О версиях FastAPI](./versions.md){.internal-link target=_blank}. + +Ваш файл `requirements.txt` может выглядеть как-то так: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +Устанавливать зависимости проще всего с помощью `pip`: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info "Информация" + Существуют и другие инструменты управления зависимостями. + + В этом же разделе, но позже, я покажу Вам пример использования Poetry. 👇 + +### Создать приложение **FastAPI** + +* Создайте директорию `app` и перейдите в неё. +* Создайте пустой файл `__init__.py`. +* Создайте файл `main.py` и заполните его: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile + +В этой же директории создайте файл `Dockerfile` и заполните его: + +```{ .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. Начните с официального образа Python, который будет основой для образа приложения. + +2. Укажите, что в дальнейшем команды запускаемые в контейнере, будут выполняться в директории `/code`. + + Инструкция создаст эту директорию внутри контейнера и мы поместим в неё файл `requirements.txt` и директорию `app`. + +3. Скопируете файл с зависимостями из текущей директории в `/code`. + + Сначала копируйте **только** файл с зависимостями. + + Этот файл **изменяется довольно редко**, Docker ищет изменения при постройке образа и если не находит, то использует **кэш**, в котором хранятся предыдущии версии сборки образа. + +4. Установите библиотеки перечисленные в файле с зависимостями. + + Опция `--no-cache-dir` указывает `pip` не сохранять загружаемые библиотеки на локальной машине для использования их в случае повторной загрузки. В контейнере, в случае пересборки этого шага, они всё равно будут удалены. + + !!! note "Заметка" + Опция `--no-cache-dir` нужна только для `pip`, она никак не влияет на Docker или контейнеры. + + Опция `--upgrade` указывает `pip` обновить библиотеки, емли они уже установлены. + + Ка и в предыдущем шаге с копированием файла, этот шаг также будет использовать **кэш Docker** в случае отсутствия изменений. + + Использрвание кэша, особенно на этом шаге, позволит Вам **сэкономить** кучу времени при повторной сборке образа, так как зависимости будут сохранены в кеше, а не **загружаться и устанавливаться каждый раз**. + +5. Скопируйте директорию `./app` внутрь директории `/code` (в контейнере). + + Так как в этой директории расположен код, который **часто изменяется**, то использование **кэша** на этом шаге будет наименее эффективно, а значит лучше поместить этот шаг **ближе к концу** `Dockerfile`, дабы не терять выгоду от оптимизации предыдущих шагов. + +6. Укажите **команду**, запускающую сервер `uvicorn`. + + `CMD` принимает список строк, разделённых запятыми, но при выполнении объединит их через пробел, собрав из них одну команду, которую Вы могли бы написать в терминале. + + Эта команда будет выполнена в **текущей рабочей директории**, а именно в директории `/code`, котоая указана командой `WORKDIR /code`. + + Так как команда выполняется внутрии директории `/code`, в которую мы поместили папку `./app` с приложением, то **Uvicorn** сможет найти и **импортировать** объект `app` из файла `app.main`. + +!!! tip "Подсказка" + Если ткнёте на кружок с плюсом, то увидите пояснения. 👆 + +На данном этапе структура проекта должны выглядеть так: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### Использование прокси-сервера + +Если Вы запускаете контейнер за прокси-сервером завершения TLS (балансирующего нагрузку), таким как Nginx или Traefik, добавьте опцию `--proxy-headers`, которая укажет Uvicorn, что он работает позади прокси-сервера и может доверять заголовкам отправляемым им. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Кэш Docker'а + +В нашем `Dockerfile` использована полезная хитрость, когда сначала копируется **только файл с зависимостями**, а не вся папка с кодом приложения. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker и подобные ему инструменты **создают** образы контейнеров **пошагово**, добавляя **один слой над другим**, начиная с первой строки `Dockerfile` и добавляя файлы, создаваемые при выполнении каждой инструкции из `Dockerfile`. + +При создании образа используется **внутренний кэш** и если в файлах нет изменений с момента последней сборки образа, то будет **переиспользован** ранее созданный слой образа, а не повторное копирование файлов и создание слоя с нуля. +Заметьте, что так как слой следующего шага зависит от слоя предыдущего, то изменения внесённые в промежуточный слой, также повлияют на последующие. + +Избегание копирования файлов не обязательно улучшит ситуацию, но использование кэша на одном шаге, позволит **использовать кэш и на следующих шагах**. Например, можно использовать кэш при установке зависимостей: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +Файл со списком зависимостей **изменяется довольно редко**. Так что выполнив команду копирования только этого файла, Docker сможет **использовать кэш** на этом шаге. + +А затем **использовать кэш и на следующем шаге**, загружающем и устанавливающем зависимости. И вот тут-то мы и **сэкономим много времени**. ✨ ...а не будем томиться в тягостном ожидании. 😪😆 + +Для загрузки и установки необходимых библиотек **может понадобиться несколько минут**, но использование **кэша** занимает несколько **секунд** максимум. + +И так как во время разработки Вы будете часто пересобирать контейнер для проверки работоспособности внесённых изменений, то сэкономленные минуты сложатся в часы, а то и дни. + +Так как папка с кодом приложения **изменяется чаще всего**, то мы расположили её в конце `Dockerfile`, ведь после внесённых в код изменений кэш не будет использован на этом и следующих шагах. + +```Dockerfile +COPY ./app /code/app +``` + +### Создать Docker-образ + +Теперь, когда все файлы на своих местах, давайте создадим образ контейнера. + +* Перейдите в директорию проекта (в ту, где расположены `Dockerfile` и папка `app` с приложением). +* Создай образ приложения FastAPI: + +
+ +```console +$ docker build -t myimage . + +---> 100% +``` + +
+ +!!! tip "Подсказка" + Обратите внимание, что в конце написана точка - `.`, это то же самое что и `./`, тем самым мы указываем Docker директорию, из которой нужно выполнять сборку образа контейнера. + + В данном случае это та же самая директория (`.`). + +### Запуск Docker-контейнера + +* Запустите контейнер, основанный на Вашем образе: + +
+ +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +
+ +## Проверка + +Вы можете проверить, что Ваш Docker-контейнер работает перейдя по ссылке: http://192.168.99.100/items/5?q=somequery или http://127.0.0.1/items/5?q=somequery (или похожей, которую использует Ваш Docker-хост). + +Там Вы увидите: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## Интерактивная документация API + +Теперь перейдите по ссылке http://192.168.99.100/docs или http://127.0.0.1/docs (или похожей, которую использует Ваш Docker-хост). + +Здесь Вы увидите автоматическую интерактивную документацию API (предоставляемую Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## Альтернативная документация API + +Также Вы можете перейти по ссылке http://192.168.99.100/redoc or http://127.0.0.1/redoc (или похожей, которую использует Ваш Docker-хост). + +Здесь Вы увидите альтернативную автоматическую документацию API (предоставляемую ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Создание Docker-образа на основе однофайлового приложения FastAPI + +Если Ваше приложение FastAPI помещено в один файл, например, `main.py` и структура Ваших файлов похожа на эту: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +Вам нужно изменить в `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. Скопируйте непосредственно файл `main.py` в директорию `/code` (не указывайте `./app`). + +2. При запуске Uvicorn укажите ему, что объект `app` нужно импортировать из файла `main` (вместо импортирования из `app.main`). + +Настройте Uvicorn на использование `main` вместо `app.main` для импорта объекта `app`. + +## Концепции развёртывания + +Давайте вспомним о [Концепциях развёртывания](./concepts.md){.internal-link target=_blank} и применим их к контейнерам. + +Контейнеры - это, в основном, инструмент упрощающий **сборку и развёртывание** приложения и они не обязыают к применению какой-то определённой **концепции развёртывания**, а значит мы можем выбирать нужную стратегию. + +**Хорошая новость** в том, что независимо от выбранной стратегии, мы всё равно можем покрыть все концепции развёртывания. 🎉 + +Рассмотрим эти **концепции развёртывания** применительно к контейнерам: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +## Использование более безопасного протокола HTTPS + +Если мы определимся, что **образ контейнера** будет содержать только приложение FastAPI, то работу с HTTPS можно организовать **снаружи** контейнера при помощи другого инструмента. + +Это может быть другой контейнер, в котором есть, например, Traefik, работающий с **HTTPS** и **самостоятельно** обновляющий **сертификаты**. + +!!! tip "Подсказка" + Traefik совместим с Docker, Kubernetes и им подобными инструментами. Он очень прост в установке и настройке использования HTTPS для Ваших контейнеров. + +В качестве альтернативы, работу с HTTPS можно доверить облачному провайдеру, если он предоставляет такую услугу. + +## Настройки запуска и перезагрузки приложения + +Обычно **запуском контейнера с приложением** занимается какой-то отдельный инструмент. + +Это может быть сам **Docker**, **Docker Compose**, **Kubernetes**, **облачный провайдер** и т.п. + +В большинстве случаев это простейшие настройки запуска и перезагрузки приложения (при падении). Например, команде запуска Docker-контейнера можно добавить опцию `--restart`. + +Управление запуском и перезагрузкой приложений без использования контейнеров - весьма затруднительно. Но при **работе с контейнерами** - это всего лишь функционал доступный по умолчанию. ✨ + +## Запуск нескольких экземпляров приложения - Указание количества процессов + +Если у Вас есть кластер машин под управлением **Kubernetes**, Docker Swarm Mode, Nomad или аналогичной сложной системой оркестрации контейнеров, скорее всего, вместо использования менеджера процессов (типа Gunicorn и его воркеры) в каждом контейнере, Вы захотите **управлять количеством запущенных экземпляров приложения** на **уровне кластера**. + +В любую из этих систем управления контейнерами обычно встроен способ управления **количеством запущенных контейнеров** для распределения **нагрузки** от входящих запросов на **уровне кластера**. + +В такой ситуации Вы, вероятно, захотите создать **образ Docker**, как [описано выше](#dockerfile), с установленными зависимостями и запускающий **один процесс Uvicorn** вместо того, чтобы запускать Gunicorn управляющий несколькими воркерами Uvicorn. + +### Балансировщик нагрузки + +Обычно при использовании контейнеров один компонент **прослушивает главный порт**. Это может быть контейнер содержащий **прокси-сервер завершения работы TLS** для работы с **HTTPS** или что-то подобное. + +Поскольку этот компонент **принимает запросы** и равномерно **распределяет** их между компонентами, его также называют **балансировщиком нагрузки**. + +!!! tip "Подсказка" + **Прокси-сервер завершения работы TLS** одновременно может быть **балансировщиком нагрузки**. + +Система оркестрации, которую Вы используете для запуска и управления контейнерами, имеет встроенный инструмент **сетевого взаимодействия** (например, для передачи HTTP-запросов) между контейнерами с Вашими приложениями и **балансировщиком нагрузки** (который также может быть **прокси-сервером**). + +### Один балансировщик - Множество контейнеров + +При работе с **Kubernetes** или аналогичными системами оркестрации использование их внутреннней сети позволяет иметь один **балансировщик нагрузки**, который прослушивает **главный** порт и передаёт запросы **множеству запущенных контейнеров** с Вашими приложениями. + +В каждом из контейнеров обычно работает **только один процесс** (например, процесс Uvicorn управляющий Вашим приложением FastAPI). Контейнеры могут быть **идентичными**, запущенными на основе одного и того же образа, но у каждого будут свои отдельные процесс, память и т.п. Таким образом мы получаем преимущества **распараллеливания** работы по **разным ядрам** процессора или даже **разным машинам**. + +Система управления контейнерами с **балансировщиком нагрузки** будет **распределять запросы** к контейнерам с приложениями **по очереди**. То есть каждый запрос будет обработан одним из множества **одинаковых контейнеров** с одним и тем же приложением. + +**Балансировщик нагрузки** может обрабатывать запросы к *разным* приложениям, расположенным в Вашем кластере (например, если у них разные домены или префиксы пути) и передавать запросы нужному контейнеру с требуемым приложением. + +### Один процесс на контейнер + +В этом варианте **в одном контейнере будет запущен только один процесс (Uvicorn)**, а управление изменением количества запущенных копий приложения происходит на уровне кластера. + +Здесь **не нужен** менеджер процессов типа Gunicorn, управляющий процессами Uvicorn, или же Uvicorn, управляющий другими процессами Uvicorn. Достаточно **только одного процесса Uvicorn** на контейнер (но запуск нескольких процессов не запрещён). + +Использование менеджера процессов (Gunicorn или Uvicorn) внутри контейнера только добавляет **излишнее усложнение**, так как управление следует осуществлять системой оркестрации. + +### Множество процессов внутри контейнера для особых случаев + +Безусловно, бывают **особые случаи**, когда может понадобится внутри контейнера запускать **менеджер процессов Gunicorn**, управляющий несколькими **процессами Uvicorn**. + +Для таких случаев Вы можете использовать **официальный Docker-образ** (прим. пер: - *здесь и далее на этой странице, если Вы встретите сочетание "официальный Docker-образ" без уточнений, то автор имеет в виду именно предоставляемый им образ*), где в качестве менеджера процессов используется **Gunicorn**, запускающий несколько **процессов Uvicorn** и некоторые настройки по умолчанию, автоматически устанавливающие количество запущенных процессов в зависимости от количества ядер Вашего процессора. Я расскажу Вам об этом подробнее тут: [Официальный Docker-образ со встроенными Gunicorn и Uvicorn](#docker-gunicorn-uvicorn). + +Некоторые примеры подобных случаев: + +#### Простое приложение + +Вы можете использовать менеджер процессов внутри контейнера, если Ваше приложение **настолько простое**, что у Вас нет необходимости (по крайней мере, пока нет) в тщательных настройках количества процессов и Вам достаточно имеющихся настроек по умолчанию (если используется официальный Docker-образ) для запуска приложения **только на одном сервере**, а не в кластере. + +#### Docker Compose + +С помощью **Docker Compose** можно разворачивать несколько контейнеров на **одном сервере** (не кластере), но при это у Вас не будет простого способа управления количеством запущенных контейнеров с одновременным сохранением общей сети и **балансировки нагрузки**. + +В этом случае можно использовать **менеджер процессов**, управляющий **несколькими процессами**, внутри **одного контейнера**. + +#### Prometheus и прочие причины + +У Вас могут быть и **другие причины**, когда использование **множества процессов** внутри **одного контейнера** будет проще, нежели запуск **нескольких контейнеров** с **единственным процессом** в каждом из них. + +Например (в зависимости от конфигурации), у Вас могут быть инструменты подобные экспортёру Prometheus, которые должны иметь доступ к **каждому запросу** приходящему в контейнер. + +Если у Вас будет **несколько контейнеров**, то Prometheus, по умолчанию, **при сборе метрик** получит их **только с одного контейнера**, который обрабатывает конкретный запрос, вместо **сбора метрик** со всех работающих контейнеров. + +В таком случае может быть проще иметь **один контейнер** со **множеством процессов**, с нужным инструментом (таким как экспортёр Prometheus) в этом же контейнере и собирающем метрики со всех внутренних процессов этого контейнера. + +--- + +Самое главное - **ни одно** из перечисленных правил не является **высеченным в камне** и Вы не обязаны слепо их повторять. Вы можете использовать эти идеи при **рассмотрении Вашего конкретного случая** и самостоятельно решать, какая из концепции подходит лучше: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +## Управление памятью + +При **запуске одного процесса на контейнер** Вы получаете относительно понятный, стабильный и ограниченный объём памяти, потребляемый одним контейнером. + +Вы можете установить аналогичные ограничения по памяти при конфигурировании своей системы управления контейнерами (например, **Kubernetes**). Таким образом система сможет **изменять количество контейнеров** на **доступных ей машинах** приводя в соответствие количество памяти нужной контейнерам с количеством памяти доступной в кластере (наборе доступных машин). + +Если у Вас **простенькое** приложение, вероятно у Вас не будет **необходимости** устанавливать жёсткие ограничения на выделяемую ему память. Но если приложение **использует много памяти** (например, оно использует модели **машинного обучения**), Вам следует проверить, как много памяти ему требуется и отрегулировать **количество контейнеров** запущенных на **каждой машине** (может быть даже добавить машин в кластер). + +Если Вы запускаете **несколько процессов в контейнере**, то должны быть уверены, что эти процессы не **займут памяти больше**, чем доступно для контейнера. + +## Подготовительные шаги при запуске контейнеров + +Есть два основных подхода, которые Вы можете использовать при запуске контейнеров (Docker, Kubernetes и т.п.). + +### Множество контейнеров + +Когда Вы запускаете **множество контейнеров**, в каждом из которых работает **только один процесс** (например, в кластере **Kubernetes**), может возникнуть необходимость иметь **отдельный контейнер**, который осуществит **предварительные шаги перед запуском** остальных контейнеров (например, применяет миграции к базе данных). + +!!! info "Информация" + При использовании Kubernetes, это может быть Инициализирующий контейнер. + +При отсутствии такой необходимости (допустим, не нужно применять миграции к базе данных, а только проверить, что она готова принимать соединения), Вы можете проводить такую проверку в каждом контейнере перед запуском его основного процесса и запускать все контейнеры **одновременно**. + +### Только один контейнер + +Если у Вас несложное приложение для работы которого достаточно **одного контейнера**, но в котором работает **несколько процессов** (или один процесс), то прохождение предварительных шагов можно осуществить в этом же контейнере до запуска основного процесса. Официальный Docker-образ поддерживает такие действия. + +## Официальный Docker-образ с Gunicorn и Uvicorn + +Я подготовил для Вас Docker-образ, в который включён Gunicorn управляющий процессами (воркерами) Uvicorn, в соответствии с концепциями рассмотренными в предыдущей главе: [Рабочие процессы сервера (воркеры) - Gunicorn совместно с Uvicorn](./server-workers.md){.internal-link target=_blank}. + +Этот образ может быть полезен для ситуаций описанных тут: [Множество процессов внутри контейнера для особых случаев](#special-cases). + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning "Предупреждение" + Скорее всего у Вас **нет необходимости** в использовании этого образа или подобного ему и лучше создать свой образ с нуля как описано тут: [Создать Docker-образ для FastAPI](#docker-fastapi). + +В этом образе есть **автоматический** механизм подстройки для запуска **необходимого количества процессов** в соответствии с доступным количеством ядер процессора. + +В нём установлены **разумные значения по умолчанию**, но можно изменять и обновлять конфигурацию с помощью **переменных окружения** или конфигурационных файлов. + +Он также поддерживает прохождение **Подготовительных шагов при запуске контейнеров** при помощи скрипта. + +!!! tip "Подсказка" + Для просмотра всех возможных настроек перейдите на страницу этого Docker-образа: tiangolo/uvicorn-gunicorn-fastapi. + +### Количество процессов в официальном Docker-образе + +**Количество процессов** в этом образе **вычисляется автоматически** и зависит от доступного количества **ядер** центрального процессора. + +Это означает, что он будет пытаться **выжать** из процессора как можно больше **производительности**. + +Но Вы можете изменять и обновлять конфигурацию с помощью **переменных окружения** и т.п. + +Поскольку количество процессов зависит от процессора, на котором работает контейнер, **объём потребляемой памяти** также будет зависеть от этого. + +А значит, если Вашему приложению требуется много оперативной памяти (например, оно использует модели машинного обучения) и Ваш сервер имеет центральный процессор с большим количеством ядер, но **не слишком большим объёмом оперативной памяти**, то может дойти до того, что контейнер попытается занять памяти больше, чем доступно, из-за чего будет падение производительности (или сервер вовсе упадёт). 🚨 + + +### Написание `Dockerfile` + +Итак, теперь мы можем написать `Dockerfile` основанный на этом официальном Docker-образе: + +```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 +``` + +### Большие приложения + +Если Вы успели ознакомиться с разделом [Приложения содержащие много файлов](../tutorial/bigger-applications.md){.internal-link target=_blank}, состоящие из множества файлов, Ваш Dockerfile может выглядеть так: + +```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 +``` + +### Как им пользоваться + +Если Вы используете **Kubernetes** (или что-то вроде того), скорее всего Вам **не нужно** использовать официальный Docker-образ (или другой похожий) в качестве основы, так как управление **количеством запущенных контейнеров** должно быть настроено на уровне кластера. В таком случае лучше **создать образ с нуля**, как описано в разделе Создать [Docker-образ для FastAPI](#docker-fastapi). + +Официальный образ может быть полезен в отдельных случаях, описанных выше в разделе [Множество процессов внутри контейнера для особых случаев](#special-cases). Например, если Ваше приложение **достаточно простое**, не требует запуска в кластере и способно уместиться в один контейнер, то его настройки по умолчанию будут работать довольно хорошо. Или же Вы развертываете его с помощью **Docker Compose**, работаете на одном сервере и т. д + +## Развёртывание образа контейнера + +После создания образа контейнера существует несколько способов его развёртывания. + +Например: + +* С использованием **Docker Compose** при развёртывании на одном сервере +* С использованием **Kubernetes** в кластере +* С использованием режима Docker Swarm в кластере +* С использованием других инструментов, таких как Nomad +* С использованием облачного сервиса, который будет управлять разворачиванием Вашего контейнера + +## Docker-образ и Poetry + +Если Вы пользуетесь Poetry для управления зависимостями Вашего проекта, то можете использовать многоэтапную сборку образа: + +```{ .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. Это первый этап, которому мы дадим имя `requirements-stage`. + +2. Установите директорию `/tmp` в качестве рабочей директории. + + В ней будет создан файл `requirements.txt` + +3. На этом шаге установите Poetry. + +4. Скопируйте файлы `pyproject.toml` и `poetry.lock` в директорию `/tmp`. + + Поскольку название файла написано как `./poetry.lock*` (с `*` в конце), то ничего не сломается, если такой файл не будет найден. + +5. Создайте файл `requirements.txt`. + +6. Это второй (и последний) этап сборки, который и создаст окончательный образ контейнера. + +7. Установите директорию `/code` в качестве рабочей. + +8. Скопируйте файл `requirements.txt` в директорию `/code`. + + Этот файл находится в образе, созданном на предыдущем этапе, которому мы дали имя requirements-stage, потому при копировании нужно написать `--from-requirements-stage`. + +9. Установите зависимости, указанные в файле `requirements.txt`. + +10. Скопируйте папку `app` в папку `/code`. + +11. Запустите `uvicorn`, указав ему использовать объект `app`, расположенный в `app.main`. + +!!! tip "Подсказка" + Если ткнёте на кружок с плюсом, то увидите объяснения, что происходит в этой строке. + +**Этапы сборки Docker-образа** являются частью `Dockerfile` и работают как **временные образы контейнеров**. Они нужны только для создания файлов, используемых в дальнейших этапах. + +Первый этап был нужен только для **установки Poetry** и **создания файла `requirements.txt`**, в которым прописаны зависимости Вашего проекта, взятые из файла `pyproject.toml`. + +На **следующем этапе** `pip` будет использовать файл `requirements.txt`. + +В итоговом образе будет содержаться **только последний этап сборки**, предыдущие этапы будут отброшены. + +При использовании Poetry, имеет смысл использовать **многоэтапную сборку Docker-образа**, потому что на самом деле Вам не нужен Poetry и его зависимости в окончательном образе контейнера, Вам **нужен только** сгенерированный файл `requirements.txt` для установки зависимостей Вашего проекта. + +А на последнем этапе, придерживаясь описанных ранее правил, создаётся итоговый образ + +### Использование прокси-сервера завершения TLS и Poetry + +И снова повторюсь, если используете прокси-сервер (балансировщик нагрузки), такой, как Nginx или Traefik, добавьте в команду запуска опцию `--proxy-headers`: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## Резюме + +При помощи систем контейнеризации (таких, как **Docker** и **Kubernetes**), становится довольно просто обрабатывать все **концепции развертывания**: + +* Использование более безопасного протокола HTTPS +* Настройки запуска приложения +* Перезагрузка приложения +* Запуск нескольких экземпляров приложения +* Управление памятью +* Использование перечисленных функций перед запуском приложения + +В большинстве случаев Вам, вероятно, не нужно использовать какой-либо базовый образ, **лучше создать образ контейнера с нуля** на основе официального Docker-образа Python. + +Позаботившись о **порядке написания** инструкций в `Dockerfile`, Вы сможете использовать **кэш Docker'а**, **минимизировав время сборки**, максимально повысив свою производительность (и избежать скуки). 😎 + +В некоторых особых случаях вы можете использовать официальный образ Docker для FastAPI. 🤓 From 5c2a155809aced43e6eae6b90ac08daaa2ca4e4f Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Aug 2023 15:55:32 +0000 Subject: [PATCH 1256/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8ee9d1af5..b40b68ba3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). * 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). * 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). From e6afc5911b98bfa2ad2240864929a3c3e43929a2 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Thu, 10 Aug 2023 18:58:13 +0300 Subject: [PATCH 1257/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/tutorial/cookie-params.md`=20(#10?= =?UTF-8?q?032)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/tutorial/cookie-params.md | 96 ++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/uk/docs/tutorial/cookie-params.md diff --git a/docs/uk/docs/tutorial/cookie-params.md b/docs/uk/docs/tutorial/cookie-params.md new file mode 100644 index 000000000..2b0e8993c --- /dev/null +++ b/docs/uk/docs/tutorial/cookie-params.md @@ -0,0 +1,96 @@ +# Параметри Cookie + +Ви можете визначити параметри Cookie таким же чином, як визначаються параметри `Query` і `Path`. + +## Імпорт `Cookie` + +Спочатку імпортуйте `Cookie`: + +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="3" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +## Визначення параметрів `Cookie` + +Потім визначте параметри cookie, використовуючи таку ж конструкцію як для `Path` і `Query`. + +Перше значення це значення за замовчуванням, ви можете також передати всі додаткові параметри валідації чи анотації: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/cookie_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="7" + {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="9" + {!> ../../../docs_src/cookie_params/tutorial001.py!} + ``` + +!!! note "Технічні Деталі" + `Cookie` це "сестра" класів `Path` і `Query`. Вони наслідуються від одного батьківського класу `Param`. + Але пам'ятайте, що коли ви імпортуєте `Query`, `Path`, `Cookie` та інше з `fastapi`, це фактично функції, що повертають спеціальні класи. + +!!! info + Для визначення cookies ви маєте використовувати `Cookie`, тому що в іншому випадку параметри будуть інтерпритовані, як параметри запиту. + +## Підсумки + +Визначайте cookies за допомогою `Cookie`, використовуючи той же спільний шаблон, що і `Query` та `Path`. From 78f38c6bfda67b16d1f84ad6981ffb90a2936679 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 10 Aug 2023 15:59:15 +0000 Subject: [PATCH 1258/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b40b68ba3..868628ae8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). * 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). * 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). From 47166ed56c4b431bf9449b83ebfbb96ad10dd230 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Mon, 14 Aug 2023 12:10:06 +0300 Subject: [PATCH 1259/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/fastapi-people.md`=20(#10059)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/fastapi-people.md | 178 +++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/uk/docs/fastapi-people.md diff --git a/docs/uk/docs/fastapi-people.md b/docs/uk/docs/fastapi-people.md new file mode 100644 index 000000000..b32f0e5ce --- /dev/null +++ b/docs/uk/docs/fastapi-people.md @@ -0,0 +1,178 @@ +# Люди FastAPI + +FastAPI має дивовижну спільноту, яка вітає людей різного походження. + +## Творець – Супроводжувач + +Привіт! 👋 + +Це я: + +{% if people %} +
+{% for user in people.maintainers %} + +
@{{ user.login }}
Answers: {{ user.answers }}
Pull Requests: {{ user.prs }}
+{% endfor %} + +
+{% endif %} + +Я - творець і супроводжувач **FastAPI**. Детальніше про це можна прочитати в [Довідка FastAPI - Отримати довідку - Зв'язатися з автором](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. + +...Але тут я хочу показати вам спільноту. + +--- + +**FastAPI** отримує велику підтримку від спільноти. І я хочу відзначити їхній внесок. + +Це люди, які: + +* [Допомагають іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. +* [Створюють пул реквести](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* Переглядають пул реквести, [особливо важливо для перекладів](contributing.md#translations){.internal-link target=_blank}. + +Оплески їм. 👏 🙇 + +## Найбільш активні користувачі минулого місяця + +Це користувачі, які [найбільше допомагали іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} протягом минулого місяця. ☕ + +{% if people %} +
+{% for user in people.last_month_active %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Експерти + +Ось **експерти FastAPI**. 🤓 + +Це користувачі, які [найбільше допомагали іншим із проблемами (запитаннями) у GitHub](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} протягом *всього часу*. + +Вони зарекомендували себе як експерти, допомагаючи багатьом іншим. ✨ + +{% if people %} +
+{% for user in people.experts %} + +
@{{ user.login }}
Issues replied: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Найкращі контрибютори + +Ось **Найкращі контрибютори**. 👷 + +Ці користувачі [створили найбільшу кількість пул реквестів](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} які були *змержені*. + +Вони надали програмний код, документацію, переклади тощо. 📦 + +{% if people %} +
+{% for user in people.top_contributors %} + +
@{{ user.login }}
Pull Requests: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +Є багато інших контрибюторів (більше сотні), їх усіх можна побачити на сторінці FastAPI GitHub Contributors. 👷 + +## Найкращі рецензенти + +Ці користувачі є **Найкращими рецензентами**. 🕵️ + +### Рецензенти на переклади + +Я розмовляю лише кількома мовами (і не дуже добре 😅). Отже, рецензенти – це ті, хто має [**повноваження схвалювати переклади**](contributing.md#translations){.internal-link target=_blank} документації. Без них не було б документації кількома іншими мовами. + +--- + +**Найкращі рецензенти** 🕵️ переглянули більшість пул реквестів від інших, забезпечуючи якість коду, документації і особливо **перекладів**. + +{% if people %} +
+{% for user in people.top_reviewers %} + +
@{{ user.login }}
Reviews: {{ user.count }}
+{% endfor %} + +
+{% endif %} + +## Спонсори + +Це **Спонсори**. 😎 + +Вони підтримують мою роботу з **FastAPI** (та іншими), переважно через GitHub Sponsors. + +{% if sponsors %} + +{% if sponsors.gold %} + +### Золоті спонсори + +{% for sponsor in sponsors.gold -%} + +{% endfor %} +{% endif %} + +{% if sponsors.silver %} + +### Срібні спонсори + +{% for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + +{% if sponsors.bronze %} + +### Бронзові спонсори + +{% for sponsor in sponsors.bronze -%} + +{% endfor %} +{% endif %} + +{% endif %} + +### Індивідуальні спонсори + +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} + +
+ +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} + + + +{% endif %} +{% endfor %} + +
+ +{% endfor %} +{% endif %} + +## Про дані - технічні деталі + +Основна мета цієї сторінки – висвітлити зусилля спільноти, щоб допомогти іншим. + +Особливо враховуючи зусилля, які зазвичай менш помітні, а в багатьох випадках більш важкі, як-от допомога іншим із проблемами та перегляд пул реквестів перекладів. + +Дані розраховуються щомісяця, ви можете ознайомитися з вихідним кодом тут. + +Тут я також підкреслюю внески спонсорів. + +Я також залишаю за собою право оновлювати алгоритми підрахунку, види рейтингів, порогові значення тощо (про всяк випадок 🤷). From 48d203a1e7936e3af4b243dba0b3588cba9fbd06 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 14 Aug 2023 09:10:51 +0000 Subject: [PATCH 1260/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 868628ae8..9fe64b1c8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/fastapi-people.md`. PR [#10059](https://github.com/tiangolo/fastapi/pull/10059) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). * 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). From 9d6ce823c1d498a00df8531b6c44da1200258f55 Mon Sep 17 00:00:00 2001 From: Yusuke Tamura <62091034+tamtam-fitness@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:12:14 +0900 Subject: [PATCH 1261/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Japanese=20tr?= =?UTF-8?q?anslation=20for=20`docs/ja/docs/deployment/docker.md`=20(#10073?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ja/docs/deployment/docker.md | 704 ++++++++++++++++++++++++++---- 1 file changed, 620 insertions(+), 84 deletions(-) diff --git a/docs/ja/docs/deployment/docker.md b/docs/ja/docs/deployment/docker.md index f10312b51..ca9dedc3c 100644 --- a/docs/ja/docs/deployment/docker.md +++ b/docs/ja/docs/deployment/docker.md @@ -1,71 +1,157 @@ -# Dockerを使用したデプロイ +# コンテナ内のFastAPI - Docker -このセクションでは以下の使い方の紹介とガイドへのリンクが確認できます: +FastAPIアプリケーションをデプロイする場合、一般的なアプローチは**Linuxコンテナ・イメージ**をビルドすることです。 -* **5分**程度で、**FastAPI** のアプリケーションを、パフォーマンスを最大限に発揮するDockerイメージ (コンテナ)にする。 -* (オプション) 開発者として必要な範囲でHTTPSを理解する。 -* **20分**程度で、自動的なHTTPS生成とともにDockerのSwarmモード クラスタをセットアップする (月5ドルのシンプルなサーバー上で)。 -* **10分**程度で、DockerのSwarmモード クラスタを使って、HTTPSなどを使用した完全な**FastAPI** アプリケーションの作成とデプロイ。 +基本的には **Docker**を用いて行われます。生成されたコンテナ・イメージは、いくつかの方法のいずれかでデプロイできます。 -デプロイのために、**Docker** を利用できます。セキュリティ、再現性、開発のシンプルさなどに利点があります。 +Linuxコンテナの使用には、**セキュリティ**、**反復可能性(レプリカビリティ)**、**シンプリシティ**など、いくつかの利点があります。 -Dockerを使う場合、公式のDockerイメージが利用できます: +!!! tip + TODO: なぜか遷移できない + お急ぎで、すでにこれらの情報をご存じですか? [以下の`Dockerfile`の箇所👇](#build-a-docker-image-for-fastapi)へジャンプしてください。 -## tiangolo/uvicorn-gunicorn-fastapi - -このイメージは「自動チューニング」機構を含んでいます。犠牲を払うことなく、ただコードを加えるだけで自動的に高パフォーマンスを実現できます。 - -ただし、環境変数や設定ファイルを使って全ての設定の変更や更新を行えます。 - -!!! tip "豆知識" - 全ての設定とオプションを確認するには、Dockerイメージページを開いて下さい: tiangolo/uvicorn-gunicorn-fastapi. - -## `Dockerfile` の作成 - -* プロジェクトディレクトリへ移動。 -* 以下の`Dockerfile` を作成: +
+Dockerfile プレビュー 👀 ```Dockerfile -FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7 +FROM python:3.9 -COPY ./app /app -``` +WORKDIR /code -### より大きなアプリケーション +COPY ./requirements.txt /code/requirements.txt -[Bigger Applications with Multiple Files](tutorial/bigger-applications.md){.internal-link target=_blank} セクションに倣う場合は、`Dockerfile` は上記の代わりに、以下の様になるかもしれません: +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などのアーキテクチャ - -Raspberry Pi (ARMプロセッサ搭載)やそれ以外のアーキテクチャでDockerが作動している場合、(マルチアーキテクチャである) Pythonベースイメージを使って、一から`Dockerfile`を作成し、Uvicornを単体で使用できます。 - -この場合、`Dockerfile` は以下の様になるかもしれません: - -```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"] ``` -## **FastAPI** コードの作成 +
-* `app` ディレクトリを作成し、移動。 -* 以下の`main.py` ファイルを作成: +## コンテナとは何か + +コンテナ(主にLinuxコンテナ)は、同じシステム内の他のコンテナ(他のアプリケーションやコンポーネント)から隔離された状態を保ちながら、すべての依存関係や必要なファイルを含むアプリケーションをパッケージ化する非常に**軽量**な方法です。 + +Linuxコンテナは、ホスト(マシン、仮想マシン、クラウドサーバーなど)の同じLinuxカーネルを使用して実行されます。これは、(OS全体をエミュレートする完全な仮想マシンと比べて)非常に軽量であることを意味します。 + +このように、コンテナは**リソースをほとんど消費しません**が、プロセスを直接実行するのに匹敵する量です(仮想マシンはもっと消費します)。 + +コンテナはまた、独自の**分離された**実行プロセス(通常は1つのプロセスのみ)や、ファイルシステム、ネットワークを持ちます。 このことはデプロイ、セキュリティ、開発などを簡素化させます。 + +## コンテナ・イメージとは何か + +**コンテナ**は、**コンテナ・イメージ**から実行されます。 + +コンテナ・イメージは、コンテナ内に存在すべきすべてのファイルや環境変数、そしてデフォルトのコマンド/プログラムを**静的に**バージョン化したものです。 ここでの**静的**とは、コンテナ**イメージ**は実行されておらず、パッケージ化されたファイルとメタデータのみであることを意味します。 + +保存された静的コンテンツである「**コンテナイメージ**」とは対照的に、「**コンテナ**」は通常、実行中のインスタンス、つまり**実行**されているものを指します。 + +**コンテナ**が起動され実行されるとき(**コンテナイメージ**から起動されるとき)、ファイルや環境変数などが作成されたり変更されたりする可能性があります。 + +これらの変更はそのコンテナ内にのみ存在しますが、基盤となるコンテナ・イメージには残りません(ディスクに保存されません)。 + +コンテナイメージは **プログラム** ファイルやその内容、例えば `python` と `main.py` ファイルに匹敵します。 + +そして、**コンテナ**自体は(**コンテナイメージ**とは対照的に)イメージをもとにした実際の実行中のインスタンスであり、**プロセス**に匹敵します。 + +実際、コンテナが実行されているのは、**プロセスが実行されている**ときだけです(通常は単一のプロセスだけです)。 コンテナ内で実行中のプロセスがない場合、コンテナは停止します。 + +## コンテナ・イメージ + +Dockerは、**コンテナ・イメージ**と**コンテナ**を作成・管理するための主要なツールの1つです。 + +そして、DockerにはDockerイメージ(コンテナ)を共有するDocker Hubというものがあります。 + +Docker Hubは 多くのツールや環境、データベース、アプリケーションに対応している予め作成された**公式のコンテナ・イメージ**をパブリックに提供しています。 + +例えば、公式イメージの1つにPython Imageがあります。 + +その他にも、データベースなどさまざまなイメージがあります: + +* PostgreSQL +* MySQL +* MongoDB +* Redis, etc. + +予め作成されたコンテナ・イメージを使用することで、異なるツールを**組み合わせて**使用することが非常に簡単になります。例えば、新しいデータベースを試す場合に特に便利です。ほとんどの場合、**公式イメージ**を使い、環境変数で設定するだけで良いです。 + +そうすれば多くの場合、コンテナとDockerについて学び、その知識をさまざまなツールやコンポーネントによって再利用することができます。 + +つまり、データベース、Pythonアプリケーション、Reactフロントエンド・アプリケーションを備えたウェブ・サーバーなど、さまざまなものを**複数のコンテナ**で実行し、それらを内部ネットワーク経由で接続します。 + +すべてのコンテナ管理システム(DockerやKubernetesなど)には、こうしたネットワーキング機能が統合されています。 + +## コンテナとプロセス + +通常、**コンテナ・イメージ**はそのメタデータに**コンテナ**の起動時に実行されるデフォルトのプログラムまたはコマンドと、そのプログラムに渡されるパラメータを含みます。コマンドラインでの操作とよく似ています。 + +**コンテナ**が起動されると、そのコマンド/プログラムが実行されます(ただし、別のコマンド/プログラムをオーバーライドして実行させることもできます)。 + +コンテナは、**メイン・プロセス**(コマンドまたはプログラム)が実行されている限り実行されます。 + +コンテナは通常**1つのプロセス**を持ちますが、メイン・プロセスからサブ・プロセスを起動することも可能で、そうすれば同じコンテナ内に**複数のプロセス**を持つことになります。 + +しかし、**少なくとも1つの実行中のプロセス**がなければ、実行中のコンテナを持つことはできないです。メイン・プロセスが停止すれば、コンテナも停止します。 + +## Build a Docker Image for FastAPI + +ということで、何か作りましょう!🚀 + +FastAPI用の**Dockerイメージ**を、**公式Python**イメージに基づいて**ゼロから**ビルドする方法をお見せします。 + +これは**ほとんどの場合**にやりたいことです。例えば: + +* **Kubernetes**または同様のツールを使用する場合 +* **Raspberry Pi**で実行する場合 +* コンテナ・イメージを実行してくれるクラウド・サービスなどを利用する場合 + +### パッケージ要件(package requirements) + +アプリケーションの**パッケージ要件**は通常、何らかのファイルに記述されているはずです。 + +パッケージ要件は主に**インストール**するために使用するツールに依存するでしょう。 + +最も一般的な方法は、`requirements.txt` ファイルにパッケージ名とそのバージョンを 1 行ずつ書くことです。 + +もちろん、[FastAPI バージョンについて](./versions.md){.internal-link target=_blank}で読んだのと同じアイデアを使用して、バージョンの範囲を設定します。 + +例えば、`requirements.txt` は次のようになります: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +そして通常、例えば `pip` を使ってこれらのパッケージの依存関係をインストールします: + +
+ +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +
+ +!!! info + パッケージの依存関係を定義しインストールするためのフォーマットやツールは他にもあります。 + + Poetryを使った例は、後述するセクションでご紹介します。👇 + +### **FastAPI**コードを作成する + +* `app` ディレクトリを作成し、その中に入ります +* 空のファイル `__init__.py` を作成します +* `main.py` ファイルを作成します: ```Python -from typing import Optional +from typing import Union from fastapi import FastAPI @@ -78,23 +164,136 @@ def read_root(): @app.get("/items/{item_id}") -def read_item(item_id: int, q: Optional[str] = None): +def read_item(item_id: int, q: Union[str, None] = None): return {"item_id": item_id, "q": q} ``` -* ここでは、以下の様なディレクトリ構造になっているはずです: +### Dockerfile + +同じプロジェクト・ディレクトリに`Dockerfile`というファイルを作成します: + +```{ .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. 公式のPythonベースイメージから始めます + +2. 現在の作業ディレクトリを `/code` に設定します + + ここに `requirements.txt` ファイルと `app` ディレクトリを置きます。 + +3. 要件が書かれたファイルを `/code` ディレクトリにコピーします + + 残りのコードではなく、最初に必要なファイルだけをコピーしてください。 + + このファイルは**頻繁には変更されない**ので、Dockerはこのステップではそれを検知し**キャッシュ**を使用し、次のステップでもキャッシュを有効にします。 + +4. 要件ファイルにあるパッケージの依存関係をインストールします + `--no-cache-dir` オプションはダウンロードしたパッケージをローカルに保存しないように `pip` に指示します。これは、同じパッケージをインストールするために `pip` を再度実行する場合にのみ有効ですが、コンテナで作業する場合はそうではないです。 + + !!! note + `--no-cache-dir`は`pip`に関連しているだけで、Dockerやコンテナとは何の関係もないです。 + + `--upgrade` オプションは、パッケージが既にインストールされている場合、`pip` にアップグレードするように指示します。 + + 何故ならファイルをコピーする前のステップは**Dockerキャッシュ**によって検出される可能性があるためであり、このステップも利用可能な場合は**Dockerキャッシュ**を使用します。 + + このステップでキャッシュを使用すると、開発中にイメージを何度もビルドする際に、**毎回**すべての依存関係を**ダウンロードしてインストールする**代わりに多くの**時間**を**節約**できます。 + +5. ./app` ディレクトリを `/code` ディレクトリの中にコピーする。 + + これには**最も頻繁に変更される**すべてのコードが含まれているため、Dockerの**キャッシュ**は**これ以降のステップ**に簡単に使用されることはありません。 + + そのため、コンテナイメージのビルド時間を最適化するために、`Dockerfile`の **最後** にこれを置くことが重要です。 + +6. `uvicorn`サーバーを実行するための**コマンド**を設定します + + `CMD` は文字列のリストを取り、それぞれの文字列はスペースで区切られたコマンドラインに入力するものです。 + + このコマンドは **現在の作業ディレクトリ**から実行され、上記の `WORKDIR /code` にて設定した `/code` ディレクトリと同じです。 + + そのためプログラムは `/code` で開始しその中にあなたのコードがある `./app` ディレクトリがあるので、**Uvicorn** は `app.main` から `app` を参照し、**インポート** することができます。 + +!!! tip + コード内の"+"の吹き出しをクリックして、各行が何をするのかをレビューしてください。👆 + +これで、次のようなディレクトリ構造になるはずです: ``` . ├── app +│   ├── __init__.py │ └── main.py -└── Dockerfile +├── Dockerfile +└── requirements.txt ``` -## Dockerイメージをビルド +#### TLS Termination Proxyの裏側 -* プロジェクトディレクトリ (`app` ディレクトリを含んだ、`Dockerfile` のある場所) へ移動 -* FastAPIイメージのビルド: +Nginx や Traefik のような TLS Termination Proxy (ロードバランサ) の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションを追加します。 + +このオプションは、Uvicornにプロキシ経由でHTTPSで動作しているアプリケーションに対して、送信されるヘッダを信頼するよう指示します。 + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Dockerキャッシュ + +この`Dockerfile`には重要なトリックがあり、まず**依存関係だけのファイル**をコピーします。その理由を説明します。 + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Dockerや他のツールは、これらのコンテナイメージを**段階的に**ビルドし、**1つのレイヤーを他のレイヤーの上に**追加します。`Dockerfile`の先頭から開始し、`Dockerfile`の各命令によって作成されたファイルを追加していきます。 + +Dockerや同様のツールは、イメージをビルドする際に**内部キャッシュ**も使用します。前回コンテナイメージを構築したときからファイルが変更されていない場合、ファイルを再度コピーしてゼロから新しいレイヤーを作成する代わりに、**前回作成した同じレイヤーを再利用**します。 + +ただファイルのコピーを避けるだけではあまり改善されませんが、そのステップでキャッシュを利用したため、**次のステップ**でキャッシュを使うことができます。 + +例えば、依存関係をインストールする命令のためにキャッシュを使うことができます: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +パッケージ要件のファイルは**頻繁に変更されることはありません**。そのため、そのファイルだけをコピーすることで、Dockerはそのステップでは**キャッシュ**を使用することができます。 + +そして、Dockerは**次のステップのためにキャッシュ**を使用し、それらの依存関係をダウンロードしてインストールすることができます。そして、ここで**多くの時間を節約**します。✨ ...そして退屈な待ち時間を避けることができます。😪😆 + +パッケージの依存関係をダウンロードしてインストールするには**数分**かかりますが、**キャッシュ**を使えば**せいぜい数秒**です。 + +加えて、開発中にコンテナ・イメージを何度もビルドして、コードの変更が機能しているかどうかをチェックすることになるため、多くの時間を節約することができます。 + +そして`Dockerfile`の最終行の近くですべてのコードをコピーします。この理由は、**最も頻繁に**変更されるものなので、このステップの後にあるものはほとんどキャッシュを使用することができないのためです。 + +```Dockerfile +COPY ./app /code/app +``` + +### Dockerイメージをビルドする + +すべてのファイルが揃ったので、コンテナ・イメージをビルドしましょう。 + +* プロジェクトディレクトリに移動します(`Dockerfile`がある場所で、`app`ディレクトリがあります) +* FastAPI イメージをビルドします:
@@ -106,9 +305,14 @@ $ docker build -t myimage .
-## Dockerコンテナを起動 +!!! tip + 末尾の `.` に注目してほしいです。これは `./` と同じ意味です。 これはDockerにコンテナイメージのビルドに使用するディレクトリを指示します。 -* 用意したイメージを基にしたコンテナの起動: + この場合、同じカレント・ディレクトリ(`.`)です。 + +### Dockerコンテナの起動する + +* イメージに基づいてコンテナを実行します:
@@ -118,62 +322,394 @@ $ docker run -d --name mycontainer -p 80:80 myimage
-これで、Dockerコンテナ内に最適化されたFastAPIサーバが動作しています。使用しているサーバ (そしてCPUコア数) に沿った自動チューニングが行われています。 +## 確認する -## 確認 +Dockerコンテナのhttp://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (またはそれに相当するDockerホストを使用したもの)といったURLで確認できるはずです。 -DockerコンテナのURLで確認できるはずです。例えば: http://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery (もしくはDockerホストを使用したこれらと同等のもの)。 - -以下の様なものが返されます: +アクセスすると以下のようなものが表示されます: ```JSON {"item_id": 5, "q": "somequery"} ``` -## 対話的APIドキュメント +## インタラクティブなAPIドキュメント -ここで、http://192.168.99.100/docshttp://127.0.0.1/docs (もしくはDockerホストを使用したこれらと同等のもの) を開いて下さい。 +これらのURLにもアクセスできます: http://192.168.99.100/docshttp://127.0.0.1/docs (またはそれに相当するDockerホストを使用したもの) -自動生成された対話的APIドキュメントが確認できます (Swagger UIによって提供されます): +アクセスすると、自動対話型APIドキュメント(Swagger UIが提供)が表示されます: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -## その他のAPIドキュメント +## 代替のAPIドキュメント -また同様に、http://192.168.99.100/redochttp://127.0.0.1/redoc (もしくはDockerホストを使用したこれらと同等のもの) を開いて下さい。 +また、http://192.168.99.100/redochttp://127.0.0.1/redoc (またはそれに相当するDockerホストを使用したもの)にもアクセスできます。 -他の自動生成された対話的なAPIドキュメントが確認できます (ReDocによって提供されます): +代替の自動ドキュメント(ReDocによって提供される)が表示されます: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Traefik +## 単一ファイルのFastAPIでDockerイメージをビルドする -Traefikは、高性能なリバースプロキシ/ロードバランサーです。「TLSターミネーションプロキシ」ジョブを実行できます(他の機能と切り離して)。 +FastAPI が単一のファイル、例えば `./app` ディレクトリのない `main.py` の場合、ファイル構造は次のようになります: +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` -Let's Encryptと統合されています。そのため、証明書の取得と更新を含むHTTPSに関するすべての処理を実行できます。 +そうすれば、`Dockerfile`の中にファイルをコピーするために、対応するパスを変更するだけでよいです: -また、Dockerとも統合されています。したがって、各アプリケーション構成でドメインを宣言し、それらの構成を読み取って、HTTPS証明書を生成し、構成に変更を加えることなく、アプリケーションにHTTPSを自動的に提供できます。 +```{ .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. main.py`ファイルを `/code` ディレクトリに直接コピーします。 + +2. Uvicornを実行し、`main`から`app`オブジェクトをインポートするように指示します(`app.main`からインポートするのではなく)。 + +次にUvicornコマンドを調整して、`app.main` の代わりに新しいモジュール `main` を使用し、FastAPIオブジェクトである `app` をインポートします。 + +## デプロイメントのコンセプト + +コンテナという観点から、[デプロイのコンセプト](./concepts.md){.internal-link target=_blank}に共通するいくつかについて、もう一度説明しましょう。 + +コンテナは主に、アプリケーションの**ビルドとデプロイ**のプロセスを簡素化するためのツールですが、これらの**デプロイのコンセプト**を扱うための特定のアプローチを強制するものではないです。 + +**良いニュース**は、それぞれの異なる戦略には、すべてのデプロイメントのコンセプトをカバーする方法があるということです。🎉 + +これらの**デプロイメントのコンセプト**をコンテナの観点から見直してみましょう: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +## HTTPS + +FastAPI アプリケーションの **コンテナ・イメージ**(および後で実行中の **コンテナ**)だけに焦点を当てると、通常、HTTPSは別のツールを用いて**外部で**処理されます。 + +例えばTraefikのように、**HTTPS**と**証明書**の**自動**取得を扱う別のコンテナである可能性もあります。 + +!!! tip + TraefikはDockerやKubernetesなどと統合されているので、コンテナ用のHTTPSの設定や構成はとても簡単です。 + +あるいは、(コンテナ内でアプリケーションを実行しながら)クラウド・プロバイダーがサービスの1つとしてHTTPSを処理することもできます。 + +## 起動時および再起動時の実行 + +通常、コンテナの**起動と実行**を担当する別のツールがあります。 + +それは直接**Docker**であったり、**Docker Compose**であったり、**Kubernetes**であったり、**クラウドサービス**であったりします。 + +ほとんどの場合(またはすべての場合)、起動時にコンテナを実行し、失敗時に再起動を有効にする簡単なオプションがあります。例えばDockerでは、コマンドラインオプションの`--restart`が該当します。 + +コンテナを使わなければ、アプリケーションを起動時や再起動時に実行させるのは面倒で難しいかもしれません。しかし、**コンテナ**で作業する場合、ほとんどのケースでその機能はデフォルトで含まれています。✨ + +## レプリケーション - プロセス数 + +**Kubernetes** や Docker Swarm モード、Nomad、あるいは複数のマシン上で分散コンテナを管理するための同様の複雑なシステムを使ってマシンのクラスターを構成している場合、 各コンテナで(Workerを持つGunicornのような)**プロセスマネージャ**を使用する代わりに、**クラスター・レベル**で**レプリケーション**を処理したいと思うでしょう。 + +Kubernetesのような分散コンテナ管理システムの1つは通常、入ってくるリクエストの**ロードバランシング**をサポートしながら、**コンテナのレプリケーション**を処理する統合された方法を持っています。このことはすべて**クラスタレベル**にてです。 + +そのような場合、UvicornワーカーでGunicornのようなものを実行するのではなく、[上記の説明](#dockerfile)のように**Dockerイメージをゼロから**ビルドし、依存関係をインストールして、**単一のUvicornプロセス**を実行したいでしょう。 + +### ロードバランサー + +コンテナを使用する場合、通常はメイン・ポート**でリスニング**しているコンポーネントがあるはずです。それはおそらく、**HTTPS**を処理するための**TLS Termination Proxy**でもある別のコンテナであったり、同様のツールであったりするでしょう。 + +このコンポーネントはリクエストの **負荷** を受け、 (うまくいけば) その負荷を**バランスよく** ワーカーに分配するので、一般に **ロードバランサ** とも呼ばれます。 + +!!! tip +  HTTPSに使われるものと同じ**TLS Termination Proxy**コンポーネントは、おそらく**ロードバランサー**にもなるでしょう。 + +そしてコンテナで作業する場合、コンテナの起動と管理に使用する同じシステムには、**ロードバランサー**(**TLS Termination Proxy**の可能性もある)から**ネットワーク通信**(HTTPリクエストなど)をアプリのあるコンテナ(複数可)に送信するための内部ツールが既にあるはずです。 + +### 1つのロードバランサー - 複数のワーカーコンテナー + +**Kubernetes**や同様の分散コンテナ管理システムで作業する場合、その内部のネットワーキングのメカニズムを使用することで、メインの**ポート**でリッスンしている単一の**ロードバランサー**が、アプリを実行している可能性のある**複数のコンテナ**に通信(リクエスト)を送信できるようになります。 + +アプリを実行するこれらのコンテナには、通常**1つのプロセス**(たとえば、FastAPIアプリケーションを実行するUvicornプロセス)があります。これらはすべて**同一のコンテナ**であり同じものを実行しますが、それぞれが独自のプロセスやメモリなどを持ちます。そうすることで、CPUの**異なるコア**、あるいは**異なるマシン**での**並列化**を利用できます。 + +そして、**ロードバランサー**を備えた分散コンテナシステムは、**順番に**あなたのアプリを含む各コンテナに**リクエストを分配**します。つまり、各リクエストは、あなたのアプリを実行している複数の**レプリケートされたコンテナ**の1つによって処理されます。 + +そして通常、この**ロードバランサー**は、クラスタ内の*他の*アプリケーション(例えば、異なるドメインや異なるURLパスのプレフィックスの配下)へのリクエストを処理することができ、その通信をクラスタ内で実行されている*他の*アプリケーションのための適切なコンテナに送信します。 + +### 1コンテナにつき1プロセス + +この種のシナリオでは、すでにクラスタ・レベルでレプリケーションを処理しているため、おそらくコンテナごとに**単一の(Uvicorn)プロセス**を持ちたいでしょう。 + +この場合、Uvicornワーカーを持つGunicornのようなプロセスマネージャーや、Uvicornワーカーを使うUvicornは**避けたい**でしょう。**コンテナごとにUvicornのプロセスは1つだけ**にしたいでしょう(おそらく複数のコンテナが必要でしょう)。 + +(GunicornやUvicornがUvicornワーカーを管理するように)コンテナ内に別のプロセスマネージャーを持つことは、クラスターシステムですでに対処しているであろう**不要な複雑さ**を追加するだけです。 + +### Containers with Multiple Processes and Special Cases + +もちろん、**特殊なケース**として、**Gunicornプロセスマネージャ**を持つ**コンテナ**内で複数の**Uvicornワーカープロセス**を起動させたい場合があります。 + +このような場合、**公式のDockerイメージ**を使用することができます。このイメージには、複数の**Uvicornワーカープロセス**を実行するプロセスマネージャとして**Gunicorn**が含まれており、現在のCPUコアに基づいてワーカーの数を自動的に調整するためのデフォルト設定がいくつか含まれています。詳しくは後述の[Gunicornによる公式Dockerイメージ - Uvicorn](#gunicornによる公式dockerイメージ---Uvicorn)で説明します。 + +以下は、それが理にかなっている場合の例です: + +#### シンプルなアプリケーション + +アプリケーションを**シンプル**な形で実行する場合、プロセス数の細かい調整が必要ない場合、自動化されたデフォルトを使用するだけで、コンテナ内にプロセスマネージャが必要かもしれません。例えば、公式Dockerイメージでシンプルな設定が可能です。 + +#### Docker Compose + +Docker Composeで**シングルサーバ**(クラスタではない)にデプロイすることもできますので、共有ネットワークと**ロードバランシング**を維持しながら(Docker Composeで)コンテナのレプリケーションを管理する簡単な方法はないでしょう。 + +その場合、**単一のコンテナ**で、**プロセスマネージャ**が内部で**複数のワーカープロセス**を起動するようにします。 + +#### Prometheusとその他の理由 + +また、**1つのコンテナ**に**1つのプロセス**を持たせるのではなく、**1つのコンテナ**に**複数のプロセス**を持たせる方が簡単だという**他の理由**もあるでしょう。 + +例えば、(セットアップにもよりますが)Prometheusエクスポーターのようなツールを同じコンテナ内に持つことができます。 + +この場合、**複数のコンテナ**があると、デフォルトでは、Prometheusが**メトリクスを**読みに来たとき、すべてのレプリケートされたコンテナの**蓄積されたメトリクス**を取得するのではなく、毎回**単一のコンテナ**(その特定のリクエストを処理したコンテナ)のものを取得することになります。 + +その場合、**複数のプロセス**を持つ**1つのコンテナ**を用意し、同じコンテナ上のローカルツール(例えばPrometheusエクスポーター)がすべての内部プロセスのPrometheusメトリクスを収集し、その1つのコンテナ上でそれらのメトリクスを公開する方がシンプルかもしれません。 --- -次のセクションに進み、この情報とツールを使用して、すべてを組み合わせます。 +重要なのは、盲目的に従わなければならない普遍のルールはないということです。 -## TraefikとHTTPSを使用したDocker Swarmモードのクラスタ +これらのアイデアは、**あなた自身のユースケース**を評価し、あなたのシステムに最適なアプローチを決定するために使用することができます: -HTTPSを処理する(証明書の取得と更新を含む)Traefikを使用して、Docker Swarmモードのクラスタを数分(20分程度)でセットアップできます。 +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ -Docker Swarmモードを使用することで、1台のマシンの「クラスタ」から開始でき(1か月あたり5ドルのサーバーでもできます)、後から必要なだけサーバーを拡張できます。 +## メモリー -TraefikおよびHTTPS処理を備えたDocker Swarm Modeクラスターをセットアップするには、次のガイドに従います: +コンテナごとに**単一のプロセスを実行する**と、それらのコンテナ(レプリケートされている場合は1つ以上)によって消費される多かれ少なかれ明確に定義された、安定し制限された量のメモリを持つことになります。 -### Docker Swarm Mode and Traefik for an HTTPS cluster +そして、コンテナ管理システム(**Kubernetes**など)の設定で、同じメモリ制限と要件を設定することができます。 -### FastAPIアプリケーションのデプロイ +そうすれば、コンテナが必要とするメモリ量とクラスタ内のマシンで利用可能なメモリ量を考慮して、**利用可能なマシン**に**コンテナ**をレプリケートできるようになります。 -すべてを設定するための最も簡単な方法は、[**FastAPI** Project Generators](../project-generation.md){.internal-link target=_blank}を使用することでしょう。 +アプリケーションが**シンプル**なものであれば、これはおそらく**問題にはならない**でしょうし、ハードなメモリ制限を指定する必要はないかもしれないです。 -上述したTraefikとHTTPSを備えたDocker Swarm クラスタが統合されるように設計されています。 +しかし、**多くのメモリを使用**している場合(たとえば**機械学習**モデルなど)、どれだけのメモリを消費しているかを確認し、**各マシンで実行するコンテナの数**を調整する必要があります(そしておそらくクラスタにマシンを追加します)。 -2分程度でプロジェクトが生成されます。 +**コンテナごとに複数のプロセス**を実行する場合(たとえば公式のDockerイメージで)、起動するプロセスの数が**利用可能なメモリ以上に消費しない**ようにする必要があります。 -生成されたプロジェクトはデプロイの指示がありますが、それを実行するとさらに2分かかります。 +## 開始前の事前ステップとコンテナ + +コンテナ(DockerやKubernetesなど)を使っている場合、主に2つのアプローチがあります。 + +### 複数のコンテナ + +複数の**コンテナ**があり、おそらくそれぞれが**単一のプロセス**を実行している場合(**Kubernetes**クラスタなど)、レプリケートされたワーカーコンテナを実行する**前に**、単一のコンテナで**事前のステップ**の作業を行う**別のコンテナ**を持ちたいと思うでしょう。 + +!!! info + もしKubernetesを使用している場合, これはおそらくInit コンテナでしょう。 + +ユースケースが事前のステップを**並列で複数回**実行するのに問題がない場合(例:データベースの準備チェック)、メインプロセスを開始する前に、それらのステップを各コンテナに入れることが可能です。 + +### 単一コンテナ + +単純なセットアップで、**単一のコンテナ**で複数の**ワーカー・プロセス**(または1つのプロセスのみ)を起動する場合、アプリでプロセスを開始する直前に、同じコンテナで事前のステップを実行できます。公式Dockerイメージは、内部的にこれをサポートしています。 + +## Gunicornによる公式Dockerイメージ - Uvicorn + +前の章で詳しく説明したように、Uvicornワーカーで動作するGunicornを含む公式のDockerイメージがあります: [Server Workers - Gunicorn と Uvicorn](./server-workers.md){.internal-link target=_blank}で詳しく説明しています。 + +このイメージは、主に上記で説明した状況で役に立つでしょう: [複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases) + +* tiangolo/uvicorn-gunicorn-fastapi. + +!!! warning + このベースイメージや類似のイメージは**必要ない**可能性が高いので、[上記の: FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi)のようにゼロからイメージをビルドする方が良いでしょう。 + +このイメージには、利用可能なCPUコアに基づいて**ワーカー・プロセスの数**を設定する**オートチューニング**メカニズムが含まれています。 + +これは**賢明なデフォルト**を備えていますが、**環境変数**や設定ファイルを使ってすべての設定を変更したり更新したりすることができます。 + +また、スクリプトで**開始前の事前ステップ**を実行することもサポートしている。 + +!!! tip + すべての設定とオプションを見るには、Dockerイメージのページをご覧ください: tiangolo/uvicorn-gunicorn-fastapi + +### 公式Dockerイメージのプロセス数 + +このイメージの**プロセス数**は、利用可能なCPU**コア**から**自動的に計算**されます。 + +つまり、CPUから可能な限り**パフォーマンス**を**引き出そう**とします。 + +また、**環境変数**などを使った設定で調整することもできます。 + +しかし、プロセスの数はコンテナが実行しているCPUに依存するため、**消費されるメモリの量**もそれに依存することになります。 + +そのため、(機械学習モデルなどで)大量のメモリを消費するアプリケーションで、サーバーのCPUコアが多いが**メモリが少ない**場合、コンテナは利用可能なメモリよりも多くのメモリを使おうとすることになります。 + +その結果、パフォーマンスが大幅に低下する(あるいはクラッシュする)可能性があります。🚨 + +### Dockerfileを作成する + +この画像に基づいて`Dockerfile`を作成する方法を以下に示します: + +```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 +``` + +### より大きなアプリケーション + +[複数のファイルを持つ大きなアプリケーション](../tutorial/bigger-applications.md){.internal-link target=_blank}を作成するセクションに従った場合、`Dockerfile`は次のようになります: + +```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 +``` + +### いつ使うのか + +おそらく、**Kubernetes**(または他のもの)を使用していて、すでにクラスタレベルで複数の**コンテナ**で**レプリケーション**を設定している場合は、この公式ベースイメージ(または他の類似のもの)は**使用すべきではありません**。 + +そのような場合は、上記のように**ゼロから**イメージを構築する方がよいでしょう: [FastAPI用のDockerイメージをビルドする(Build a Docker Image for FastAPI)](#build-a-docker-image-for-fastapi) を参照してください。 + +このイメージは、主に上記の[複数のプロセスと特殊なケースを持つコンテナ(Containers with Multiple Processes and Special Cases)](#containers-with-multiple-processes-and-special-cases)で説明したような特殊なケースで役に立ちます。 + +例えば、アプリケーションが**シンプル**で、CPUに応じたデフォルトのプロセス数を設定すればうまくいく場合や、クラスタレベルでレプリケーションを手動で設定する手間を省きたい場合、アプリで複数のコンテナを実行しない場合などです。 + +または、**Docker Compose**でデプロイし、単一のサーバで実行している場合などです。 + +## コンテナ・イメージのデプロイ + +コンテナ(Docker)イメージを手に入れた後、それをデプロイするにはいくつかの方法があります。 + +例えば以下のリストの方法です: + +* 単一サーバーの**Docker Compose** +* **Kubernetes**クラスタ +* Docker Swarmモードのクラスター +* Nomadのような別のツール +* コンテナ・イメージをデプロイするクラウド・サービス + +## Poetryを利用したDockerイメージ + +もしプロジェクトの依存関係を管理するためにPoetryを利用する場合、マルチステージビルドを使うと良いでしょう。 + +```{ .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. これは最初のステージで、`requirements-stage`と名付けられます +2. `/tmp` を現在の作業ディレクトリに設定します + ここで `requirements.txt` というファイルを生成します。 + +3. このDockerステージにPoetryをインストールします + +4. pyproject.toml`と`poetry.lock`ファイルを`/tmp` ディレクトリにコピーします + + `./poetry.lock*`(末尾に`*`)を使用するため、そのファイルがまだ利用できない場合でもクラッシュすることはないです。 +5. requirements.txt`ファイルを生成します + +6. これは最後のステージであり、ここにあるものはすべて最終的なコンテナ・イメージに保存されます +7. 現在の作業ディレクトリを `/code` に設定します +8. `requirements.txt`ファイルを `/code` ディレクトリにコピーします + このファイルは前のDockerステージにしか存在しないため、`--from-requirements-stage`を使ってコピーします。 +9. 生成された `requirements.txt` ファイルにあるパッケージの依存関係をインストールします +10. app` ディレクトリを `/code` ディレクトリにコピーします +11. uvicorn` コマンドを実行して、`app.main` からインポートした `app` オブジェクトを使用するように指示します +!!! tip + "+"の吹き出しをクリックすると、それぞれの行が何をするのかを見ることができます + +**Dockerステージ**は`Dockerfile`の一部で、**一時的なコンテナイメージ**として動作します。 + +最初のステージは **Poetryのインストール**と Poetry の `pyproject.toml` ファイルからプロジェクトの依存関係を含む**`requirements.txt`を生成**するためだけに使用されます。 + +この `requirements.txt` ファイルは後半の **次のステージ**で `pip` と共に使用されます。 + +最終的なコンテナイメージでは、**最終ステージ**のみが保存されます。前のステージは破棄されます。 + +Poetryを使用する場合、**Dockerマルチステージビルド**を使用することは理にかなっています。 + +なぜなら、最終的なコンテナイメージにPoetryとその依存関係がインストールされている必要はなく、**必要なのは**プロジェクトの依存関係をインストールするために生成された `requirements.txt` ファイルだけだからです。 + +そして次の(そして最終的な)ステージでは、前述とほぼ同じ方法でイメージをビルドします。 + +### TLS Termination Proxyの裏側 - Poetry + +繰り返しになりますが、NginxやTraefikのようなTLS Termination Proxy(ロードバランサー)の後ろでコンテナを動かしている場合は、`--proxy-headers`オプションをコマンドに追加します: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## まとめ + +コンテナ・システム(例えば**Docker**や**Kubernetes**など)を使えば、すべての**デプロイメントのコンセプト**を扱うのがかなり簡単になります: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +ほとんどの場合、ベースとなるイメージは使用せず、公式のPython Dockerイメージをベースにした**コンテナイメージをゼロからビルド**します。 + +`Dockerfile`と**Dockerキャッシュ**内の命令の**順番**に注意することで、**ビルド時間を最小化**することができ、生産性を最大化することができます(そして退屈を避けることができます)。😎 + +特別なケースでは、FastAPI用の公式Dockerイメージを使いたいかもしれません。🤓 From 014262c2030b686878d3e72ebddf3fb7d7928b37 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 14 Aug 2023 09:13:05 +0000 Subject: [PATCH 1262/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9fe64b1c8..00d04c44f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Ukrainian translation for `docs/uk/docs/fastapi-people.md`. PR [#10059](https://github.com/tiangolo/fastapi/pull/10059) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). From 2a5cc5fff38fad76969bde0a630952d8ac174496 Mon Sep 17 00:00:00 2001 From: Yusuke Tamura <62091034+tamtam-fitness@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:13:28 +0900 Subject: [PATCH 1263/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/deployment/server-workers.md`=20(#?= =?UTF-8?q?10064)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ja/docs/deployment/server-workers.md | 182 ++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 docs/ja/docs/deployment/server-workers.md diff --git a/docs/ja/docs/deployment/server-workers.md b/docs/ja/docs/deployment/server-workers.md new file mode 100644 index 000000000..e1ea165a2 --- /dev/null +++ b/docs/ja/docs/deployment/server-workers.md @@ -0,0 +1,182 @@ +# Server Workers - Gunicorn と Uvicorn + +前回のデプロイメントのコンセプトを振り返ってみましょう: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* **レプリケーション(実行中のプロセス数)** +* メモリ +* 開始前の事前ステップ + +ここまでのドキュメントのチュートリアルでは、おそらくUvicornのような**サーバープログラム**を**単一のプロセス**で実行しています。 + +アプリケーションをデプロイする際には、**複数のコア**を利用し、そしてより多くのリクエストを処理できるようにするために、プロセスの**レプリケーション**を持つことを望むでしょう。 + +前のチャプターである[デプロイメントのコンセプト](./concepts.md){.internal-link target=_blank}にて見てきたように、有効な戦略がいくつかあります。 + +ここでは**Gunicorn**が**Uvicornのワーカー・プロセス**を管理する場合の使い方について紹介していきます。 + +!!! info + + DockerやKubernetesなどのコンテナを使用している場合は、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank} + + 特に**Kubernetes**上で実行する場合は、おそらく**Gunicornを使用せず**、**コンテナごとに単一のUvicornプロセス**を実行することになりますが、それについてはこの章の後半で説明します。 + +## GunicornによるUvicornのワーカー・プロセスの管理 + +**Gunicorn**は**WSGI標準**のアプリケーションサーバーです。このことは、GunicornはFlaskやDjangoのようなアプリケーションにサービスを提供できることを意味します。Gunicornそれ自体は**FastAPI**と互換性がないですが、というのもFastAPIは最新の**ASGI 標準**を使用しているためです。 + +しかし、Gunicornは**プロセスマネージャー**として動作し、ユーザーが特定の**ワーカー・プロセスクラス**を使用するように指示することができます。するとGunicornはそのクラスを使い1つ以上の**ワーカー・プロセス**を開始します。 + +そして**Uvicorn**には**Gunicorn互換のワーカークラス**があります。 + +この組み合わせで、Gunicornは**プロセスマネージャー**として動作し、**ポート**と**IP**をリッスンします。そして、**Uvicornクラス**を実行しているワーカー・プロセスに通信を**転送**します。 + +そして、Gunicorn互換の**Uvicornワーカー**クラスが、FastAPIが使えるように、Gunicornから送られてきたデータをASGI標準に変換する役割を担います。 + +## GunicornとUvicornをインストールする + +
+ +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +
+ +これによりUvicornと(高性能を得るための)標準(`standard`)の追加パッケージとGunicornの両方がインストールされます。 + +## UvicornのワーカーとともにGunicornを実行する + +Gunicornを以下のように起動させることができます: + +
+ +```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. +``` + +
+ +それぞれのオプションの意味を見てみましょう: + +* `main:app`: `main`は"`main`"という名前のPythonモジュール、つまりファイル`main.py`を意味します。そして `app` は **FastAPI** アプリケーションの変数名です。 + * main:app`はPythonの`import`文と同じようなものだと想像できます: + + ```Python + from main import app + ``` + + * つまり、`main:app`のコロンは、`from main import app`のPythonの`import`の部分と同じになります。 + +* `--workers`: 使用するワーカー・プロセスの数で、それぞれがUvicornのワーカーを実行します。 + +* `--worker-class`: ワーカー・プロセスで使用するGunicorn互換のワーカークラスです。 + * ここではGunicornがインポートして使用できるクラスを渡します: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: GunicornにリッスンするIPとポートを伝えます。コロン(`:`)でIPとポートを区切ります。 + * Uvicornを直接実行している場合は、`--bind 0.0.0.0:80` (Gunicornのオプション)の代わりに、`--host 0.0.0.0`と `--port 80`を使います。 + +出力では、各プロセスの**PID**(プロセスID)が表示されているのがわかります(単なる数字です)。 + +以下の通りです: + +* Gunicornの**プロセス・マネージャー**はPID `19499`(あなたの場合は違う番号でしょう)で始まります。 +* 次に、`Listening at: http://0.0.0.0:80`を開始します。 +* それから `uvicorn.workers.UvicornWorker` でワーカークラスを使用することを検出します。 +* そして、**4つのワーカー**を起動します。それぞれのワーカーのPIDは、`19511`、`19513`、`19514`、`19515`です。 + +Gunicornはまた、ワーカーの数を維持するために必要であれば、**ダウンしたプロセス**を管理し、**新しいプロセスを**再起動**させます。そのため、上記のリストにある**再起動**の概念に一部役立ちます。 + +しかしながら、必要であればGunicornを**再起動**させ、**起動時に実行**させるなど、外部のコンポーネントを持たせることも必要かもしれません。 + +## Uvicornとワーカー + +Uvicornには複数の**ワーカー・プロセス**を起動し実行するオプションもあります。 + +とはいうものの、今のところUvicornのワーカー・プロセスを扱う機能はGunicornよりも制限されています。そのため、このレベル(Pythonレベル)でプロセスマネージャーを持ちたいのであれば、Gunicornをプロセスマネージャーとして使ってみた方が賢明かもしれないです。 + +どんな場合であれ、以下のように実行します: + +
+ +```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. +``` + +
+ +ここで唯一の新しいオプションは `--workers` で、Uvicornに4つのワーカー・プロセスを起動するように指示しています。 + +各プロセスの **PID** が表示され、親プロセスの `27365` (これは **プロセスマネージャ**) と、各ワーカー・プロセスの **PID** が表示されます: `27368`、`27369`、`27370`、`27367`になります。 + +## デプロイメントのコンセプト + +ここでは、アプリケーションの実行を**並列化**し、CPUの**マルチコア**を活用し、**より多くのリクエスト**に対応できるようにするために、**Gunicorn**(またはUvicorn)を使用して**Uvicornワーカー・プロセス**を管理する方法を見ていきました。 + +上記のデプロイのコンセプトのリストから、ワーカーを使うことは主に**レプリケーション**の部分と、**再起動**を少し助けてくれます: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前のステップ + + +## コンテナとDocker + +次章の[コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}では、その他の**デプロイのコンセプト**を扱うために実施するであろう戦略をいくつか紹介します。 + +また、**GunicornとUvicornワーカー**を含む**公式Dockerイメージ**と、簡単なケースに役立ついくつかのデフォルト設定も紹介します。 + +また、(Gunicornを使わずに)Uvicornプロセスを1つだけ実行するために、**ゼロから独自のイメージを**構築する方法も紹介します。これは簡単なプロセスで、おそらく**Kubernetes**のような分散コンテナ管理システムを使うときにやりたいことでしょう。 + +## まとめ + +Uvicornワーカーを使ったプロセスマネージャとして**Gunicorn**(またはUvicorn)を使えば、**マルチコアCPU**を活用して**複数のプロセスを並列実行**できます。 + +これらのツールやアイデアは、**あなた自身のデプロイシステム**をセットアップしながら、他のデプロイコンセプトを自分で行う場合にも使えます。 + +次の章では、コンテナ(DockerやKubernetesなど)を使った**FastAPI**について学んでいきましょう。これらのツールには、他の**デプロイのコンセプト**も解決する簡単な方法があることがわかるでしょう。✨ From 25059a7717e27a1fce3070ab48dba83fca7d89d6 Mon Sep 17 00:00:00 2001 From: Yusuke Tamura <62091034+tamtam-fitness@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:14:37 +0900 Subject: [PATCH 1264/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/deployment/concepts.md`=20(#10062)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ja/docs/deployment/concepts.md | 323 ++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/ja/docs/deployment/concepts.md diff --git a/docs/ja/docs/deployment/concepts.md b/docs/ja/docs/deployment/concepts.md new file mode 100644 index 000000000..38cbca219 --- /dev/null +++ b/docs/ja/docs/deployment/concepts.md @@ -0,0 +1,323 @@ +# デプロイメントのコンセプト + +**FastAPI**を用いたアプリケーションをデプロイするとき、もしくはどのようなタイプのWeb APIであっても、おそらく気になるコンセプトがいくつかあります。 + +それらを活用することでアプリケーションを**デプロイするための最適な方法**を見つけることができます。 + +重要なコンセプトのいくつかを紹介します: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前のステップ + +これらが**デプロイメント**にどのような影響を与えるかを見ていきましょう。 + +最終的な目的は、**安全な方法で**APIクライアントに**サービスを提供**し、**中断を回避**するだけでなく、**計算リソース**(例えばリモートサーバー/仮想マシン)を可能な限り効率的に使用することです。🚀 + +この章では前述した**コンセプト**についてそれぞれ説明します。 + +この説明を通して、普段とは非常に異なる環境や存在しないであろう**将来の**環境に対し、デプロイの方法を決める上で必要な**直感**を与えてくれることを願っています。 + +これらのコンセプトを意識することにより、**あなた自身のAPI**をデプロイするための最適な方法を**評価**し、**設計**することができるようになるでしょう。 + +次の章では、FastAPIアプリケーションをデプロイするための**具体的なレシピ**を紹介します。 + +しかし、今はこれらの重要な**コンセプトに基づくアイデア**を確認しましょう。これらのコンセプトは、他のどのタイプのWeb APIにも当てはまります。💡 + +## セキュリティ - HTTPS + + +[前チャプターのHTTPSについて](./https.md){.internal-link target=_blank}では、HTTPSがどのようにAPIを暗号化するのかについて学びました。 + +通常、アプリケーションサーバにとって**外部の**コンポーネントである**TLS Termination Proxy**によって提供されることが一般的です。このプロキシは通信の暗号化を担当します。 + +さらにセキュアな通信において、HTTPS証明書の定期的な更新を行いますが、これはTLS Termination Proxyと同じコンポーネントが担当することもあれば、別のコンポーネントが担当することもあります。 + +### HTTPS 用ツールの例 +TLS Termination Proxyとして使用できるツールには以下のようなものがあります: + +* Traefik + * 証明書の更新を自動的に処理 ✨ +* Caddy + * 証明書の更新を自動的に処理 ✨ +* Nginx + * 証明書更新のためにCertbotのような外部コンポーネントを使用 +* HAProxy + * 証明書更新のためにCertbotのような外部コンポーネントを使用 +* Nginx のような Ingress Controller を持つ Kubernetes + * 証明書の更新に cert-manager のような外部コンポーネントを使用 +* クラウド・プロバイダーがサービスの一部として内部的に処理(下記を参照👇) + +もう1つの選択肢は、HTTPSのセットアップを含んだより多くの作業を行う**クラウド・サービス**を利用することです。 このサービスには制限があったり、料金が高くなったりする可能性があります。しかしその場合、TLS Termination Proxyを自分でセットアップする必要はないです。 + +次の章で具体例をいくつか紹介します。 + +--- + +次に考慮すべきコンセプトは、実際のAPIを実行するプログラム(例:Uvicorn)に関連するものすべてです。 + +## プログラム と プロセス + +私たちは「**プロセス**」という言葉についてたくさん話すので、その意味や「**プログラム**」という言葉との違いを明確にしておくと便利です。 + +### プログラムとは何か + +**プログラム**という言葉は、一般的にいろいろなものを表現するのに使われます: + +* プログラマが書く**コード**、**Pythonファイル** +* OSによって実行することができるファイル(例: `python`, `python.exe` or `uvicorn`) +* OS上で**実行**している間、CPUを使用し、メモリ上に何かを保存する特定のプログラム(**プロセス**とも呼ばれる) + +### プロセスとは何か + +**プロセス**という言葉は通常、より具体的な意味で使われ、OSで実行されているものだけを指します(先ほどの最後の説明のように): + +* OS上で**実行**している特定のプログラム + * これはファイルやコードを指すのではなく、OSによって**実行**され、管理されているものを指します。 +* どんなプログラムやコードも、それが**実行されているときにだけ機能**します。つまり、**プロセスとして実行されているときだけ**です。 +* プロセスは、ユーザーにあるいはOSによって、 **終了**(あるいは "kill")させることができます。その時点で、プロセスは実行/実行されることを停止し、それ以降は**何もできなくなります**。 +* コンピュータで実行されている各アプリケーションは、実行中のプログラムや各ウィンドウなど、その背後にいくつかのプロセスを持っています。そして通常、コンピュータが起動している間、**多くのプロセスが**同時に実行されています。 +* **同じプログラム**の**複数のプロセス**が同時に実行されていることがあります。 + +OSの「タスク・マネージャー」や「システム・モニター」(または同様のツール)を確認すれば、これらのプロセスの多くが実行されているの見ることができるでしょう。 + +例えば、同じブラウザプログラム(Firefox、Chrome、Edgeなど)を実行しているプロセスが複数あることがわかります。通常、1つのタブにつき1つのプロセスが実行され、さらに他のプロセスも実行されます。 + + + +--- + +さて、**プロセス**と**プログラム**という用語の違いを確認したところで、デプロイメントについて話を続けます。 + +## 起動時の実行 + +ほとんどの場合、Web APIを作成するときは、クライアントがいつでもアクセスできるように、**常に**中断されることなく**実行される**ことを望みます。もちろん、特定の状況でのみ実行させたい特別な理由がある場合は別ですが、その時間のほとんどは、常に実行され、**利用可能**であることを望みます。 + +### リモートサーバー上での実行 + +リモートサーバー(クラウドサーバー、仮想マシンなど)をセットアップするときにできる最も簡単なことは、ローカルで開発するときと同じように、Uvicorn(または同様のもの)を手動で実行することです。 この方法は**開発中**には役に立つと思われます。 + +しかし、サーバーへの接続が切れた場合、**実行中のプロセス**はおそらくダウンしてしまうでしょう。 + +そしてサーバーが再起動された場合(アップデートやクラウドプロバイダーからのマイグレーションの後など)、おそらくあなたはそれに**気づかないでしょう**。そのため、プロセスを手動で再起動しなければならないことすら気づかないでしょう。つまり、APIはダウンしたままなのです。😱 + +### 起動時に自動的に実行 + +一般的に、サーバープログラム(Uvicornなど)はサーバー起動時に自動的に開始され、**人の介入**を必要とせずに、APIと一緒にプロセスが常に実行されるようにしたいと思われます(UvicornがFastAPIアプリを実行するなど)。 + +### 別のプログラムの用意 + +これを実現するために、通常は**別のプログラム**を用意し、起動時にアプリケーションが実行されるようにします。そして多くの場合、他のコンポーネントやアプリケーション、例えばデータベースも実行されるようにします。 + +### 起動時に実行するツールの例 + +実行するツールの例をいくつか挙げます: + +* Docker +* Kubernetes +* Docker Compose +* Swarm モードによる Docker +* Systemd +* Supervisor +* クラウドプロバイダーがサービスの一部として内部的に処理 +* そのほか... + +次の章で、より具体的な例を挙げていきます。 + +## 再起動 + +起動時にアプリケーションが実行されることを確認するのと同様に、失敗後にアプリケーションが**再起動**されることも確認したいと思われます。 + +### 我々は間違いを犯す + +私たち人間は常に**間違い**を犯します。ソフトウェアには、ほとんど常に**バグ**があらゆる箇所に隠されています。🐛 + +### 小さなエラーは自動的に処理される + +FastAPIでWeb APIを構築する際に、コードにエラーがある場合、FastAPIは通常、エラーを引き起こした単一のリクエストにエラーを含めます。🛡 + +クライアントはそのリクエストに対して**500 Internal Server Error**を受け取りますが、アプリケーションは完全にクラッシュするのではなく、次のリクエストのために動作を続けます。 + +### 重大なエラー - クラッシュ + +しかしながら、**アプリケーション全体をクラッシュさせるようなコードを書いて**UvicornとPythonをクラッシュさせるようなケースもあるかもしれません。💥 + +それでも、ある箇所でエラーが発生したからといって、アプリケーションを停止させたままにしたくないでしょう。 少なくとも壊れていない*パスオペレーション*については、**実行し続けたい**はずです。 + +### クラッシュ後の再起動 + +しかし、実行中の**プロセス**をクラッシュさせるような本当にひどいエラーの場合、少なくとも2〜3回ほどプロセスを**再起動**させる外部コンポーネントが必要でしょう。 + +!!! tip + ...とはいえ、アプリケーション全体が**すぐにクラッシュする**のであれば、いつまでも再起動し続けるのは意味がないでしょう。しかし、その場合はおそらく開発中か少なくともデプロイ直後に気づくと思われます。 + + そこで、**将来**クラッシュする可能性があり、それでも再スタートさせることに意味があるような、主なケースに焦点を当ててみます。 + +あなたはおそらく**外部コンポーネント**がアプリケーションの再起動を担当することを望むと考えます。 なぜなら、その時点でUvicornとPythonを使った同じアプリケーションはすでにクラッシュしており、同じアプリケーションの同じコードに対して何もできないためです。 + +### 自動的に再起動するツールの例 + +ほとんどの場合、前述した**起動時にプログラムを実行する**ために使用されるツールは、自動で**再起動**することにも利用されます。 + +例えば、次のようなものがあります: + +* Docker +* Kubernetes +* Docker Compose +* Swarm モードによる Docker +* Systemd +* Supervisor +* クラウドプロバイダーがサービスの一部として内部的に処理 +* そのほか... + +## レプリケーション - プロセスとメモリー + +FastAPI アプリケーションでは、Uvicorn のようなサーバープログラムを使用し、**1つのプロセス**で1度に複数のクライアントに同時に対応できます。 + +しかし、多くの場合、複数のワーカー・プロセスを同時に実行したいと考えるでしょう。 + +### 複数のプロセス - Worker + +クライアントの数が単一のプロセスで処理できる数を超えており(たとえば仮想マシンがそれほど大きくない場合)、かつサーバーの CPU に**複数のコア**がある場合、同じアプリケーションで同時に**複数のプロセス**を実行させ、すべてのリクエストを分散させることができます。 + +同じAPIプログラムの**複数のプロセス**を実行する場合、それらは一般的に**Worker/ワーカー**と呼ばれます。 + +### ワーカー・プロセス と ポート + + +[HTTPSについて](./https.md){.internal-link target=_blank}のドキュメントで、1つのサーバーで1つのポートとIPアドレスの組み合わせでリッスンできるのは1つのプロセスだけであることを覚えていますでしょうか? + +これはいまだに同じです。 + +そのため、**複数のプロセス**を同時に持つには**ポートでリッスンしている単一のプロセス**が必要であり、それが何らかの方法で各ワーカー・プロセスに通信を送信することが求められます。 + +### プロセスあたりのメモリー + +さて、プログラムがメモリにロードする際には、例えば機械学習モデルや大きなファイルの内容を変数に入れたりする場合では、**サーバーのメモリ(RAM)**を少し消費します。 + +そして複数のプロセスは通常、**メモリを共有しません**。これは、実行中の各プロセスがそれぞれ独自の変数やメモリ等を持っていることを意味します。つまり、コード内で大量のメモリを消費している場合、**各プロセス**は同等の量のメモリを消費することになります。 + +### サーバーメモリー + +例えば、あなたのコードが **1GBのサイズの機械学習モデル**をロードする場合、APIで1つのプロセスを実行すると、少なくとも1GBのRAMを消費します。 + +また、**4つのプロセス**(4つのワーカー)を起動すると、それぞれが1GBのRAMを消費します。つまり、合計でAPIは**4GBのRAM**を消費することになります。 + +リモートサーバーや仮想マシンのRAMが3GBしかない場合、4GB以上のRAMをロードしようとすると問題が発生します。🚨 + +### 複数プロセス - 例 + +この例では、2つの**ワーカー・プロセス**を起動し制御する**マネージャー・ プロセス**があります。 + +このマネージャー・ プロセスは、おそらくIPの**ポート**でリッスンしているものです。そして、すべての通信をワーカー・プロセスに転送します。 + +これらのワーカー・プロセスは、アプリケーションを実行するものであり、**リクエスト**を受けて**レスポンス**を返すための主要な計算を行い、あなたが変数に入れたものは何でもRAMにロードします。 + + + +そしてもちろん、同じマシンでは、あなたのアプリケーションとは別に、**他のプロセス**も実行されているでしょう。 + +興味深いことに、各プロセスが使用する**CPU**の割合は時間とともに大きく**変動**する可能性がありますが、**メモリ(RAM)**は通常、多かれ少なかれ**安定**します。 + +毎回同程度の計算を行うAPIがあり、多くのクライアントがいるのであれば、**CPU使用率**もおそらく**安定**するでしょう(常に急激に上下するのではなく)。 + +### レプリケーション・ツールと戦略の例 + +これを実現するにはいくつかのアプローチがありますが、具体的な戦略については次の章(Dockerやコンテナの章など)で詳しく説明します。 + +考慮すべき主な制約は、**パブリックIP**の**ポート**を処理する**単一の**コンポーネントが存在しなければならないということです。 + +そして、レプリケートされた**プロセス/ワーカー**に通信を**送信**する方法を持つ必要があります。 + +考えられる組み合わせと戦略をいくつか紹介します: + +* **Gunicorn**が**Uvicornワーカー**を管理 + * Gunicornは**IP**と**ポート**をリッスンする**プロセスマネージャ**で、レプリケーションは**複数のUvicornワーカー・プロセス**を持つことによって行われる。 +* **Uvicorn**が**Uvicornワーカー**を管理 + * 1つのUvicornの**プロセスマネージャー**が**IP**と**ポート**をリッスンし、**複数のUvicornワーカー・プロセス**を起動する。 +* **Kubernetes**やその他の分散**コンテナ・システム** + * **Kubernetes**レイヤーの何かが**IP**と**ポート**をリッスンする。レプリケーションは、**複数のコンテナ**にそれぞれ**1つのUvicornプロセス**を実行させることで行われる。 +* **クラウド・サービス**によるレプリケーション + * クラウド・サービスはおそらく**あなたのためにレプリケーションを処理**します。**実行するプロセス**や使用する**コンテナイメージ**を定義できるかもしれませんが、いずれにせよ、それはおそらく**単一のUvicornプロセス**であり、クラウドサービスはそのレプリケーションを担当するでしょう。 + +!!! tip + これらの**コンテナ**やDockerそしてKubernetesに関する項目が、まだあまり意味をなしていなくても心配しないでください。 + + + コンテナ・イメージ、Docker、Kubernetesなどについては、次の章で詳しく説明します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}. + +## 開始前の事前のステップ + +アプリケーションを**開始する前**に、いくつかのステップを実行したい場合が多くあります。 + +例えば、**データベース・マイグレーション** を実行したいかもしれません。 + +しかしほとんどの場合、これらの手順を**1度**に実行したいと考えるでしょう。 + +そのため、アプリケーションを開始する前の**事前のステップ**を実行する**単一のプロセス**を用意したいと思われます。 + +そして、それらの事前のステップを実行しているのが単一のプロセスであることを確認する必要があります。このことはその後アプリケーション自体のために**複数のプロセス**(複数のワーカー)を起動した場合も同様です。 + +これらのステップが**複数のプロセス**によって実行された場合、**並列**に実行されることによって作業が**重複**することになります。そして、もしそのステップがデータベースのマイグレーションのような繊細なものであった場合、互いに競合を引き起こす可能性があります。 + +もちろん、事前のステップを何度も実行しても問題がない場合もあり、その際は対処がかなり楽になります。 + +!!! tip + また、セットアップによっては、アプリケーションを開始する前の**事前のステップ**が必要ない場合もあることを覚えておいてください。 + + その場合は、このようなことを心配する必要はないです。🤷 + +### 事前ステップの戦略例 + +これは**システムを**デプロイする方法に**大きく依存**するだろうし、おそらくプログラムの起動方法や再起動の処理などにも関係してくるでしょう。 + +考えられるアイデアをいくつか挙げてみます: + +* アプリコンテナの前に実行されるKubernetesのInitコンテナ +* 事前のステップを実行し、アプリケーションを起動するbashスクリプト + * 利用するbashスクリプトを起動/再起動したり、エラーを検出したりする方法は以前として必要になるでしょう。 + +!!! tip + + コンテナを使った具体的な例については、次の章で紹介します: [コンテナ内のFastAPI - Docker](./docker.md){.internal-link target=_blank}. + +## リソースの利用 + +あなたのサーバーは**リソース**であり、プログラムを実行しCPUの計算時間や利用可能なRAMメモリを消費または**利用**することができます。 + +システムリソースをどれくらい消費/利用したいですか? 「少ない方が良い」と考えるのは簡単かもしれないですが、実際には、**クラッシュせずに可能な限り**最大限に活用したいでしょう。 + +3台のサーバーにお金を払っているにも関わらず、そのRAMとCPUを少ししか使っていないとしたら、おそらく**お金を無駄にしている** 💸、おそらく**サーバーの電力を無駄にしている** 🌎ことになるでしょう。 + +その場合は、サーバーを2台だけにして、そのリソース(CPU、メモリ、ディスク、ネットワーク帯域幅など)をより高い割合で使用する方がよいでしょう。 + +一方、2台のサーバーがあり、そのCPUとRAMの**100%を使用している**場合、ある時点で1つのプロセスがより多くのメモリを要求し、サーバーはディスクを「メモリ」として使用しないといけません。(何千倍も遅くなる可能性があります。) +もしくは**クラッシュ**することもあれば、あるいはあるプロセスが何らかの計算をする必要があり、そしてCPUが再び空くまで待たなければならないかもしれません。 + +この場合、**1つ余分なサーバー**を用意し、その上でいくつかのプロセスを実行し、すべてのサーバーが**十分なRAMとCPU時間を持つようにする**のがよいでしょう。 + +また、何らかの理由でAPIの利用が急増する可能性もあります。もしかしたらそれが流行ったのかもしれないし、他のサービスやボットが使い始めたのかもしれないです。そのような場合に備えて、余分なリソースを用意しておくと安心でしょう。 + +例えば、リソース使用率の**50%から90%の範囲**で**任意の数字**をターゲットとすることができます。 + +重要なのは、デプロイメントを微調整するためにターゲットを設定し測定することが、おそらく使用したい主要な要素であることです。 + +`htop`のような単純なツールを使って、サーバーで使用されているCPUやRAM、あるいは各プロセスで使用されている量を見ることができます。あるいは、より複雑な監視ツールを使って、サーバに分散して使用することもできます。 + +## まとめ + +アプリケーションのデプロイ方法を決定する際に、考慮すべきであろう主要なコンセプトのいくつかを紹介していきました: + +* セキュリティ - HTTPS +* 起動時の実行 +* 再起動 +* レプリケーション(実行中のプロセス数) +* メモリー +* 開始前の事前ステップ + +これらの考え方とその適用方法を理解することで、デプロイメントを設定したり調整したりする際に必要な直感的な判断ができるようになるはずです。🤓 + +次のセクションでは、あなたが取り得る戦略について、より具体的な例を挙げます。🚀 From 87cc40e483790f1226022a9b69162507c056d198 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 14 Aug 2023 09:15:26 +0000 Subject: [PATCH 1265/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 00d04c44f..ed67dbca8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/server-workers.md`. PR [#10064](https://github.com/tiangolo/fastapi/pull/10064) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Ukrainian translation for `docs/uk/docs/fastapi-people.md`. PR [#10059](https://github.com/tiangolo/fastapi/pull/10059) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). From dafaf6a34c7ed8723400d16b15926cb06ad926fb Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 14 Aug 2023 09:17:05 +0000 Subject: [PATCH 1266/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed67dbca8..e8539075d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/concepts.md`. PR [#10062](https://github.com/tiangolo/fastapi/pull/10062) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/server-workers.md`. PR [#10064](https://github.com/tiangolo/fastapi/pull/10064) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Ukrainian translation for `docs/uk/docs/fastapi-people.md`. PR [#10059](https://github.com/tiangolo/fastapi/pull/10059) by [@rostik1410](https://github.com/rostik1410). From 5e8f7f13d704734b8d75b3a0645360e4f77a6a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 14 Aug 2023 11:49:57 +0200 Subject: [PATCH 1267/1881] =?UTF-8?q?=E2=9C=A8=20Add=20`ResponseValidation?= =?UTF-8?q?Error`=20printable=20details,=20to=20show=20up=20in=20server=20?= =?UTF-8?q?error=20logs=20(#10078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/exceptions.py | 6 ++++++ ...test_response_model_as_return_annotation.py | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index c1692f396..42f4709fb 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -47,3 +47,9 @@ class ResponseValidationError(ValidationException): def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: super().__init__(errors) self.body = body + + def __str__(self) -> str: + message = f"{len(self._errors)} validation errors:\n" + for err in self._errors: + message += f" {err}\n" + return message diff --git a/tests/test_response_model_as_return_annotation.py b/tests/test_response_model_as_return_annotation.py index 85dd450eb..6948430a1 100644 --- a/tests/test_response_model_as_return_annotation.py +++ b/tests/test_response_model_as_return_annotation.py @@ -277,13 +277,15 @@ def test_response_model_no_annotation_return_exact_dict(): def test_response_model_no_annotation_return_invalid_dict(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model-no_annotation-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_response_model_no_annotation_return_invalid_model(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model-no_annotation-return_invalid_model") + assert "missing" in str(excinfo.value) def test_response_model_no_annotation_return_dict_with_extra_data(): @@ -313,13 +315,15 @@ def test_no_response_model_annotation_return_exact_dict(): def test_no_response_model_annotation_return_invalid_dict(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/no_response_model-annotation-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_no_response_model_annotation_return_invalid_model(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/no_response_model-annotation-return_invalid_model") + assert "missing" in str(excinfo.value) def test_no_response_model_annotation_return_dict_with_extra_data(): @@ -395,13 +399,15 @@ def test_response_model_model1_annotation_model2_return_exact_dict(): def test_response_model_model1_annotation_model2_return_invalid_dict(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model_model1-annotation_model2-return_invalid_dict") + assert "missing" in str(excinfo.value) def test_response_model_model1_annotation_model2_return_invalid_model(): - with pytest.raises(ResponseValidationError): + with pytest.raises(ResponseValidationError) as excinfo: client.get("/response_model_model1-annotation_model2-return_invalid_model") + assert "missing" in str(excinfo.value) def test_response_model_model1_annotation_model2_return_dict_with_extra_data(): From d46cd0b1f0f5ef246286a011959cd6a6b6b98fd6 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 14 Aug 2023 09:50:40 +0000 Subject: [PATCH 1268/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e8539075d..2d09fc400 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add `ResponseValidationError` printable details, to show up in server error logs. PR [#10078](https://github.com/tiangolo/fastapi/pull/10078) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/concepts.md`. PR [#10062](https://github.com/tiangolo/fastapi/pull/10062) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/server-workers.md`. PR [#10064](https://github.com/tiangolo/fastapi/pull/10064) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). From 50b6ff7da65bbd0074f2ae56c688db0115a64c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 14 Aug 2023 12:02:43 +0200 Subject: [PATCH 1269/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2d09fc400..9570bef36 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,7 +2,26 @@ ## Latest Changes +## 0.101.1 + +### Fixes + * ✨ Add `ResponseValidationError` printable details, to show up in server error logs. PR [#10078](https://github.com/tiangolo/fastapi/pull/10078) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). +* ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). + +### Docs + +* ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). +* 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). +* 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). +* 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). + +### Translations + * 🌐 Add Japanese translation for `docs/ja/docs/deployment/concepts.md`. PR [#10062](https://github.com/tiangolo/fastapi/pull/10062) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/server-workers.md`. PR [#10064](https://github.com/tiangolo/fastapi/pull/10064) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Update Japanese translation for `docs/ja/docs/deployment/docker.md`. PR [#10073](https://github.com/tiangolo/fastapi/pull/10073) by [@tamtam-fitness](https://github.com/tamtam-fitness). @@ -10,16 +29,13 @@ * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/cookie-params.md`. PR [#10032](https://github.com/tiangolo/fastapi/pull/10032) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Russian translation for `docs/ru/docs/deployment/docker.md`. PR [#9971](https://github.com/tiangolo/fastapi/pull/9971) by [@Xewus](https://github.com/Xewus). * 🌐 Add Vietnamese translation for `docs/vi/docs/python-types.md`. PR [#10047](https://github.com/tiangolo/fastapi/pull/10047) by [@magiskboy](https://github.com/magiskboy). -* 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). -* 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). -* ✏️ Fix typo in deprecation warnings in `fastapi/params.py`. PR [#9854](https://github.com/tiangolo/fastapi/pull/9854) by [@russbiggs](https://github.com/russbiggs). -* ✏️ Fix typo in release notes. PR [#9835](https://github.com/tiangolo/fastapi/pull/9835) by [@francisbergin](https://github.com/francisbergin). -* ✏️ Fix typos in comments on internal code in `fastapi/concurrency.py` and `fastapi/routing.py`. PR [#9590](https://github.com/tiangolo/fastapi/pull/9590) by [@ElliottLarsen](https://github.com/ElliottLarsen). -* 📝 Add external article: Build an SMS Spam Classifier Serverless Database with FaunaDB and FastAPI. PR [#9847](https://github.com/tiangolo/fastapi/pull/9847) by [@adejumoridwan](https://github.com/adejumoridwan). -* 📝 Fix typo in `docs/en/docs/contributing.md`. PR [#9878](https://github.com/tiangolo/fastapi/pull/9878) by [@VicenteMerino](https://github.com/VicenteMerino). -* 📝 Fix code highlighting in `docs/en/docs/tutorial/bigger-applications.md`. PR [#9806](https://github.com/tiangolo/fastapi/pull/9806) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/global-dependencies.md`. PR [#9970](https://github.com/tiangolo/fastapi/pull/9970) by [@dudyaosuplayer](https://github.com/dudyaosuplayer). * 🌐 Add Urdu translation for `docs/ur/docs/benchmarks.md`. PR [#9974](https://github.com/tiangolo/fastapi/pull/9974) by [@AhsanSheraz](https://github.com/AhsanSheraz). + +### Internal + +* 🔧 Add sponsor Porter. PR [#10051](https://github.com/tiangolo/fastapi/pull/10051) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Update sponsors, add Jina back as bronze sponsor. PR [#10050](https://github.com/tiangolo/fastapi/pull/10050) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump mypy from 1.4.0 to 1.4.1. PR [#9756](https://github.com/tiangolo/fastapi/pull/9756) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.17 to 9.1.21. PR [#9960](https://github.com/tiangolo/fastapi/pull/9960) by [@dependabot[bot]](https://github.com/apps/dependabot). From 63e7edb2951b36b494176e2f6a22819fcd3feb17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 14 Aug 2023 12:03:14 +0200 Subject: [PATCH 1270/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?101.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c113ac1fd..d8abf2103 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.101.0" +__version__ = "0.101.1" from starlette import status as status From e93d15cf9a4820cfa69e6d2f62094720a71d619d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 17 Aug 2023 10:51:58 +0200 Subject: [PATCH 1271/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Speakeasy=20(#10098)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 ++ docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 1 + docs/en/docs/img/sponsors/speakeasy.png | Bin 0 -> 4790 bytes 4 files changed, 6 insertions(+) create mode 100644 docs/en/docs/img/sponsors/speakeasy.png diff --git a/README.md b/README.md index 50f80ded6..266213426 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The key features are: + @@ -56,6 +57,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 6d9119520..0d9597f07 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -33,6 +33,9 @@ silver: - url: https://databento.com/ title: Pay as you go for market data img: https://fastapi.tiangolo.com/img/sponsors/databento.svg + - url: https://speakeasyapi.dev?utm_source=fastapi+repo&utm_medium=github+sponsorship + title: SDKs for your API | Speakeasy + img: https://fastapi.tiangolo.com/img/sponsors/speakeasy.png bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 7c3bb2f47..7b605e0ff 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -19,3 +19,4 @@ logins: - Flint-company - porter-dev - fern-api + - ndimares diff --git a/docs/en/docs/img/sponsors/speakeasy.png b/docs/en/docs/img/sponsors/speakeasy.png new file mode 100644 index 0000000000000000000000000000000000000000..001b4b4caffe26d75892dd040064c5d5f4c1ff22 GIT binary patch literal 4790 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 JRI6we`aev(U(5gi literal 0 HcmV?d00001 From a6ae5af7d6c9e7e33490307cccff66a49671433b Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 17 Aug 2023 08:52:40 +0000 Subject: [PATCH 1272/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9570bef36..6ef2e6a45 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). ## 0.101.1 ### Fixes From d4201a49bc2e693afba554c1d73d9ce06a042383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 15:11:35 +0200 Subject: [PATCH 1273/1881] =?UTF-8?q?=F0=9F=93=9D=20Restructure=20docs=20f?= =?UTF-8?q?or=20cloud=20providers,=20include=20links=20to=20sponsors=20(#1?= =?UTF-8?q?0110)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + docs/em/docs/deployment/deta.md | 258 -------------------- docs/en/docs/deployment/cloud.md | 17 ++ docs/en/docs/deployment/deta.md | 391 ------------------------------- docs/en/mkdocs.yml | 2 +- docs/fr/docs/deployment/deta.md | 245 ------------------- docs/ja/docs/deployment/deta.md | 240 ------------------- docs/pt/docs/deployment/deta.md | 258 -------------------- 8 files changed, 21 insertions(+), 1393 deletions(-) delete mode 100644 docs/em/docs/deployment/deta.md create mode 100644 docs/en/docs/deployment/cloud.md delete mode 100644 docs/en/docs/deployment/deta.md delete mode 100644 docs/fr/docs/deployment/deta.md delete mode 100644 docs/ja/docs/deployment/deta.md delete mode 100644 docs/pt/docs/deployment/deta.md diff --git a/.gitignore b/.gitignore index d380d16b7..9be494cec 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ archive.zip *~ .*.sw? .cache + +# macOS +.DS_Store diff --git a/docs/em/docs/deployment/deta.md b/docs/em/docs/deployment/deta.md deleted file mode 100644 index 89b6c4bdb..000000000 --- a/docs/em/docs/deployment/deta.md +++ /dev/null @@ -1,258 +0,0 @@ -# 🛠️ FastAPI 🔛 🪔 - -👉 📄 👆 🔜 💡 ❔ 💪 🛠️ **FastAPI** 🈸 🔛 🪔 ⚙️ 🆓 📄. 👶 - -⚫️ 🔜 ✊ 👆 🔃 **1️⃣0️⃣ ⏲**. - -!!! info - 🪔 **FastAPI** 💰. 👶 - -## 🔰 **FastAPI** 📱 - -* ✍ 📁 👆 📱, 🖼, `./fastapideta/` & ⛔ 🔘 ⚫️. - -### FastAPI 📟 - -* ✍ `main.py` 📁 ⏮️: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} -``` - -### 📄 - -🔜, 🎏 📁 ✍ 📁 `requirements.txt` ⏮️: - -```text -fastapi -``` - -!!! tip - 👆 🚫 💪 ❎ Uvicorn 🛠️ 🔛 🪔, 👐 👆 🔜 🎲 💚 ❎ ⚫️ 🌐 💯 👆 📱. - -### 📁 📊 - -👆 🔜 🔜 ✔️ 1️⃣ 📁 `./fastapideta/` ⏮️ 2️⃣ 📁: - -``` -. -└── main.py -└── requirements.txt -``` - -## ✍ 🆓 🪔 🏧 - -🔜 ✍ 🆓 🏧 🔛 🪔, 👆 💪 📧 & 🔐. - -👆 🚫 💪 💳. - -## ❎ ✳ - -🕐 👆 ✔️ 👆 🏧, ❎ 🪔 : - -=== "💾, 🇸🇻" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "🚪 📋" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -⏮️ ❎ ⚫️, 📂 🆕 📶 👈 ❎ ✳ 🔍. - -🆕 📶, ✔ 👈 ⚫️ ☑ ❎ ⏮️: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip - 🚥 👆 ✔️ ⚠ ❎ ✳, ✅ 🛂 🪔 🩺. - -## 💳 ⏮️ ✳ - -🔜 💳 🪔 ⚪️➡️ ✳ ⏮️: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -👉 🔜 📂 🕸 🖥 & 🔓 🔁. - -## 🛠️ ⏮️ 🪔 - -⏭, 🛠️ 👆 🈸 ⏮️ 🪔 ✳: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -👆 🔜 👀 🎻 📧 🎏: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip - 👆 🛠️ 🔜 ✔️ 🎏 `"endpoint"` 📛. - -## ✅ ⚫️ - -🔜 📂 👆 🖥 👆 `endpoint` 📛. 🖼 🔛 ⚫️ `https://qltnci.deta.dev`, ✋️ 👆 🔜 🎏. - -👆 🔜 👀 🎻 📨 ⚪️➡️ 👆 FastAPI 📱: - -```JSON -{ - "Hello": "World" -} -``` - -& 🔜 🚶 `/docs` 👆 🛠️, 🖼 🔛 ⚫️ 🔜 `https://qltnci.deta.dev/docs`. - -⚫️ 🔜 🎦 👆 🩺 💖: - - - -## 🛠️ 📢 🔐 - -🔢, 🪔 🔜 🍵 🤝 ⚙️ 🍪 👆 🏧. - -✋️ 🕐 👆 🔜, 👆 💪 ⚒ ⚫️ 📢 ⏮️: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -🔜 👆 💪 💰 👈 📛 ⏮️ 🙆 & 👫 🔜 💪 🔐 👆 🛠️. 👶 - -## 🇺🇸🔍 - -㊗ ❗ 👆 🛠️ 👆 FastAPI 📱 🪔 ❗ 👶 👶 - -, 👀 👈 🪔 ☑ 🍵 🇺🇸🔍 👆, 👆 🚫 ✔️ ✊ 💅 👈 & 💪 💭 👈 👆 👩‍💻 🔜 ✔️ 🔐 🗜 🔗. 👶 👶 - -## ✅ 🕶 - -⚪️➡️ 👆 🩺 🎚 (👫 🔜 📛 💖 `https://qltnci.deta.dev/docs`) 📨 📨 👆 *➡ 🛠️* `/items/{item_id}`. - -🖼 ⏮️ 🆔 `5`. - -🔜 🚶 https://web.deta.sh. - -👆 🔜 👀 📤 📄 ◀️ 🤙 "◾" ⏮️ 🔠 👆 📱. - -👆 🔜 👀 📑 ⏮️ "ℹ", & 📑 "🕶", 🚶 📑 "🕶". - -📤 👆 💪 ✔ ⏮️ 📨 📨 👆 📱. - -👆 💪 ✍ 👫 & 🏤-🤾 👫. - - - -## 💡 🌅 - -☝, 👆 🔜 🎲 💚 🏪 💽 👆 📱 🌌 👈 😣 🔘 🕰. 👈 👆 💪 ⚙️ 🪔 🧢, ⚫️ ✔️ 👍 **🆓 🎚**. - -👆 💪 ✍ 🌅 🪔 🩺. - -## 🛠️ 🔧 - -👟 🔙 🔧 👥 🔬 [🛠️ 🔧](./concepts.md){.internal-link target=_blank}, 📥 ❔ 🔠 👫 🔜 🍵 ⏮️ 🪔: - -* **🇺🇸🔍**: 🍵 🪔, 👫 🔜 🤝 👆 📁 & 🍵 🇺🇸🔍 🔁. -* **🏃‍♂ 🔛 🕴**: 🍵 🪔, 🍕 👫 🐕‍🦺. -* **⏏**: 🍵 🪔, 🍕 👫 🐕‍🦺. -* **🧬**: 🍵 🪔, 🍕 👫 🐕‍🦺. -* **💾**: 📉 🔁 🪔, 👆 💪 📧 👫 📈 ⚫️. -* **⏮️ 🔁 ⏭ ▶️**: 🚫 🔗 🐕‍🦺, 👆 💪 ⚒ ⚫️ 👷 ⏮️ 👫 💾 ⚙️ ⚖️ 🌖 ✍. - -!!! note - 🪔 🔧 ⚒ ⚫️ ⏩ (& 🆓) 🛠️ 🙅 🈸 🔜. - - ⚫️ 💪 📉 📚 ⚙️ 💼, ✋️ 🎏 🕰, ⚫️ 🚫 🐕‍🦺 🎏, 💖 ⚙️ 🔢 💽 (↖️ ⚪️➡️ 🪔 👍 ☁ 💽 ⚙️), 🛃 🕹 🎰, ♒️. - - 👆 💪 ✍ 🌅 ℹ 🪔 🩺 👀 🚥 ⚫️ ▶️️ ⚒ 👆. diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md new file mode 100644 index 000000000..b2836aeb4 --- /dev/null +++ b/docs/en/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# Deploy FastAPI on Cloud Providers + +You can use virtually **any cloud provider** to deploy your FastAPI application. + +In most of the cases, the main cloud providers have guides to deploy FastAPI with them. + +## Cloud Providers - Sponsors + +Some cloud providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +You might want to try their services and follow their guides: + +* Platform.sh +* Porter +* Deta diff --git a/docs/en/docs/deployment/deta.md b/docs/en/docs/deployment/deta.md deleted file mode 100644 index 229d7fd5d..000000000 --- a/docs/en/docs/deployment/deta.md +++ /dev/null @@ -1,391 +0,0 @@ -# Deploy FastAPI on Deta Space - -In this section you will learn how to easily deploy a **FastAPI** application on Deta Space, for free. 🎁 - -It will take you about **10 minutes** to deploy an API that you can use. After that, you can optionally release it to anyone. - -Let's dive in. - -!!! info - Deta is a **FastAPI** sponsor. 🎉 - -## A simple **FastAPI** app - -* To start, create an empty directory with the name of your app, for example `./fastapi-deta/`, and then navigate into it. - -```console -$ mkdir fastapi-deta -$ cd fastapi-deta -``` - -### FastAPI code - -* Create a `main.py` file with: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} -``` - -### Requirements - -Now, in the same directory create a file `requirements.txt` with: - -```text -fastapi -uvicorn[standard] -``` - -### Directory structure - -You will now have a directory `./fastapi-deta/` with two files: - -``` -. -└── main.py -└── requirements.txt -``` - -## Create a free **Deta Space** account - -Next, create a free account on Deta Space, you just need an email and password. - -You don't even need a credit card, but make sure **Developer Mode** is enabled when you sign up. - - -## Install the CLI - -Once you have your account, install the Deta Space CLI: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/space-cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/space-cli.ps1 -useb | iex - ``` - -
- -After installing it, open a new terminal so that the installed CLI is detected. - -In a new terminal, confirm that it was correctly installed with: - -
- -```console -$ space --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://deta.space/docs - -Usage: - space [flags] - space [command] - -Available Commands: - help Help about any command - link link code to project - login login to space - new create new project - push push code for project - release create release for a project - validate validate spacefile in dir - version Space CLI version -... -``` - -
- -!!! tip - If you have problems installing the CLI, check the official Deta Space Documentation. - -## Login with the CLI - -In order to authenticate your CLI with Deta Space, you will need an access token. - -To obtain this token, open your Deta Space Canvas, open the **Teletype** (command bar at the bottom of the Canvas), and then click on **Settings**. From there, select **Generate Token** and copy the resulting token. - - - -Now run `space login` from the Space CLI. Upon pasting the token into the CLI prompt and pressing enter, you should see a confirmation message. - -
- -```console -$ space login - -To authenticate the Space CLI with your Space account, generate a new access token in your Space settings and paste it below: - -# Enter access token (41 chars) >$ ***************************************** - -👍 Login Successful! -``` - -
- -## Create a new project in Space - -Now that you've authenticated with the Space CLI, use it to create a new Space Project: - -```console -$ space new - -# What is your project's name? >$ fastapi-deta -``` - -The Space CLI will ask you to name the project, we will call ours `fastapi-deta`. - -Then, it will try to automatically detect which framework or language you are using, showing you what it finds. In our case it will identify the Python app with the following message, prompting you to confirm: - -```console -⚙️ No Spacefile found, trying to auto-detect configuration ... -👇 Deta detected the following configuration: - -Micros: -name: fastapi-deta - L src: . - L engine: python3.9 - -# Do you want to bootstrap "fastapi-deta" with this configuration? (y/n)$ y -``` - -After you confirm, your project will be created in Deta Space inside a special app called Builder. Builder is a toolbox that helps you to create and manage your apps in Deta Space. - -The CLI will also create a `Spacefile` locally in the `fastapi-deta` directory. The Spacefile is a configuration file which tells Deta Space how to run your app. The `Spacefile` for your app will be as follows: - -```yaml -v: 0 -micros: - - name: fastapi-deta - src: . - engine: python3.9 -``` - -It is a `yaml` file, and you can use it to add features like scheduled tasks or modify how your app functions, which we'll do later. To learn more, read the `Spacefile` documentation. - -!!! tip - The Space CLI will also create a hidden `.space` folder in your local directory to link your local environment with Deta Space. This folder should not be included in your version control and will automatically be added to your `.gitignore` file, if you have initialized a Git repository. - -## Define the run command in the Spacefile - -The `run` command in the Spacefile tells Space what command should be executed to start your app. In this case it would be `uvicorn main:app`. - -```diff -v: 0 -micros: - - name: fastapi-deta - src: . - engine: python3.9 -+ run: uvicorn main:app -``` - -## Deploy to Deta Space - -To get your FastAPI live in the cloud, use one more CLI command: - -
- -```console -$ space push - ----> 100% - -build complete... created revision: satyr-jvjk - -✔ Successfully pushed your code and created a new Revision! -ℹ Updating your development instance with the latest Revision, it will be available on your Canvas shortly. -``` -
- -This command will package your code, upload all the necessary files to Deta Space, and run a remote build of your app, resulting in a **revision**. Whenever you run `space push` successfully, a live instance of your API is automatically updated with the latest revision. - -!!! tip - You can manage your revisions by opening your project in the Builder app. The live copy of your API will be visible under the **Develop** tab in Builder. - -## Check it - -The live instance of your API will also be added automatically to your Canvas (the dashboard) on Deta Space. - - - -Click on the new app called `fastapi-deta`, and it will open your API in a new browser tab on a URL like `https://fastapi-deta-gj7ka8.deta.app/`. - -You will get a JSON response from your FastAPI app: - -```JSON -{ - "Hello": "World" -} -``` - -And now you can head over to the `/docs` of your API. For this example, it would be `https://fastapi-deta-gj7ka8.deta.app/docs`. - - - -## Enable public access - -Deta will handle authentication for your account using cookies. By default, every app or API that you `push` or install to your Space is personal - it's only accessible to you. - -But you can also make your API public using the `Spacefile` from earlier. - -With a `public_routes` parameter, you can specify which paths of your API should be available to the public. - -Set your `public_routes` to `"*"` to open every route of your API to the public: - -```yaml -v: 0 -micros: - - name: fastapi-deta - src: . - engine: python3.9 - public_routes: - - "/*" -``` - -Then run `space push` again to update your live API on Deta Space. - -Once it deploys, you can share your URL with anyone and they will be able to access your API. 🚀 - -## HTTPS - -Congrats! You deployed your FastAPI app to Deta Space! 🎉 🍰 - -Also, notice that Deta Space correctly handles HTTPS for you, so you don't have to take care of that and can be sure that your users will have a secure encrypted connection. ✅ 🔒 - -## Create a release - -Space also allows you to publish your API. When you publish it, anyone else can install their own copy of your API, in their own Deta Space cloud. - -To do so, run `space release` in the Space CLI to create an **unlisted release**: - -
- -```console -$ space release - -# Do you want to use the latest revision (buzzard-hczt)? (y/n)$ y - -~ Creating a Release with the latest Revision - ----> 100% - -creating release... -publishing release in edge locations.. -completed... -released: fastapi-deta-exp-msbu -https://deta.space/discovery/r/5kjhgyxewkdmtotx - - Lift off -- successfully created a new Release! - Your Release is available globally on 5 Deta Edges - Anyone can install their own copy of your app. -``` -
- -This command publishes your revision as a release and gives you a link. Anyone you give this link to can install your API. - - -You can also make your app publicly discoverable by creating a **listed release** with `space release --listed` in the Space CLI: - -
- -```console -$ space release --listed - -# Do you want to use the latest revision (buzzard-hczt)? (y/n)$ y - -~ Creating a listed Release with the latest Revision ... - -creating release... -publishing release in edge locations.. -completed... -released: fastapi-deta-exp-msbu -https://deta.space/discovery/@user/fastapi-deta - - Lift off -- successfully created a new Release! - Your Release is available globally on 5 Deta Edges - Anyone can install their own copy of your app. - Listed on Discovery for others to find! -``` -
- -This will allow anyone to find and install your app via Deta Discovery. Read more about releasing your app in the docs. - -## Check runtime logs - -Deta Space also lets you inspect the logs of every app you build or install. - -Add some logging functionality to your app by adding a `print` statement to your `main.py` file. - -```py -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - print(item_id) - return {"item_id": item_id} -``` - -The code within the `read_item` function includes a print statement that will output the `item_id` that is included in the URL. Send a request to your _path operation_ `/items/{item_id}` from the docs UI (which will have a URL like `https://fastapi-deta-gj7ka8.deta.app/docs`), using an ID like `5` as an example. - -Now go to your Space's Canvas. Click on the context menu (`...`) of your live app instance, and then click on **View Logs**. Here you can view your app's logs, sorted by time. - - - -## Learn more - -At some point, you will probably want to store some data for your app in a way that persists through time. For that you can use Deta Base and Deta Drive, both of which have a generous **free tier**. - -You can also read more in the Deta Space Documentation. - -!!! tip - If you have any Deta related questions, comments, or feedback, head to the Deta Discord server. - - -## 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 Space: - -- **HTTPS**: Handled by Deta Space, they will give you a subdomain and handle HTTPS automatically. -- **Running on startup**: Handled by Deta Space, as part of their service. -- **Restarts**: Handled by Deta Space, as part of their service. -- **Replication**: Handled by Deta Space, as part of their service. -- **Authentication**: Handled by Deta Space, as part of their service. -- **Memory**: Limit predefined by Deta Space, you could contact them to increase it. -- **Previous steps before starting**: Can be configured using the `Spacefile`. - -!!! note - Deta Space is designed to make it easy and free to build cloud applications for yourself. Then you can optionally share them with anyone. - - It can simplify 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 Space Documentation to see if it's the right choice for you. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a66b6c147..2a59be4b0 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -159,7 +159,7 @@ nav: - deployment/https.md - deployment/manually.md - deployment/concepts.md - - deployment/deta.md + - deployment/cloud.md - deployment/server-workers.md - deployment/docker.md - project-generation.md diff --git a/docs/fr/docs/deployment/deta.md b/docs/fr/docs/deployment/deta.md deleted file mode 100644 index cceb7b058..000000000 --- a/docs/fr/docs/deployment/deta.md +++ /dev/null @@ -1,245 +0,0 @@ -# Déployer FastAPI sur Deta - -Dans cette section, vous apprendrez à déployer facilement une application **FastAPI** sur Deta en utilisant le plan tarifaire gratuit. 🎁 - -Cela vous prendra environ **10 minutes**. - -!!! info - Deta sponsorise **FastAPI**. 🎉 - -## Une application **FastAPI** de base - -* Créez un répertoire pour votre application, par exemple `./fastapideta/` et déplacez-vous dedans. - -### Le code FastAPI - -* Créer un fichier `main.py` avec : - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} -``` - -### Dépendances - -Maintenant, dans le même répertoire, créez un fichier `requirements.txt` avec : - -```text -fastapi -``` - -!!! tip "Astuce" - Il n'est pas nécessaire d'installer Uvicorn pour déployer sur Deta, bien qu'il soit probablement souhaitable de l'installer localement pour tester votre application. - -### Structure du répertoire - -Vous aurez maintenant un répertoire `./fastapideta/` avec deux fichiers : - -``` -. -└── main.py -└── requirements.txt -``` - -## Créer un compte gratuit sur Deta - -Créez maintenant un compte gratuit -sur Deta, vous avez juste besoin d'une adresse email et d'un mot de passe. - -Vous n'avez même pas besoin d'une carte de crédit. - -## Installer le CLI (Interface en Ligne de Commande) - -Une fois que vous avez votre compte, installez le CLI de Deta : - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -Après l'avoir installé, ouvrez un nouveau terminal afin que la nouvelle installation soit détectée. - -Dans un nouveau terminal, confirmez qu'il a été correctement installé avec : - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "Astuce" - Si vous rencontrez des problèmes pour installer le CLI, consultez la documentation officielle de Deta (en anglais). - -## Connexion avec le CLI - -Maintenant, connectez-vous à Deta depuis le CLI avec : - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -Cela ouvrira un navigateur web et permettra une authentification automatique. - -## Déployer avec Deta - -Ensuite, déployez votre application avec le CLI de Deta : - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -Vous verrez un message JSON similaire à : - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "Astuce" - Votre déploiement aura une URL `"endpoint"` différente. - -## Vérifiez - -Maintenant, dans votre navigateur ouvrez votre URL `endpoint`. Dans l'exemple ci-dessus, c'était -`https://qltnci.deta.dev`, mais la vôtre sera différente. - -Vous verrez la réponse JSON de votre application FastAPI : - -```JSON -{ - "Hello": "World" -} -``` - -Et maintenant naviguez vers `/docs` dans votre API, dans l'exemple ci-dessus ce serait `https://qltnci.deta.dev/docs`. - -Vous verrez votre documentation comme suit : - - - -## Activer l'accès public - -Par défaut, Deta va gérer l'authentification en utilisant des cookies pour votre compte. - -Mais une fois que vous êtes prêt, vous pouvez le rendre public avec : - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -Maintenant, vous pouvez partager cette URL avec n'importe qui et ils seront en mesure d'accéder à votre API. 🚀 - -## HTTPS - -Félicitations ! Vous avez déployé votre application FastAPI sur Deta ! 🎉 🍰 - -Remarquez également que Deta gère correctement HTTPS pour vous, vous n'avez donc pas à vous en occuper et pouvez être sûr que vos clients auront une connexion cryptée sécurisée. ✅ 🔒 - -## Vérifiez le Visor - -À partir de l'interface graphique de votre documentation (dans une URL telle que `https://qltnci.deta.dev/docs`) -envoyez une requête à votre *opération de chemin* `/items/{item_id}`. - -Par exemple avec l'ID `5`. - -Allez maintenant sur https://web.deta.sh. - -Vous verrez qu'il y a une section à gauche appelée "Micros" avec chacune de vos applications. - -Vous verrez un onglet avec "Details", et aussi un onglet "Visor", allez à l'onglet "Visor". - -Vous pouvez y consulter les requêtes récentes envoyées à votre application. - -Vous pouvez également les modifier et les relancer. - - - -## En savoir plus - -À un moment donné, vous voudrez probablement stocker certaines données pour votre application d'une manière qui -persiste dans le temps. Pour cela, vous pouvez utiliser Deta Base, il dispose également d'un généreux **plan gratuit**. - -Vous pouvez également en lire plus dans la documentation Deta. diff --git a/docs/ja/docs/deployment/deta.md b/docs/ja/docs/deployment/deta.md deleted file mode 100644 index 723f169a0..000000000 --- a/docs/ja/docs/deployment/deta.md +++ /dev/null @@ -1,240 +0,0 @@ -# Deta にデプロイ - -このセクションでは、**FastAPI** アプリケーションを Deta の無料プランを利用して、簡単にデプロイする方法を学習します。🎁 - -所要時間は約**10分**です。 - -!!! info "備考" - Deta は **FastAPI** のスポンサーです。🎉 - -## ベーシックな **FastAPI** アプリ - -* アプリのためのディレクトリ (例えば `./fastapideta/`) を作成し、その中に入ってください。 - -### FastAPI のコード - -* 以下の `main.py` ファイルを作成してください: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} -``` - -### Requirements - -では、同じディレクトリに以下の `requirements.txt` ファイルを作成してください: - -```text -fastapi -``` - -!!! tip "豆知識" - アプリのローカルテストのために Uvicorn をインストールしたくなるかもしれませんが、Deta へのデプロイには不要です。 - -### ディレクトリ構造 - -以下の2つのファイルと1つの `./fastapideta/` ディレクトリがあるはずです: - -``` -. -└── main.py -└── requirements.txt -``` - -## Detaの無料アカウントの作成 - -それでは、Detaの無料アカウントを作成しましょう。必要なものはメールアドレスとパスワードだけです。 - -クレジットカードさえ必要ありません。 - -## CLIのインストール - -アカウントを取得したら、Deta CLI をインストールしてください: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -インストールしたら、インストールした CLI を有効にするために新たなターミナルを開いてください。 - -新たなターミナル上で、正しくインストールされたか確認します: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "豆知識" - CLI のインストールに問題が発生した場合は、Deta 公式ドキュメントを参照してください。 - -## CLIでログイン - -CLI から Deta にログインしてみましょう: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -自動的にウェブブラウザが開いて、認証処理が行われます。 - -## Deta でデプロイ - -次に、アプリケーションを Deta CLIでデプロイしましょう: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -次のようなJSONメッセージが表示されます: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "豆知識" - あなたのデプロイでは異なる `"endpoint"` URLが表示されるでしょう。 - -## 確認 - -それでは、`endpoint` URLをブラウザで開いてみましょう。上記の例では `https://qltnci.deta.dev` ですが、あなたのURLは異なるはずです。 - -FastAPIアプリから返ってきたJSONレスポンスが表示されます: - -```JSON -{ - "Hello": "World" -} -``` - -そして `/docs` へ移動してください。上記の例では、`https://qltnci.deta.dev/docs` です。 - -次のようなドキュメントが表示されます: - - - -## パブリックアクセスの有効化 - -デフォルトでは、Deta はクッキーを用いてアカウントの認証を行います。 - -しかし、準備が整えば、以下の様に公開できます: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -ここで、URLを共有するとAPIにアクセスできるようになります。🚀 - -## HTTPS - -おめでとうございます!あなたの FastAPI アプリが Deta へデプロイされました!🎉 🍰 - -また、DetaがHTTPSを正しく処理するため、その処理を行う必要がなく、クライアントは暗号化された安全な通信が利用できます。✅ 🔒 - -## Visor を確認 - -ドキュメントUI (`https://qltnci.deta.dev/docs` のようなURLにある) は *path operation* `/items/{item_id}` へリクエストを送ることができます。 - -ID `5` の例を示します。 - -まず、https://web.deta.sh へアクセスします。 - -左側に各アプリの 「Micros」 というセクションが表示されます。 - -また、「Details」や「Visor」タブが表示されています。「Visor」タブへ移動してください。 - -そこでアプリに送られた直近のリクエストが調べられます。 - -また、それらを編集してリプレイできます。 - - - -## さらに詳しく知る - -様々な箇所で永続的にデータを保存したくなるでしょう。そのためには Deta Base を使用できます。惜しみない **無料利用枠** もあります。 - -詳しくは Deta ドキュメントを参照してください。 diff --git a/docs/pt/docs/deployment/deta.md b/docs/pt/docs/deployment/deta.md deleted file mode 100644 index 9271bba42..000000000 --- a/docs/pt/docs/deployment/deta.md +++ /dev/null @@ -1,258 +0,0 @@ -# Implantação FastAPI na Deta - -Nessa seção você aprenderá sobre como realizar a implantação de uma aplicação **FastAPI** na Deta utilizando o plano gratuito. 🎁 - -Isso tudo levará aproximadamente **10 minutos**. - -!!! info "Informação" - Deta é uma patrocinadora do **FastAPI**. 🎉 - -## Uma aplicação **FastAPI** simples - -* Crie e entre em um diretório para a sua aplicação, por exemplo, `./fastapideta/`. - -### Código FastAPI - -* Crie o arquivo `main.py` com: - -```Python -from fastapi import FastAPI - -app = FastAPI() - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -@app.get("/items/{item_id}") -def read_item(item_id: int): - return {"item_id": item_id} -``` - -### Requisitos - -Agora, no mesmo diretório crie o arquivo `requirements.txt` com: - -```text -fastapi -``` - -!!! tip "Dica" - Você não precisa instalar Uvicorn para realizar a implantação na Deta, embora provavelmente queira instalá-lo para testar seu aplicativo localmente. - -### Estrutura de diretório - -Agora você terá o diretório `./fastapideta/` com dois arquivos: - -``` -. -└── main.py -└── requirements.txt -``` - -## Crie uma conta gratuita na Deta - -Agora crie uma conta gratuita na Deta, você precisará apenas de um email e senha. - -Você nem precisa de um cartão de crédito. - -## Instale a CLI - -Depois de ter sua conta criada, instale Deta CLI: - -=== "Linux, macOS" - -
- - ```console - $ curl -fsSL https://get.deta.dev/cli.sh | sh - ``` - -
- -=== "Windows PowerShell" - -
- - ```console - $ iwr https://get.deta.dev/cli.ps1 -useb | iex - ``` - -
- -Após a instalação, abra um novo terminal para que a CLI seja detectada. - -Em um novo terminal, confirme se foi instalado corretamente com: - -
- -```console -$ deta --help - -Deta command line interface for managing deta micros. -Complete documentation available at https://docs.deta.sh - -Usage: - deta [flags] - deta [command] - -Available Commands: - auth Change auth settings for a deta micro - -... -``` - -
- -!!! tip "Dica" - Se você tiver problemas ao instalar a CLI, verifique a documentação oficial da Deta. - -## Login pela CLI - -Agora faça login na Deta pela CLI com: - -
- -```console -$ deta login - -Please, log in from the web page. Waiting.. -Logged in successfully. -``` - -
- -Isso abrirá um navegador da Web e autenticará automaticamente. - -## Implantação com Deta - -Em seguida, implante seu aplicativo com a Deta CLI: - -
- -```console -$ deta new - -Successfully created a new micro - -// Notice the "endpoint" 🔍 - -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} - -Adding dependencies... - - ----> 100% - - -Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 -``` - -
- -Você verá uma mensagem JSON semelhante a: - -```JSON hl_lines="4" -{ - "name": "fastapideta", - "runtime": "python3.7", - "endpoint": "https://qltnci.deta.dev", - "visor": "enabled", - "http_auth": "enabled" -} -``` - -!!! tip "Dica" - Sua implantação terá um URL `"endpoint"` diferente. - -## Confira - -Agora, abra seu navegador na URL do `endpoint`. No exemplo acima foi `https://qltnci.deta.dev`, mas o seu será diferente. - -Você verá a resposta JSON do seu aplicativo FastAPI: - -```JSON -{ - "Hello": "World" -} -``` - -Agora vá para o `/docs` da sua API, no exemplo acima seria `https://qltnci.deta.dev/docs`. - -Ele mostrará sua documentação como: - - - -## Permitir acesso público - -Por padrão, a Deta lidará com a autenticação usando cookies para sua conta. - -Mas quando estiver pronto, você pode torná-lo público com: - -
- -```console -$ deta auth disable - -Successfully disabled http auth -``` - -
- -Agora você pode compartilhar essa URL com qualquer pessoa e elas conseguirão acessar sua API. 🚀 - -## HTTPS - -Parabéns! Você realizou a implantação do seu app FastAPI na Deta! 🎉 🍰 - -Além disso, observe que a Deta lida corretamente com HTTPS para você, para que você não precise cuidar disso e tenha a certeza de que seus clientes terão uma conexão criptografada segura. ✅ 🔒 - -## Verifique o Visor - -Na UI da sua documentação (você estará em um URL como `https://qltnci.deta.dev/docs`) envie um request para *operação de rota* `/items/{item_id}`. - -Por exemplo com ID `5`. - -Agora vá para https://web.deta.sh. - -Você verá que há uma seção à esquerda chamada "Micros" com cada um dos seus apps. - -Você verá uma aba com "Detalhes", e também a aba "Visor", vá para "Visor". - -Lá você pode inspecionar as solicitações recentes enviadas ao seu aplicativo. - -Você também pode editá-los e reproduzi-los novamente. - - - -## Saiba mais - -Em algum momento, você provavelmente desejará armazenar alguns dados para seu aplicativo de uma forma que persista ao longo do tempo. Para isso você pode usar Deta Base, que também tem um generoso **nível gratuito**. - -Você também pode ler mais na documentação da Deta. - -## Conceitos de implantação - -Voltando aos conceitos que discutimos em [Deployments Concepts](./concepts.md){.internal-link target=_blank}, veja como cada um deles seria tratado com a Deta: - -* **HTTPS**: Realizado pela Deta, eles fornecerão um subdomínio e lidarão com HTTPS automaticamente. -* **Executando na inicialização**: Realizado pela Deta, como parte de seu serviço. -* **Reinicialização**: Realizado pela Deta, como parte de seu serviço. -* **Replicação**: Realizado pela Deta, como parte de seu serviço. -* **Memória**: Limite predefinido pela Deta, você pode contatá-los para aumentá-lo. -* **Etapas anteriores a inicialização**: Não suportado diretamente, você pode fazê-lo funcionar com o sistema Cron ou scripts adicionais. - -!!! note "Nota" - O Deta foi projetado para facilitar (e gratuitamente) a implantação rápida de aplicativos simples. - - Ele pode simplificar vários casos de uso, mas, ao mesmo tempo, não suporta outros, como o uso de bancos de dados externos (além do próprio sistema de banco de dados NoSQL da Deta), máquinas virtuais personalizadas, etc. - - Você pode ler mais detalhes na documentação da Deta para ver se é a escolha certa para você. From e04953a9e0c60a4afdf78652ac9f62bfce5a9349 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 13:12:09 +0000 Subject: [PATCH 1274/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6ef2e6a45..f55d8281a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). ## 0.101.1 From d1c0e5a89f07311384c4ea579d421dfa02f9f6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 15:33:32 +0200 Subject: [PATCH 1275/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20MkDocs=20and?= =?UTF-8?q?=20add=20redirects=20(#10111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 16 +++++++++++----- requirements-docs.txt | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 2a59be4b0..22babe745 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -42,6 +42,9 @@ plugins: search: null markdownextradata: data: ../en/data + redirects: + redirect_maps: + deployment/deta.md: deployment/cloud.md nav: - FastAPI: index.md - Languages: @@ -60,6 +63,7 @@ nav: - ru: /ru/ - tr: /tr/ - uk: /uk/ + - ur: /ur/ - vi: /vi/ - zh: /zh/ - features.md @@ -178,9 +182,9 @@ markdown_extensions: guess_lang: false mdx_include: base_path: docs - admonition: - codehilite: - extra: + admonition: null + codehilite: null + extra: null pymdownx.superfences: custom_fences: - name: mermaid @@ -188,8 +192,8 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format '' pymdownx.tabbed: alternate_style: true - attr_list: - md_in_html: + attr_list: null + md_in_html: null extra: analytics: provider: google @@ -240,6 +244,8 @@ extra: name: tr - Türkçe - link: /uk/ name: uk + - link: /ur/ + name: ur - link: /vi/ name: vi - Tiếng Việt - link: /zh/ diff --git a/requirements-docs.txt b/requirements-docs.txt index 220d1ec3a..2e667720e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -2,6 +2,7 @@ mkdocs-material==9.1.21 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 +mkdocs-redirects>=1.2.1,<1.3.0 typer-cli >=0.0.13,<0.0.14 typer[all] >=0.6.1,<0.8.0 pyyaml >=5.3.1,<7.0.0 From 0fe434ca683e4c1786e4f190a62eaad7aea71240 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 13:34:10 +0000 Subject: [PATCH 1276/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f55d8281a..fe1153254 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). * 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). ## 0.101.1 From 08feaf0cc43a76feef2bb02170792cb79dda7c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 15:49:54 +0200 Subject: [PATCH 1277/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20for=20?= =?UTF-8?q?generating=20clients=20(#10112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/generate-clients.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index 3fed48b0b..f439ed93a 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -12,10 +12,18 @@ A common tool is openapi-typescript-codegen. -Another option you could consider for several languages is Fern. +## Client and SDK Generators - Sponsor -!!! info - Fern is also a FastAPI sponsor. 😎🎉 +There are also some **company-backed** Client and SDK generators based on OpenAPI (FastAPI), in some cases they can offer you **additional features** on top of high-quality generated SDKs/clients. + +Some of them also ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +You might want to try their services and follow their guides: + +* Fern +* Speakeasy ## Generate a TypeScript Frontend Client From 486cd139a94668a0a5a5b24aedb6c763aa26857e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 13:51:12 +0000 Subject: [PATCH 1278/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fe1153254..d7915d079 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). * 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). From 8e382617873665cc7f2323e8742aa5eb04292600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 16:08:16 +0200 Subject: [PATCH 1279/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20Advanced=20do?= =?UTF-8?q?cs,=20add=20links=20to=20sponsor=20courses=20(#10113)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/index.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/index.md b/docs/en/docs/advanced/index.md index 467f0833e..d8dcd4ca6 100644 --- a/docs/en/docs/advanced/index.md +++ b/docs/en/docs/advanced/index.md @@ -17,8 +17,17 @@ You could still use most of the features in **FastAPI** with the knowledge from And the next sections assume you already read it, and assume that you know those main ideas. -## TestDriven.io course +## External Courses -If you would like to take an advanced-beginner course to complement this section of the docs, you might want to check: Test-Driven Development with FastAPI and Docker by **TestDriven.io**. +Although the [Tutorial - User Guide](../tutorial/){.internal-link target=_blank} and this **Advanced User Guide** are written as a guided tutorial (like a book) and should be enough for you to **learn FastAPI**, you might want to complement it with additional courses. -They are currently donating 10% of all profits to the development of **FastAPI**. 🎉 😄 +Or it might be the case that you just prefer to take other courses because they adapt better to your learning style. + +Some course providers ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, this ensures the continued and healthy **development** of FastAPI and its **ecosystem**. + +And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good learning experience** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 + +You might want to try their courses: + +* Talk Python Training +* Test-Driven Development From b406dd917486cbb429541930370b6a20d906cf99 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 14:09:02 +0000 Subject: [PATCH 1280/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d7915d079..1916564ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). * 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). From 7a06de2bb9de12727fe544b602f9d74f7a1b0bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 20:47:59 +0200 Subject: [PATCH 1281/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20tests?= =?UTF-8?q?=20for=20new=20Pydantic=202.2.1=20(#10115)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 +-- tests/test_multi_body_errors.py | 36 +++---------------- .../test_body_updates/test_tutorial001.py | 10 +++--- .../test_tutorial001_py310.py | 10 +++--- .../test_tutorial001_py39.py | 10 +++--- .../test_dataclasses/test_tutorial003.py | 10 +++--- .../test_tutorial004.py | 8 ++--- .../test_tutorial005.py | 8 ++--- .../test_tutorial005_py310.py | 8 ++--- .../test_tutorial005_py39.py | 8 ++--- 10 files changed, 44 insertions(+), 68 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a512a019..c9723b25b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v04 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -62,7 +62,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v04 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py index 931f08fc1..a51ca7253 100644 --- a/tests/test_multi_body_errors.py +++ b/tests/test_multi_body_errors.py @@ -51,7 +51,7 @@ def test_jsonable_encoder_requiring_error(): "loc": ["body", 0, "age"], "msg": "Input should be greater than 0", "input": -1.0, - "ctx": {"gt": "0"}, + "ctx": {"gt": 0}, "url": match_pydantic_error_url("greater_than"), } ] @@ -84,25 +84,12 @@ def test_put_incorrect_body_multiple(): "input": {"age": "five"}, "url": match_pydantic_error_url("missing"), }, - { - "ctx": {"class": "Decimal"}, - "input": "five", - "loc": ["body", 0, "age", "is-instance[Decimal]"], - "msg": "Input should be an instance of Decimal", - "type": "is_instance_of", - "url": match_pydantic_error_url("is_instance_of"), - }, { "type": "decimal_parsing", - "loc": [ - "body", - 0, - "age", - "function-after[to_decimal(), " - "union[int,constrained-str,function-plain[str()]]]", - ], + "loc": ["body", 0, "age"], "msg": "Input should be a valid decimal", "input": "five", + "url": match_pydantic_error_url("decimal_parsing"), }, { "type": "missing", @@ -111,25 +98,12 @@ def test_put_incorrect_body_multiple(): "input": {"age": "six"}, "url": match_pydantic_error_url("missing"), }, - { - "ctx": {"class": "Decimal"}, - "input": "six", - "loc": ["body", 1, "age", "is-instance[Decimal]"], - "msg": "Input should be an instance of Decimal", - "type": "is_instance_of", - "url": match_pydantic_error_url("is_instance_of"), - }, { "type": "decimal_parsing", - "loc": [ - "body", - 1, - "age", - "function-after[to_decimal(), " - "union[int,constrained-str,function-plain[str()]]]", - ], + "loc": ["body", 1, "age"], "msg": "Input should be a valid decimal", "input": "six", + "url": match_pydantic_error_url("decimal_parsing"), }, ] } diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index f1a46210a..58587885e 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -53,7 +53,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -87,7 +87,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -116,7 +116,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -126,7 +126,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "type": "object", "properties": { @@ -151,7 +151,7 @@ def test_openapi_schema(client: TestClient): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "type": "object", "required": ["name", "description", "price", "tax", "tags"], diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index ab696e4c8..d8a62502f 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -56,7 +56,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -90,7 +90,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -119,7 +119,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -129,7 +129,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "type": "object", "properties": { @@ -154,7 +154,7 @@ def test_openapi_schema(client: TestClient): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "type": "object", "required": ["name", "description", "price", "tax", "tags"], diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index 2ee6a5cb4..c604df6ec 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -56,7 +56,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -90,7 +90,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -119,7 +119,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -129,7 +129,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "type": "object", "properties": { @@ -154,7 +154,7 @@ def test_openapi_schema(client: TestClient): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "type": "object", "required": ["name", "description", "price", "tax", "tags"], diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index 2e5809914..f2ca85823 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -79,7 +79,9 @@ def test_openapi_schema(): "schema": { "title": "Items", "type": "array", - "items": {"$ref": "#/components/schemas/ItemInput"}, + "items": { + "$ref": "#/components/schemas/Item-Input" + }, } } }, @@ -141,7 +143,7 @@ def test_openapi_schema(): "items": { "title": "Items", "type": "array", - "items": {"$ref": "#/components/schemas/ItemOutput"}, + "items": {"$ref": "#/components/schemas/Item-Output"}, }, }, }, @@ -156,7 +158,7 @@ def test_openapi_schema(): } }, }, - "ItemInput": { + "Item-Input": { "title": "Item", "required": ["name"], "type": "object", @@ -168,7 +170,7 @@ def test_openapi_schema(): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "required": ["name", "description"], "type": "object", diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index 3ffc0bca7..c5b2fb670 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -35,7 +35,7 @@ def test_openapi_schema(): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -57,7 +57,7 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -67,7 +67,7 @@ def test_openapi_schema(): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -91,7 +91,7 @@ def test_openapi_schema(): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "required": ["name", "description", "price", "tax", "tags"], "type": "object", diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index ff98295a6..458923b5a 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -35,7 +35,7 @@ def test_openapi_schema(): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -57,7 +57,7 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -67,7 +67,7 @@ def test_openapi_schema(): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -91,7 +91,7 @@ def test_openapi_schema(): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "required": ["name", "description", "price", "tax", "tags"], "type": "object", diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index ad1c09eae..1fcc5c4e0 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -42,7 +42,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -64,7 +64,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -74,7 +74,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "required": ["name", "description", "price", "tax", "tags"], "type": "object", diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 045d1d402..470fe032b 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -42,7 +42,7 @@ def test_openapi_schema(client: TestClient): "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ItemOutput" + "$ref": "#/components/schemas/Item-Output" } } }, @@ -64,7 +64,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInput"} + "schema": {"$ref": "#/components/schemas/Item-Input"} } }, "required": True, @@ -74,7 +74,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "ItemInput": { + "Item-Input": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -98,7 +98,7 @@ def test_openapi_schema(client: TestClient): }, }, }, - "ItemOutput": { + "Item-Output": { "title": "Item", "required": ["name", "description", "price", "tax", "tags"], "type": "object", From 3971c44a38fa703f56eb1801765d3f076a5c1b2c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 18:48:35 +0000 Subject: [PATCH 1282/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1916564ae..48a809021 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ♻️ Refactor tests for new Pydantic 2.2.1. PR [#10115](https://github.com/tiangolo/fastapi/pull/10115) by [@tiangolo](https://github.com/tiangolo). * 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). From 8cd7cfc2b622fad03455c324e2ae87014a3fd166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 19 Aug 2023 21:54:04 +0200 Subject: [PATCH 1283/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20new=20docs=20sec?= =?UTF-8?q?tion,=20How=20To=20-=20Recipes,=20move=20docs=20that=20don't=20?= =?UTF-8?q?have=20to=20be=20read=20by=20everyone=20to=20How=20To=20(#10114?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Start How To docs section, move Peewee, remove Peewee from dependencies * 🚚 Move em files to new locations * 🚚 Move and re-structure advanced docs, move relevant to How To * 🔧 Update MkDocs config, new files in How To * 📝 Move docs for Conditional OpenAPI for Japanese to How To * 📝 Move example source files for Extending OpenAPI into each of the new sections * ✅ Update tests with new locations for source files * 🔥 Remove init from Peewee examples --- docs/em/docs/advanced/extending-openapi.md | 314 ------------ .../conditional-openapi.md | 0 .../custom-request-and-route.md | 0 docs/em/docs/how-to/extending-openapi.md | 90 ++++ docs/em/docs/{advanced => how-to}/graphql.md | 0 .../sql-databases-peewee.md | 0 docs/en/docs/advanced/extending-openapi.md | 318 ------------ .../async-sql-encode-databases.md} | 2 +- .../conditional-openapi.md | 0 docs/en/docs/how-to/configure-swagger-ui.md | 78 +++ docs/en/docs/how-to/custom-docs-ui-assets.md | 199 ++++++++ .../custom-request-and-route.md | 0 docs/en/docs/how-to/extending-openapi.md | 87 ++++ docs/en/docs/how-to/general.md | 39 ++ docs/en/docs/{advanced => how-to}/graphql.md | 0 docs/en/docs/how-to/index.md | 11 + .../nosql-databases-couchbase.md} | 2 +- .../sql-databases-peewee.md | 2 + docs/en/mkdocs.yml | 26 +- .../conditional-openapi.md | 0 .../tutorial001.py} | 0 .../tutorial002.py} | 0 .../tutorial003.py} | 0 docs_src/custom_docs_ui/tutorial001.py | 38 ++ .../tutorial002.py | 0 requirements-tests.txt | 1 - .../test_configure_swagger_ui}/__init__.py | 0 .../test_tutorial001.py} | 2 +- .../test_tutorial002.py} | 2 +- .../test_tutorial003.py} | 2 +- .../__init__.py | 0 .../test_custom_docs_ui/test_tutorial001.py | 42 ++ .../test_tutorial002.py | 2 +- .../test_sql_databases_peewee.py | 454 ------------------ 34 files changed, 611 insertions(+), 1100 deletions(-) delete mode 100644 docs/em/docs/advanced/extending-openapi.md rename docs/em/docs/{advanced => how-to}/conditional-openapi.md (100%) rename docs/em/docs/{advanced => how-to}/custom-request-and-route.md (100%) create mode 100644 docs/em/docs/how-to/extending-openapi.md rename docs/em/docs/{advanced => how-to}/graphql.md (100%) rename docs/em/docs/{advanced => how-to}/sql-databases-peewee.md (100%) delete mode 100644 docs/en/docs/advanced/extending-openapi.md rename docs/en/docs/{advanced/async-sql-databases.md => how-to/async-sql-encode-databases.md} (98%) rename docs/en/docs/{advanced => how-to}/conditional-openapi.md (100%) create mode 100644 docs/en/docs/how-to/configure-swagger-ui.md create mode 100644 docs/en/docs/how-to/custom-docs-ui-assets.md rename docs/en/docs/{advanced => how-to}/custom-request-and-route.md (100%) create mode 100644 docs/en/docs/how-to/extending-openapi.md create mode 100644 docs/en/docs/how-to/general.md rename docs/en/docs/{advanced => how-to}/graphql.md (100%) create mode 100644 docs/en/docs/how-to/index.md rename docs/en/docs/{advanced/nosql-databases.md => how-to/nosql-databases-couchbase.md} (99%) rename docs/en/docs/{advanced => how-to}/sql-databases-peewee.md (99%) rename docs/ja/docs/{advanced => how-to}/conditional-openapi.md (100%) rename docs_src/{extending_openapi/tutorial003.py => configure_swagger_ui/tutorial001.py} (100%) rename docs_src/{extending_openapi/tutorial004.py => configure_swagger_ui/tutorial002.py} (100%) rename docs_src/{extending_openapi/tutorial005.py => configure_swagger_ui/tutorial003.py} (100%) create mode 100644 docs_src/custom_docs_ui/tutorial001.py rename docs_src/{extending_openapi => custom_docs_ui}/tutorial002.py (100%) rename {docs_src/sql_databases_peewee => tests/test_tutorial/test_configure_swagger_ui}/__init__.py (100%) rename tests/test_tutorial/{test_extending_openapi/test_tutorial003.py => test_configure_swagger_ui/test_tutorial001.py} (95%) rename tests/test_tutorial/{test_extending_openapi/test_tutorial004.py => test_configure_swagger_ui/test_tutorial002.py} (96%) rename tests/test_tutorial/{test_extending_openapi/test_tutorial005.py => test_configure_swagger_ui/test_tutorial003.py} (96%) rename tests/test_tutorial/{test_sql_databases_peewee => test_custom_docs_ui}/__init__.py (100%) create mode 100644 tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py rename tests/test_tutorial/{test_extending_openapi => test_custom_docs_ui}/test_tutorial002.py (95%) delete mode 100644 tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py diff --git a/docs/em/docs/advanced/extending-openapi.md b/docs/em/docs/advanced/extending-openapi.md deleted file mode 100644 index 496a8d9de..000000000 --- a/docs/em/docs/advanced/extending-openapi.md +++ /dev/null @@ -1,314 +0,0 @@ -# ↔ 🗄 - -!!! warning - 👉 👍 🏧 ⚒. 👆 🎲 💪 🚶 ⚫️. - - 🚥 👆 📄 🔰 - 👩‍💻 🦮, 👆 💪 🎲 🚶 👉 📄. - - 🚥 👆 ⏪ 💭 👈 👆 💪 🔀 🏗 🗄 🔗, 😣 👂. - -📤 💼 🌐❔ 👆 💪 💪 🔀 🏗 🗄 🔗. - -👉 📄 👆 🔜 👀 ❔. - -## 😐 🛠️ - -😐 (🔢) 🛠️, ⏩. - -`FastAPI` 🈸 (👐) ✔️ `.openapi()` 👩‍🔬 👈 📈 📨 🗄 🔗. - -🍕 🈸 🎚 🏗, *➡ 🛠️* `/openapi.json` (⚖️ ⚫️❔ 👆 ⚒ 👆 `openapi_url`) ®. - -⚫️ 📨 🎻 📨 ⏮️ 🏁 🈸 `.openapi()` 👩‍🔬. - -🔢, ⚫️❔ 👩‍🔬 `.openapi()` 🔨 ✅ 🏠 `.openapi_schema` 👀 🚥 ⚫️ ✔️ 🎚 & 📨 👫. - -🚥 ⚫️ 🚫, ⚫️ 🏗 👫 ⚙️ 🚙 🔢 `fastapi.openapi.utils.get_openapi`. - -& 👈 🔢 `get_openapi()` 📨 🔢: - -* `title`: 🗄 📛, 🎦 🩺. -* `version`: ⏬ 👆 🛠️, ✅ `2.5.0`. -* `openapi_version`: ⏬ 🗄 🔧 ⚙️. 🔢, ⏪: `3.0.2`. -* `description`: 📛 👆 🛠️. -* `routes`: 📇 🛣, 👫 🔠 ® *➡ 🛠️*. 👫 ✊ ⚪️➡️ `app.routes`. - -## 🔑 🔢 - -⚙️ ℹ 🔛, 👆 💪 ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗 & 🔐 🔠 🍕 👈 👆 💪. - -🖼, ➡️ 🚮 📄 🗄 ↔ 🔌 🛃 🔱. - -### 😐 **FastAPI** - -🥇, ✍ 🌐 👆 **FastAPI** 🈸 🛎: - -```Python hl_lines="1 4 7-9" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### 🏗 🗄 🔗 - -⤴️, ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗, 🔘 `custom_openapi()` 🔢: - -```Python hl_lines="2 15-20" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### 🔀 🗄 🔗 - -🔜 👆 💪 🚮 📄 ↔, ❎ 🛃 `x-logo` `info` "🎚" 🗄 🔗: - -```Python hl_lines="21-23" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### 💾 🗄 🔗 - -👆 💪 ⚙️ 🏠 `.openapi_schema` "💾", 🏪 👆 🏗 🔗. - -👈 🌌, 👆 🈸 🏆 🚫 ✔️ 🏗 🔗 🔠 🕰 👩‍💻 📂 👆 🛠️ 🩺. - -⚫️ 🔜 🏗 🕴 🕐, & ⤴️ 🎏 💾 🔗 🔜 ⚙️ ⏭ 📨. - -```Python hl_lines="13-14 24-25" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### 🔐 👩‍🔬 - -🔜 👆 💪 ❎ `.openapi()` 👩‍🔬 ⏮️ 👆 🆕 🔢. - -```Python hl_lines="28" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### ✅ ⚫️ - -🕐 👆 🚶 http://127.0.0.1:8000/redoc 👆 🔜 👀 👈 👆 ⚙️ 👆 🛃 🔱 (👉 🖼, **FastAPI**'Ⓜ 🔱): - - - -## 👤-🕸 🕸 & 🎚 🩺 - -🛠️ 🩺 ⚙️ **🦁 🎚** & **📄**, & 🔠 👈 💪 🕸 & 🎚 📁. - -🔢, 👈 📁 🍦 ⚪️➡️ 💲. - -✋️ ⚫️ 💪 🛃 ⚫️, 👆 💪 ⚒ 🎯 💲, ⚖️ 🍦 📁 👆. - -👈 ⚠, 🖼, 🚥 👆 💪 👆 📱 🚧 👷 ⏪ 📱, 🍵 📂 🕸 🔐, ⚖️ 🇧🇿 🕸. - -📥 👆 🔜 👀 ❔ 🍦 👈 📁 👆, 🎏 FastAPI 📱, & 🔗 🩺 ⚙️ 👫. - -### 🏗 📁 📊 - -➡️ 💬 👆 🏗 📁 📊 👀 💖 👉: - -``` -. -├── app -│ ├── __init__.py -│ ├── main.py -``` - -🔜 ✍ 📁 🏪 📚 🎻 📁. - -👆 🆕 📁 📊 💪 👀 💖 👉: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static/ -``` - -### ⏬ 📁 - -⏬ 🎻 📁 💪 🩺 & 🚮 👫 🔛 👈 `static/` 📁. - -👆 💪 🎲 ▶️️-🖊 🔠 🔗 & 🖊 🎛 🎏 `Save link as...`. - -**🦁 🎚** ⚙️ 📁: - -* `swagger-ui-bundle.js` -* `swagger-ui.css` - -& **📄** ⚙️ 📁: - -* `redoc.standalone.js` - -⏮️ 👈, 👆 📁 📊 💪 👀 💖: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static - ├── redoc.standalone.js - ├── swagger-ui-bundle.js - └── swagger-ui.css -``` - -### 🍦 🎻 📁 - -* 🗄 `StaticFiles`. -* "🗻" `StaticFiles()` 👐 🎯 ➡. - -```Python hl_lines="7 11" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### 💯 🎻 📁 - -▶️ 👆 🈸 & 🚶 http://127.0.0.1:8000/static/redoc.standalone.js. - -👆 🔜 👀 📶 📏 🕸 📁 **📄**. - -⚫️ 💪 ▶️ ⏮️ 🕳 💖: - -```JavaScript -/*! - * ReDoc - OpenAPI/Swagger-generated API Reference Documentation - * ------------------------------------------------------------- - * Version: "2.0.0-rc.18" - * Repo: https://github.com/Redocly/redoc - */ -!function(e,t){"object"==typeof exports&&"object"==typeof m - -... -``` - -👈 ✔ 👈 👆 💆‍♂ 💪 🍦 🎻 📁 ⚪️➡️ 👆 📱, & 👈 👆 🥉 🎻 📁 🩺 ☑ 🥉. - -🔜 👥 💪 🔗 📱 ⚙️ 📚 🎻 📁 🩺. - -### ❎ 🏧 🩺 - -🥇 🔁 ❎ 🏧 🩺, 📚 ⚙️ 💲 🔢. - -❎ 👫, ⚒ 👫 📛 `None` 🕐❔ 🏗 👆 `FastAPI` 📱: - -```Python hl_lines="9" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### 🔌 🛃 🩺 - -🔜 👆 💪 ✍ *➡ 🛠️* 🛃 🩺. - -👆 💪 🏤-⚙️ FastAPI 🔗 🔢 ✍ 🕸 📃 🩺, & 🚶‍♀️ 👫 💪 ❌: - -* `openapi_url`: 📛 🌐❔ 🕸 📃 🩺 💪 🤚 🗄 🔗 👆 🛠️. 👆 💪 ⚙️ 📥 🔢 `app.openapi_url`. -* `title`: 📛 👆 🛠️. -* `oauth2_redirect_url`: 👆 💪 ⚙️ `app.swagger_ui_oauth2_redirect_url` 📥 ⚙️ 🔢. -* `swagger_js_url`: 📛 🌐❔ 🕸 👆 🦁 🎚 🩺 💪 🤚 **🕸** 📁. 👉 1️⃣ 👈 👆 👍 📱 🔜 🍦. -* `swagger_css_url`: 📛 🌐❔ 🕸 👆 🦁 🎚 🩺 💪 🤚 **🎚** 📁. 👉 1️⃣ 👈 👆 👍 📱 🔜 🍦. - -& ➡ 📄... - -```Python hl_lines="2-6 14-22 25-27 30-36" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -!!! tip - *➡ 🛠️* `swagger_ui_redirect` 👩‍🎓 🕐❔ 👆 ⚙️ Oauth2️⃣. - - 🚥 👆 🛠️ 👆 🛠️ ⏮️ Oauth2️⃣ 🐕‍🦺, 👆 🔜 💪 🔓 & 👟 🔙 🛠️ 🩺 ⏮️ 📎 🎓. & 🔗 ⏮️ ⚫️ ⚙️ 🎰 Oauth2️⃣ 🤝. - - 🦁 🎚 🔜 🍵 ⚫️ ⛅ 🎑 👆, ✋️ ⚫️ 💪 👉 "❎" 👩‍🎓. - -### ✍ *➡ 🛠️* 💯 ⚫️ - -🔜, 💪 💯 👈 🌐 👷, ✍ *➡ 🛠️*: - -```Python hl_lines="39-41" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### 💯 ⚫️ - -🔜, 👆 🔜 💪 🔌 👆 📻, 🚶 👆 🩺 http://127.0.0.1:8000/docs, & 🔃 📃. - -& 🍵 🕸, 👆 🔜 💪 👀 🩺 👆 🛠️ & 🔗 ⏮️ ⚫️. - -## 🛠️ 🦁 🎚 - -👆 💪 🔗 ➕ 🦁 🎚 🔢. - -🔗 👫, 🚶‍♀️ `swagger_ui_parameters` ❌ 🕐❔ 🏗 `FastAPI()` 📱 🎚 ⚖️ `get_swagger_ui_html()` 🔢. - -`swagger_ui_parameters` 📨 📖 ⏮️ 📳 🚶‍♀️ 🦁 🎚 🔗. - -FastAPI 🗜 📳 **🎻** ⚒ 👫 🔗 ⏮️ 🕸, 👈 ⚫️❔ 🦁 🎚 💪. - -### ❎ ❕ 🎦 - -🖼, 👆 💪 ❎ ❕ 🎦 🦁 🎚. - -🍵 🔀 ⚒, ❕ 🎦 🛠️ 🔢: - - - -✋️ 👆 💪 ❎ ⚫️ ⚒ `syntaxHighlight` `False`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial003.py!} -``` - -...& ⤴️ 🦁 🎚 🏆 🚫 🎦 ❕ 🎦 🚫🔜: - - - -### 🔀 🎢 - -🎏 🌌 👆 💪 ⚒ ❕ 🎦 🎢 ⏮️ 🔑 `"syntaxHighlight.theme"` (👀 👈 ⚫️ ✔️ ❣ 🖕): - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial004.py!} -``` - -👈 📳 🔜 🔀 ❕ 🎦 🎨 🎢: - - - -### 🔀 🔢 🦁 🎚 🔢 - -FastAPI 🔌 🔢 📳 🔢 ☑ 🌅 ⚙️ 💼. - -⚫️ 🔌 👫 🔢 📳: - -```Python -{!../../../fastapi/openapi/docs.py[ln:7-13]!} -``` - -👆 💪 🔐 🙆 👫 ⚒ 🎏 💲 ❌ `swagger_ui_parameters`. - -🖼, ❎ `deepLinking` 👆 💪 🚶‍♀️ 👉 ⚒ `swagger_ui_parameters`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial005.py!} -``` - -### 🎏 🦁 🎚 🔢 - -👀 🌐 🎏 💪 📳 👆 💪 ⚙️, ✍ 🛂 🩺 🦁 🎚 🔢. - -### 🕸-🕴 ⚒ - -🦁 🎚 ✔ 🎏 📳 **🕸-🕴** 🎚 (🖼, 🕸 🔢). - -FastAPI 🔌 👫 🕸-🕴 `presets` ⚒: - -```JavaScript -presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIBundle.SwaggerUIStandalonePreset -] -``` - -👫 **🕸** 🎚, 🚫 🎻, 👆 💪 🚫 🚶‍♀️ 👫 ⚪️➡️ 🐍 📟 🔗. - -🚥 👆 💪 ⚙️ 🕸-🕴 📳 💖 📚, 👆 💪 ⚙️ 1️⃣ 👩‍🔬 🔛. 🔐 🌐 🦁 🎚 *➡ 🛠️* & ❎ ✍ 🙆 🕸 👆 💪. diff --git a/docs/em/docs/advanced/conditional-openapi.md b/docs/em/docs/how-to/conditional-openapi.md similarity index 100% rename from docs/em/docs/advanced/conditional-openapi.md rename to docs/em/docs/how-to/conditional-openapi.md diff --git a/docs/em/docs/advanced/custom-request-and-route.md b/docs/em/docs/how-to/custom-request-and-route.md similarity index 100% rename from docs/em/docs/advanced/custom-request-and-route.md rename to docs/em/docs/how-to/custom-request-and-route.md diff --git a/docs/em/docs/how-to/extending-openapi.md b/docs/em/docs/how-to/extending-openapi.md new file mode 100644 index 000000000..6b3bc0075 --- /dev/null +++ b/docs/em/docs/how-to/extending-openapi.md @@ -0,0 +1,90 @@ +# ↔ 🗄 + +!!! warning + 👉 👍 🏧 ⚒. 👆 🎲 💪 🚶 ⚫️. + + 🚥 👆 📄 🔰 - 👩‍💻 🦮, 👆 💪 🎲 🚶 👉 📄. + + 🚥 👆 ⏪ 💭 👈 👆 💪 🔀 🏗 🗄 🔗, 😣 👂. + +📤 💼 🌐❔ 👆 💪 💪 🔀 🏗 🗄 🔗. + +👉 📄 👆 🔜 👀 ❔. + +## 😐 🛠️ + +😐 (🔢) 🛠️, ⏩. + +`FastAPI` 🈸 (👐) ✔️ `.openapi()` 👩‍🔬 👈 📈 📨 🗄 🔗. + +🍕 🈸 🎚 🏗, *➡ 🛠️* `/openapi.json` (⚖️ ⚫️❔ 👆 ⚒ 👆 `openapi_url`) ®. + +⚫️ 📨 🎻 📨 ⏮️ 🏁 🈸 `.openapi()` 👩‍🔬. + +🔢, ⚫️❔ 👩‍🔬 `.openapi()` 🔨 ✅ 🏠 `.openapi_schema` 👀 🚥 ⚫️ ✔️ 🎚 & 📨 👫. + +🚥 ⚫️ 🚫, ⚫️ 🏗 👫 ⚙️ 🚙 🔢 `fastapi.openapi.utils.get_openapi`. + +& 👈 🔢 `get_openapi()` 📨 🔢: + +* `title`: 🗄 📛, 🎦 🩺. +* `version`: ⏬ 👆 🛠️, ✅ `2.5.0`. +* `openapi_version`: ⏬ 🗄 🔧 ⚙️. 🔢, ⏪: `3.0.2`. +* `description`: 📛 👆 🛠️. +* `routes`: 📇 🛣, 👫 🔠 ® *➡ 🛠️*. 👫 ✊ ⚪️➡️ `app.routes`. + +## 🔑 🔢 + +⚙️ ℹ 🔛, 👆 💪 ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗 & 🔐 🔠 🍕 👈 👆 💪. + +🖼, ➡️ 🚮 📄 🗄 ↔ 🔌 🛃 🔱. + +### 😐 **FastAPI** + +🥇, ✍ 🌐 👆 **FastAPI** 🈸 🛎: + +```Python hl_lines="1 4 7-9" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🏗 🗄 🔗 + +⤴️, ⚙️ 🎏 🚙 🔢 🏗 🗄 🔗, 🔘 `custom_openapi()` 🔢: + +```Python hl_lines="2 15-20" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔀 🗄 🔗 + +🔜 👆 💪 🚮 📄 ↔, ❎ 🛃 `x-logo` `info` "🎚" 🗄 🔗: + +```Python hl_lines="21-23" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 💾 🗄 🔗 + +👆 💪 ⚙️ 🏠 `.openapi_schema` "💾", 🏪 👆 🏗 🔗. + +👈 🌌, 👆 🈸 🏆 🚫 ✔️ 🏗 🔗 🔠 🕰 👩‍💻 📂 👆 🛠️ 🩺. + +⚫️ 🔜 🏗 🕴 🕐, & ⤴️ 🎏 💾 🔗 🔜 ⚙️ ⏭ 📨. + +```Python hl_lines="13-14 24-25" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### 🔐 👩‍🔬 + +🔜 👆 💪 ❎ `.openapi()` 👩‍🔬 ⏮️ 👆 🆕 🔢. + +```Python hl_lines="28" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### ✅ ⚫️ + +🕐 👆 🚶 http://127.0.0.1:8000/redoc 👆 🔜 👀 👈 👆 ⚙️ 👆 🛃 🔱 (👉 🖼, **FastAPI**'Ⓜ 🔱): + + diff --git a/docs/em/docs/advanced/graphql.md b/docs/em/docs/how-to/graphql.md similarity index 100% rename from docs/em/docs/advanced/graphql.md rename to docs/em/docs/how-to/graphql.md diff --git a/docs/em/docs/advanced/sql-databases-peewee.md b/docs/em/docs/how-to/sql-databases-peewee.md similarity index 100% rename from docs/em/docs/advanced/sql-databases-peewee.md rename to docs/em/docs/how-to/sql-databases-peewee.md diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md deleted file mode 100644 index bec184dee..000000000 --- a/docs/en/docs/advanced/extending-openapi.md +++ /dev/null @@ -1,318 +0,0 @@ -# Extending OpenAPI - -!!! warning - This is a rather advanced feature. You probably can skip it. - - If you are just following the tutorial - user guide, you can probably skip this section. - - If you already know that you need to modify the generated OpenAPI schema, continue reading. - -There are some cases where you might need to modify the generated OpenAPI schema. - -In this section you will see how. - -## The normal process - -The normal (default) process, is as follows. - -A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema. - -As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered. - -It just returns a JSON response with the result of the application's `.openapi()` method. - -By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them. - -If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`. - -And that function `get_openapi()` receives as parameters: - -* `title`: The OpenAPI title, shown in the docs. -* `version`: The version of your API, e.g. `2.5.0`. -* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. -* `summary`: A short summary of the API. -* `description`: The description of your API, this can include markdown and will be shown in the docs. -* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. - -!!! info - The parameter `summary` is available in OpenAPI 3.1.0 and above, supported by FastAPI 0.99.0 and above. - -## Overriding the defaults - -Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. - -For example, let's add ReDoc's OpenAPI extension to include a custom logo. - -### Normal **FastAPI** - -First, write all your **FastAPI** application as normally: - -```Python hl_lines="1 4 7-9" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Generate the OpenAPI schema - -Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: - -```Python hl_lines="2 15-21" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Modify the OpenAPI schema - -Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema: - -```Python hl_lines="22-24" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Cache the OpenAPI schema - -You can use the property `.openapi_schema` as a "cache", to store your generated schema. - -That way, your application won't have to generate the schema every time a user opens your API docs. - -It will be generated only once, and then the same cached schema will be used for the next requests. - -```Python hl_lines="13-14 25-26" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Override the method - -Now you can replace the `.openapi()` method with your new function. - -```Python hl_lines="29" -{!../../../docs_src/extending_openapi/tutorial001.py!} -``` - -### Check it - -Once you go to http://127.0.0.1:8000/redoc you will see that you are using your custom logo (in this example, **FastAPI**'s logo): - - - -## Self-hosting JavaScript and CSS for docs - -The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files. - -By default, those files are served from a CDN. - -But it's possible to customize it, you can set a specific CDN, or serve the files yourself. - -That's useful, for example, if you need your app to keep working even while offline, without open Internet access, or in a local network. - -Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them. - -### Project file structure - -Let's say your project file structure looks like this: - -``` -. -├── app -│ ├── __init__.py -│ ├── main.py -``` - -Now create a directory to store those static files. - -Your new file structure could look like this: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static/ -``` - -### Download the files - -Download the static files needed for the docs and put them on that `static/` directory. - -You can probably right-click each link and select an option similar to `Save link as...`. - -**Swagger UI** uses the files: - -* `swagger-ui-bundle.js` -* `swagger-ui.css` - -And **ReDoc** uses the file: - -* `redoc.standalone.js` - -After that, your file structure could look like: - -``` -. -├── app -│   ├── __init__.py -│   ├── main.py -└── static - ├── redoc.standalone.js - ├── swagger-ui-bundle.js - └── swagger-ui.css -``` - -### Serve the static files - -* Import `StaticFiles`. -* "Mount" a `StaticFiles()` instance in a specific path. - -```Python hl_lines="7 11" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Test the static files - -Start your application and go to http://127.0.0.1:8000/static/redoc.standalone.js. - -You should see a very long JavaScript file for **ReDoc**. - -It could start with something like: - -```JavaScript -/*! - * ReDoc - OpenAPI/Swagger-generated API Reference Documentation - * ------------------------------------------------------------- - * Version: "2.0.0-rc.18" - * Repo: https://github.com/Redocly/redoc - */ -!function(e,t){"object"==typeof exports&&"object"==typeof m - -... -``` - -That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place. - -Now we can configure the app to use those static files for the docs. - -### Disable the automatic docs - -The first step is to disable the automatic docs, as those use the CDN by default. - -To disable them, set their URLs to `None` when creating your `FastAPI` app: - -```Python hl_lines="9" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Include the custom docs - -Now you can create the *path operations* for the custom docs. - -You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: - -* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. -* `title`: the title of your API. -* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. -* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the one that your own app is now serving. -* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the one that your own app is now serving. - -And similarly for ReDoc... - -```Python hl_lines="2-6 14-22 25-27 30-36" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -!!! tip - The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. - - If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. - - Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. - -### Create a *path operation* to test it - -Now, to be able to test that everything works, create a *path operation*: - -```Python hl_lines="39-41" -{!../../../docs_src/extending_openapi/tutorial002.py!} -``` - -### Test it - -Now, you should be able to disconnect your WiFi, go to your docs at http://127.0.0.1:8000/docs, and reload the page. - -And even without Internet, you would be able to see the docs for your API and interact with it. - -## Configuring Swagger UI - -You can configure some extra Swagger UI parameters. - -To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. - -`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. - -FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. - -### Disable Syntax Highlighting - -For example, you could disable syntax highlighting in Swagger UI. - -Without changing the settings, syntax highlighting is enabled by default: - - - -But you can disable it by setting `syntaxHighlight` to `False`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial003.py!} -``` - -...and then Swagger UI won't show the syntax highlighting anymore: - - - -### Change the Theme - -The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial004.py!} -``` - -That configuration would change the syntax highlighting color theme: - - - -### Change Default Swagger UI Parameters - -FastAPI includes some default configuration parameters appropriate for most of the use cases. - -It includes these default configurations: - -```Python -{!../../../fastapi/openapi/docs.py[ln:7-13]!} -``` - -You can override any of them by setting a different value in the argument `swagger_ui_parameters`. - -For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: - -```Python hl_lines="3" -{!../../../docs_src/extending_openapi/tutorial005.py!} -``` - -### Other Swagger UI Parameters - -To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. - -### JavaScript-only settings - -Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). - -FastAPI also includes these JavaScript-only `presets` settings: - -```JavaScript -presets: [ - SwaggerUIBundle.presets.apis, - SwaggerUIBundle.SwaggerUIStandalonePreset -] -``` - -These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. - -If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/advanced/async-sql-databases.md b/docs/en/docs/how-to/async-sql-encode-databases.md similarity index 98% rename from docs/en/docs/advanced/async-sql-databases.md rename to docs/en/docs/how-to/async-sql-encode-databases.md index 12549a190..697167f79 100644 --- a/docs/en/docs/advanced/async-sql-databases.md +++ b/docs/en/docs/how-to/async-sql-encode-databases.md @@ -1,4 +1,4 @@ -# Async SQL (Relational) Databases +# Async SQL (Relational) Databases with Encode/Databases !!! info These docs are about to be updated. 🎉 diff --git a/docs/en/docs/advanced/conditional-openapi.md b/docs/en/docs/how-to/conditional-openapi.md similarity index 100% rename from docs/en/docs/advanced/conditional-openapi.md rename to docs/en/docs/how-to/conditional-openapi.md diff --git a/docs/en/docs/how-to/configure-swagger-ui.md b/docs/en/docs/how-to/configure-swagger-ui.md new file mode 100644 index 000000000..f36ba5ba8 --- /dev/null +++ b/docs/en/docs/how-to/configure-swagger-ui.md @@ -0,0 +1,78 @@ +# Configure Swagger UI + +You can configure some extra Swagger UI parameters. + +To configure them, pass the `swagger_ui_parameters` argument when creating the `FastAPI()` app object or to the `get_swagger_ui_html()` function. + +`swagger_ui_parameters` receives a dictionary with the configurations passed to Swagger UI directly. + +FastAPI converts the configurations to **JSON** to make them compatible with JavaScript, as that's what Swagger UI needs. + +## Disable Syntax Highlighting + +For example, you could disable syntax highlighting in Swagger UI. + +Without changing the settings, syntax highlighting is enabled by default: + + + +But you can disable it by setting `syntaxHighlight` to `False`: + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial001.py!} +``` + +...and then Swagger UI won't show the syntax highlighting anymore: + + + +## Change the Theme + +The same way you could set the syntax highlighting theme with the key `"syntaxHighlight.theme"` (notice that it has a dot in the middle): + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial002.py!} +``` + +That configuration would change the syntax highlighting color theme: + + + +## Change Default Swagger UI Parameters + +FastAPI includes some default configuration parameters appropriate for most of the use cases. + +It includes these default configurations: + +```Python +{!../../../fastapi/openapi/docs.py[ln:7-13]!} +``` + +You can override any of them by setting a different value in the argument `swagger_ui_parameters`. + +For example, to disable `deepLinking` you could pass these settings to `swagger_ui_parameters`: + +```Python hl_lines="3" +{!../../../docs_src/configure_swagger_ui/tutorial003.py!} +``` + +## Other Swagger UI Parameters + +To see all the other possible configurations you can use, read the official docs for Swagger UI parameters. + +## JavaScript-only settings + +Swagger UI also allows other configurations to be **JavaScript-only** objects (for example, JavaScript functions). + +FastAPI also includes these JavaScript-only `presets` settings: + +```JavaScript +presets: [ + SwaggerUIBundle.presets.apis, + SwaggerUIBundle.SwaggerUIStandalonePreset +] +``` + +These are **JavaScript** objects, not strings, so you can't pass them from Python code directly. + +If you need to use JavaScript-only configurations like those, you can use one of the methods above. Override all the Swagger UI *path operation* and manually write any JavaScript you need. diff --git a/docs/en/docs/how-to/custom-docs-ui-assets.md b/docs/en/docs/how-to/custom-docs-ui-assets.md new file mode 100644 index 000000000..f26324869 --- /dev/null +++ b/docs/en/docs/how-to/custom-docs-ui-assets.md @@ -0,0 +1,199 @@ +# Custom Docs UI Static Assets (Self-Hosting) + +The API docs use **Swagger UI** and **ReDoc**, and each of those need some JavaScript and CSS files. + +By default, those files are served from a CDN. + +But it's possible to customize it, you can set a specific CDN, or serve the files yourself. + +## Custom CDN for JavaScript and CSS + +Let's say that you want to use a different CDN, for example you want to use `https://unpkg.com/`. + +This could be useful if for example you live in a country that restricts some URLs. + +### Disable the automatic docs + +The first step is to disable the automatic docs, as by default, those use the default CDN. + +To disable them, set their URLs to `None` when creating your `FastAPI` app: + +```Python hl_lines="8" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +### Include the custom docs + +Now you can create the *path operations* for the custom docs. + +You can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: + +* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. +* `title`: the title of your API. +* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. +* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. This is the custom CDN URL. +* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. This is the custom CDN URL. + +And similarly for ReDoc... + +```Python hl_lines="2-6 11-19 22-24 27-33" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +!!! tip + The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. + + If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. + + Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. + +### Create a *path operation* to test it + +Now, to be able to test that everything works, create a *path operation*: + +```Python hl_lines="36-38" +{!../../../docs_src/custom_docs_ui/tutorial001.py!} +``` + +### Test it + +Now, you should be able to go to your docs at http://127.0.0.1:8000/docs, and reload the page, it will load those assets from the new CDN. + +## Self-hosting JavaScript and CSS for docs + +Self-hosting the JavaScript and CSS could be useful if, for example, you need your app to keep working even while offline, without open Internet access, or in a local network. + +Here you'll see how to serve those files yourself, in the same FastAPI app, and configure the docs to use them. + +### Project file structure + +Let's say your project file structure looks like this: + +``` +. +├── app +│ ├── __init__.py +│ ├── main.py +``` + +Now create a directory to store those static files. + +Your new file structure could look like this: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static/ +``` + +### Download the files + +Download the static files needed for the docs and put them on that `static/` directory. + +You can probably right-click each link and select an option similar to `Save link as...`. + +**Swagger UI** uses the files: + +* `swagger-ui-bundle.js` +* `swagger-ui.css` + +And **ReDoc** uses the file: + +* `redoc.standalone.js` + +After that, your file structure could look like: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +└── static + ├── redoc.standalone.js + ├── swagger-ui-bundle.js + └── swagger-ui.css +``` + +### Serve the static files + +* Import `StaticFiles`. +* "Mount" a `StaticFiles()` instance in a specific path. + +```Python hl_lines="7 11" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Test the static files + +Start your application and go to http://127.0.0.1:8000/static/redoc.standalone.js. + +You should see a very long JavaScript file for **ReDoc**. + +It could start with something like: + +```JavaScript +/*! + * ReDoc - OpenAPI/Swagger-generated API Reference Documentation + * ------------------------------------------------------------- + * Version: "2.0.0-rc.18" + * Repo: https://github.com/Redocly/redoc + */ +!function(e,t){"object"==typeof exports&&"object"==typeof m + +... +``` + +That confirms that you are being able to serve static files from your app, and that you placed the static files for the docs in the correct place. + +Now we can configure the app to use those static files for the docs. + +### Disable the automatic docs for static files + +The same as when using a custom CDN, the first step is to disable the automatic docs, as those use the CDN by default. + +To disable them, set their URLs to `None` when creating your `FastAPI` app: + +```Python hl_lines="9" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Include the custom docs for static files + +And the same way as with a custom CDN, now you can create the *path operations* for the custom docs. + +Again, you can re-use FastAPI's internal functions to create the HTML pages for the docs, and pass them the needed arguments: + +* `openapi_url`: the URL where the HTML page for the docs can get the OpenAPI schema for your API. You can use here the attribute `app.openapi_url`. +* `title`: the title of your API. +* `oauth2_redirect_url`: you can use `app.swagger_ui_oauth2_redirect_url` here to use the default. +* `swagger_js_url`: the URL where the HTML for your Swagger UI docs can get the **JavaScript** file. **This is the one that your own app is now serving**. +* `swagger_css_url`: the URL where the HTML for your Swagger UI docs can get the **CSS** file. **This is the one that your own app is now serving**. + +And similarly for ReDoc... + +```Python hl_lines="2-6 14-22 25-27 30-36" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +!!! tip + The *path operation* for `swagger_ui_redirect` is a helper for when you use OAuth2. + + If you integrate your API with an OAuth2 provider, you will be able to authenticate and come back to the API docs with the acquired credentials. And interact with it using the real OAuth2 authentication. + + Swagger UI will handle it behind the scenes for you, but it needs this "redirect" helper. + +### Create a *path operation* to test static files + +Now, to be able to test that everything works, create a *path operation*: + +```Python hl_lines="39-41" +{!../../../docs_src/custom_docs_ui/tutorial002.py!} +``` + +### Test Static Files UI + +Now, you should be able to disconnect your WiFi, go to your docs at http://127.0.0.1:8000/docs, and reload the page. + +And even without Internet, you would be able to see the docs for your API and interact with it. diff --git a/docs/en/docs/advanced/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md similarity index 100% rename from docs/en/docs/advanced/custom-request-and-route.md rename to docs/en/docs/how-to/custom-request-and-route.md diff --git a/docs/en/docs/how-to/extending-openapi.md b/docs/en/docs/how-to/extending-openapi.md new file mode 100644 index 000000000..a18fd737e --- /dev/null +++ b/docs/en/docs/how-to/extending-openapi.md @@ -0,0 +1,87 @@ +# Extending OpenAPI + +There are some cases where you might need to modify the generated OpenAPI schema. + +In this section you will see how. + +## The normal process + +The normal (default) process, is as follows. + +A `FastAPI` application (instance) has an `.openapi()` method that is expected to return the OpenAPI schema. + +As part of the application object creation, a *path operation* for `/openapi.json` (or for whatever you set your `openapi_url`) is registered. + +It just returns a JSON response with the result of the application's `.openapi()` method. + +By default, what the method `.openapi()` does is check the property `.openapi_schema` to see if it has contents and return them. + +If it doesn't, it generates them using the utility function at `fastapi.openapi.utils.get_openapi`. + +And that function `get_openapi()` receives as parameters: + +* `title`: The OpenAPI title, shown in the docs. +* `version`: The version of your API, e.g. `2.5.0`. +* `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.1.0`. +* `summary`: A short summary of the API. +* `description`: The description of your API, this can include markdown and will be shown in the docs. +* `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`. + +!!! info + The parameter `summary` is available in OpenAPI 3.1.0 and above, supported by FastAPI 0.99.0 and above. + +## Overriding the defaults + +Using the information above, you can use the same utility function to generate the OpenAPI schema and override each part that you need. + +For example, let's add ReDoc's OpenAPI extension to include a custom logo. + +### Normal **FastAPI** + +First, write all your **FastAPI** application as normally: + +```Python hl_lines="1 4 7-9" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Generate the OpenAPI schema + +Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function: + +```Python hl_lines="2 15-21" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Modify the OpenAPI schema + +Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema: + +```Python hl_lines="22-24" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Cache the OpenAPI schema + +You can use the property `.openapi_schema` as a "cache", to store your generated schema. + +That way, your application won't have to generate the schema every time a user opens your API docs. + +It will be generated only once, and then the same cached schema will be used for the next requests. + +```Python hl_lines="13-14 25-26" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Override the method + +Now you can replace the `.openapi()` method with your new function. + +```Python hl_lines="29" +{!../../../docs_src/extending_openapi/tutorial001.py!} +``` + +### Check it + +Once you go to http://127.0.0.1:8000/redoc you will see that you are using your custom logo (in this example, **FastAPI**'s logo): + + diff --git a/docs/en/docs/how-to/general.md b/docs/en/docs/how-to/general.md new file mode 100644 index 000000000..04367c6b7 --- /dev/null +++ b/docs/en/docs/how-to/general.md @@ -0,0 +1,39 @@ +# General - How To - Recipes + +Here are several pointers to other places in the docs, for general or frequent questions. + +## Filter Data - Security + +To ensure that you don't return more data than you should, read the docs for [Tutorial - Response Model - Return Type](../tutorial/response-model.md){.internal-link target=_blank}. + +## Documentation Tags - OpenAPI + +To add tags to your *path operations*, and group them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Tags](../tutorial/path-operation-configuration.md#tags){.internal-link target=_blank}. + +## Documentation Summary and Description - OpenAPI + +To add a summary and description to your *path operations*, and show them in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Summary and Description](../tutorial/path-operation-configuration.md#summary-and-description){.internal-link target=_blank}. + +## Documentation Response description - OpenAPI + +To define the description of the response, shown in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Response description](../tutorial/path-operation-configuration.md#response-description){.internal-link target=_blank}. + +## Documentation Deprecate a *Path Operation* - OpenAPI + +To deprecate a *path operation*, and show it in the docs UI, read the docs for [Tutorial - Path Operation Configurations - Deprecation](../tutorial/path-operation-configuration.md#deprecate-a-path-operation){.internal-link target=_blank}. + +## Convert any Data to JSON-compatible + +To convert any data to JSON-compatible, read the docs for [Tutorial - JSON Compatible Encoder](../tutorial/encoder.md){.internal-link target=_blank}. + +## OpenAPI Metadata - Docs + +To add metadata to your OpenAPI schema, including a license, version, contact, etc, read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md){.internal-link target=_blank}. + +## OpenAPI Custom URL + +To customize the OpenAPI URL (or remove it), read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#openapi-url){.internal-link target=_blank}. + +## OpenAPI Docs URLs + +To update the URLs used for the automatically generated docs user interfaces, read the docs for [Tutorial - Metadata and Docs URLs](../tutorial/metadata.md#docs-urls){.internal-link target=_blank}. diff --git a/docs/en/docs/advanced/graphql.md b/docs/en/docs/how-to/graphql.md similarity index 100% rename from docs/en/docs/advanced/graphql.md rename to docs/en/docs/how-to/graphql.md diff --git a/docs/en/docs/how-to/index.md b/docs/en/docs/how-to/index.md new file mode 100644 index 000000000..ec7fd38f8 --- /dev/null +++ b/docs/en/docs/how-to/index.md @@ -0,0 +1,11 @@ +# How To - Recipes + +Here you will see different recipes or "how to" guides for **several topics**. + +Most of these ideas would be more or less **independent**, and in most cases you should only need to study them if they apply directly to **your project**. + +If something seems interesting and useful to your project, go ahead and check it, but otherwise, you might probably just skip them. + +!!! tip + + If you want to **learn FastAPI** in a structured way (recommended), go and read the [Tutorial - User Guide](../tutorial/index.md){.internal-link target=_blank} chapter by chapter instead. diff --git a/docs/en/docs/advanced/nosql-databases.md b/docs/en/docs/how-to/nosql-databases-couchbase.md similarity index 99% rename from docs/en/docs/advanced/nosql-databases.md rename to docs/en/docs/how-to/nosql-databases-couchbase.md index 606db35c7..ae6ad604b 100644 --- a/docs/en/docs/advanced/nosql-databases.md +++ b/docs/en/docs/how-to/nosql-databases-couchbase.md @@ -1,4 +1,4 @@ -# NoSQL (Distributed / Big Data) Databases +# NoSQL (Distributed / Big Data) Databases with Couchbase !!! info These docs are about to be updated. 🎉 diff --git a/docs/en/docs/advanced/sql-databases-peewee.md b/docs/en/docs/how-to/sql-databases-peewee.md similarity index 99% rename from docs/en/docs/advanced/sql-databases-peewee.md rename to docs/en/docs/how-to/sql-databases-peewee.md index 6a469634f..bf2f2e714 100644 --- a/docs/en/docs/advanced/sql-databases-peewee.md +++ b/docs/en/docs/how-to/sql-databases-peewee.md @@ -12,6 +12,8 @@ Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes. + The examples here are no longer tested in CI (as they were before). + If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM. If you already have a code base that uses Peewee ORM, you can check here how to use it with **FastAPI**. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 22babe745..f75b84ff5 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -45,6 +45,13 @@ plugins: redirects: redirect_maps: deployment/deta.md: deployment/cloud.md + advanced/sql-databases-peewee.md: how-to/sql-databases-peewee.md + advanced/async-sql-databases.md: how-to/async-sql-encode-databases.md + advanced/nosql-databases.md: how-to/nosql-databases-couchbase.md + advanced/graphql.md: how-to/graphql.md + advanced/custom-request-and-route.md: how-to/custom-request-and-route.md + advanced/conditional-openapi.md: how-to/conditional-openapi.md + advanced/extending-openapi.md: how-to/extending-openapi.md nav: - FastAPI: index.md - Languages: @@ -134,24 +141,17 @@ nav: - advanced/using-request-directly.md - advanced/dataclasses.md - advanced/middleware.md - - advanced/sql-databases-peewee.md - - advanced/async-sql-databases.md - - advanced/nosql-databases.md - advanced/sub-applications.md - advanced/behind-a-proxy.md - advanced/templates.md - - advanced/graphql.md - advanced/websockets.md - advanced/events.md - - advanced/custom-request-and-route.md - advanced/testing-websockets.md - advanced/testing-events.md - advanced/testing-dependencies.md - advanced/testing-database.md - advanced/async-tests.md - advanced/settings.md - - advanced/conditional-openapi.md - - advanced/extending-openapi.md - advanced/openapi-callbacks.md - advanced/openapi-webhooks.md - advanced/wsgi.md @@ -166,6 +166,18 @@ nav: - deployment/cloud.md - deployment/server-workers.md - deployment/docker.md +- How To - Recipes: + - how-to/index.md + - how-to/general.md + - how-to/sql-databases-peewee.md + - how-to/async-sql-encode-databases.md + - how-to/nosql-databases-couchbase.md + - how-to/graphql.md + - how-to/custom-request-and-route.md + - how-to/conditional-openapi.md + - how-to/extending-openapi.md + - how-to/custom-docs-ui-assets.md + - how-to/configure-swagger-ui.md - project-generation.md - alternatives.md - history-design-future.md diff --git a/docs/ja/docs/advanced/conditional-openapi.md b/docs/ja/docs/how-to/conditional-openapi.md similarity index 100% rename from docs/ja/docs/advanced/conditional-openapi.md rename to docs/ja/docs/how-to/conditional-openapi.md diff --git a/docs_src/extending_openapi/tutorial003.py b/docs_src/configure_swagger_ui/tutorial001.py similarity index 100% rename from docs_src/extending_openapi/tutorial003.py rename to docs_src/configure_swagger_ui/tutorial001.py diff --git a/docs_src/extending_openapi/tutorial004.py b/docs_src/configure_swagger_ui/tutorial002.py similarity index 100% rename from docs_src/extending_openapi/tutorial004.py rename to docs_src/configure_swagger_ui/tutorial002.py diff --git a/docs_src/extending_openapi/tutorial005.py b/docs_src/configure_swagger_ui/tutorial003.py similarity index 100% rename from docs_src/extending_openapi/tutorial005.py rename to docs_src/configure_swagger_ui/tutorial003.py diff --git a/docs_src/custom_docs_ui/tutorial001.py b/docs_src/custom_docs_ui/tutorial001.py new file mode 100644 index 000000000..f7ceb0c2f --- /dev/null +++ b/docs_src/custom_docs_ui/tutorial001.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI +from fastapi.openapi.docs import ( + get_redoc_html, + get_swagger_ui_html, + get_swagger_ui_oauth2_redirect_html, +) + +app = FastAPI(docs_url=None, redoc_url=None) + + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=app.title + " - Swagger UI", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js", + swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css", + ) + + +@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) +async def swagger_ui_redirect(): + return get_swagger_ui_oauth2_redirect_html() + + +@app.get("/redoc", include_in_schema=False) +async def redoc_html(): + return get_redoc_html( + openapi_url=app.openapi_url, + title=app.title + " - ReDoc", + redoc_js_url="https://unpkg.com/redoc@next/bundles/redoc.standalone.js", + ) + + +@app.get("/users/{username}") +async def read_user(username: str): + return {"message": f"Hello {username}"} diff --git a/docs_src/extending_openapi/tutorial002.py b/docs_src/custom_docs_ui/tutorial002.py similarity index 100% rename from docs_src/extending_openapi/tutorial002.py rename to docs_src/custom_docs_ui/tutorial002.py diff --git a/requirements-tests.txt b/requirements-tests.txt index 0113b6f7a..6f7f4ac23 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -11,7 +11,6 @@ dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy # probably when including SQLModel sqlalchemy >=1.3.18,<1.4.43 -peewee >=3.13.3,<4.0.0 databases[sqlite] >=0.3.2,<0.7.0 orjson >=3.2.1,<4.0.0 ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0 diff --git a/docs_src/sql_databases_peewee/__init__.py b/tests/test_tutorial/test_configure_swagger_ui/__init__.py similarity index 100% rename from docs_src/sql_databases_peewee/__init__.py rename to tests/test_tutorial/test_configure_swagger_ui/__init__.py diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py similarity index 95% rename from tests/test_tutorial/test_extending_openapi/test_tutorial003.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py index 0184dd9f8..72db54bd2 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial003.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial001.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial003 import app +from docs_src.configure_swagger_ui.tutorial001 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py similarity index 96% rename from tests/test_tutorial/test_extending_openapi/test_tutorial004.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py index 4f7615126..166901188 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial004.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial002.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial004 import app +from docs_src.configure_swagger_ui.tutorial002 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py similarity index 96% rename from tests/test_tutorial/test_extending_openapi/test_tutorial005.py rename to tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py index 24aeb93db..187e89ace 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial005.py +++ b/tests/test_tutorial/test_configure_swagger_ui/test_tutorial003.py @@ -1,6 +1,6 @@ from fastapi.testclient import TestClient -from docs_src.extending_openapi.tutorial005 import app +from docs_src.configure_swagger_ui.tutorial003 import app client = TestClient(app) diff --git a/tests/test_tutorial/test_sql_databases_peewee/__init__.py b/tests/test_tutorial/test_custom_docs_ui/__init__.py similarity index 100% rename from tests/test_tutorial/test_sql_databases_peewee/__init__.py rename to tests/test_tutorial/test_custom_docs_ui/__init__.py diff --git a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py new file mode 100644 index 000000000..aff070d74 --- /dev/null +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py @@ -0,0 +1,42 @@ +import os +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + static_dir: Path = Path(os.getcwd()) / "static" + print(static_dir) + static_dir.mkdir(exist_ok=True) + from docs_src.custom_docs_ui.tutorial001 import app + + with TestClient(app) as client: + yield client + static_dir.rmdir() + + +def test_swagger_ui_html(client: TestClient): + response = client.get("/docs") + assert response.status_code == 200, response.text + assert "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" in response.text + assert "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" in response.text + + +def test_swagger_ui_oauth2_redirect_html(client: TestClient): + response = client.get("/docs/oauth2-redirect") + assert response.status_code == 200, response.text + assert "window.opener.swaggerUIRedirectOauth2" in response.text + + +def test_redoc_html(client: TestClient): + response = client.get("/redoc") + assert response.status_code == 200, response.text + assert "https://unpkg.com/redoc@next/bundles/redoc.standalone.js" in response.text + + +def test_api(client: TestClient): + response = client.get("/users/john") + assert response.status_code == 200, response.text + assert response.json()["message"] == "Hello john" diff --git a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py similarity index 95% rename from tests/test_tutorial/test_extending_openapi/test_tutorial002.py rename to tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py index 654db2e4c..712618807 100644 --- a/tests/test_tutorial/test_extending_openapi/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial002.py @@ -10,7 +10,7 @@ def client(): static_dir: Path = Path(os.getcwd()) / "static" print(static_dir) static_dir.mkdir(exist_ok=True) - from docs_src.extending_openapi.tutorial002 import app + from docs_src.custom_docs_ui.tutorial002 import app with TestClient(app) as client: yield client diff --git a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py b/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py deleted file mode 100644 index 4350567d1..000000000 --- a/tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py +++ /dev/null @@ -1,454 +0,0 @@ -import time -from pathlib import Path -from unittest.mock import MagicMock - -import pytest -from fastapi.testclient import TestClient - -from ...utils import needs_pydanticv1 - - -@pytest.fixture(scope="module") -def client(): - # Import while creating the client to create the DB after starting the test session - from docs_src.sql_databases_peewee.sql_app.main import app - - test_db = Path("./test.db") - with TestClient(app) as c: - yield c - test_db.unlink() - - -@needs_pydanticv1 -def test_create_user(client): - test_user = {"email": "johndoe@example.com", "password": "secret"} - response = client.post("/users/", json=test_user) - assert response.status_code == 200, response.text - data = response.json() - assert test_user["email"] == data["email"] - assert "id" in data - response = client.post("/users/", json=test_user) - assert response.status_code == 400, response.text - - -@needs_pydanticv1 -def test_get_user(client): - response = client.get("/users/1") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data - assert "id" in data - - -@needs_pydanticv1 -def test_inexistent_user(client): - response = client.get("/users/999") - assert response.status_code == 404, response.text - - -@needs_pydanticv1 -def test_get_users(client): - response = client.get("/users/") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data[0] - assert "id" in data[0] - - -time.sleep = MagicMock() - - -@needs_pydanticv1 -def test_get_slowusers(client): - response = client.get("/slowusers/") - assert response.status_code == 200, response.text - data = response.json() - assert "email" in data[0] - assert "id" in data[0] - - -@needs_pydanticv1 -def test_create_item(client): - item = {"title": "Foo", "description": "Something that fights"} - response = client.post("/users/1/items/", json=item) - assert response.status_code == 200, response.text - item_data = response.json() - assert item["title"] == item_data["title"] - assert item["description"] == item_data["description"] - assert "id" in item_data - assert "owner_id" in item_data - response = client.get("/users/1") - assert response.status_code == 200, response.text - user_data = response.json() - item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0] - assert item_to_check["title"] == item["title"] - assert item_to_check["description"] == item["description"] - response = client.get("/users/1") - assert response.status_code == 200, response.text - user_data = response.json() - item_to_check = [it for it in user_data["items"] if it["id"] == item_data["id"]][0] - assert item_to_check["title"] == item["title"] - assert item_to_check["description"] == item["description"] - - -@needs_pydanticv1 -def test_read_items(client): - response = client.get("/items/") - assert response.status_code == 200, response.text - data = response.json() - assert data - first_item = data[0] - assert "title" in first_item - assert "description" in first_item - - -@needs_pydanticv1 -def test_openapi_schema(client): - response = client.get("/openapi.json") - assert response.status_code == 200, response.text - assert response.json() == { - "openapi": "3.1.0", - "info": {"title": "FastAPI", "version": "0.1.0"}, - "paths": { - "/users/": { - "get": { - "summary": "Read Users", - "operationId": "read_users_users__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Users Users Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - "post": { - "summary": "Create User", - "operationId": "create_user_users__post", - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/UserCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - }, - }, - "/users/{user_id}": { - "get": { - "summary": "Read User", - "operationId": "read_user_users__user_id__get", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/User"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/users/{user_id}/items/": { - "post": { - "summary": "Create Item For User", - "operationId": "create_item_for_user_users__user_id__items__post", - "parameters": [ - { - "required": True, - "schema": {"title": "User Id", "type": "integer"}, - "name": "user_id", - "in": "path", - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemCreate"} - } - }, - "required": True, - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/Item"} - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/items/": { - "get": { - "summary": "Read Items", - "operationId": "read_items_items__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Items Items Get", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - "/slowusers/": { - "get": { - "summary": "Read Slow Users", - "operationId": "read_slow_users_slowusers__get", - "parameters": [ - { - "required": False, - "schema": { - "title": "Skip", - "type": "integer", - "default": 0, - }, - "name": "skip", - "in": "query", - }, - { - "required": False, - "schema": { - "title": "Limit", - "type": "integer", - "default": 100, - }, - "name": "limit", - "in": "query", - }, - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Read Slow Users Slowusers Get", - "type": "array", - "items": {"$ref": "#/components/schemas/User"}, - } - } - }, - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - }, - }, - } - }, - }, - "components": { - "schemas": { - "HTTPValidationError": { - "title": "HTTPValidationError", - "type": "object", - "properties": { - "detail": { - "title": "Detail", - "type": "array", - "items": {"$ref": "#/components/schemas/ValidationError"}, - } - }, - }, - "Item": { - "title": "Item", - "required": ["title", "id", "owner_id"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "owner_id": {"title": "Owner Id", "type": "integer"}, - }, - }, - "ItemCreate": { - "title": "ItemCreate", - "required": ["title"], - "type": "object", - "properties": { - "title": {"title": "Title", "type": "string"}, - "description": {"title": "Description", "type": "string"}, - }, - }, - "User": { - "title": "User", - "required": ["email", "id", "is_active"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "id": {"title": "Id", "type": "integer"}, - "is_active": {"title": "Is Active", "type": "boolean"}, - "items": { - "title": "Items", - "type": "array", - "items": {"$ref": "#/components/schemas/Item"}, - "default": [], - }, - }, - }, - "UserCreate": { - "title": "UserCreate", - "required": ["email", "password"], - "type": "object", - "properties": { - "email": {"title": "Email", "type": "string"}, - "password": {"title": "Password", "type": "string"}, - }, - }, - "ValidationError": { - "title": "ValidationError", - "required": ["loc", "msg", "type"], - "type": "object", - "properties": { - "loc": { - "title": "Location", - "type": "array", - "items": { - "anyOf": [{"type": "string"}, {"type": "integer"}] - }, - }, - "msg": {"title": "Message", "type": "string"}, - "type": {"title": "Error Type", "type": "string"}, - }, - }, - } - }, - } From 10a127ea4adfe59b7d275fa0bc764b23910aea5c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 19 Aug 2023 19:54:40 +0000 Subject: [PATCH 1284/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 48a809021..61ec91b4a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add new docs section, How To - Recipes, move docs that don't have to be read by everyone to How To. PR [#10114](https://github.com/tiangolo/fastapi/pull/10114) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor tests for new Pydantic 2.2.1. PR [#10115](https://github.com/tiangolo/fastapi/pull/10115) by [@tiangolo](https://github.com/tiangolo). * 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). From ea43f227e5c7128c09e3e2d5dceea212552b19dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 25 Aug 2023 21:10:22 +0200 Subject: [PATCH 1285/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20dis?= =?UTF-8?q?abling=20the=20separation=20of=20input=20and=20output=20JSON=20?= =?UTF-8?q?Schemas=20in=20OpenAPI=20with=20Pydantic=20v2=20(#10145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Add docs for Separate OpenAPI Schemas for Input and Output * 🔧 Add new docs page to MkDocs config * ✨ Add separate_input_output_schemas parameter to FastAPI class * 📝 Add source examples for separating OpenAPI schemas * ✅ Add tests for separated OpenAPI schemas * 📝 Add source examples for Python 3.10, 3.9, and 3.7+ * 📝 Update docs for Separate OpenAPI Schemas with new multi-version examples * ✅ Add and update tests for different Python versions * ✅ Add tests for corner cases with separate_input_output_schemas * 📝 Update tutorial to use Union instead of Optional * 🐛 Fix type annotations * 🐛 Fix correct import in test * 💄 Add CSS to simulate browser windows for screenshots * ➕ Add playwright as a dev dependency to automate generating screenshots * 🔨 Add Playwright scripts to generate screenshots for new docs * 📝 Update docs, tweak text to match screenshots * 🍱 Add screenshots for new docs --- docs/en/docs/css/custom.css | 36 ++ .../docs/how-to/separate-openapi-schemas.md | 228 ++++++++ .../separate-openapi-schemas/image01.png | Bin 0 -> 81548 bytes .../separate-openapi-schemas/image02.png | Bin 0 -> 92417 bytes .../separate-openapi-schemas/image03.png | Bin 0 -> 85171 bytes .../separate-openapi-schemas/image04.png | Bin 0 -> 58285 bytes .../separate-openapi-schemas/image05.png | Bin 0 -> 45618 bytes docs/en/mkdocs.yml | 1 + .../separate_openapi_schemas/tutorial001.py | 28 + .../tutorial001_py310.py | 26 + .../tutorial001_py39.py | 28 + .../separate_openapi_schemas/tutorial002.py | 28 + .../tutorial002_py310.py | 26 + .../tutorial002_py39.py | 28 + fastapi/_compat.py | 15 +- fastapi/applications.py | 3 + fastapi/openapi/utils.py | 14 + requirements.txt | 2 + .../separate_openapi_schemas/image01.py | 29 ++ .../separate_openapi_schemas/image02.py | 30 ++ .../separate_openapi_schemas/image03.py | 30 ++ .../separate_openapi_schemas/image04.py | 29 ++ .../separate_openapi_schemas/image05.py | 29 ++ ...t_openapi_separate_input_output_schemas.py | 490 ++++++++++++++++++ .../test_separate_openapi_schemas/__init__.py | 0 .../test_tutorial001.py | 147 ++++++ .../test_tutorial001_py310.py | 150 ++++++ .../test_tutorial001_py39.py | 150 ++++++ .../test_tutorial002.py | 133 +++++ .../test_tutorial002_py310.py | 136 +++++ .../test_tutorial002_py39.py | 136 +++++ 31 files changed, 1950 insertions(+), 2 deletions(-) create mode 100644 docs/en/docs/how-to/separate-openapi-schemas.md create mode 100644 docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png create mode 100644 docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png create mode 100644 docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png create mode 100644 docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png create mode 100644 docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png create mode 100644 docs_src/separate_openapi_schemas/tutorial001.py create mode 100644 docs_src/separate_openapi_schemas/tutorial001_py310.py create mode 100644 docs_src/separate_openapi_schemas/tutorial001_py39.py create mode 100644 docs_src/separate_openapi_schemas/tutorial002.py create mode 100644 docs_src/separate_openapi_schemas/tutorial002_py310.py create mode 100644 docs_src/separate_openapi_schemas/tutorial002_py39.py create mode 100644 scripts/playwright/separate_openapi_schemas/image01.py create mode 100644 scripts/playwright/separate_openapi_schemas/image02.py create mode 100644 scripts/playwright/separate_openapi_schemas/image03.py create mode 100644 scripts/playwright/separate_openapi_schemas/image04.py create mode 100644 scripts/playwright/separate_openapi_schemas/image05.py create mode 100644 tests/test_openapi_separate_input_output_schemas.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/__init__.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py create mode 100644 tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 066b51725..187040792 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -144,3 +144,39 @@ code { margin-top: 2em; margin-bottom: 2em; } + +/* Screenshots */ +/* +Simulate a browser window frame. +Inspired by Termynal's CSS tricks with modifications +*/ + +.screenshot { + display: block; + background-color: #d3e0de; + border-radius: 4px; + padding: 45px 5px 5px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.screenshot img { + display: block; + border-radius: 2px; +} + +.screenshot:before { + content: ''; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; +} diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md new file mode 100644 index 000000000..39d96ea39 --- /dev/null +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -0,0 +1,228 @@ +# Separate OpenAPI Schemas for Input and Output or Not + +When using **Pydantic v2**, the generated OpenAPI is a bit more exact and **correct** than before. 😎 + +In fact, in some cases, it will even have **two JSON Schemas** in OpenAPI for the same Pydantic model, for input and output, depending on if they have **default values**. + +Let's see how that works and how to change it if you need to do that. + +## Pydantic Models for Input and Output + +Let's say you have a Pydantic model with default values, like this one: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-7]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +
+ +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-9]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +
+ +=== "Python 3.7+" + + ```Python hl_lines="9" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-9]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +
+ +### Model for Input + +If you use this model as an input like here: + +=== "Python 3.10+" + + ```Python hl_lines="14" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py[ln:1-15]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +
+ +=== "Python 3.9+" + + ```Python hl_lines="16" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py[ln:1-17]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +
+ +=== "Python 3.7+" + + ```Python hl_lines="16" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-17]!} + + # Code below omitted 👇 + ``` + +
+ 👀 Full file preview + + ```Python + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +
+ +...then the `description` field will **not be required**. Because it has a default value of `None`. + +### Input Model in Docs + +You can confirm that in the docs, the `description` field doesn't have a **red asterisk**, it's not marked as required: + +
+ +
+ +### Model for Output + +But if you use the same model as an output, like here: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="21" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} + ``` + +=== "Python 3.7+" + + ```Python hl_lines="21" + {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} + ``` + +...then because `description` has a default value, if you **don't return anything** for that field, it will still have that **default value**. + +### Model for Output Response Data + +If you interact with the docs and check the response, even though the code didn't add anything in one of the `description` fields, the JSON response contains the default value (`null`): + +
+ +
+ +This means that it will **always have a value**, it's just that sometimes the value could be `None` (or `null` in JSON). + +That means that, clients using your API don't have to check if the value exists or not, they can **asume the field will always be there**, but just that in some cases it will have the default value of `None`. + +The way to describe this in OpenAPI, is to mark that field as **required**, because it will always be there. + +Because of that, the JSON Schema for a model can be different depending on if it's used for **input or output**: + +* for **input** the `description` will **not be required** +* for **output** it will be **required** (and possibly `None`, or in JSON terms, `null`) + +### Model for Output in Docs + +You can check the output model in the docs too, **both** `name` and `description` are marked as **required** with a **red asterisk**: + +
+ +
+ +### Model for Input and Output in Docs + +And if you check all the available Schemas (JSON Schemas) in OpenAPI, you will see that there are two, one `Item-Input` and one `Item-Output`. + +For `Item-Input`, `description` is **not required**, it doesn't have a red asterisk. + +But for `Item-Output`, `description` is **required**, it has a red asterisk. + +
+ +
+ +With this feature from **Pydantic v2**, your API documentation is more **precise**, and if you have autogenerated clients and SDKs, they will be more precise too, with a better **developer experience** and consistency. 🎉 + +## Do not Separate Schemas + +Now, there are some cases where you might want to have the **same schema for input and output**. + +Probably the main use case for this is if you already have some autogenerated client code/SDKs and you don't want to update all the autogenerated client code/SDKs yet, you probably will want to do it at some point, but maybe not right now. + +In that case, you can disable this feature in **FastAPI**, with the parameter `separate_input_output_schemas=False`. + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py39.py!} + ``` + +=== "Python 3.7+" + + ```Python hl_lines="12" + {!> ../../../docs_src/separate_openapi_schemas/tutorial002.py!} + ``` + +### Same Schema for Input and Output Models in Docs + +And now there will be one single schema for input and output for the model, only `Item`, and it will have `description` as **not required**: + +
+ +
+ +This is the same behavior as in Pydantic v1. 🤓 diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png new file mode 100644 index 0000000000000000000000000000000000000000..aa085f88df45e53aaddb11e35b346075eeff2670 GIT binary patch literal 81548 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¥b$&#Qf4~ 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png new file mode 100644 index 0000000000000000000000000000000000000000..672ef1d2b71df96e9046fd3080ba5d2837721e41 GIT binary patch literal 92417 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%0A=&HIJw6ofo2IQv*&ppKc@i)9TDSQ1L41B(-m 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 ZmdSXHb5bb@l<>c~vlh+_pKh`HKLGA#a;E?Q literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png new file mode 100644 index 0000000000000000000000000000000000000000..81340fbece9420cb01981aeb401fc3a6f532e047 GIT binary patch literal 85171 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png new file mode 100644 index 0000000000000000000000000000000000000000..fc2302aa77ce27ef23caeb7ef0144725c919738a GIT binary patch literal 58285 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png b/docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png new file mode 100644 index 0000000000000000000000000000000000000000..674dd0b2e273078ffea526c0dd8572547a4c04f2 GIT binary patch literal 45618 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 z List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial001_py310.py b/docs_src/separate_openapi_schemas/tutorial001_py310.py new file mode 100644 index 000000000..289cb54ed --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial001_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +app = FastAPI() + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial001_py39.py b/docs_src/separate_openapi_schemas/tutorial001_py39.py new file mode 100644 index 000000000..63cffd1e3 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial001_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Optional[str] = None + + +app = FastAPI() + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002.py b/docs_src/separate_openapi_schemas/tutorial002.py new file mode 100644 index 000000000..7df93783b --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002.py @@ -0,0 +1,28 @@ +from typing import List, Union + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002_py310.py b/docs_src/separate_openapi_schemas/tutorial002_py310.py new file mode 100644 index 000000000..5db210872 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002_py310.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: str | None = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/docs_src/separate_openapi_schemas/tutorial002_py39.py b/docs_src/separate_openapi_schemas/tutorial002_py39.py new file mode 100644 index 000000000..50d997d92 --- /dev/null +++ b/docs_src/separate_openapi_schemas/tutorial002_py39.py @@ -0,0 +1,28 @@ +from typing import Optional + +from fastapi import FastAPI +from pydantic import BaseModel + + +class Item(BaseModel): + name: str + description: Optional[str] = None + + +app = FastAPI(separate_input_output_schemas=False) + + +@app.post("/items/") +def create_item(item: Item): + return item + + +@app.get("/items/") +def read_items() -> list[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + ), + Item(name="Plumbus"), + ] diff --git a/fastapi/_compat.py b/fastapi/_compat.py index 9ffcaf409..eb55b08f2 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -181,9 +181,13 @@ if PYDANTIC_V2: field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], + separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) # This expects that GenerateJsonSchema was already used to generate the definitions - json_schema = field_mapping[(field, field.mode)] + json_schema = field_mapping[(field, override_mode or field.mode)] if "$ref" not in json_schema: # TODO remove when deprecating Pydantic v1 # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 @@ -200,14 +204,19 @@ if PYDANTIC_V2: fields: List[ModelField], schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, ) -> Tuple[ Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], Dict[str, Dict[str, Any]], ]: + override_mode: Union[Literal["validation"], None] = ( + None if separate_input_output_schemas else "validation" + ) inputs = [ - (field, field.mode, field._type_adapter.core_schema) for field in fields + (field, override_mode or field.mode, field._type_adapter.core_schema) + for field in fields ] field_mapping, definitions = schema_generator.generate_definitions( inputs=inputs @@ -429,6 +438,7 @@ else: field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], + separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: # This expects that GenerateJsonSchema was already used to generate the definitions return field_schema( # type: ignore[no-any-return] @@ -444,6 +454,7 @@ else: fields: List[ModelField], schema_generator: GenerateJsonSchema, model_name_map: ModelNameMap, + separate_input_output_schemas: bool = True, ) -> Tuple[ Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue diff --git a/fastapi/applications.py b/fastapi/applications.py index e32cfa03d..b681e50b3 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -92,6 +92,7 @@ class FastAPI(Starlette): generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), + separate_input_output_schemas: bool = True, **extra: Any, ) -> None: self.debug = debug @@ -111,6 +112,7 @@ class FastAPI(Starlette): self.swagger_ui_init_oauth = swagger_ui_init_oauth self.swagger_ui_parameters = swagger_ui_parameters self.servers = servers or [] + self.separate_input_output_schemas = separate_input_output_schemas self.extra = extra self.openapi_version = "3.1.0" self.openapi_schema: Optional[Dict[str, Any]] = None @@ -227,6 +229,7 @@ class FastAPI(Starlette): webhooks=self.webhooks.routes, tags=self.openapi_tags, servers=self.servers, + separate_input_output_schemas=self.separate_input_output_schemas, ) return self.openapi_schema diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index e295361e6..9498375fe 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -95,6 +95,7 @@ def get_openapi_operation_parameters( field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], + separate_input_output_schemas: bool = True, ) -> List[Dict[str, Any]]: parameters = [] for param in all_route_params: @@ -107,6 +108,7 @@ def get_openapi_operation_parameters( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) parameter = { "name": param.alias, @@ -132,6 +134,7 @@ def get_openapi_operation_request_body( field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], + separate_input_output_schemas: bool = True, ) -> Optional[Dict[str, Any]]: if not body_field: return None @@ -141,6 +144,7 @@ def get_openapi_operation_request_body( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) field_info = cast(Body, body_field.field_info) request_media_type = field_info.media_type @@ -211,6 +215,7 @@ def get_openapi_path( field_mapping: Dict[ Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue ], + separate_input_output_schemas: bool = True, ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]: path = {} security_schemes: Dict[str, Any] = {} @@ -242,6 +247,7 @@ def get_openapi_path( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) parameters.extend(operation_parameters) if parameters: @@ -263,6 +269,7 @@ def get_openapi_path( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) if request_body_oai: operation["requestBody"] = request_body_oai @@ -280,6 +287,7 @@ def get_openapi_path( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) callbacks[callback.name] = {callback.path: cb_path} operation["callbacks"] = callbacks @@ -310,6 +318,7 @@ def get_openapi_path( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) else: response_schema = {} @@ -343,6 +352,7 @@ def get_openapi_path( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) media_type = route_response_media_type or "application/json" additional_schema = ( @@ -433,6 +443,7 @@ def get_openapi( terms_of_service: Optional[str] = None, contact: Optional[Dict[str, Union[str, Any]]] = None, license_info: Optional[Dict[str, Union[str, Any]]] = None, + separate_input_output_schemas: bool = True, ) -> Dict[str, Any]: info: Dict[str, Any] = {"title": title, "version": version} if summary: @@ -459,6 +470,7 @@ def get_openapi( fields=all_fields, schema_generator=schema_generator, model_name_map=model_name_map, + separate_input_output_schemas=separate_input_output_schemas, ) for route in routes or []: if isinstance(route, routing.APIRoute): @@ -468,6 +480,7 @@ def get_openapi( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) if result: path, security_schemes, path_definitions = result @@ -487,6 +500,7 @@ def get_openapi( schema_generator=schema_generator, model_name_map=model_name_map, field_mapping=field_mapping, + separate_input_output_schemas=separate_input_output_schemas, ) if result: path, security_schemes, path_definitions = result diff --git a/requirements.txt b/requirements.txt index 7e746016a..ef25ec483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ -r requirements-docs.txt uvicorn[standard] >=0.12.0,<0.23.0 pre-commit >=2.17.0,<4.0.0 +# For generating screenshots +playwright diff --git a/scripts/playwright/separate_openapi_schemas/image01.py b/scripts/playwright/separate_openapi_schemas/image01.py new file mode 100644 index 000000000..0b40f3bbc --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image01.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("POST/items/Create Item").click() + page.get_by_role("tab", name="Schema").first.click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image01.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image02.py b/scripts/playwright/separate_openapi_schemas/image02.py new file mode 100644 index 000000000..f76af7ee2 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image02.py @@ -0,0 +1,30 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("GET/items/Read Items").click() + page.get_by_role("button", name="Try it out").click() + page.get_by_role("button", name="Execute").click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image02.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image03.py b/scripts/playwright/separate_openapi_schemas/image03.py new file mode 100644 index 000000000..127f5c428 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image03.py @@ -0,0 +1,30 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_text("GET/items/Read Items").click() + page.get_by_role("tab", name="Schema").click() + page.get_by_label("Schema").get_by_role("button", name="Expand all").click() + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image03.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image04.py b/scripts/playwright/separate_openapi_schemas/image04.py new file mode 100644 index 000000000..208eaf8a0 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image04.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="Item-Input").click() + page.get_by_role("button", name="Item-Output").click() + page.set_viewport_size({"width": 960, "height": 820}) + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image04.png" + ) + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial001:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/scripts/playwright/separate_openapi_schemas/image05.py b/scripts/playwright/separate_openapi_schemas/image05.py new file mode 100644 index 000000000..83966b449 --- /dev/null +++ b/scripts/playwright/separate_openapi_schemas/image05.py @@ -0,0 +1,29 @@ +import subprocess + +from playwright.sync_api import Playwright, sync_playwright + + +def run(playwright: Playwright) -> None: + browser = playwright.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 960, "height": 1080}) + page = context.new_page() + page.goto("http://localhost:8000/docs") + page.get_by_role("button", name="Item", exact=True).click() + page.set_viewport_size({"width": 960, "height": 700}) + page.screenshot( + path="docs/en/docs/img/tutorial/separate-openapi-schemas/image05.png" + ) + + # --------------------- + context.close() + browser.close() + + +process = subprocess.Popen( + ["uvicorn", "docs_src.separate_openapi_schemas.tutorial002:app"] +) +try: + with sync_playwright() as playwright: + run(playwright) +finally: + process.terminate() diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py new file mode 100644 index 000000000..70f4b90d7 --- /dev/null +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -0,0 +1,490 @@ +from typing import List, Optional + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel + +from .utils import needs_pydanticv2 + + +class SubItem(BaseModel): + subname: str + sub_description: Optional[str] = None + tags: List[str] = [] + + +class Item(BaseModel): + name: str + description: Optional[str] = None + sub: Optional[SubItem] = None + + +def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: + app = FastAPI(separate_input_output_schemas=separate_input_output_schemas) + + @app.post("/items/") + def create_item(item: Item): + return item + + @app.post("/items-list/") + def create_item_list(item: List[Item]): + return item + + @app.get("/items/") + def read_items() -> List[Item]: + return [ + Item( + name="Portal Gun", + description="Device to travel through the multi-rick-verse", + sub=SubItem(subname="subname"), + ), + Item(name="Plumbus"), + ] + + client = TestClient(app) + return client + + +def test_create_item(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.post("/items/", json={"name": "Plumbus"}) + response2 = client_no.post("/items/", json={"name": "Plumbus"}) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == {"name": "Plumbus", "description": None, "sub": None} + ) + + +def test_create_item_with_sub(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + data = { + "name": "Plumbus", + "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF"}, + } + response = client.post("/items/", json=data) + response2 = client_no.post("/items/", json=data) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == { + "name": "Plumbus", + "description": None, + "sub": {"subname": "SubPlumbus", "sub_description": "Sub WTF", "tags": []}, + } + ) + + +def test_create_item_list(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + data = [ + {"name": "Plumbus"}, + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + ] + response = client.post("/items-list/", json=data) + response2 = client_no.post("/items-list/", json=data) + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == [ + {"name": "Plumbus", "description": None, "sub": None}, + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + "sub": None, + }, + ] + ) + + +def test_read_items(): + client = get_app_client() + client_no = get_app_client(separate_input_output_schemas=False) + response = client.get("/items/") + response2 = client_no.get("/items/") + assert response.status_code == response2.status_code == 200, response.text + assert ( + response.json() + == response2.json() + == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + "sub": {"subname": "subname", "sub_description": None, "tags": []}, + }, + {"name": "Plumbus", "description": None, "sub": None}, + ] + ) + + +@needs_pydanticv2 +def test_openapi_schema(): + client = get_app_client() + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item-Input"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Input" + }, + "type": "array", + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Input"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem-Output"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name", "description", "sub"], + "title": "Item", + }, + "SubItem-Input": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "SubItem-Output": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname", "sub_description", "tags"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } + + +@needs_pydanticv2 +def test_openapi_schema_no_separate(): + client = get_app_client(separate_input_output_schemas=False) + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + }, + "/items-list/": { + "post": { + "summary": "Create Item List", + "operationId": "create_item_list_items_list__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Item", + } + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + "sub": { + "anyOf": [ + {"$ref": "#/components/schemas/SubItem"}, + {"type": "null"}, + ] + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "SubItem": { + "properties": { + "subname": {"type": "string", "title": "Subname"}, + "sub_description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Sub Description", + }, + "tags": { + "items": {"type": "string"}, + "type": "array", + "title": "Tags", + "default": [], + }, + }, + "type": "object", + "required": ["subname"], + "title": "SubItem", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/__init__.py b/tests/test_tutorial/test_separate_openapi_schemas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py new file mode 100644 index 000000000..8079c1134 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -0,0 +1,147 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001 import app + + client = TestClient(app) + return client + + +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item-Input"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name", "description"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py new file mode 100644 index 000000000..4fa98ccbe --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py @@ -0,0 +1,150 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py310 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item-Input"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name", "description"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py new file mode 100644 index 000000000..ad36582ed --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py @@ -0,0 +1,150 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial001_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py39 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Item-Output" + }, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item-Input"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item-Input": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "Item-Output": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name", "description"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py new file mode 100644 index 000000000..d2cf7945b --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002.py @@ -0,0 +1,133 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002 import app + + client = TestClient(app) + return client + + +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py new file mode 100644 index 000000000..89c9ce977 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py310.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py310, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py310 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py310 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py new file mode 100644 index 000000000..6ac3d8f79 --- /dev/null +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial002_py39.py @@ -0,0 +1,136 @@ +import pytest +from fastapi.testclient import TestClient + +from ...utils import needs_py39, needs_pydanticv2 + + +@pytest.fixture(name="client") +def get_client() -> TestClient: + from docs_src.separate_openapi_schemas.tutorial002_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_create_item(client: TestClient) -> None: + response = client.post("/items/", json={"name": "Foo"}) + assert response.status_code == 200, response.text + assert response.json() == {"name": "Foo", "description": None} + + +@needs_py39 +def test_read_items(client: TestClient) -> None: + response = client.get("/items/") + assert response.status_code == 200, response.text + assert response.json() == [ + { + "name": "Portal Gun", + "description": "Device to travel through the multi-rick-verse", + }, + {"name": "Plumbus", "description": None}, + ] + + +@needs_py39 +@needs_pydanticv2 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/": { + "get": { + "summary": "Read Items", + "operationId": "read_items_items__get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": {"$ref": "#/components/schemas/Item"}, + "type": "array", + "title": "Response Read Items Items Get", + } + } + }, + } + }, + }, + "post": { + "summary": "Create Item", + "operationId": "create_item_items__post", + "requestBody": { + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/Item"} + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + }, + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": { + "name": {"type": "string", "title": "Name"}, + "description": { + "anyOf": [{"type": "string"}, {"type": "null"}], + "title": "Description", + }, + }, + "type": "object", + "required": ["name"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } From 098778e07f8d04d3fbf3a21f812ca97ab7daa46e Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 25 Aug 2023 19:11:02 +0000 Subject: [PATCH 1286/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 61ec91b4a..3858f47da 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2. PR [#10145](https://github.com/tiangolo/fastapi/pull/10145) by [@tiangolo](https://github.com/tiangolo). * 📝 Add new docs section, How To - Recipes, move docs that don't have to be read by everyone to How To. PR [#10114](https://github.com/tiangolo/fastapi/pull/10114) by [@tiangolo](https://github.com/tiangolo). * ♻️ Refactor tests for new Pydantic 2.2.1. PR [#10115](https://github.com/tiangolo/fastapi/pull/10115) by [@tiangolo](https://github.com/tiangolo). * 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). From 859d40407caa3850862526ee689e2d22177cf19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 25 Aug 2023 21:18:09 +0200 Subject: [PATCH 1287/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3858f47da..517553c9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,14 +2,28 @@ ## Latest Changes -* ✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2. PR [#10145](https://github.com/tiangolo/fastapi/pull/10145) by [@tiangolo](https://github.com/tiangolo). -* 📝 Add new docs section, How To - Recipes, move docs that don't have to be read by everyone to How To. PR [#10114](https://github.com/tiangolo/fastapi/pull/10114) by [@tiangolo](https://github.com/tiangolo). + +### Features + +* ✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2 with `separate_input_output_schemas=False`. PR [#10145](https://github.com/tiangolo/fastapi/pull/10145) by [@tiangolo](https://github.com/tiangolo). + * New docs [Separate OpenAPI Schemas for Input and Output or Not](https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/). + +### Refactors + * ♻️ Refactor tests for new Pydantic 2.2.1. PR [#10115](https://github.com/tiangolo/fastapi/pull/10115) by [@tiangolo](https://github.com/tiangolo). + +### Docs + +* 📝 Add new docs section, How To - Recipes, move docs that don't have to be read by everyone to How To. PR [#10114](https://github.com/tiangolo/fastapi/pull/10114) by [@tiangolo](https://github.com/tiangolo). * 📝 Update Advanced docs, add links to sponsor courses. PR [#10113](https://github.com/tiangolo/fastapi/pull/10113) by [@tiangolo](https://github.com/tiangolo). * 📝 Update docs for generating clients. PR [#10112](https://github.com/tiangolo/fastapi/pull/10112) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak MkDocs and add redirects. PR [#10111](https://github.com/tiangolo/fastapi/pull/10111) by [@tiangolo](https://github.com/tiangolo). * 📝 Restructure docs for cloud providers, include links to sponsors. PR [#10110](https://github.com/tiangolo/fastapi/pull/10110) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * 🔧 Update sponsors, add Speakeasy. PR [#10098](https://github.com/tiangolo/fastapi/pull/10098) by [@tiangolo](https://github.com/tiangolo). + ## 0.101.1 ### Fixes From 9cf9e1084dceb45e50946e4130801012ac0c0c59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 25 Aug 2023 21:18:38 +0200 Subject: [PATCH 1288/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?102.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 1 + fastapi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 517553c9a..2375bfee5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +## 0.102.0 ### Features diff --git a/fastapi/__init__.py b/fastapi/__init__.py index d8abf2103..6979ec5fb 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.101.1" +__version__ = "0.102.0" from starlette import status as status From f3ab547c0c6e5b42b81e3669aaea753f742b742c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 25 Aug 2023 21:23:44 +0200 Subject: [PATCH 1289/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2375bfee5..1a9721e2c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -8,6 +8,7 @@ * ✨ Add support for disabling the separation of input and output JSON Schemas in OpenAPI with Pydantic v2 with `separate_input_output_schemas=False`. PR [#10145](https://github.com/tiangolo/fastapi/pull/10145) by [@tiangolo](https://github.com/tiangolo). * New docs [Separate OpenAPI Schemas for Input and Output or Not](https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/). + * This PR also includes a new setup (internal tools) for generating screenshots for the docs. ### Refactors From 594b1ae0c38550ddfdedab314276c1a659ce4719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Aug 2023 15:20:04 +0200 Subject: [PATCH 1290/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20note=20to=20docs?= =?UTF-8?q?=20about=20Separate=20Input=20and=20Output=20Schemas=20with=20F?= =?UTF-8?q?astAPI=20version=20(#10150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/how-to/separate-openapi-schemas.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md index 39d96ea39..d289391ca 100644 --- a/docs/en/docs/how-to/separate-openapi-schemas.md +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -199,6 +199,9 @@ Probably the main use case for this is if you already have some autogenerated cl In that case, you can disable this feature in **FastAPI**, with the parameter `separate_input_output_schemas=False`. +!!! info + Support for `separate_input_output_schemas` was added in FastAPI `0.102.0`. 🤓 + === "Python 3.10+" ```Python hl_lines="10" From 5f855b1179d06c8b82f479013e5888c48e68c716 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Aug 2023 13:20:54 +0000 Subject: [PATCH 1291/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1a9721e2c..5d15e1c25 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📝 Add note to docs about Separate Input and Output Schemas with FastAPI version. PR [#10150](https://github.com/tiangolo/fastapi/pull/10150) by [@tiangolo](https://github.com/tiangolo). ## 0.102.0 ### Features From 1b714b317732df1e08983e4a8ffe2e4e02c995e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Aug 2023 20:03:13 +0200 Subject: [PATCH 1292/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20`op?= =?UTF-8?q?enapi=5Fexamples`=20in=20all=20FastAPI=20parameters=20(#10152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor model for OpenAPI Examples to use a reusable TypedDict * ✨ Add support for openapi_examples in parameters * 📝 Add new docs examples for new parameter openapi_examples * 📝 Update docs for Schema Extra to include OpenAPI examples * ✅ Add tests for new source examples, for openapi_examples * ✅ Add tests for openapi_examples corner cases and all parameters * 💡 Tweak and ignore type annotation checks for custom TypedDict --- docs/en/docs/tutorial/schema-extra-example.md | 105 +++- docs_src/schema_extra_example/tutorial005.py | 51 ++ .../schema_extra_example/tutorial005_an.py | 55 +++ .../tutorial005_an_py310.py | 54 +++ .../tutorial005_an_py39.py | 54 +++ .../schema_extra_example/tutorial005_py310.py | 49 ++ fastapi/openapi/models.py | 16 +- fastapi/openapi/utils.py | 10 +- fastapi/param_functions.py | 15 + fastapi/params.py | 17 + tests/test_openapi_examples.py | 455 ++++++++++++++++++ .../test_tutorial005.py | 166 +++++++ .../test_tutorial005_an.py | 166 +++++++ .../test_tutorial005_an_py310.py | 170 +++++++ .../test_tutorial005_an_py39.py | 170 +++++++ .../test_tutorial005_py310.py | 170 +++++++ 16 files changed, 1695 insertions(+), 28 deletions(-) create mode 100644 docs_src/schema_extra_example/tutorial005.py create mode 100644 docs_src/schema_extra_example/tutorial005_an.py create mode 100644 docs_src/schema_extra_example/tutorial005_an_py310.py create mode 100644 docs_src/schema_extra_example/tutorial005_an_py39.py create mode 100644 docs_src/schema_extra_example/tutorial005_py310.py create mode 100644 tests/test_openapi_examples.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py create mode 100644 tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 39d184763..8cf1b9c09 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -74,7 +74,7 @@ When using `Field()` with Pydantic models, you can also declare additional `exam {!> ../../../docs_src/schema_extra_example/tutorial002.py!} ``` -## `examples` in OpenAPI +## `examples` in JSON Schema - OpenAPI When using any of: @@ -86,7 +86,7 @@ When using any of: * `Form()` * `File()` -you can also declare a group of `examples` with additional information that will be added to **OpenAPI**. +you can also declare a group of `examples` with additional information that will be added to their **JSON Schemas** inside of **OpenAPI**. ### `Body` with `examples` @@ -174,9 +174,84 @@ You can of course also pass multiple `examples`: {!> ../../../docs_src/schema_extra_example/tutorial004.py!} ``` -### Examples in the docs UI +When you do this, the examples will be part of the internal **JSON Schema** for that body data. -With `examples` added to `Body()` the `/docs` would look like: +Nevertheless, at the time of writing this, Swagger UI, the tool in charge of showing the docs UI, doesn't support showing multiple examples for the data in **JSON Schema**. But read below for a workaround. + +### OpenAPI-specific `examples` + +Since before **JSON Schema** supported `examples` OpenAPI had support for a different field also called `examples`. + +This **OpenAPI-specific** `examples` goes in another section in the OpenAPI specification. It goes in the **details for each *path operation***, not inside each JSON Schema. + +And Swagger UI has supported this particular `examples` field for a while. So, you can use it to **show** different **examples in the docs UI**. + +The shape of this OpenAPI-specific field `examples` is a `dict` with **multiple examples** (instead of a `list`), each with extra information that will be added to **OpenAPI** too. + +This doesn't go inside of each JSON Schema contained in OpenAPI, this goes outside, in the *path operation* directly. + +### Using the `openapi_examples` Parameter + +You can declare the OpenAPI-specific `examples` in FastAPI with the parameter `openapi_examples` for: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +The keys of the `dict` identify each example, and each value is another `dict`. + +Each specific example `dict` in the `examples` can contain: + +* `summary`: Short description for the example. +* `description`: A long description that can contain Markdown text. +* `value`: This is the actual example shown, e.g. a `dict`. +* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. + +You can use it like this: + +=== "Python 3.10+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23-49" + {!> ../../../docs_src/schema_extra_example/tutorial005_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="24-50" + {!> ../../../docs_src/schema_extra_example/tutorial005_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="19-45" + {!> ../../../docs_src/schema_extra_example/tutorial005_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="21-47" + {!> ../../../docs_src/schema_extra_example/tutorial005.py!} + ``` + +### OpenAPI Examples in the Docs UI + +With `openapi_examples` added to `Body()` the `/docs` would look like: @@ -210,20 +285,8 @@ OpenAPI also added `example` and `examples` fields to other parts of the specifi * `File()` * `Form()` -### OpenAPI's `examples` field - -The shape of this field `examples` from OpenAPI is a `dict` with **multiple examples**, each with extra information that will be added to **OpenAPI** too. - -The keys of the `dict` identify each example, and each value is another `dict`. - -Each specific example `dict` in the `examples` can contain: - -* `summary`: Short description for the example. -* `description`: A long description that can contain Markdown text. -* `value`: This is the actual example shown, e.g. a `dict`. -* `externalValue`: alternative to `value`, a URL pointing to the example. Although this might not be supported by as many tools as `value`. - -This applies to those other parts of the OpenAPI specification apart from JSON Schema. +!!! info + This old OpenAPI-specific `examples` parameter is now `openapi_examples` since FastAPI `0.103.0`. ### JSON Schema's `examples` field @@ -250,6 +313,12 @@ In versions of FastAPI before 0.99.0 (0.99.0 and above use the newer OpenAPI 3.1 But now that FastAPI 0.99.0 and above uses OpenAPI 3.1.0, that uses JSON Schema 2020-12, and Swagger UI 5.0.0 and above, everything is more consistent and the examples are included in JSON Schema. +### Swagger UI and OpenAPI-specific `examples` + +Now, as Swagger UI didn't support multiple JSON Schema examples (as of 2023-08-26), users didn't have a way to show multiple examples in the docs. + +To solve that, FastAPI `0.103.0` **added support** for declaring the same old **OpenAPI-specific** `examples` field with the new parameter `openapi_examples`. 🤓 + ### Summary I used to say I didn't like history that much... and look at me now giving "tech history" lessons. 😅 diff --git a/docs_src/schema_extra_example/tutorial005.py b/docs_src/schema_extra_example/tutorial005.py new file mode 100644 index 000000000..b8217c27e --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005.py @@ -0,0 +1,51 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an.py b/docs_src/schema_extra_example/tutorial005_an.py new file mode 100644 index 000000000..4b2d9c662 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an.py @@ -0,0 +1,55 @@ +from typing import Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel +from typing_extensions import Annotated + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an_py310.py b/docs_src/schema_extra_example/tutorial005_an_py310.py new file mode 100644 index 000000000..64dc2cf90 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an_py310.py @@ -0,0 +1,54 @@ +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_an_py39.py b/docs_src/schema_extra_example/tutorial005_an_py39.py new file mode 100644 index 000000000..edeb1affc --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_an_py39.py @@ -0,0 +1,54 @@ +from typing import Annotated, Union + +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: Union[str, None] = None + price: float + tax: Union[float, None] = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Annotated[ + Item, + Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), + ], +): + results = {"item_id": item_id, "item": item} + return results diff --git a/docs_src/schema_extra_example/tutorial005_py310.py b/docs_src/schema_extra_example/tutorial005_py310.py new file mode 100644 index 000000000..eef973343 --- /dev/null +++ b/docs_src/schema_extra_example/tutorial005_py310.py @@ -0,0 +1,49 @@ +from fastapi import Body, FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float + tax: float | None = None + + +@app.put("/items/{item_id}") +async def update_item( + *, + item_id: int, + item: Item = Body( + openapi_examples={ + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": { + "name": "Bar", + "price": "35.4", + }, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + ), +): + results = {"item_id": item_id, "item": item} + return results diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 2268dd229..3d982eb9a 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -11,7 +11,7 @@ from fastapi._compat import ( ) from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field -from typing_extensions import Annotated, Literal +from typing_extensions import Annotated, Literal, TypedDict from typing_extensions import deprecated as typing_deprecated try: @@ -267,14 +267,14 @@ class Schema(BaseModel): SchemaOrBool = Union[Schema, bool] -class Example(BaseModel): - summary: Optional[str] = None - description: Optional[str] = None - value: Optional[Any] = None - externalValue: Optional[AnyUrl] = None +class Example(TypedDict, total=False): + summary: Optional[str] + description: Optional[str] + value: Optional[Any] + externalValue: Optional[AnyUrl] - if PYDANTIC_V2: - model_config = {"extra": "allow"} + if PYDANTIC_V2: # type: ignore [misc] + __pydantic_config__ = {"extra": "allow"} else: diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 9498375fe..5bfb5acef 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -118,7 +118,9 @@ def get_openapi_operation_parameters( } if field_info.description: parameter["description"] = field_info.description - if field_info.example != Undefined: + if field_info.openapi_examples: + parameter["examples"] = jsonable_encoder(field_info.openapi_examples) + elif field_info.example != Undefined: parameter["example"] = jsonable_encoder(field_info.example) if field_info.deprecated: parameter["deprecated"] = field_info.deprecated @@ -153,7 +155,11 @@ def get_openapi_operation_request_body( if required: request_body_oai["required"] = required request_media_content: Dict[str, Any] = {"schema": body_schema} - if field_info.example != Undefined: + if field_info.openapi_examples: + request_media_content["examples"] = jsonable_encoder( + field_info.openapi_examples + ) + elif field_info.example != Undefined: request_media_content["example"] = jsonable_encoder(field_info.example) request_body_oai["content"] = {request_media_type: request_media_content} return request_body_oai diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index a43afaf31..63914d1d6 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -2,6 +2,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi import params from fastapi._compat import Undefined +from fastapi.openapi.models import Example from typing_extensions import Annotated, deprecated _Unset: Any = Undefined @@ -46,6 +47,7 @@ def Path( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -76,6 +78,7 @@ def Path( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -122,6 +125,7 @@ def Query( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -152,6 +156,7 @@ def Query( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -199,6 +204,7 @@ def Header( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -230,6 +236,7 @@ def Header( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -276,6 +283,7 @@ def Cookie( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -306,6 +314,7 @@ def Cookie( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -354,6 +363,7 @@ def Body( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -386,6 +396,7 @@ def Body( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -433,6 +444,7 @@ def Form( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -464,6 +476,7 @@ def Form( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, @@ -511,6 +524,7 @@ def File( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -542,6 +556,7 @@ def File( # noqa: N802 decimal_places=decimal_places, example=example, examples=examples, + openapi_examples=openapi_examples, deprecated=deprecated, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, diff --git a/fastapi/params.py b/fastapi/params.py index 2d8100650..b40944dba 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -2,6 +2,7 @@ import warnings from enum import Enum from typing import Any, Callable, Dict, List, Optional, Sequence, Union +from fastapi.openapi.models import Example from pydantic.fields import FieldInfo from typing_extensions import Annotated, deprecated @@ -61,6 +62,7 @@ class Param(FieldInfo): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -75,6 +77,7 @@ class Param(FieldInfo): ) self.example = example self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples kwargs = dict( default=default, default_factory=default_factory, @@ -170,6 +173,7 @@ class Path(Param): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -204,6 +208,7 @@ class Path(Param): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, @@ -254,6 +259,7 @@ class Query(Param): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -286,6 +292,7 @@ class Query(Param): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, @@ -337,6 +344,7 @@ class Header(Param): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -370,6 +378,7 @@ class Header(Param): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, @@ -420,6 +429,7 @@ class Cookie(Param): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -452,6 +462,7 @@ class Cookie(Param): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, @@ -502,6 +513,7 @@ class Body(FieldInfo): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -518,6 +530,7 @@ class Body(FieldInfo): ) self.example = example self.include_in_schema = include_in_schema + self.openapi_examples = openapi_examples kwargs = dict( default=default, default_factory=default_factory, @@ -613,6 +626,7 @@ class Form(Body): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -647,6 +661,7 @@ class Form(Body): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, @@ -696,6 +711,7 @@ class File(Form): "although still supported. Use examples instead." ), ] = _Unset, + openapi_examples: Optional[Dict[str, Example]] = None, deprecated: Optional[bool] = None, include_in_schema: bool = True, json_schema_extra: Union[Dict[str, Any], None] = None, @@ -729,6 +745,7 @@ class File(Form): deprecated=deprecated, example=example, examples=examples, + openapi_examples=openapi_examples, include_in_schema=include_in_schema, json_schema_extra=json_schema_extra, **extra, diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py new file mode 100644 index 000000000..d0e35953e --- /dev/null +++ b/tests/test_openapi_examples.py @@ -0,0 +1,455 @@ +from typing import Union + +from dirty_equals import IsDict +from fastapi import Body, Cookie, FastAPI, Header, Path, Query +from fastapi.testclient import TestClient +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + data: str + + +@app.post("/examples/") +def examples( + item: Item = Body( + examples=[ + {"data": "Data in Body examples, example1"}, + ], + openapi_examples={ + "Example One": { + "summary": "Example One Summary", + "description": "Example One Description", + "value": {"data": "Data in Body examples, example1"}, + }, + "Example Two": { + "value": {"data": "Data in Body examples, example2"}, + }, + }, + ) +): + return item + + +@app.get("/path_examples/{item_id}") +def path_examples( + item_id: str = Path( + examples=[ + "json_schema_item_1", + "json_schema_item_2", + ], + openapi_examples={ + "Path One": { + "summary": "Path One Summary", + "description": "Path One Description", + "value": "item_1", + }, + "Path Two": { + "value": "item_2", + }, + }, + ), +): + return item_id + + +@app.get("/query_examples/") +def query_examples( + data: Union[str, None] = Query( + default=None, + examples=[ + "json_schema_query1", + "json_schema_query2", + ], + openapi_examples={ + "Query One": { + "summary": "Query One Summary", + "description": "Query One Description", + "value": "query1", + }, + "Query Two": { + "value": "query2", + }, + }, + ), +): + return data + + +@app.get("/header_examples/") +def header_examples( + data: Union[str, None] = Header( + default=None, + examples=[ + "json_schema_header1", + "json_schema_header2", + ], + openapi_examples={ + "Header One": { + "summary": "Header One Summary", + "description": "Header One Description", + "value": "header1", + }, + "Header Two": { + "value": "header2", + }, + }, + ), +): + return data + + +@app.get("/cookie_examples/") +def cookie_examples( + data: Union[str, None] = Cookie( + default=None, + examples=["json_schema_cookie1", "json_schema_cookie2"], + openapi_examples={ + "Cookie One": { + "summary": "Cookie One Summary", + "description": "Cookie One Description", + "value": "cookie1", + }, + "Cookie Two": { + "value": "cookie2", + }, + }, + ), +): + return data + + +client = TestClient(app) + + +def test_call_api(): + response = client.get("/path_examples/foo") + assert response.status_code == 200, response.text + + response = client.get("/query_examples/") + assert response.status_code == 200, response.text + + response = client.get("/header_examples/") + assert response.status_code == 200, response.text + + response = client.get("/cookie_examples/") + assert response.status_code == 200, response.text + + +def test_openapi_schema(): + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/examples/": { + "post": { + "summary": "Examples", + "operationId": "examples_examples__post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [{"$ref": "#/components/schemas/Item"}], + "title": "Item", + "examples": [ + {"data": "Data in Body examples, example1"} + ], + }, + "examples": { + "Example One": { + "summary": "Example One Summary", + "description": "Example One Description", + "value": { + "data": "Data in Body examples, example1" + }, + }, + "Example Two": { + "value": { + "data": "Data in Body examples, example2" + } + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/path_examples/{item_id}": { + "get": { + "summary": "Path Examples", + "operationId": "path_examples_path_examples__item_id__get", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": True, + "schema": { + "type": "string", + "examples": [ + "json_schema_item_1", + "json_schema_item_2", + ], + "title": "Item Id", + }, + "examples": { + "Path One": { + "summary": "Path One Summary", + "description": "Path One Description", + "value": "item_1", + }, + "Path Two": {"value": "item_2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/query_examples/": { + "get": { + "summary": "Query Examples", + "operationId": "query_examples_query_examples__get", + "parameters": [ + { + "name": "data", + "in": "query", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "examples": [ + "json_schema_query1", + "json_schema_query2", + ], + "type": "string", + "title": "Data", + } + ), + "examples": { + "Query One": { + "summary": "Query One Summary", + "description": "Query One Description", + "value": "query1", + }, + "Query Two": {"value": "query2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/header_examples/": { + "get": { + "summary": "Header Examples", + "operationId": "header_examples_header_examples__get", + "parameters": [ + { + "name": "data", + "in": "header", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "examples": [ + "json_schema_header1", + "json_schema_header2", + ], + "title": "Data", + } + ), + "examples": { + "Header One": { + "summary": "Header One Summary", + "description": "Header One Description", + "value": "header1", + }, + "Header Two": {"value": "header2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/cookie_examples/": { + "get": { + "summary": "Cookie Examples", + "operationId": "cookie_examples_cookie_examples__get", + "parameters": [ + { + "name": "data", + "in": "cookie", + "required": False, + "schema": IsDict( + { + "anyOf": [{"type": "string"}, {"type": "null"}], + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "type": "string", + "examples": [ + "json_schema_cookie1", + "json_schema_cookie2", + ], + "title": "Data", + } + ), + "examples": { + "Cookie One": { + "summary": "Cookie One Summary", + "description": "Cookie One Description", + "value": "cookie1", + }, + "Cookie Two": {"value": "cookie2"}, + }, + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + }, + "components": { + "schemas": { + "HTTPValidationError": { + "properties": { + "detail": { + "items": {"$ref": "#/components/schemas/ValidationError"}, + "type": "array", + "title": "Detail", + } + }, + "type": "object", + "title": "HTTPValidationError", + }, + "Item": { + "properties": {"data": {"type": "string", "title": "Data"}}, + "type": "object", + "required": ["data"], + "title": "Item", + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + "type": "array", + "title": "Location", + }, + "msg": {"type": "string", "title": "Message"}, + "type": {"type": "string", "title": "Error Type"}, + }, + "type": "object", + "required": ["loc", "msg", "type"], + "title": "ValidationError", + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py new file mode 100644 index 000000000..94a40ed5a --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005.py @@ -0,0 +1,166 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005 import app + + client = TestClient(app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py new file mode 100644 index 000000000..da92f98f6 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an.py @@ -0,0 +1,166 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an import app + + client = TestClient(app) + return client + + +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py new file mode 100644 index 000000000..9109cb14e --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py new file mode 100644 index 000000000..fd4ec0575 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_an_py39.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py39 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_an_py39 import app + + client = TestClient(app) + return client + + +@needs_py39 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py39 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py new file mode 100644 index 000000000..05df53422 --- /dev/null +++ b/tests/test_tutorial/test_schema_extra_example/test_tutorial005_py310.py @@ -0,0 +1,170 @@ +import pytest +from dirty_equals import IsDict +from fastapi.testclient import TestClient + +from ...utils import needs_py310 + + +@pytest.fixture(name="client") +def get_client(): + from docs_src.schema_extra_example.tutorial005_py310 import app + + client = TestClient(app) + return client + + +@needs_py310 +def test_post_body_example(client: TestClient): + response = client.put( + "/items/5", + json={ + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + ) + assert response.status_code == 200 + + +@needs_py310 +def test_openapi_schema(client: TestClient) -> None: + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items/{item_id}": { + "put": { + "summary": "Update Item", + "operationId": "update_item_items__item_id__put", + "parameters": [ + { + "required": True, + "schema": {"title": "Item Id", "type": "integer"}, + "name": "item_id", + "in": "path", + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": IsDict({"$ref": "#/components/schemas/Item"}) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + { + "allOf": [ + {"$ref": "#/components/schemas/Item"} + ], + "title": "Item", + } + ), + "examples": { + "normal": { + "summary": "A normal example", + "description": "A **normal** item works correctly.", + "value": { + "name": "Foo", + "description": "A very nice Item", + "price": 35.4, + "tax": 3.2, + }, + }, + "converted": { + "summary": "An example with converted data", + "description": "FastAPI can convert price `strings` to actual `numbers` automatically", + "value": {"name": "Bar", "price": "35.4"}, + }, + "invalid": { + "summary": "Invalid data is rejected with an error", + "value": { + "name": "Baz", + "price": "thirty five point four", + }, + }, + }, + } + }, + "required": True, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + } + }, + "components": { + "schemas": { + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": {"$ref": "#/components/schemas/ValidationError"}, + } + }, + }, + "Item": { + "title": "Item", + "required": ["name", "price"], + "type": "object", + "properties": { + "name": {"title": "Name", "type": "string"}, + "description": IsDict( + { + "title": "Description", + "anyOf": [{"type": "string"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Description", "type": "string"} + ), + "price": {"title": "Price", "type": "number"}, + "tax": IsDict( + { + "title": "Tax", + "anyOf": [{"type": "number"}, {"type": "null"}], + } + ) + | IsDict( + # TODO: remove when deprecating Pydantic v1 + {"title": "Tax", "type": "number"} + ), + }, + }, + "ValidationError": { + "title": "ValidationError", + "required": ["loc", "msg", "type"], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, + }, + "msg": {"title": "Message", "type": "string"}, + "type": {"title": "Error Type", "type": "string"}, + }, + }, + } + }, + } From df16699dd8a5909b7de2d8c79f53f7153d922625 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 26 Aug 2023 18:03:56 +0000 Subject: [PATCH 1293/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5d15e1c25..4021532d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✨ Add support for `openapi_examples` in all FastAPI parameters. PR [#10152](https://github.com/tiangolo/fastapi/pull/10152) by [@tiangolo](https://github.com/tiangolo). * 📝 Add note to docs about Separate Input and Output Schemas with FastAPI version. PR [#10150](https://github.com/tiangolo/fastapi/pull/10150) by [@tiangolo](https://github.com/tiangolo). ## 0.102.0 From bd32bca55cdbf3ca3e9de66b0e790772e019b55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Aug 2023 20:09:59 +0200 Subject: [PATCH 1294/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4021532d2..f61d1360d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,8 +2,14 @@ ## Latest Changes +### Features + * ✨ Add support for `openapi_examples` in all FastAPI parameters. PR [#10152](https://github.com/tiangolo/fastapi/pull/10152) by [@tiangolo](https://github.com/tiangolo). + +### Docs + * 📝 Add note to docs about Separate Input and Output Schemas with FastAPI version. PR [#10150](https://github.com/tiangolo/fastapi/pull/10150) by [@tiangolo](https://github.com/tiangolo). + ## 0.102.0 ### Features From 415eb1405a5bc93a32e14c15d690517c95d26743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Aug 2023 20:10:27 +0200 Subject: [PATCH 1295/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?103.0?= 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 f61d1360d..91b29e772 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.103.0 + ### Features * ✨ Add support for `openapi_examples` in all FastAPI parameters. PR [#10152](https://github.com/tiangolo/fastapi/pull/10152) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 6979ec5fb..ff8b98d3b 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.102.0" +__version__ = "0.103.0" from starlette import status as status From a3f1689d78abef3d5d54ba655ec87cef81bf7384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 26 Aug 2023 20:14:42 +0200 Subject: [PATCH 1296/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 91b29e772..b60e024e5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -8,6 +8,7 @@ ### Features * ✨ Add support for `openapi_examples` in all FastAPI parameters. PR [#10152](https://github.com/tiangolo/fastapi/pull/10152) by [@tiangolo](https://github.com/tiangolo). + * New docs: [OpenAPI-specific examples](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#openapi-specific-examples). ### Docs From 37d46e6b6c58c0e38747d91c4a0cdc0ea76c9d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 1 Sep 2023 23:36:08 +0200 Subject: [PATCH 1297/1881] =?UTF-8?q?=E2=9C=85=20Add=20missing=20test=20fo?= =?UTF-8?q?r=20OpenAPI=20examples,=20it=20was=20missing=20in=20coverage=20?= =?UTF-8?q?(#10188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Add missing test for OpenAPI examples, it seems it was discovered in coverage by an upgrade in AnyIO --- tests/test_openapi_examples.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index d0e35953e..70664a8a4 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -125,6 +125,9 @@ client = TestClient(app) def test_call_api(): + response = client.post("/examples/", json={"data": "example1"}) + assert response.status_code == 200, response.text + response = client.get("/path_examples/foo") assert response.status_code == 200, response.text From 7a63d11093d17de34a62dd10f81b1cf149c2e37b Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 1 Sep 2023 21:36:46 +0000 Subject: [PATCH 1298/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b60e024e5..198bf3823 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). ## 0.103.0 From 4bfe83bd2706536fcb0fa911c0e1e0bb95ba39ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 2 Sep 2023 01:32:40 +0200 Subject: [PATCH 1299/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#10186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 80 ++++++++++------- docs/en/data/people.yml | 148 +++++++++++++++---------------- 2 files changed, 120 insertions(+), 108 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 3a68ba62b..1ec71dd49 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -2,6 +2,9 @@ sponsors: - - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi + - login: porter-dev + avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 + url: https://github.com/porter-dev - login: fern-api avatarUrl: https://avatars.githubusercontent.com/u/102944815?v=4 url: https://github.com/fern-api @@ -17,24 +20,21 @@ sponsors: - - login: mikeckennedy avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4 url: https://github.com/mikeckennedy + - login: ndimares + avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 + url: https://github.com/ndimares - login: deta avatarUrl: https://avatars.githubusercontent.com/u/47275976?v=4 url: https://github.com/deta - login: deepset-ai avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 url: https://github.com/deepset-ai - - login: svix - avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 - url: https://github.com/svix - login: databento-bot avatarUrl: https://avatars.githubusercontent.com/u/98378480?u=494f679996e39427f7ddb1a7de8441b7c96fb670&v=4 url: https://github.com/databento-bot - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes -- - login: arcticfly - avatarUrl: https://avatars.githubusercontent.com/u/41524992?u=03c88529a86cf51f7a380e890d84d84c71468848&v=4 - url: https://github.com/arcticfly - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry @@ -89,18 +89,18 @@ sponsors: - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb + - login: mukulmantosh + avatarUrl: https://avatars.githubusercontent.com/u/15572034?u=006f0a33c0e15bb2de57474a2cf7d2f8ee05f5a0&v=4 + url: https://github.com/mukulmantosh - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io - - login: indeedeng avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 url: https://github.com/indeedeng - - login: iguit0 - avatarUrl: https://avatars.githubusercontent.com/u/12905770?u=63a1a96d1e6c27d85c4f946b84836599de047f65&v=4 - url: https://github.com/iguit0 - - login: JacobKochems - avatarUrl: https://avatars.githubusercontent.com/u/41692189?u=a75f62ddc0d060ee6233a91e19c433d2687b8eb6&v=4 - url: https://github.com/JacobKochems + - login: NateXVI + avatarUrl: https://avatars.githubusercontent.com/u/48195620?u=4bc8751ae50cb087c40c1fe811764aa070b9eea6&v=4 + url: https://github.com/NateXVI - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -200,12 +200,12 @@ sponsors: - login: hiancdtrsnm avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 url: https://github.com/hiancdtrsnm - - login: Shackelford-Arden - avatarUrl: https://avatars.githubusercontent.com/u/7362263?v=4 - url: https://github.com/Shackelford-Arden - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow + - login: jsoques + avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 + url: https://github.com/jsoques - login: joeds13 avatarUrl: https://avatars.githubusercontent.com/u/13631604?u=628eb122e08bef43767b3738752b883e8e7f6259&v=4 url: https://github.com/joeds13 @@ -227,9 +227,6 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: LarryGF - avatarUrl: https://avatars.githubusercontent.com/u/26148349?u=431bb34d36d41c172466252242175281ae132152&v=4 - url: https://github.com/LarryGF - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy @@ -239,6 +236,9 @@ sponsors: - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 + - login: dvlpjrs + avatarUrl: https://avatars.githubusercontent.com/u/32254642?u=fbd6ad0324d4f1eb6231cf775be1c7bd4404e961&v=4 + url: https://github.com/dvlpjrs - login: ygorpontelo avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 url: https://github.com/ygorpontelo @@ -284,15 +284,15 @@ sponsors: - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare + - login: hbakri + avatarUrl: https://avatars.githubusercontent.com/u/92298226?u=36eee6d75db1272264d3ee92f1871f70b8917bd8&v=4 + url: https://github.com/hbakri - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h - - login: Dagmaara - avatarUrl: https://avatars.githubusercontent.com/u/115501964?v=4 - url: https://github.com/Dagmaara - - login: SebTota avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 url: https://github.com/SebTota @@ -329,6 +329,9 @@ sponsors: - login: janfilips avatarUrl: https://avatars.githubusercontent.com/u/870699?u=80702ec63f14e675cd4cdcc6ce3821d2ed207fd7&v=4 url: https://github.com/janfilips + - login: dodo5522 + avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 + url: https://github.com/dodo5522 - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan @@ -374,9 +377,6 @@ sponsors: - login: katnoria avatarUrl: https://avatars.githubusercontent.com/u/7674948?u=09767eb13e07e09496c5fee4e5ce21d9eac34a56&v=4 url: https://github.com/katnoria - - login: mattwelke - avatarUrl: https://avatars.githubusercontent.com/u/7719209?u=80f02a799323b1472b389b836d95957c93a6d856&v=4 - url: https://github.com/mattwelke - login: harsh183 avatarUrl: https://avatars.githubusercontent.com/u/7780198?v=4 url: https://github.com/harsh183 @@ -422,6 +422,9 @@ sponsors: - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia + - login: timzaz + avatarUrl: https://avatars.githubusercontent.com/u/19709244?u=e6658f6b0b188294ce2db20dad94a678fcea718d&v=4 + url: https://github.com/timzaz - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu @@ -431,6 +434,9 @@ sponsors: - login: kxzk avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 url: https://github.com/kxzk + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec - login: hoenie-ams avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 url: https://github.com/hoenie-ams @@ -450,7 +456,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=527044d90b5ebb7f8dad517db5da1f45253b774b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=1a104991a2ea90bfe304bc0b9ef191c7e4891a0e&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 @@ -482,6 +488,15 @@ sponsors: - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun + - login: romabozhanovgithub + avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 + url: https://github.com/romabozhanovgithub + - login: mnicolleUTC + avatarUrl: https://avatars.githubusercontent.com/u/68548924?u=1fdd7f436bb1f4857c3415e62aa8fbc055fc1a76&v=4 + url: https://github.com/mnicolleUTC + - login: mbukeRepo + avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=e125069c6ac5c49355e2b3ca2aa66a445fc4cccd&v=4 + url: https://github.com/mbukeRepo - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea @@ -494,21 +509,18 @@ sponsors: - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu - - login: ruizdiazever - avatarUrl: https://avatars.githubusercontent.com/u/29817086?u=2df54af55663d246e3a4dc8273711c37f1adb117&v=4 - url: https://github.com/ruizdiazever - login: samnimoh avatarUrl: https://avatars.githubusercontent.com/u/33413170?u=147bc516be6cb647b28d7e3b3fea3a018a331145&v=4 url: https://github.com/samnimoh - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - - login: iharshgor - avatarUrl: https://avatars.githubusercontent.com/u/35490011?u=2dea054476e752d9e92c9d71a9a7cc919b1c2f8e&v=4 - url: https://github.com/iharshgor + - login: koalawangyang + avatarUrl: https://avatars.githubusercontent.com/u/40114367?u=3bb94010f0473b8d4c2edea5a8c1cab61e6a1b22&v=4 + url: https://github.com/koalawangyang - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: ThomasPalma1 - avatarUrl: https://avatars.githubusercontent.com/u/66331874?u=5763f7402d784ba189b60d704ff5849b4d0a63fb&v=4 - url: https://github.com/ThomasPalma1 + - login: lodine-software + avatarUrl: https://avatars.githubusercontent.com/u/133536046?v=4 + url: https://github.com/lodine-software diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 89d189564..8ebd2f84c 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,16 +1,16 @@ maintainers: - login: tiangolo - answers: 1849 - prs: 466 + answers: 1866 + prs: 486 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 463 + count: 480 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 239 + count: 240 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: Mause @@ -26,7 +26,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: jgould22 - count: 157 + count: 164 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: euri10 @@ -38,7 +38,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 - login: iudeen - count: 121 + count: 122 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: raphaelauv @@ -61,34 +61,34 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: yinziyan1206 count: 45 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 -- login: acidjunk +- login: insomnes count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa +- login: acidjunk + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: adriangb count: 44 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb -- login: frankie567 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=85c025e3fcc7bd79a5665c63ee87cdf8aae13374&v=4 - url: https://github.com/frankie567 - login: odiseo0 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + url: https://github.com/frankie567 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -97,14 +97,14 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary +- login: chbndrhnns + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: chbndrhnns - count: 35 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -117,26 +117,30 @@ experts: count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty +- login: n8sty + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: wshayes count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: acnebs - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 - url: https://github.com/acnebs - login: SirTelemak count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak +- login: acnebs + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 + url: https://github.com/acnebs - login: rafsaf count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=f8f0d6d6e90fac39fa786228158ba7f013c74271&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf -- login: n8sty +- login: JavierSanchezCastro count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: nsidnev count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 @@ -173,55 +177,51 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli +- login: ebottos94 + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny -- login: abhint +- login: chrisK824 count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 - login: nymous count: 15 avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 url: https://github.com/nymous -- login: ghost +- login: abhint count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 - url: https://github.com/ghost -- login: simondale00 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/33907262?v=4 - url: https://github.com/simondale00 -- login: jorgerpo - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/12537771?u=7444d20019198e34911082780cc7ad73f2b97cb3&v=4 - url: https://github.com/jorgerpo + avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 + url: https://github.com/abhint last_month_active: +- login: JavierSanchezCastro + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: Kludex - count: 24 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: jgould22 - count: 17 + count: 8 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: arjwilliams - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/22227620?v=4 - url: https://github.com/arjwilliams -- login: Ahmed-Abdou14 - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/104530599?u=05365b155a1ff911532e8be316acfad2e0736f98&v=4 - url: https://github.com/Ahmed-Abdou14 -- login: iudeen +- login: romabozhanovgithub + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 + url: https://github.com/romabozhanovgithub +- login: n8sty + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty +- login: chrisK824 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen -- login: mikeedjones - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/4087139?u=cc4a242896ac2fcf88a53acfaf190d0fe0a1f0c9&v=4 - url: https://github.com/mikeedjones + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 top_contributors: - login: waynerv count: 25 @@ -235,22 +235,22 @@ top_contributors: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: dmontagu + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 + url: https://github.com/dmontagu - login: jaystone776 count: 17 avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 url: https://github.com/jaystone776 -- login: dmontagu - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 - url: https://github.com/dmontagu +- login: Xewus + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: euri10 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 -- login: Xewus - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: mariacamilagl count: 12 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 @@ -360,6 +360,10 @@ top_reviewers: count: 78 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 +- login: iudeen + count: 53 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: tokusumi count: 51 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 @@ -372,10 +376,6 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 -- login: iudeen - count: 46 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 @@ -385,7 +385,7 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay - login: Xewus - count: 38 + count: 39 avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 url: https://github.com/Xewus - login: JarroVGIT @@ -482,7 +482,7 @@ top_reviewers: url: https://github.com/sh0nk - login: peidrao count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=5b94b548ef0002ef3219d7c07ac0fac17c6201a2&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 url: https://github.com/peidrao - login: r0b2g1t count: 13 From ee0b28a398db5e4f7bfe8d53284e9c0575c9543d Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 1 Sep 2023 23:33:31 +0000 Subject: [PATCH 1300/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 198bf3823..3b10fe405 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 [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). * ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). ## 0.103.0 From bf952d345c36807b5931ce6f74c40aa4e7308ccf Mon Sep 17 00:00:00 2001 From: PieCat <53287533+funny-cat-happy@users.noreply.github.com> Date: Sat, 2 Sep 2023 23:18:30 +0800 Subject: [PATCH 1301/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/generate-clients.md`=20(#9?= =?UTF-8?q?883)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: xzmeng Co-authored-by: Sebastián Ramírez --- docs/zh/docs/advanced/generate-clients.md | 266 ++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 docs/zh/docs/advanced/generate-clients.md diff --git a/docs/zh/docs/advanced/generate-clients.md b/docs/zh/docs/advanced/generate-clients.md new file mode 100644 index 000000000..f3a58c062 --- /dev/null +++ b/docs/zh/docs/advanced/generate-clients.md @@ -0,0 +1,266 @@ +# 生成客户端 + +因为 **FastAPI** 是基于OpenAPI规范的,自然您可以使用许多相匹配的工具,包括自动生成API文档 (由 Swagger UI 提供)。 + +一个不太明显而又特别的优势是,你可以为你的API针对不同的**编程语言**来**生成客户端**(有时候被叫做 **SDKs** )。 + +## OpenAPI 客户端生成 + +有许多工具可以从**OpenAPI**生成客户端。 + +一个常见的工具是 OpenAPI Generator。 + +如果您正在开发**前端**,一个非常有趣的替代方案是 openapi-typescript-codegen。 + +## 生成一个 TypeScript 前端客户端 + +让我们从一个简单的 FastAPI 应用开始: + +=== "Python 3.9+" + + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + +请注意,*路径操作* 定义了他们所用于请求数据和回应数据的模型,所使用的模型是`Item` 和 `ResponseMessage`。 + +### API 文档 + +如果您访问API文档,您将看到它具有在请求中发送和在响应中接收数据的**模式(schemas)**: + + + +您可以看到这些模式,因为它们是用程序中的模型声明的。 + +那些信息可以在应用的 **OpenAPI模式** 被找到,然后显示在API文档中(通过Swagger UI)。 + +OpenAPI中所包含的模型里有相同的信息可以用于 **生成客户端代码**。 + +### 生成一个TypeScript 客户端 + +现在我们有了带有模型的应用,我们可以为前端生成客户端代码。 + +#### 安装 `openapi-typescript-codegen` + +您可以使用以下工具在前端代码中安装 `openapi-typescript-codegen`: + +
+ +```console +$ npm install openapi-typescript-codegen --save-dev + +---> 100% +``` + +
+ +#### 生成客户端代码 + +要生成客户端代码,您可以使用现在将要安装的命令行应用程序 `openapi`。 + +因为它安装在本地项目中,所以您可能无法直接使用此命令,但您可以将其放在 `package.json` 文件中。 + +它可能看起来是这样的: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +在这里添加 NPM `generate-client` 脚本后,您可以使用以下命令运行它: + +
+ +```console +$ npm run generate-client + +frontend-app@1.0.0 generate-client /home/user/code/frontend-app +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +``` + +
+ +此命令将在 `./src/client` 中生成代码,并将在其内部使用 `axios`(前端HTTP库)。 + +### 尝试客户端代码 + +现在您可以导入并使用客户端代码,它可能看起来像这样,请注意,您可以为这些方法使用自动补全: + + + +您还将自动补全要发送的数据: + + + +!!! tip + 请注意, `name` 和 `price` 的自动补全,是通过其在`Item`模型(FastAPI)中的定义实现的。 + +如果发送的数据字段不符,你也会看到编辑器的错误提示: + + + +响应(response)对象也拥有自动补全: + + + +## 带有标签的 FastAPI 应用 + +在许多情况下,你的FastAPI应用程序会更复杂,你可能会使用标签来分隔不同组的*路径操作(path operations)*。 + +例如,您可以有一个用 `items` 的部分和另一个用于 `users` 的部分,它们可以用标签来分隔: + +=== "Python 3.9+" + + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + +### 生成带有标签的 TypeScript 客户端 + +如果您使用标签为FastAPI应用生成客户端,它通常也会根据标签分割客户端代码。 + +通过这种方式,您将能够为客户端代码进行正确地排序和分组: + + + +在这个案例中,您有: + +* `ItemsService` +* `UsersService` + +### 客户端方法名称 + +现在生成的方法名像 `createItemItemsPost` 看起来不太简洁: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +...这是因为客户端生成器为每个 *路径操作* 使用OpenAPI的内部 **操作 ID(operation ID)**。 + +OpenAPI要求每个操作 ID 在所有 *路径操作* 中都是唯一的,因此 FastAPI 使用**函数名**、**路径**和**HTTP方法/操作**来生成此操作ID,因为这样可以确保这些操作 ID 是唯一的。 + +但接下来我会告诉你如何改进。 🤓 + +## 自定义操作ID和更好的方法名 + +您可以**修改**这些操作ID的**生成**方式,以使其更简洁,并在客户端中具有**更简洁的方法名称**。 + +在这种情况下,您必须确保每个操作ID在其他方面是**唯一**的。 + +例如,您可以确保每个*路径操作*都有一个标签,然后根据**标签**和*路径操作***名称**(函数名)来生成操作ID。 + +### 自定义生成唯一ID函数 + +FastAPI为每个*路径操作*使用一个**唯一ID**,它用于**操作ID**,也用于任何所需自定义模型的名称,用于请求或响应。 + +你可以自定义该函数。它接受一个 `APIRoute` 对象作为输入,并输出一个字符串。 + +例如,以下是一个示例,它使用第一个标签(你可能只有一个标签)和*路径操作*名称(函数名)。 + +然后,你可以将这个自定义函数作为 `generate_unique_id_function` 参数传递给 **FastAPI**: + +=== "Python 3.9+" + + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + +### 使用自定义操作ID生成TypeScript客户端 + +现在,如果你再次生成客户端,你会发现它具有改善的方法名称: + + + +正如你所见,现在方法名称中只包含标签和函数名,不再包含URL路径和HTTP操作的信息。 + +### 预处理用于客户端生成器的OpenAPI规范 + +生成的代码仍然存在一些**重复的信息**。 + +我们已经知道该方法与 **items** 相关,因为它在 `ItemsService` 中(从标签中获取),但方法名中仍然有标签名作为前缀。😕 + +一般情况下对于OpenAPI,我们可能仍然希望保留它,因为这将确保操作ID是**唯一的**。 + +但对于生成的客户端,我们可以在生成客户端之前**修改** OpenAPI 操作ID,以使方法名称更加美观和**简洁**。 + +我们可以将 OpenAPI JSON 下载到一个名为`openapi.json`的文件中,然后使用以下脚本**删除此前缀的标签**: + +```Python +{!../../../docs_src/generate_clients/tutorial004.py!} +``` + +通过这样做,操作ID将从类似于 `items-get_items` 的名称重命名为 `get_items` ,这样客户端生成器就可以生成更简洁的方法名称。 + +### 使用预处理的OpenAPI生成TypeScript客户端 + +现在,由于最终结果保存在文件openapi.json中,你可以修改 package.json 文件以使用此本地文件,例如: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +生成新的客户端之后,你现在将拥有**清晰的方法名称**,具备**自动补全**、**错误提示**等功能: + + + +## 优点 + +当使用自动生成的客户端时,你将获得以下的自动补全功能: + +* 方法。 +* 请求体中的数据、查询参数等。 +* 响应数据。 + +你还将获得针对所有内容的错误提示。 + +每当你更新后端代码并**重新生成**前端代码时,新的*路径操作*将作为方法可用,旧的方法将被删除,并且其他任何更改将反映在生成的代码中。 🤓 + +这也意味着如果有任何更改,它将自动**反映**在客户端代码中。如果你**构建**客户端,在使用的数据上存在**不匹配**时,它将报错。 + +因此,你将在开发周期的早期**检测到许多错误**,而不必等待错误在生产环境中向最终用户展示,然后尝试调试问题所在。 ✨ From 82ff9a6920115f6b2c00acdf03452a297b926604 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:19:08 +0000 Subject: [PATCH 1302/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3b10fe405..5852de5d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). * 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). * ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). From d8f2f39f6ddc07aa9bc25f7eea8cb752330d7374 Mon Sep 17 00:00:00 2001 From: xzmeng Date: Sat, 2 Sep 2023 23:22:24 +0800 Subject: [PATCH 1303/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`docs/en/docs/how-to/separate-openapi-schemas.md`=20and=20`d?= =?UTF-8?q?ocs/en/docs/tutorial/schema-extra-example.md`=20(#10189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/how-to/separate-openapi-schemas.md | 2 +- docs/en/docs/tutorial/schema-extra-example.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md index d289391ca..d38be3c59 100644 --- a/docs/en/docs/how-to/separate-openapi-schemas.md +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -160,7 +160,7 @@ If you interact with the docs and check the response, even though the code didn' This means that it will **always have a value**, it's just that sometimes the value could be `None` (or `null` in JSON). -That means that, clients using your API don't have to check if the value exists or not, they can **asume the field will always be there**, but just that in some cases it will have the default value of `None`. +That means that, clients using your API don't have to check if the value exists or not, they can **assume the field will always be there**, but just that in some cases it will have the default value of `None`. The way to describe this in OpenAPI, is to mark that field as **required**, because it will always be there. diff --git a/docs/en/docs/tutorial/schema-extra-example.md b/docs/en/docs/tutorial/schema-extra-example.md index 8cf1b9c09..9eeb3f1f2 100644 --- a/docs/en/docs/tutorial/schema-extra-example.md +++ b/docs/en/docs/tutorial/schema-extra-example.md @@ -38,13 +38,13 @@ That extra info will be added as-is to the output **JSON Schema** for that model In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in Pydantic's docs: Model Config. - You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + You can set `"json_schema_extra"` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. === "Pydantic v1" In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in Pydantic's docs: Schema customization. - You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`. + You can set `schema_extra` with a `dict` containing any additional data you would like to show up in the generated JSON Schema, including `examples`. !!! tip You could use the same technique to extend the JSON Schema and add your own custom extra info. From 8cb33e9b477a32ccac867bd5be655e9426d4ccb9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:24:05 +0000 Subject: [PATCH 1304/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5852de5d0..357adc8d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). * 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). * ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). From 1d688a062e59b11b160191d77a9f11971b316cb5 Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Sat, 2 Sep 2023 18:29:36 +0300 Subject: [PATCH 1305/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/tutorial/index.md`=20(#10079)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/uk/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/uk/docs/tutorial/index.md diff --git a/docs/uk/docs/tutorial/index.md b/docs/uk/docs/tutorial/index.md new file mode 100644 index 000000000..e5bae74bc --- /dev/null +++ b/docs/uk/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Туторіал - Посібник користувача + +У цьому посібнику показано, як користуватися **FastAPI** з більшістю його функцій, крок за кроком. + +Кожен розділ поступово надбудовується на попередні, але він структурований на окремі теми, щоб ви могли перейти безпосередньо до будь-якої конкретної, щоб вирішити ваші конкретні потреби API. + +Він також створений як довідник для роботи у майбутньому. + +Тож ви можете повернутися і побачити саме те, що вам потрібно. + +## Запустіть код + +Усі блоки коду можна скопіювати та використовувати безпосередньо (це фактично перевірені файли Python). + +Щоб запустити будь-який із прикладів, скопіюйте код у файл `main.py` і запустіть `uvicorn` за допомогою: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +**ДУЖЕ радимо** написати або скопіювати код, відредагувати його та запустити локально. + +Використання його у своєму редакторі – це те, що дійсно показує вам переваги FastAPI, бачите, як мало коду вам потрібно написати, всі перевірки типів, автозаповнення тощо. + +--- + +## Встановлення FastAPI + +Першим кроком є встановлення FastAPI. + +Для туторіалу ви можете встановити його з усіма необов’язковими залежностями та функціями: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...який також включає `uvicorn`, який ви можете використовувати як сервер, який запускає ваш код. + +!!! note + Ви також можете встановити його частина за частиною. + + Це те, що ви, ймовірно, зробили б, коли захочете розгорнути свою програму у виробничому середовищі: + + ``` + pip install fastapi + ``` + + Також встановіть `uvicorn`, щоб він працював як сервер: + + ``` + pip install "uvicorn[standard]" + ``` + + І те саме для кожної з опціональних залежностей, які ви хочете використовувати. + +## Розширений посібник користувача + +Існує також **Розширений посібник користувача**, який ви зможете прочитати пізніше після цього **Туторіал - Посібник користувача**. + +**Розширений посібник користувача** засновано на цьому, використовує ті самі концепції та навчає вас деяким додатковим функціям. + +Але вам слід спочатку прочитати **Туторіал - Посібник користувача** (те, що ви зараз читаєте). + +Він розроблений таким чином, що ви можете створити повну програму лише за допомогою **Туторіал - Посібник користувача**, а потім розширити її різними способами, залежно від ваших потреб, використовуючи деякі з додаткових ідей з **Розширеного посібника користувача** . From 48f6ccfe7d852867ca1be3aa0e0d782e613cfe61 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:31:37 +0000 Subject: [PATCH 1306/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 357adc8d3..09f19f0f6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). * ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). * 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). From ad76dd1aa806fb62ad64a2b5c14fe00393ed2589 Mon Sep 17 00:00:00 2001 From: whysage <67018871+whysage@users.noreply.github.com> Date: Sat, 2 Sep 2023 18:32:30 +0300 Subject: [PATCH 1307/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/alternatives.md`=20(#10060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/uk/docs/alternatives.md | 412 +++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 docs/uk/docs/alternatives.md diff --git a/docs/uk/docs/alternatives.md b/docs/uk/docs/alternatives.md new file mode 100644 index 000000000..e71257976 --- /dev/null +++ b/docs/uk/docs/alternatives.md @@ -0,0 +1,412 @@ +# Альтернативи, натхнення та порівняння + +Що надихнуло на створення **FastAPI**, який він у порінянні з іншими альтернативами та чого він у них навчився. + +## Вступ + +**FastAPI** не існувало б, якби не попередні роботи інших. + +Раніше було створено багато інструментів, які надихнули на його створення. + +Я кілька років уникав створення нового фреймворку. Спочатку я спробував вирішити всі функції, охоплені **FastAPI**, використовуючи багато різних фреймворків, плагінів та інструментів. + +Але в якийсь момент не було іншого виходу, окрім створення чогось, що надавало б усі ці функції, взявши найкращі ідеї з попередніх інструментів і поєднавши їх найкращим чином, використовуючи мовні функції, які навіть не були доступні раніше (Python 3.6+ підказки типів). + +## Попередні інструменти + +### Django + +Це найпопулярніший фреймворк Python, який користується широкою довірою. Він використовується для створення таких систем, як Instagram. + +Він відносно тісно пов’язаний з реляційними базами даних (наприклад, MySQL або PostgreSQL), тому мати базу даних NoSQL (наприклад, Couchbase, MongoDB, Cassandra тощо) як основний механізм зберігання не дуже просто. + +Він був створений для створення HTML у серверній частині, а не для створення API, які використовуються сучасним інтерфейсом (як-от React, Vue.js і Angular) або іншими системами (як-от IoT пристрої), які спілкуються з ним. + +### Django REST Framework + +Фреймворк Django REST був створений як гнучкий інструментарій для створення веб-інтерфейсів API використовуючи Django в основі, щоб покращити його можливості API. + +Його використовують багато компаній, включаючи Mozilla, Red Hat і Eventbrite. + +Це був один із перших прикладів **автоматичної документації API**, і саме це була одна з перших ідей, яка надихнула на «пошук» **FastAPI**. + +!!! Примітка + Django REST Framework створив Том Крісті. Той самий творець Starlette і Uvicorn, на яких базується **FastAPI**. + + +!!! Перегляньте "Надихнуло **FastAPI** на" + Мати автоматичний веб-інтерфейс документації API. + +### Flask + +Flask — це «мікрофреймворк», він не включає інтеграцію бази даних, а також багато речей, які за замовчуванням є в Django. + +Ця простота та гнучкість дозволяють використовувати бази даних NoSQL як основну систему зберігання даних. + +Оскільки він дуже простий, він порівняно легкий та інтуїтивний для освоєння, хоча в деяких моментах документація стає дещо технічною. + +Він також зазвичай використовується для інших програм, яким не обов’язково потрібна база даних, керування користувачами або будь-яка з багатьох функцій, які є попередньо вбудованими в Django. Хоча багато з цих функцій можна додати за допомогою плагінів. + +Відокремлення частин було ключовою особливістю, яку я хотів зберегти, при цьому залишаючись «мікрофреймворком», який можна розширити, щоб охопити саме те, що потрібно. + +Враховуючи простоту Flask, він здавався хорошим підходом для створення API. Наступним, що знайшов, був «Django REST Framework» для Flask. + +!!! Переглянте "Надихнуло **FastAPI** на" + Бути мікрофреймоворком. Зробити легким комбінування та поєднання необхідних інструментів та частин. + + Мати просту та легку у використанні систему маршрутизації. + + +### Requests + +**FastAPI** насправді не є альтернативою **Requests**. Сфера їх застосування дуже різна. + +Насправді цілком звична річ використовувати Requests *всередині* програми FastAPI. + +Але все ж FastAPI черпав натхнення з Requests. + +**Requests** — це бібліотека для *взаємодії* з API (як клієнт), а **FastAPI** — це бібліотека для *створення* API (як сервер). + +Вони більш-менш знаходяться на протилежних кінцях, доповнюючи одна одну. + +Requests мають дуже простий та інтуїтивно зрозумілий дизайн, дуже простий у використанні, з розумними параметрами за замовчуванням. Але в той же час він дуже потужний і налаштовується. + +Ось чому, як сказано на офіційному сайті: + +> Requests є одним із найбільш завантажуваних пакетів Python усіх часів + +Використовувати його дуже просто. Наприклад, щоб виконати запит `GET`, ви повинні написати: + +```Python +response = requests.get("http://example.com/some/url") +``` + +Відповідна операція *роуту* API FastAPI може виглядати так: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World"} +``` + +Зверніть увагу на схожість у `requests.get(...)` і `@app.get(...)`. + +!!! Перегляньте "Надихнуло **FastAPI** на" + * Майте простий та інтуїтивно зрозумілий API. + * Використовуйте імена (операції) методів HTTP безпосередньо, простим та інтуїтивно зрозумілим способом. + * Розумні параметри за замовчуванням, але потужні налаштування. + + +### Swagger / OpenAPI + +Головною функцією, яку я хотів від Django REST Framework, була автоматична API документація. + +Потім я виявив, що існує стандарт для документування API з використанням JSON (або YAML, розширення JSON) під назвою Swagger. + +І вже був створений веб-інтерфейс користувача для Swagger API. Отже, можливість генерувати документацію Swagger для API дозволить використовувати цей веб-інтерфейс автоматично. + +У якийсь момент Swagger було передано Linux Foundation, щоб перейменувати його на OpenAPI. + +Тому, коли говорять про версію 2.0, прийнято говорити «Swagger», а про версію 3+ «OpenAPI». + +!!! Перегляньте "Надихнуло **FastAPI** на" + Прийняти і використовувати відкритий стандарт для специфікацій API замість спеціальної схеми. + + Інтегрувати інструменти інтерфейсу на основі стандартів: + + * Інтерфейс Swagger + * ReDoc + + Ці два було обрано через те, що вони досить популярні та стабільні, але, виконавши швидкий пошук, ви можете знайти десятки додаткових альтернативних інтерфейсів для OpenAPI (які можна використовувати з **FastAPI**). + +### Фреймворки REST для Flask + +Існує кілька фреймворків Flask REST, але, витративши час і роботу на їх дослідження, я виявив, що багато з них припинено або залишено, з кількома постійними проблемами, які зробили їх непридатними. + +### Marshmallow + +Однією з головних функцій, необхідних для систем API, є "серіалізація", яка бере дані з коду (Python) і перетворює їх на щось, що можна надіслати через мережу. Наприклад, перетворення об’єкта, що містить дані з бази даних, на об’єкт JSON. Перетворення об’єктів `datetime` на строки тощо. + +Іншою важливою функцією, необхідною для API, є перевірка даних, яка забезпечує дійсність даних за певними параметрами. Наприклад, що деяке поле є `int`, а не деяка випадкова строка. Це особливо корисно для вхідних даних. + +Без системи перевірки даних вам довелося б виконувати всі перевірки вручну, у коді. + +Marshmallow створено для забезпечення цих функцій. Це чудова бібліотека, і я часто нею користувався раніше. + +Але він був створений до того, як існували підказки типу Python. Отже, щоб визначити кожну схему, вам потрібно використовувати спеціальні утиліти та класи, надані Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Використовувати код для автоматичного визначення "схем", які надають типи даних і перевірку. + +### Webargs + +Іншою важливою функцією, необхідною для API, є аналіз даних із вхідних запитів. + +Webargs — це інструмент, створений, щоб забезпечити це поверх кількох фреймворків, включаючи Flask. + +Він використовує Marshmallow в основі для перевірки даних. І створений тими ж розробниками. + +Це чудовий інструмент, і я також часто використовував його, перш ніж створити **FastAPI**. + +!!! Інформація + Webargs був створений тими ж розробниками Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Мати автоматичну перевірку даних вхідного запиту. + +### APISpec + +Marshmallow і Webargs забезпечують перевірку, аналіз і серіалізацію як плагіни. + +Але документація досі відсутня. Потім було створено APISpec. + +Це плагін для багатьох фреймворків (також є плагін для Starlette). + +Принцип роботи полягає в тому, що ви пишете визначення схеми, використовуючи формат YAML, у docstring кожної функції, що обробляє маршрут. + +І він генерує схеми OpenAPI. + +Так це працює у Flask, Starlette, Responder тощо. + +Але потім ми знову маємо проблему наявності мікросинтаксису всередині Python строки (великий YAML). + +Редактор тут нічим не може допомогти. І якщо ми змінимо параметри чи схеми Marshmallow і забудемо також змінити цю строку документа YAML, згенерована схема буде застарілою. + +!!! Інформація + APISpec був створений тими ж розробниками Marshmallow. + + +!!! Перегляньте "Надихнуло **FastAPI** на" + Підтримувати відкритий стандарт API, OpenAPI. + +### Flask-apispec + +Це плагін Flask, який об’єднує Webargs, Marshmallow і APISpec. + +Він використовує інформацію з Webargs і Marshmallow для автоматичного створення схем OpenAPI за допомогою APISpec. + +Це чудовий інструмент, дуже недооцінений. Він має бути набагато популярнішим, ніж багато плагінів Flask. Це може бути пов’язано з тим, що його документація надто стисла й абстрактна. + +Це вирішило необхідність писати YAML (інший синтаксис) всередині рядків документів Python. + +Ця комбінація Flask, Flask-apispec із Marshmallow і Webargs була моїм улюбленим бекенд-стеком до створення **FastAPI**. + +Їі використання призвело до створення кількох генераторів повного стека Flask. Це основний стек, який я (та кілька зовнішніх команд) використовував досі: + +* https://github.com/tiangolo/full-stack +* https://github.com/tiangolo/full-stack-flask-couchbase +* https://github.com/tiangolo/full-stack-flask-couchdb + +І ці самі генератори повного стеку були основою [**FastAPI** генераторів проектів](project-generation.md){.internal-link target=_blank}. + +!!! Інформація + Flask-apispec був створений тими ж розробниками Marshmallow. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Створення схеми OpenAPI автоматично з того самого коду, який визначає серіалізацію та перевірку. + +### NestJS (та Angular) + +Це навіть не Python, NestJS — це фреймворк NodeJS JavaScript (TypeScript), натхненний Angular. + +Це досягає чогось подібного до того, що можна зробити з Flask-apispec. + +Він має інтегровану систему впровадження залежностей, натхненну Angular two. Він потребує попередньої реєстрації «injectables» (як і всі інші системи впровадження залежностей, які я знаю), тому це збільшує багатослівність та повторення коду. + +Оскільки параметри описані за допомогою типів TypeScript (подібно до підказок типу Python), підтримка редактора досить хороша. + +Але оскільки дані TypeScript не зберігаються після компіляції в JavaScript, вони не можуть покладатися на типи для визначення перевірки, серіалізації та документації одночасно. Через це та деякі дизайнерські рішення, щоб отримати перевірку, серіалізацію та автоматичну генерацію схеми, потрібно додати декоратори в багатьох місцях. Таким чином код стає досить багатослівним. + +Він не дуже добре обробляє вкладені моделі. Отже, якщо тіло JSON у запиті є об’єктом JSON із внутрішніми полями, які, у свою чергу, є вкладеними об’єктами JSON, його неможливо належним чином задокументувати та перевірити. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Використовувати типи Python, щоб мати чудову підтримку редактора. + + Мати потужну систему впровадження залежностей. Знайдіть спосіб звести до мінімуму повторення коду. + +### Sanic + +Це був один із перших надзвичайно швидких фреймворків Python на основі `asyncio`. Він був дуже схожий на Flask. + +!!! Примітка "Технічні деталі" + Він використовував `uvloop` замість стандартного циклу Python `asyncio`. Ось що зробило його таким швидким. + + Це явно надихнуло Uvicorn і Starlette, які зараз швидші за Sanic у відкритих тестах. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Знайти спосіб отримати божевільну продуктивність. + + Ось чому **FastAPI** базується на Starlette, оскільки це найшвидша доступна структура (перевірена тестами сторонніх розробників). + +### Falcon + +Falcon — ще один високопродуктивний фреймворк Python, він розроблений як мінімальний і працює як основа інших фреймворків, таких як Hug. + +Він розроблений таким чином, щоб мати функції, які отримують два параметри, один «запит» і один «відповідь». Потім ви «читаєте» частини запиту та «записуєте» частини у відповідь. Через такий дизайн неможливо оголосити параметри запиту та тіла за допомогою стандартних підказок типу Python як параметри функції. + +Таким чином, перевірка даних, серіалізація та документація повинні виконуватися в коді, а не автоматично. Або вони повинні бути реалізовані як фреймворк поверх Falcon, як Hug. Така сама відмінність спостерігається в інших фреймворках, натхненних дизайном Falcon, що мають один об’єкт запиту та один об’єкт відповіді як параметри. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Знайти способи отримати чудову продуктивність. + + Разом із Hug (оскільки Hug базується на Falcon) надихнув **FastAPI** оголосити параметр `response` у функціях. + + Хоча у FastAPI це необов’язково, і використовується в основному для встановлення заголовків, файлів cookie та альтернативних кодів стану. + +### Molten + +Я відкрив для себе Molten на перших етапах створення **FastAPI**. І він має досить схожі ідеї: + +* Базується на підказках типу Python. +* Перевірка та документація цих типів. +* Система впровадження залежностей. + +Він не використовує перевірку даних, серіалізацію та бібліотеку документації сторонніх розробників, як Pydantic, він має свою власну. Таким чином, ці визначення типів даних не можна було б використовувати повторно так легко. + +Це вимагає трохи більш докладних конфігурацій. І оскільки він заснований на WSGI (замість ASGI), він не призначений для використання високопродуктивних інструментів, таких як Uvicorn, Starlette і Sanic. + +Система впровадження залежностей вимагає попередньої реєстрації залежностей, і залежності вирішуються на основі оголошених типів. Отже, неможливо оголосити більше ніж один «компонент», який надає певний тип. + +Маршрути оголошуються в одному місці з використанням функцій, оголошених в інших місцях (замість використання декораторів, які можна розмістити безпосередньо поверх функції, яка обробляє кінцеву точку). Це ближче до того, як це робить Django, ніж до Flask (і Starlette). Він розділяє в коді речі, які відносно тісно пов’язані. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Визначити додаткові перевірки для типів даних, використовуючи значення "за замовчуванням" атрибутів моделі. Це покращує підтримку редактора, а раніше вона була недоступна в Pydantic. + + Це фактично надихнуло оновити частини Pydantic, щоб підтримувати той самий стиль оголошення перевірки (всі ці функції вже доступні в Pydantic). + +### Hug + +Hug був одним із перших фреймворків, який реалізував оголошення типів параметрів API за допомогою підказок типу Python. Це була чудова ідея, яка надихнула інші інструменти зробити те саме. + +Він використовував спеціальні типи у своїх оголошеннях замість стандартних типів Python, але це все одно був величезний крок вперед. + +Це також був один із перших фреймворків, який генерував спеціальну схему, що оголошувала весь API у JSON. + +Він не базувався на таких стандартах, як OpenAPI та JSON Schema. Тому було б непросто інтегрувати його з іншими інструментами, як-от Swagger UI. Але знову ж таки, це була дуже інноваційна ідея. + +Він має цікаву незвичайну функцію: використовуючи ту саму структуру, можна створювати API, а також CLI. + +Оскільки він заснований на попередньому стандарті для синхронних веб-фреймворків Python (WSGI), він не може працювати з Websockets та іншими речами, хоча він також має високу продуктивність. + +!!! Інформація + Hug створив Тімоті Крослі, той самий творець `isort`, чудовий інструмент для автоматичного сортування імпорту у файлах Python. + +!!! Перегляньте "Надихнуло **FastAPI** на" + Hug надихнув частину APIStar і був одним із найбільш перспективних інструментів, поряд із APIStar. + + Hug надихнув **FastAPI** на використання підказок типу Python для оголошення параметрів і автоматичного створення схеми, що визначає API. + + Hug надихнув **FastAPI** оголосити параметр `response` у функціях для встановлення заголовків і файлів cookie. + +### APIStar (<= 0,5) + +Безпосередньо перед тим, як вирішити створити **FastAPI**, я знайшов сервер **APIStar**. Він мав майже все, що я шукав, і мав чудовий дизайн. + +Це була одна з перших реалізацій фреймворку, що використовує підказки типу Python для оголошення параметрів і запитів, яку я коли-небудь бачив (до NestJS і Molten). Я знайшов його більш-менш одночасно з Hug. Але APIStar використовував стандарт OpenAPI. + +Він мав автоматичну перевірку даних, серіалізацію даних і генерацію схеми OpenAPI на основі підказок того самого типу в кількох місцях. + +Визначення схеми тіла не використовували ті самі підказки типу Python, як Pydantic, воно було трохи схоже на Marshmallow, тому підтримка редактора була б не такою хорошою, але все ж APIStar був найкращим доступним варіантом. + +Він мав найкращі показники продуктивності на той час (перевершив лише Starlette). + +Спочатку він не мав автоматичного веб-інтерфейсу документації API, але я знав, що можу додати до нього інтерфейс користувача Swagger. + +Він мав систему введення залежностей. Він вимагав попередньої реєстрації компонентів, як і інші інструменти, розглянуті вище. Але все одно це була чудова функція. + +Я ніколи не міг використовувати його в повноцінному проекті, оскільки він не мав інтеграції безпеки, тому я не міг замінити всі функції, які мав, генераторами повного стеку на основі Flask-apispec. У моїх невиконаних проектах я мав створити запит на вилучення, додавши цю функцію. + +Але потім фокус проекту змінився. + +Це вже не був веб-фреймворк API, оскільки творцю потрібно було зосередитися на Starlette. + +Тепер APIStar — це набір інструментів для перевірки специфікацій OpenAPI, а не веб-фреймворк. + +!!! Інформація + APIStar створив Том Крісті. Той самий хлопець, який створив: + + * Django REST Framework + * Starlette (на якому базується **FastAPI**) + * Uvicorn (використовується Starlette і **FastAPI**) + +!!! Перегляньте "Надихнуло **FastAPI** на" + Існувати. + + Ідею оголошення кількох речей (перевірки даних, серіалізації та документації) за допомогою тих самих типів Python, які в той же час забезпечували чудову підтримку редактора, я вважав геніальною ідеєю. + + І після тривалого пошуку подібної структури та тестування багатьох різних альтернатив, APIStar став найкращим доступним варіантом. + + Потім APIStar перестав існувати як сервер, і було створено Starlette, який став новою кращою основою для такої системи. Це стало останнім джерелом натхнення для створення **FastAPI**. Я вважаю **FastAPI** «духовним спадкоємцем» APIStar, удосконалюючи та розширюючи функції, систему введення тексту та інші частини на основі досвіду, отриманого від усіх цих попередніх інструментів. + +## Використовується **FastAPI** + +### Pydantic + +Pydantic — це бібліотека для визначення перевірки даних, серіалізації та документації (за допомогою схеми JSON) на основі підказок типу Python. + +Це робить його надзвичайно інтуїтивним. + +Його можна порівняти з Marshmallow. Хоча він швидший за Marshmallow у тестах. Оскільки він базується на тих самих підказках типу Python, підтримка редактора чудова. + +!!! Перегляньте "**FastAPI** використовує його для" + Виконання перевірки всіх даних, серіалізації даних і автоматичної документацію моделі (на основі схеми JSON). + + Потім **FastAPI** бере ці дані схеми JSON і розміщує їх у OpenAPI, окремо від усіх інших речей, які він робить. + +### Starlette + +Starlette — це легкий фреймворк/набір інструментів ASGI, який ідеально підходить для створення високопродуктивних asyncio сервісів. + +Він дуже простий та інтуїтивно зрозумілий. Його розроблено таким чином, щоб його можна було легко розширювати та мати модульні компоненти. + +Він має: + +* Серйозно вражаючу продуктивність. +* Підтримку WebSocket. +* Фонові завдання в процесі. +* Події запуску та завершення роботи. +* Тестового клієнта, побудований на HTTPX. +* CORS, GZip, статичні файли, потокові відповіді. +* Підтримку сеансів і файлів cookie. +* 100% покриття тестом. +* 100% анотовану кодову базу. +* Кілька жорстких залежностей. + +Starlette наразі є найшвидшим фреймворком Python із перевірених. Перевершує лише Uvicorn, який є не фреймворком, а сервером. + +Starlette надає всі основні функції веб-мікрофреймворку. + +Але він не забезпечує автоматичної перевірки даних, серіалізації чи документації. + +Це одна з головних речей, які **FastAPI** додає зверху, все на основі підказок типу Python (з використанням Pydantic). Це, а також система впровадження залежностей, утиліти безпеки, створення схеми OpenAPI тощо. + +!!! Примітка "Технічні деталі" + ASGI — це новий «стандарт», який розробляється членами основної команди Django. Це ще не «стандарт Python» (PEP), хоча вони в процесі цього. + + Тим не менш, він уже використовується як «стандарт» кількома інструментами. Це значно покращує сумісність, оскільки ви можете переключити Uvicorn на будь-який інший сервер ASGI (наприклад, Daphne або Hypercorn), або ви можете додати інструменти, сумісні з ASGI, як-от `python-socketio`. + +!!! Перегляньте "**FastAPI** використовує його для" + Керування всіма основними веб-частинами. Додавання функцій зверху. + + Сам клас `FastAPI` безпосередньо успадковує клас `Starlette`. + + Отже, усе, що ви можете робити зі Starlette, ви можете робити це безпосередньо за допомогою **FastAPI**, оскільки це, по суті, Starlette на стероїдах. + +### Uvicorn + +Uvicorn — це блискавичний сервер ASGI, побудований на uvloop і httptools. + +Це не веб-фреймворк, а сервер. Наприклад, він не надає інструментів для маршрутизації. Це те, що фреймворк на кшталт Starlette (або **FastAPI**) забезпечить поверх нього. + +Це рекомендований сервер для Starlette і **FastAPI**. + +!!! Перегляньте "**FastAPI** рекомендує це як" + Основний веб-сервер для запуску програм **FastAPI**. + + Ви можете поєднати його з Gunicorn, щоб мати асинхронний багатопроцесний сервер. + + Додаткову інформацію див. у розділі [Розгортання](deployment/index.md){.internal-link target=_blank}. + +## Орієнтири та швидкість + +Щоб зрозуміти, порівняти та побачити різницю між Uvicorn, Starlette і FastAPI, перегляньте розділ про [Бенчмарки](benchmarks.md){.internal-link target=_blank}. From 4e93f8e0bc94f72adc79cf1517061f2a1bd17677 Mon Sep 17 00:00:00 2001 From: Ragul K <78537172+ragul-kachiappan@users.noreply.github.com> Date: Sat, 2 Sep 2023 21:02:48 +0530 Subject: [PATCH 1308/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/en/docs/tutorial/dependencies/dependencies-in-path-ope?= =?UTF-8?q?ration-decorators.md`=20(#10172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../dependencies/dependencies-in-path-operation-decorators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index 935555339..ccef5aef4 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -35,7 +35,7 @@ It should be a `list` of `Depends()`: {!> ../../../docs_src/dependencies/tutorial006.py!} ``` -These dependencies will be executed/solved the same way normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. +These dependencies will be executed/solved the same way as normal dependencies. But their value (if they return any) won't be passed to your *path operation function*. !!! tip Some editors check for unused function parameters, and show them as errors. From 59cbeccac02cf66cbdc55fd1d7ec2675fa5e8e9e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:36:34 +0000 Subject: [PATCH 1309/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 09f19f0f6..a74499d88 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). * ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). From 9fc33f8565be369ed7b40d8202a8cf4a49c951b6 Mon Sep 17 00:00:00 2001 From: Ahsan Sheraz Date: Sat, 2 Sep 2023 20:37:40 +0500 Subject: [PATCH 1310/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20comment=20in=20`fastapi/applications.py`=20(#10045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index b681e50b3..5cc568292 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -189,20 +189,20 @@ class FastAPI(Starlette): # contextvars. # This needs to happen after user middlewares because those create a # new contextvars context copy by using a new AnyIO task group. - # The initial part of dependencies with yield is executed in the - # FastAPI code, inside all the middlewares, but the teardown part - # (after yield) is executed in the AsyncExitStack in this middleware, - # if the AsyncExitStack lived outside of the custom middlewares and - # contextvars were set in a dependency with yield in that internal + # The initial part of dependencies with 'yield' is executed in the + # FastAPI code, inside all the middlewares. However, the teardown part + # (after 'yield') is executed in the AsyncExitStack in this middleware. + # If the AsyncExitStack lived outside of the custom middlewares and + # contextvars were set in a dependency with 'yield' in that internal # contextvars context, the values would not be available in the - # outside context of the AsyncExitStack. - # By putting the middleware and the AsyncExitStack here, inside all - # user middlewares, the code before and after yield in dependencies - # with yield is executed in the same contextvars context, so all values - # set in contextvars before yield is still available after yield as - # would be expected. + # outer context of the AsyncExitStack. + # By placing the middleware and the AsyncExitStack here, inside all + # user middlewares, the code before and after 'yield' in dependencies + # with 'yield' is executed in the same contextvars context. Thus, all values + # set in contextvars before 'yield' are still available after 'yield,' as + # expected. # Additionally, by having this AsyncExitStack here, after the - # ExceptionMiddleware, now dependencies can catch handled exceptions, + # ExceptionMiddleware, dependencies can now catch handled exceptions, # e.g. HTTPException, to customize the teardown code (e.g. DB session # rollback). Middleware(AsyncExitStackMiddleware), From a55f3204ef2577045a0ab0585dd58c40686a2da2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:37:56 +0000 Subject: [PATCH 1311/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a74499d88..2a9ffc954 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). * 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). * ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). From b2562c5c73440c992e2bd9a1ad07312f3358361f Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:41:53 +0000 Subject: [PATCH 1312/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2a9ffc954..8af275623 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). * 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). From 2e3295719814fdfb957c31d0720c578053c230fc Mon Sep 17 00:00:00 2001 From: Poupapaa <3238986+poupapaa@users.noreply.github.com> Date: Sat, 2 Sep 2023 22:43:16 +0700 Subject: [PATCH 1313/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/en/docs/tutorial/handling-errors.md`=20(#10170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/handling-errors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 8c30326ce..a03029e81 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -1,6 +1,6 @@ # Handling Errors -There are many situations in where you need to notify an error to a client that is using your API. +There are many situations in which you need to notify an error to a client that is using your API. This client could be a browser with a frontend, a code from someone else, an IoT device, etc. From 8979166bc308067951f5361df0cf85b0b73fb1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Kh=E1=BA=AFc=20Th=C3=A0nh?= Date: Sat, 2 Sep 2023 22:44:17 +0700 Subject: [PATCH 1314/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Vietnamese=20tra?= =?UTF-8?q?nslations=20for=20`docs/vi/docs/tutorial/first-steps.md`=20and?= =?UTF-8?q?=20`docs/vi/docs/tutorial/index.md`=20(#10088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/vi/docs/tutorial/first-steps.md | 333 +++++++++++++++++++++++++++ docs/vi/docs/tutorial/index.md | 80 +++++++ 2 files changed, 413 insertions(+) create mode 100644 docs/vi/docs/tutorial/first-steps.md create mode 100644 docs/vi/docs/tutorial/index.md diff --git a/docs/vi/docs/tutorial/first-steps.md b/docs/vi/docs/tutorial/first-steps.md new file mode 100644 index 000000000..712f00852 --- /dev/null +++ b/docs/vi/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Những bước đầu tiên + +Tệp tin FastAPI đơn giản nhất có thể trông như này: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Sao chép sang một tệp tin `main.py`. + +Chạy live server: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +!!! note + Câu lệnh `uvicorn main:app` được giải thích như sau: + + * `main`: tệp tin `main.py` (một Python "mô đun"). + * `app`: một object được tạo ra bên trong `main.py` với dòng `app = FastAPI()`. + * `--reload`: làm server khởi động lại sau mỗi lần thay đổi. Chỉ sử dụng trong môi trường phát triển. + +Trong output, có một dòng giống như: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Dòng đó cho thấy URL, nơi mà app của bạn đang được chạy, trong máy local của bạn. + +### Kiểm tra + +Mở trình duyệt của bạn tại http://127.0.0.1:8000. + +Bạn sẽ thấy một JSON response như: + +```JSON +{"message": "Hello World"} +``` + +### Tài liệu tương tác API + +Bây giờ tới http://127.0.0.1:8000/docs. + +Bạn sẽ thấy một tài liệu tương tác API (cung cấp bởi Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Phiên bản thay thế của tài liệu API + +Và bây giờ tới http://127.0.0.1:8000/redoc. + +Bạn sẽ thấy một bản thay thế của tài liệu (cung cấp bởi ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** sinh một "schema" với tất cả API của bạn sử dụng tiêu chuẩn **OpenAPI** cho định nghĩa các API. + +#### "Schema" + +Một "schema" là một định nghĩa hoặc mô tả thứ gì đó. Không phải code triển khai của nó, nhưng chỉ là một bản mô tả trừu tượng. + +#### API "schema" + +Trong trường hợp này, OpenAPI là một bản mô tả bắt buộc cơ chế định nghĩa API của bạn. + +Định nghĩa cấu trúc này bao gồm những đường dẫn API của bạn, các tham số có thể có,... + +#### "Cấu trúc" dữ liệu + +Thuật ngữ "cấu trúc" (schema) cũng có thể được coi như là hình dạng của dữ liệu, tương tự như một JSON content. + +Trong trường hợp đó, nó có nghĩa là các thuộc tính JSON và các kiểu dữ liệu họ có,... + +#### OpenAPI và JSON Schema + +OpenAPI định nghĩa một cấu trúc API cho API của bạn. Và cấu trúc đó bao gồm các dịnh nghĩa (or "schema") về dữ liệu được gửi đi và nhận về bởi API của bạn, sử dụng **JSON Schema**, một tiêu chuẩn cho cấu trúc dữ liệu JSON. + +#### Kiểm tra `openapi.json` + +Nếu bạn tò mò về việc cấu trúc OpenAPI nhìn như thế nào thì FastAPI tự động sinh một JSON (schema) với các mô tả cho tất cả API của bạn. + +Bạn có thể thấy nó trực tiếp tại: http://127.0.0.1:8000/openapi.json. + +Nó sẽ cho thấy một JSON bắt đầu giống như: + +```JSON +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### OpenAPI dùng để làm gì? + +Cấu trúc OpenAPI là sức mạnh của tài liệu tương tác. + +Và có hàng tá các bản thay thế, tất cả đều dựa trên OpenAPI. Bạn có thể dễ dàng thêm bất kì bản thay thế bào cho ứng dụng của bạn được xây dựng với **FastAPI**. + +Bạn cũng có thể sử dụng nó để sinh code tự động, với các client giao viết qua API của bạn. Ví dụ, frontend, mobile hoặc các ứng dụng IoT. + +## Tóm lại, từng bước một + +### Bước 1: import `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` là một Python class cung cấp tất cả chức năng cho API của bạn. + +!!! note "Chi tiết kĩ thuật" + `FastAPI` là một class kế thừa trực tiếp `Starlette`. + + Bạn cũng có thể sử dụng tất cả Starlette chức năng với `FastAPI`. + +### Bước 2: Tạo một `FastAPI` "instance" + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Biến `app` này là một "instance" của class `FastAPI`. + +Đây sẽ là điểm cốt lõi để tạo ra tất cả API của bạn. + +`app` này chính là điều được nhắc tới bởi `uvicorn` trong câu lệnh: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +Nếu bạn tạo ứng dụng của bạn giống như: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Và đặt nó trong một tệp tin `main.py`, sau đó bạn sẽ gọi `uvicorn` giống như: + +
+ +```console +$ uvicorn main:my_awesome_api --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +
+ +### Bước 3: tạo một *đường dẫn toán tử* + +#### Đường dẫn + +"Đường dẫn" ở đây được nhắc tới là phần cuối cùng của URL bắt đầu từ `/`. + +Do đó, trong một URL nhìn giống như: + +``` +https://example.com/items/foo +``` + +...đường dẫn sẽ là: + +``` +/items/foo +``` + +!!! info + Một đường dẫn cũng là một cách gọi chung cho một "endpoint" hoặc một "route". + +Trong khi xây dựng một API, "đường dẫn" là các chính để phân tách "mối quan hệ" và "tài nguyên". + +#### Toán tử (Operation) + +"Toán tử" ở đây được nhắc tới là một trong các "phương thức" HTTP. + +Một trong những: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +...và một trong những cái còn lại: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +Trong giao thức HTTP, bạn có thể giao tiếp trong mỗi đường dẫn sử dụng một (hoặc nhiều) trong các "phương thức này". + +--- + +Khi xây dựng các API, bạn thường sử dụng cụ thể các phương thức HTTP này để thực hiện một hành động cụ thể. + +Thông thường, bạn sử dụng + +* `POST`: để tạo dữ liệu. +* `GET`: để đọc dữ liệu. +* `PUT`: để cập nhật dữ liệu. +* `DELETE`: để xóa dữ liệu. + +Do đó, trong OpenAPI, mỗi phương thức HTTP được gọi là một "toán tử (operation)". + +Chúng ta cũng sẽ gọi chúng là "**các toán tử**". + +#### Định nghĩa moojt *decorator cho đường dẫn toán tử* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`@app.get("/")` nói **FastAPI** rằng hàm bên dưới có trách nhiệm xử lí request tới: + +* đường dẫn `/` +* sử dụng một toán tửget + +!!! info Thông tin về "`@decorator`" + Cú pháp `@something` trong Python được gọi là một "decorator". + + Bạn đặt nó trên một hàm. Giống như một chiếc mũ xinh xắn (Tôi ddonas đó là lí do mà thuật ngữ này ra đời). + + Một "decorator" lấy một hàm bên dưới và thực hiện một vài thứ với nó. + + Trong trường hợp của chúng ta, decorator này nói **FastAPI** rằng hàm bên dưới ứng với **đường dẫn** `/` và một **toán tử** `get`. + + Nó là một "**decorator đường dẫn toán tử**". + +Bạn cũng có thể sử dụng với các toán tử khác: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Và nhiều hơn với các toán tử còn lại: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip + Bạn thoải mái sử dụng mỗi toán tử (phương thức HTTP) như bạn mơ ước. + + **FastAPI** không bắt buộc bất kì ý nghĩa cụ thể nào. + + Thông tin ở đây được biểu thị như là một chỉ dẫn, không phải là một yêu cầu bắt buộc. + + Ví dụ, khi sử dụng GraphQL bạn thông thường thực hiện tất cả các hành động chỉ bằng việc sử dụng các toán tử `POST`. + +### Step 4: Định nghĩa **hàm cho đường dẫn toán tử** + +Đây là "**hàm cho đường dẫn toán tử**": + +* **đường dẫn**: là `/`. +* **toán tử**: là `get`. +* **hàm**: là hàm bên dưới "decorator" (bên dưới `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Đây là một hàm Python. + +Nó sẽ được gọi bởi **FastAPI** bất cứ khi nào nó nhận một request tới URL "`/`" sử dụng một toán tử `GET`. + +Trong trường hợp này, nó là một hàm `async`. + +--- + +Bạn cũng có thể định nghĩa nó như là một hàm thông thường thay cho `async def`: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note + Nếu bạn không biết sự khác nhau, kiểm tra [Async: *"Trong khi vội vàng?"*](../async.md#in-a-hurry){.internal-link target=_blank}. + +### Bước 5: Nội dung trả về + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Bạn có thể trả về một `dict`, `list`, một trong những giá trị đơn như `str`, `int`,... + +Bạn cũng có thể trả về Pydantic model (bạn sẽ thấy nhiều hơn về nó sau). + +Có nhiều object và model khác nhau sẽ được tự động chuyển đổi sang JSON (bao gồm cả ORM,...). Thử sử dụng loại ưa thích của bạn, nó có khả năng cao đã được hỗ trợ. + +## Tóm lại + +* Import `FastAPI`. +* Tạo một `app` instance. +* Viết một **decorator cho đường dẫn toán tử** (giống như `@app.get("/")`). +* Viết một **hàm cho đường dẫn toán tử** (giống như `def root(): ...` ở trên). +* Chạy server trong môi trường phát triển (giống như `uvicorn main:app --reload`). diff --git a/docs/vi/docs/tutorial/index.md b/docs/vi/docs/tutorial/index.md new file mode 100644 index 000000000..e8a93fe40 --- /dev/null +++ b/docs/vi/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Hướng dẫn sử dụng + +Hướng dẫn này cho bạn thấy từng bước cách sử dụng **FastAPI** đa số các tính năng của nó. + +Mỗi phần được xây dựng từ những phần trước đó, nhưng nó được cấu trúc thành các chủ đề riêng biệt, do đó bạn có thể xem trực tiếp từng phần cụ thể bất kì để giải quyết những API cụ thể mà bạn cần. + +Nó cũng được xây dựng để làm việc như một tham chiếu trong tương lai. + +Do đó bạn có thể quay lại và tìm chính xác những gì bạn cần. + +## Chạy mã + +Tất cả các code block có thể được sao chép và sử dụng trực tiếp (chúng thực chất là các tệp tin Python đã được kiểm thử). + +Để chạy bất kì ví dụ nào, sao chép code tới tệp tin `main.py`, và bắt đầu `uvicorn` với: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +**Khuyến khích** bạn viết hoặc sao chép code, sửa và chạy nó ở local. + +Sử dụng nó trong trình soạn thảo của bạn thực sự cho bạn thấy những lợi ích của FastAPI, thấy được cách bạn viết code ít hơn, tất cả đều được type check, autocompletion,... + +--- + +## Cài đặt FastAPI + +Bước đầu tiên là cài đặt FastAPI. + +Với hướng dẫn này, bạn có thể muốn cài đặt nó với tất cả các phụ thuộc và tính năng tùy chọn: + +
+ +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +
+ +...dó cũng bao gồm `uvicorn`, bạn có thể sử dụng như một server để chạy code của bạn. + +!!! note + Bạn cũng có thể cài đặt nó từng phần. + + Đây là những gì bạn có thể sẽ làm một lần duy nhất bạn muốn triển khai ứng dụng của bạn lên production: + + ``` + pip install fastapi + ``` + + Cũng cài đặt `uvicorn` để làm việc như một server: + + ``` + pip install "uvicorn[standard]" + ``` + + Và tương tự với từng phụ thuộc tùy chọn mà bạn muốn sử dụng. + +## Hướng dẫn nâng cao + +Cũng có một **Hướng dẫn nâng cao** mà bạn có thể đọc nó sau **Hướng dẫn sử dụng**. + +**Hướng dẫn sử dụng nâng cao**, xây dựng dựa trên cái này, sử dụng các khái niệm tương tự, và dạy bạn những tính năng mở rộng. + +Nhưng bạn nên đọc **Hướng dẫn sử dụng** đầu tiên (những gì bạn đang đọc). + +Nó được thiết kế do đó bạn có thể xây dựng một ứng dụng hoàn chỉnh chỉ với **Hướng dẫn sử dụng**, và sau đó mở rộng nó theo các cách khác nhau, phụ thuộc vào những gì bạn cần, sử dụng một vài ý tưởng bổ sung từ **Hướng dẫn sử dụng nâng cao**. From e6c4785959609097631fa37f393cd9f977377dfa Mon Sep 17 00:00:00 2001 From: Rostyslav Date: Sat, 2 Sep 2023 18:48:03 +0300 Subject: [PATCH 1315/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/python-types.md`=20(#10080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/uk/docs/python-types.md | 448 +++++++++++++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 docs/uk/docs/python-types.md diff --git a/docs/uk/docs/python-types.md b/docs/uk/docs/python-types.md new file mode 100644 index 000000000..f792e83a8 --- /dev/null +++ b/docs/uk/docs/python-types.md @@ -0,0 +1,448 @@ +# Вступ до типів Python + +Python підтримує додаткові "підказки типу" ("type hints") (також звані "анотаціями типу" ("type annotations")). + +Ці **"type hints"** є спеціальним синтаксисом, що дозволяє оголошувати тип змінної. + +За допомогою оголошення типів для ваших змінних, редактори та інструменти можуть надати вам кращу підтримку. + +Це просто **швидкий посібник / нагадування** про анотації типів у Python. Він покриває лише мінімум, необхідний щоб використовувати їх з **FastAPI**... що насправді дуже мало. + +**FastAPI** повністю базується на цих анотаціях типів, вони дають йому багато переваг. + +Але навіть якщо ви ніколи не використаєте **FastAPI**, вам буде корисно дізнатись трохи про них. + +!!! note + Якщо ви експерт у Python і ви вже знаєте усе про анотації типів - перейдіть до наступного розділу. + +## Мотивація + +Давайте почнемо з простого прикладу: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +Виклик цієї програми виводить: + +``` +John Doe +``` + +Функція виконує наступне: + +* Бере `first_name` та `last_name`. +* Конвертує кожну літеру кожного слова у верхній регістр за допомогою `title()`. +* Конкатенує їх разом із пробілом по середині. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### Редагуйте це + +Це дуже проста програма. + +Але тепер уявіть, що ви писали це з нуля. + +У певний момент ви розпочали б визначення функції, у вас були б готові параметри... + +Але тоді вам потрібно викликати "той метод, який переводить першу літеру у верхній регістр". + +Це буде `upper`? Чи `uppercase`? `first_uppercase`? `capitalize`? + +Тоді ви спробуєте давнього друга програміста - автозаповнення редактора коду. + +Ви надрукуєте перший параметр функції, `first_name`, тоді крапку (`.`), а тоді натиснете `Ctrl+Space`, щоб запустити автозаповнення. + +Але, на жаль, ви не отримаєте нічого корисного: + + + +### Додайте типи + +Давайте змінимо один рядок з попередньої версії. + +Ми змінимо саме цей фрагмент, параметри функції, з: + +```Python + first_name, last_name +``` + +на: + +```Python + first_name: str, last_name: str +``` + +Ось і все. + +Це "type hints": + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +Це не те саме, що оголошення значень за замовчуванням, як це було б з: + +```Python + first_name="john", last_name="doe" +``` + +Це зовсім інше. + +Ми використовуємо двокрапку (`:`), не дорівнює (`=`). + +І додавання анотації типу зазвичай не змінює того, що сталось би без них. + +Але тепер, уявіть що ви посеред процесу створення функції, але з анотаціями типів. + +В цей же момент, ви спробуєте викликати автозаповнення з допомогою `Ctrl+Space` і побачите: + + + +Разом з цим, ви можете прокручувати, переглядати опції, допоки ви не знайдете одну, що звучить схоже: + + + +## Більше мотивації + +Перевірте цю функцію, вона вже має анотацію типу: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +Оскільки редактор знає типи змінних, ви не тільки отримаєте автозаповнення, ви також отримаєте перевірку помилок: + + + +Тепер ви знаєте, щоб виправити це, вам потрібно перетворити `age` у строку з допомогою `str(age)`: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## Оголошення типів + +Щойно ви побачили основне місце для оголошення анотацій типу. Як параметри функції. + +Це також основне місце, де ви б їх використовували у **FastAPI**. + +### Прості типи + +Ви можете оголошувати усі стандартні типи у Python, не тільки `str`. + +Ви можете використовувати, наприклад: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### Generic-типи з параметрами типів + +Існують деякі структури даних, які можуть містити інші значення, наприклад `dict`, `list`, `set` та `tuple`. І внутрішні значення також можуть мати свій тип. + +Ці типи, які мають внутрішні типи, називаються "**generic**" типами. І оголосити їх можна навіть із внутрішніми типами. + +Щоб оголосити ці типи та внутрішні типи, ви можете використовувати стандартний модуль Python `typing`. Він існує спеціально для підтримки анотацій типів. + +#### Новіші версії Python + +Синтаксис із використанням `typing` **сумісний** з усіма версіями, від Python 3.6 до останніх, включаючи Python 3.9, Python 3.10 тощо. + +У міру розвитку Python **новіші версії** мають покращену підтримку анотацій типів і в багатьох випадках вам навіть не потрібно буде імпортувати та використовувати модуль `typing` для оголошення анотацій типу. + +Якщо ви можете вибрати новішу версію Python для свого проекту, ви зможете скористатися цією додатковою простотою. Дивіться кілька прикладів нижче. + +#### List (список) + +Наприклад, давайте визначимо змінну, яка буде `list` із `str`. + +=== "Python 3.6 і вище" + + З модуля `typing`, імпортуємо `List` (з великої літери `L`): + + ``` Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + + Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). + + Як тип вкажемо `List`, який ви імпортували з `typing`. + + Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: + + ```Python hl_lines="4" + {!> ../../../docs_src/python_types/tutorial006.py!} + ``` + +=== "Python 3.9 і вище" + + Оголосимо змінну з тим самим синтаксисом двокрапки (`:`). + + Як тип вкажемо `list`. + + Оскільки список є типом, який містить деякі внутрішні типи, ви поміщаєте їх у квадратні дужки: + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial006_py39.py!} + ``` + +!!! info + Ці внутрішні типи в квадратних дужках називаються "параметрами типу". + + У цьому випадку, `str` це параметр типу переданий у `List` (або `list` у Python 3.9 і вище). + +Це означає: "змінна `items` це `list`, і кожен з елементів у цьому списку - `str`". + +!!! tip + Якщо ви використовуєте Python 3.9 і вище, вам не потрібно імпортувати `List` з `typing`, ви можете використовувати натомість тип `list`. + +Зробивши це, ваш редактор може надати підтримку навіть під час обробки елементів зі списку: + + + +Без типів цього майже неможливо досягти. + +Зверніть увагу, що змінна `item` є одним із елементів у списку `items`. + +І все ж редактор знає, що це `str`, і надає підтримку для цього. + +#### Tuple and Set (кортеж та набір) + +Ви повинні зробити те ж саме, щоб оголосити `tuple` і `set`: + +=== "Python 3.6 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial007.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial007_py39.py!} + ``` + +Це означає: + +* Змінна `items_t` це `tuple` з 3 елементами, `int`, ще `int`, та `str`. +* Змінна `items_s` це `set`, і кожен його елемент типу `bytes`. + +#### Dict (словник) + +Щоб оголосити `dict`, вам потрібно передати 2 параметри типу, розділені комами. + +Перший параметр типу для ключа у `dict`. + +Другий параметр типу для значення у `dict`: + +=== "Python 3.6 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008_py39.py!} + ``` + +Це означає: + +* Змінна `prices` це `dict`: + * Ключі цього `dict` типу `str` (наприклад, назва кожного елементу). + * Значення цього `dict` типу `float` (наприклад, ціна кожного елементу). + +#### Union (об'єднання) + +Ви можете оголосити, що змінна може бути будь-яким із **кількох типів**, наприклад, `int` або `str`. + +У Python 3.6 і вище (включаючи Python 3.10) ви можете використовувати тип `Union` з `typing` і вставляти в квадратні дужки можливі типи, які можна прийняти. + +У Python 3.10 також є **альтернативний синтаксис**, у якому ви можете розділити можливі типи за допомогою вертикальної смуги (`|`). + +=== "Python 3.6 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial008b.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial008b_py310.py!} + ``` + +В обох випадках це означає, що `item` може бути `int` або `str`. + +#### Possibly `None` (Optional) + +Ви можете оголосити, що значення може мати тип, наприклад `str`, але також може бути `None`. + +У Python 3.6 і вище (включаючи Python 3.10) ви можете оголосити його, імпортувавши та використовуючи `Optional` з модуля `typing`. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +Використання `Optional[str]` замість просто `str` дозволить редактору допомогти вам виявити помилки, коли ви могли б вважати, що значенням завжди є `str`, хоча насправді воно також може бути `None`. + +`Optional[Something]` насправді є скороченням для `Union[Something, None]`, вони еквівалентні. + +Це також означає, що в Python 3.10 ви можете використовувати `Something | None`: + +=== "Python 3.6 і вище" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009.py!} + ``` + +=== "Python 3.6 і вище - альтернатива" + + ```Python hl_lines="1 4" + {!> ../../../docs_src/python_types/tutorial009b.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python hl_lines="1" + {!> ../../../docs_src/python_types/tutorial009_py310.py!} + ``` + +#### Generic типи + +Ці типи, які приймають параметри типу у квадратних дужках, називаються **Generic types** or **Generics**, наприклад: + +=== "Python 3.6 і вище" + + * `List` + * `Tuple` + * `Set` + * `Dict` + * `Union` + * `Optional` + * ...та інші. + +=== "Python 3.9 і вище" + + Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): + + * `list` + * `tuple` + * `set` + * `dict` + + І те саме, що й у Python 3.6, із модуля `typing`: + + * `Union` + * `Optional` + * ...та інші. + +=== "Python 3.10 і вище" + + Ви можете використовувати ті самі вбудовані типи, як generic (з квадратними дужками та типами всередині): + + * `list` + * `tuple` + * `set` + * `dict` + + І те саме, що й у Python 3.6, із модуля `typing`: + + * `Union` + * `Optional` (так само як у Python 3.6) + * ...та інші. + + У Python 3.10, як альтернатива використанню `Union` та `Optional`, ви можете використовувати вертикальну смугу (`|`) щоб оголосити об'єднання типів. + +### Класи як типи + +Ви також можете оголосити клас як тип змінної. + +Скажімо, у вас є клас `Person` з імʼям: + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +Потім ви можете оголосити змінну типу `Person`: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +І знову ж таки, ви отримуєте всю підтримку редактора: + + + +## Pydantic моделі + +Pydantic це бібліотека Python для валідації даних. + +Ви оголошуєте «форму» даних як класи з атрибутами. + +І кожен атрибут має тип. + +Потім ви створюєте екземпляр цього класу з деякими значеннями, і він перевірить ці значення, перетворить їх у відповідний тип (якщо є потреба) і надасть вам об’єкт з усіма даними. + +І ви отримуєте всю підтримку редактора з цим отриманим об’єктом. + +Приклад з документації Pydantic: + +=== "Python 3.6 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011.py!} + ``` + +=== "Python 3.9 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py39.py!} + ``` + +=== "Python 3.10 і вище" + + ```Python + {!> ../../../docs_src/python_types/tutorial011_py310.py!} + ``` + +!!! info + Щоб дізнатись більше про Pydantic, перегляньте його документацію. + +**FastAPI** повністю базується на Pydantic. + +Ви побачите набагато більше цього всього на практиці в [Tutorial - User Guide](tutorial/index.md){.internal-link target=_blank}. + +## Анотації типів у **FastAPI** + +**FastAPI** використовує ці підказки для виконання кількох речей. + +З **FastAPI** ви оголошуєте параметри з підказками типу, і отримуєте: + +* **Підтримку редактора**. +* **Перевірку типів**. + +...і **FastAPI** використовує ті самі оголошення для: + +* **Визначення вимог**: з параметрів шляху запиту, параметрів запиту, заголовків, тіл, залежностей тощо. +* **Перетворення даних**: із запиту в необхідний тип. +* **Перевірка даних**: що надходять від кожного запиту: + * Генерування **автоматичних помилок**, що повертаються клієнту, коли дані недійсні. +* **Документування** API за допомогою OpenAPI: + * який потім використовується для автоматичної інтерактивної документації користувальницьких інтерфейсів. + +Все це може здатися абстрактним. Не хвилюйтеся. Ви побачите все це в дії в [Туторіал - Посібник користувача](tutorial/index.md){.internal-link target=_blank}. + +Важливо те, що за допомогою стандартних типів Python в одному місці (замість того, щоб додавати більше класів, декораторів тощо), **FastAPI** зробить багато роботи за вас. + +!!! info + Якщо ви вже пройшли весь навчальний посібник і повернулися, щоб дізнатися більше про типи, ось хороший ресурс "шпаргалка" від `mypy`. From 1866abffc1485bd09971d3c6f4a98fcfe4d96b0a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:49:31 +0000 Subject: [PATCH 1316/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8af275623..44143af63 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). * ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). * 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). From 28bf4abf1fd81a6a930cca415604ebfa90898727 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:50:11 +0000 Subject: [PATCH 1317/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 44143af63..57fea97ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). * ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). * ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). From 7fe952f52255ebc2eef151fa9bb6c3a6f9d30909 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 15:54:22 +0000 Subject: [PATCH 1318/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 57fea97ef..7861a35ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). * ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). * ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). From 0ea23e2a8de9aad5c675f02f3ec54b9dd756a877 Mon Sep 17 00:00:00 2001 From: Hasnat Sajid <86589885+hasnatsajid@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:55:41 +0500 Subject: [PATCH 1319/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20link=20to?= =?UTF-8?q?=20Pydantic=20docs=20in=20`docs/en/docs/tutorial/extra-data-typ?= =?UTF-8?q?es.md`=20(#10155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs/en/docs/tutorial/extra-data-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index 7d6ffbc78..b34ccd26f 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -49,7 +49,7 @@ Here are some of the additional data types you can use: * `Decimal`: * Standard Python `Decimal`. * In requests and responses, handled the same as a `float`. -* You can check all the valid pydantic data types here: Pydantic data types. +* You can check all the valid pydantic data types here: Pydantic data types. ## Example From 0242ca756670e66aeb534054c9251176f08876bd Mon Sep 17 00:00:00 2001 From: Rahul Salgare Date: Sat, 2 Sep 2023 21:26:35 +0530 Subject: [PATCH 1320/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20Pydantic?= =?UTF-8?q?=20examples=20in=20tutorial=20for=20Python=20types=20(#9961)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- docs_src/python_types/tutorial011.py | 2 +- docs_src/python_types/tutorial011_py310.py | 2 +- docs_src/python_types/tutorial011_py39.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs_src/python_types/tutorial011.py b/docs_src/python_types/tutorial011.py index c8634cbff..297a84db6 100644 --- a/docs_src/python_types/tutorial011.py +++ b/docs_src/python_types/tutorial011.py @@ -6,7 +6,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: Union[datetime, None] = None friends: List[int] = [] diff --git a/docs_src/python_types/tutorial011_py310.py b/docs_src/python_types/tutorial011_py310.py index 7f173880f..842760c60 100644 --- a/docs_src/python_types/tutorial011_py310.py +++ b/docs_src/python_types/tutorial011_py310.py @@ -5,7 +5,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: datetime | None = None friends: list[int] = [] diff --git a/docs_src/python_types/tutorial011_py39.py b/docs_src/python_types/tutorial011_py39.py index 468496f51..4eb40b405 100644 --- a/docs_src/python_types/tutorial011_py39.py +++ b/docs_src/python_types/tutorial011_py39.py @@ -6,7 +6,7 @@ from pydantic import BaseModel class User(BaseModel): id: int - name = "John Doe" + name: str = "John Doe" signup_ts: Union[datetime, None] = None friends: list[int] = [] From aa43afa4c0aeba4615d3fde9ac96abf7a1a2e08a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 16:00:21 +0000 Subject: [PATCH 1321/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7861a35ab..c540d2426 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). * 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). * ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). From 34028290f5386011f4638ea4e36f1619d60b9b0c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 16:03:22 +0000 Subject: [PATCH 1322/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c540d2426..622f6ddc5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). * ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). * 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). From 1711c1e95f3eec71c9d29050c9901137117b54aa Mon Sep 17 00:00:00 2001 From: Olaoluwa Afolabi Date: Sat, 2 Sep 2023 17:12:44 +0100 Subject: [PATCH 1323/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Yoruba=20transla?= =?UTF-8?q?tion=20for=20`docs/yo/docs/index.md`=20(#10033)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez --- docs/en/mkdocs.yml | 3 + docs/yo/docs/index.md | 470 ++++++++++++++++++++++++++++++++++++++++++ docs/yo/mkdocs.yml | 1 + 3 files changed, 474 insertions(+) create mode 100644 docs/yo/docs/index.md create mode 100644 docs/yo/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index c56e4c942..ba1ac7924 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -72,6 +72,7 @@ nav: - uk: /uk/ - ur: /ur/ - vi: /vi/ + - yo: /yo/ - zh: /zh/ - features.md - fastapi-people.md @@ -261,6 +262,8 @@ extra: name: ur - link: /vi/ name: vi - Tiếng Việt + - link: /yo/ + name: yo - Yorùbá - link: /zh/ name: zh - 汉语 extra_css: diff --git a/docs/yo/docs/index.md b/docs/yo/docs/index.md new file mode 100644 index 000000000..ca75a6b13 --- /dev/null +++ b/docs/yo/docs/index.md @@ -0,0 +1,470 @@ +

+ FastAPI +

+

+ Ìlànà wẹ́ẹ́bù FastAPI, iṣẹ́ gíga, ó rọrùn láti kọ̀, o yára láti kóòdù, ó sì ṣetán fún iṣelọpọ ní lílo +

+

+ + Test + + + Coverage + + + Package version + + + Supported Python versions + +

+ +--- + +**Àkọsílẹ̀**: https://fastapi.tiangolo.com + +**Orisun Kóòdù**: https://github.com/tiangolo/fastapi + +--- + +FastAPI jẹ́ ìgbàlódé, tí ó yára (iṣẹ-giga), ìlànà wẹ́ẹ́bù fún kikọ àwọn API pẹ̀lú Python 3.7+ èyí tí ó da lori àwọn ìtọ́kasí àmì irúfẹ́ Python. + +Àwọn ẹya pàtàkì ni: + +* **Ó yára**: Iṣẹ tí ó ga púpọ̀, tí ó wa ni ibamu pẹ̀lú **NodeJS** àti **Go** (ọpẹ si Starlette àti Pydantic). [Ọkan nínú àwọn ìlànà Python ti o yára jùlọ ti o wa](#performance). +* **Ó yára láti kóòdù**: O mu iyara pọ si láti kọ àwọn ẹya tuntun kóòdù nipasẹ "Igba ìdá ọgọ́rùn-ún" (i.e. 200%) si "ọ̀ọ́dúrún ìdá ọgọ́rùn-ún" (i.e. 300%). +* **Àìtọ́ kékeré**: O n din aṣiṣe ku bi ọgbon ìdá ọgọ́rùn-ún (i.e. 40%) ti eda eniyan (oṣiṣẹ kóòdù) fa. * +* **Ọgbọ́n àti ìmọ̀**: Atilẹyin olootu nla. Ìparí nibi gbogbo. Àkókò díẹ̀ nipa wíwá ibi tí ìṣòro kóòdù wà. +* **Irọrun**: A kọ kí ó le rọrun láti lo àti láti kọ ẹkọ nínú rè. Ó máa fún ọ ní àkókò díẹ̀ látı ka àkọsílẹ. +* **Ó kúkurú ní kikọ**: Ó dín àtúnkọ àti àtúntò kóòdù kù. Ìkéde àṣàyàn kọ̀ọ̀kan nínú rẹ̀ ní ọ̀pọ̀lọpọ̀ àwọn ìlò. O ṣe iranlọwọ láti má ṣe ní ọ̀pọ̀lọpọ̀ àṣìṣe. +* **Ó lágbára**: Ó ń ṣe àgbéjáde kóòdù tí ó ṣetán fún ìṣelọ́pọ̀. Pẹ̀lú àkọsílẹ̀ tí ó máa ṣàlàyé ara rẹ̀ fún ẹ ní ìbáṣepọ̀ aládàáṣiṣẹ́ pẹ̀lú rè. +* **Ajohunše/Ìtọ́kasí**: Ó da lori (àti ibamu ni kikun pẹ̀lú) àwọn ìmọ ajohunše/ìtọ́kasí fún àwọn API: OpenAPI (èyí tí a mọ tẹlẹ si Swagger) àti JSON Schema. + +* iṣiro yi da lori àwọn idanwo tí ẹgbẹ ìdàgbàsókè FastAPI ṣe, nígbàtí wọn kọ àwọn ohun elo iṣelọpọ kóòdù pẹ̀lú rẹ. + +## Àwọn onígbọ̀wọ́ + + + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} + +{% endfor -%} +{%- for sponsor in sponsors.silver -%} + +{% endfor %} +{% endif %} + + + +Àwọn onígbọ̀wọ́ míràn + +## Àwọn ero àti èsì + +"_[...] Mò ń lo **FastAPI** púpọ̀ ní lẹ́nu àìpẹ́ yìí. [...] Mo n gbero láti lo o pẹ̀lú àwọn ẹgbẹ mi fún gbogbo iṣẹ **ML wa ni Microsoft**. Diẹ nínú wọn ni afikun ti ifilelẹ àwọn ẹya ara ti ọja **Windows** wa pẹ̀lú àwọn ti **Office**._" + +
Kabir Khan - Microsoft (ref)
+ +--- + +"_A gba àwọn ohun èlò ìwé afọwọkọ **FastAPI** tí kò yí padà láti ṣẹ̀dá olùpín **REST** tí a lè béèrè lọ́wọ́ rẹ̀ láti gba **àsọtẹ́lẹ̀**. [fún Ludwig]_" + +
Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - Uber (ref)
+ +--- + +"_**Netflix** ni inudidun láti kede itusilẹ orisun kóòdù ti ìlànà iṣọkan **iṣakoso Ìṣòro** wa: **Ìfiránṣẹ́**! [a kọ pẹ̀lú **FastAPI**]_" + +
Kevin Glisson, Marc Vilanova, Forest Monsen - Netflix (ref)
+ +--- + +"_Inú mi dùn púpọ̀ nípa **FastAPI**. Ó mú inú ẹnì dùn púpọ̀!_" + +
Brian Okken - Python Bytes podcast host (ref)
+ +--- + +"_Ní tòótọ́, ohun tí o kọ dára ó sì tún dán. Ní ọ̀pọ̀lọpọ̀ ọ̀nà, ohun tí mo fẹ́ kí **Hug** jẹ́ nìyẹn - ó wúni lórí gan-an láti rí ẹnìkan tí ó kọ́ nǹkan bí èyí._" + +
Timothy Crosley - Hug creator (ref)
+ +--- + +"_Ti o ba n wa láti kọ ọkan **ìlànà igbalode** fún kikọ àwọn REST API, ṣayẹwo **FastAPI** [...] Ó yára, ó rọrùn láti lò, ó sì rọrùn láti kọ́[...]_" + +"_A ti yipada si **FastAPI** fún **APIs** wa [...] Mo lérò pé wà á fẹ́ràn rẹ̀ [...]_" + +
Ines Montani - Matthew Honnibal - Explosion AI founders - spaCy creators (ref) - (ref)
+ +--- + +"_Ti ẹnikẹni ba n wa láti kọ iṣelọpọ API pẹ̀lú Python, èmi yóò ṣe'dúró fún **FastAPI**. Ó jẹ́ ohun tí **àgbékalẹ̀ rẹ̀ lẹ́wà**, **ó rọrùn láti lò** àti wipe ó ni **ìwọ̀n gíga**, o tí dí **bọtini paati** nínú alakọkọ API ìdàgbàsókè kikọ fún wa, àti pe o ni ipa lori adaṣiṣẹ àti àwọn iṣẹ gẹ́gẹ́ bíi Onímọ̀-ẹ̀rọ TAC tí órí Íńtánẹ́ẹ̀tì_" + +
Deon Pillsbury - Cisco (ref)
+ +--- + +## **Typer**, FastAPI ti CLIs + + + +Ti o ba n kọ ohun èlò CLI láti ṣeé lọ nínú ohun èlò lori ebute kọmputa dipo API, ṣayẹwo **Typer**. + +**Typer** jẹ́ àbúrò ìyá FastAPI kékeré. Àti pé wọ́n kọ́ láti jẹ́ **FastAPI ti CLIs**. ⌨️ 🚀 + +## Èròjà + +Python 3.7+ + +FastAPI dúró lórí àwọn èjìká tí àwọn òmíràn: + +* Starlette fún àwọn ẹ̀yà ayélujára. +* Pydantic fún àwọn ẹ̀yà àkójọf'áyẹ̀wò. + +## Fifi sórí ẹrọ + +
+ +```console +$ pip install fastapi + +---> 100% +``` + +
+Iwọ yóò tún nílò olupin ASGI, fún iṣelọpọ bii Uvicorn tabi Hypercorn. + +
+ +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +
+ +## Àpẹẹrẹ + +### Ṣẹ̀dá rẹ̀ + +* Ṣẹ̀dá fáìlì `main.py (èyí tíí ṣe, akọkọ.py)` pẹ̀lú: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +
+Tàbí lò async def... + +Tí kóòdù rẹ̀ bá ń lò `async` / `await`, lò `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Akiyesi**: + +Tí o kò bá mọ̀, ṣàyẹ̀wò ibi tí a ti ní _"In a hurry?"_ (i.e. _"Ní kíákíá?"_) nípa `async` and `await` nínú àkọsílẹ̀. + +
+ +### Mu ṣiṣẹ + +Mú olupin ṣiṣẹ pẹ̀lú: + +
+ +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +
+Nipa aṣẹ kóòdù náà uvicorn main:app --reload... + +Àṣẹ `uvicorn main:app` ń tọ́ka sí: + +* `main`: fáìlì náà 'main.py' (Python "module"). +* `app` jẹ object( i.e. nǹkan) tí a ṣẹ̀dá nínú `main.py` pẹ̀lú ilà `app = FastAPI()`. +* `--reload`: èyí yóò jẹ́ ki olupin tún bẹ̀rẹ̀ lẹ́hìn àwọn àyípadà kóòdù. Jọ̀wọ́, ṣe èyí fún ìdàgbàsókè kóòdù nìkan, má ṣe é ṣe lori àgbéjáde kóòdù tabi fún iṣelọpọ kóòdù. + + +
+ +### Ṣayẹwo rẹ + +Ṣii aṣàwákiri kọ̀ǹpútà rẹ ni http://127.0.0.1:8000/items/5?q=somequery. + +Ìwọ yóò sì rí ìdáhùn JSON bíi: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +O tí ṣẹ̀dá API èyí tí yóò: + +* Gbà àwọn ìbéèrè HTTP ni àwọn _ipa ọ̀nà_ `/` àti `/items/{item_id}`. +* Èyí tí àwọn _ipa ọ̀nà_ (i.e. _paths_) méjèèjì gbà àwọn iṣẹ `GET` (a tun mọ si _àwọn ọna_ HTTP). +* Èyí tí _ipa ọ̀nà_ (i.e. _paths_) `/items/{item_id}` ní _àwọn ohun-ini ipa ọ̀nà_ tí ó yẹ kí ó jẹ́ `int` i.e. `ÒǸKÀ`. +* Èyí tí _ipa ọ̀nà_ (i.e. _paths_) `/items/{item_id}` ní àṣàyàn `str` _àwọn ohun-ini_ (i.e. _query parameter_) `q`. + +### Ìbáṣepọ̀ àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/docs. + +Lẹ́yìn náà, iwọ yóò rí ìdáhùn àkọsílẹ̀ API tí ó jẹ́ ìbáṣepọ̀ alaifọwọyi/aládàáṣiṣẹ́ (tí a pèṣè nípaṣẹ̀ Swagger UI): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Ìdàkejì àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/redoc. + +Wà á rí àwọn àkọsílẹ̀ aládàáṣiṣẹ́ mìíràn (tí a pese nipasẹ ReDoc): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Àpẹẹrẹ ìgbésókè mìíràn + +Ní báyìí ṣe àtúnṣe fáìlì `main.py` láti gba kókó èsì láti inú ìbéèrè `PUT`. + +Ní báyìí, ṣe ìkéde kókó èsì API nínú kóòdù rẹ nipa lílo àwọn ìtọ́kasí àmì irúfẹ́ Python, ọpẹ́ pàtàkìsi sí Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Olupin yóò tún ṣe àtúnṣe laifọwọyi/aládàáṣiṣẹ́ (nítorí wípé ó se àfikún `-reload` si àṣẹ kóòdù `uvicorn` lókè). + +### Ìbáṣepọ̀ ìgbésókè àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/docs. + +* Ìbáṣepọ̀ àkọsílẹ̀ API yóò ṣe imudojuiwọn àkọsílẹ̀ API laifọwọyi, pẹ̀lú kókó èsì ìdáhùn API tuntun: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Tẹ bọtini "Gbiyanju rẹ" i.e. "Try it out", yóò gbà ọ́ láàyè láti jẹ́ kí ó tẹ́ àlàyé tí ó nílò kí ó le sọ̀rọ̀ tààrà pẹ̀lú API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Lẹhinna tẹ bọtini "Ṣiṣe" i.e. "Execute", olùmúlò (i.e. user interface) yóò sọrọ pẹ̀lú API rẹ, yóò ṣe afiranṣẹ àwọn èròjà, pàápàá jùlọ yóò gba àwọn àbájáde yóò si ṣafihan wọn loju ìbòjú: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Ìdàkejì ìgbésókè àkọsílẹ̀ API + +Ní báyìí, lọ sí http://127.0.0.1:8000/redoc. + +* Ìdàkejì àkọsílẹ̀ API yóò ṣ'afihan ìbéèrè èròjà/pàrámítà tuntun àti kókó èsì ti API: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Àtúnyẹ̀wò + +Ni akopọ, ìwọ yóò kéde ni **kete** àwọn iru èròjà/pàrámítà, kókó èsì API, abbl (i.e. àti bẹbẹ lọ), bi àwọn èròjà iṣẹ. + +O ṣe ìyẹn pẹ̀lú irúfẹ́ àmì ìtọ́kasí ìgbàlódé Python. + +O ò nílò láti kọ́ síńtáàsì tuntun, ìlànà tàbí ọ̀wọ́ kíláàsì kan pàtó, abbl (i.e. àti bẹbẹ lọ). + +Ìtọ́kasí **Python 3.7+** + +Fún àpẹẹrẹ, fún `int`: + +```Python +item_id: int +``` + +tàbí fún àwòṣe `Item` tí ó nira díẹ̀ síi: + +```Python +item: Item +``` + +... àti pẹ̀lú ìkéde kan ṣoṣo yẹn ìwọ yóò gbà: + +* Atilẹyin olootu, pẹ̀lú: + * Pipari. + * Àyẹ̀wò irúfẹ́ àmì ìtọ́kasí. +* Ìfọwọ́sí àkójọf'áyẹ̀wò (i.e. data): + * Aṣiṣe alaifọwọyi/aládàáṣiṣẹ́ àti aṣiṣe ti ó hàn kedere nígbàtí àwọn àkójọf'áyẹ̀wò (i.e. data) kò wulo tabi tí kò fẹsẹ̀ múlẹ̀. + * Ìfọwọ́sí fún ohun elo JSON tí ó jìn gan-an. +* Ìyípadà tí input àkójọf'áyẹ̀wò: tí ó wà láti nẹtiwọọki si àkójọf'áyẹ̀wò àti irúfẹ́ àmì ìtọ́kasí Python. Ó ń ka láti: + * JSON. + * èròjà ọ̀nà tí ò gbé gbà. + * èròjà ìbéèrè. + * Àwọn Kúkì + * Àwọn Àkọlé + * Àwọn Fọọmu + * Àwọn Fáìlì +* Ìyípadà èsì àkójọf'áyẹ̀wò: yíyípadà láti àkójọf'áyẹ̀wò àti irúfẹ́ àmì ìtọ́kasí Python si nẹtiwọọki (gẹ́gẹ́ bí JSON): + * Yí irúfẹ́ àmì ìtọ́kasí padà (`str`, `int`, `float`, `bool`, `list`, abbl i.e. àti bèbè ló). + * Àwọn ohun èlò `datetime`. + * Àwọn ohun èlò `UUID`. + * Àwọn awoṣẹ́ ibi ìpamọ́ àkójọf'áyẹ̀wò. + * ...àti ọ̀pọ̀lọpọ̀ díẹ̀ síi. +* Ìbáṣepọ̀ àkọsílẹ̀ API aládàáṣiṣẹ́, pẹ̀lú ìdàkejì àgbékalẹ̀-àwọn-olùmúlò (i.e user interfaces) méjì: + * Àgbékalẹ̀-olùmúlò Swagger. + * ReDoc. + +--- + +Nisinsin yi, tí ó padà sí àpẹẹrẹ ti tẹ́lẹ̀, **FastAPI** yóò: + +* Fọwọ́ sí i pé `item_id` wà nínú ọ̀nà ìbéèrè HTTP fún `GET` àti `PUT`. +* Fọwọ́ sí i pé `item_id` jẹ́ irúfẹ́ àmì ìtọ́kasí `int` fún ìbéèrè HTTP `GET` àti `PUT`. + * Tí kìí bá ṣe bẹ, oníbàárà yóò ríi àṣìṣe tí ó wúlò, kedere. +* Ṣàyẹ̀wò bóyá ìbéèrè àṣàyàn pàrámítà kan wà tí orúkọ rẹ̀ ń jẹ́ `q` (gẹ́gẹ́ bíi `http://127.0.0.1:8000/items/foo?q=somequery`) fún ìbéèrè HTTP `GET`. + * Bí wọ́n ṣe kéde pàrámítà `q` pẹ̀lú `= None`, ó jẹ́ àṣàyàn (i.e optional). + * Láìsí `None` yóò nílò (gẹ́gẹ́ bí kókó èsì ìbéèrè HTTP ṣe wà pẹ̀lú `PUT`). +* Fún àwọn ìbéèrè HTTP `PUT` sí `/items/{item_id}`, kà kókó èsì ìbéèrè HTTP gẹ́gẹ́ bí JSON: + * Ṣàyẹ̀wò pé ó ní àbùdá tí ó nílò èyí tíí ṣe `name` i.e. `orúkọ` tí ó yẹ kí ó jẹ́ `str`. + * Ṣàyẹ̀wò pé ó ní àbùdá tí ó nílò èyí tíí ṣe `price` i.e. `iye` tí ó gbọ́dọ̀ jẹ́ `float`. + * Ṣàyẹ̀wò pé ó ní àbùdá àṣàyàn `is_offer`, tí ó yẹ kí ó jẹ́ `bool`, tí ó bá wà níbẹ̀. + * Gbogbo èyí yóò tún ṣiṣẹ́ fún àwọn ohun èlò JSON tí ó jìn gidi gan-an. +* Yìí padà láti àti sí JSON lai fi ọwọ́ yi. +* Ṣe àkọsílẹ̀ ohun gbogbo pẹ̀lú OpenAPI, èyí tí yóò wà ní lílo nípaṣẹ̀: + * Àwọn ètò àkọsílẹ̀ ìbáṣepọ̀. + * Aládàáṣiṣẹ́ oníbárà èlètò tíí ṣẹ̀dá kóòdù, fún ọ̀pọ̀lọpọ̀ àwọn èdè. +* Pese àkọsílẹ̀ òní ìbáṣepọ̀ ti àwọn àgbékalẹ̀ ayélujára méjì tààrà. + +--- + +A ń ṣẹ̀ṣẹ̀ ń mú ẹyẹ bọ́ làpò ní, ṣùgbọ́n ó ti ni òye bí gbogbo rẹ̀ ṣe ń ṣiṣẹ́. + +Gbiyanju láti yí ìlà padà pẹ̀lú: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...láti: + +```Python + ... "item_name": item.name ... +``` + +...ṣí: + +```Python + ... "item_price": item.price ... +``` + +.. kí o sì wo bí olóòtú rẹ yóò ṣe parí àwọn àbùdá náà fúnra rẹ̀, yóò sì mọ irúfẹ́ wọn: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Fún àpẹẹrẹ pípé síi pẹ̀lú àwọn àbùdá mìíràn, wo Ìdánilẹ́kọ̀ọ́ - Ìtọ́sọ́nà Olùmúlò. + +**Itaniji gẹ́gẹ́ bí isọ'ye**: ìdánilẹ́kọ̀ọ́ - itọsọna olùmúlò pẹ̀lú: + +* Ìkéde àṣàyàn **pàrámítà** láti àwọn oriṣiriṣi ibòmíràn gẹ́gẹ́ bíi: àwọn **àkọlé èsì API**, **kúkì**, **ààyè fọọmu**, àti **fáìlì**. +* Bíi ó ṣe lé ṣètò **àwọn ìdíwọ́ ìfọwọ́sí** bí `maximum_length` tàbí `regex`. +* Ó lágbára púpọ̀ ó sì rọrùn láti lo ètò **Àfikún Ìgbẹ́kẹ̀lé Kóòdù**. +* Ààbò àti ìfọwọ́sowọ́pọ̀, pẹ̀lú àtìlẹ́yìn fún **OAuth2** pẹ̀lú **àmì JWT** àti **HTTP Ipilẹ ìfọwọ́sowọ́pọ̀**. +* Àwọn ìlànà ìlọsíwájú (ṣùgbọ́n tí ó rọrùn bákan náà) fún ìkéde **àwọn àwòṣe JSON tó jinlẹ̀** (ọpẹ́ pàtàkìsi sí Pydantic). +* Iṣọpọ **GraphQL** pẹ̀lú Strawberry àti àwọn ohun èlò ìwé kóòdù afọwọkọ mìíràn tí kò yí padà. +* Ọpọlọpọ àwọn àfikún àwọn ẹ̀yà (ọpẹ́ pàtàkìsi sí Starlette) bí: + * **WebSockets** + * àwọn ìdánwò tí ó rọrùn púpọ̀ lórí HTTPX àti `pytest` + * **CORS** + * **Cookie Sessions** + * ...àti síwájú síi. + +## Ìṣesí + +Àwọn àlá TechEmpower fi hàn pé **FastAPI** ń ṣiṣẹ́ lábẹ́ Uvicorn gẹ́gẹ́ bí ọ̀kan lára àwọn ìlànà Python tí ó yára jùlọ tí ó wà, ní ìsàlẹ̀ Starlette àti Uvicorn fúnra wọn (tí FastAPI ń lò fúnra rẹ̀). (*) + +Láti ní òye síi nípa rẹ̀, wo abala àwọn Àlá. + +## Àṣàyàn Àwọn Àfikún Ìgbẹ́kẹ̀lé Kóòdù + +Èyí tí Pydantic ń lò: + +* email_validator - fún ifọwọsi ímeèlì. +* pydantic-settings - fún ètò ìsàkóso. +* pydantic-extra-types - fún àfikún oríṣi láti lọ pẹ̀lú Pydantic. + +Èyí tí Starlette ń lò: + +* httpx - Nílò tí ó bá fẹ́ láti lọ `TestClient`. +* jinja2 - Nílò tí ó bá fẹ́ láti lọ iṣeto awoṣe aiyipada. +* python-multipart - Nílò tí ó bá fẹ́ láti ṣe àtìlẹ́yìn fún "àyẹ̀wò" fọọmu, pẹ̀lú `request.form()`. +* itsdangerous - Nílò fún àtìlẹ́yìn `SessionMiddleware`. +* pyyaml - Nílò fún àtìlẹ́yìn Starlette's `SchemaGenerator` (ó ṣe ṣe kí ó má nílò rẹ̀ fún FastAPI). +* ujson - Nílò tí ó bá fẹ́ láti lọ `UJSONResponse`. + +Èyí tí FastAPI / Starlette ń lò: + +* uvicorn - Fún olupin tí yóò sẹ́ àmúyẹ àti tí yóò ṣe ìpèsè fún iṣẹ́ rẹ tàbí ohun èlò rẹ. +* orjson - Nílò tí ó bá fẹ́ láti lọ `ORJSONResponse`. + +Ó lè fi gbogbo àwọn wọ̀nyí sórí ẹrọ pẹ̀lú `pip install "fastapi[all]"`. + +## Iwe-aṣẹ + +Iṣẹ́ yìí ni iwe-aṣẹ lábẹ́ àwọn òfin tí iwe-aṣẹ MIT. diff --git a/docs/yo/mkdocs.yml b/docs/yo/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/yo/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From a6d893fe981f270be660c3e8bceab888d38786c5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 16:16:38 +0000 Subject: [PATCH 1324/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 622f6ddc5..21b3be50e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). * ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). * ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). * 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). From caf0b688cd7e71da0a207ab5d611a57ce4ac5f13 Mon Sep 17 00:00:00 2001 From: Yusuke Tamura <62091034+tamtam-fitness@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:55:26 +0900 Subject: [PATCH 1325/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20indent=20f?= =?UTF-8?q?ormat=20in=20`docs/en/docs/deployment/server-workers.md`=20(#10?= =?UTF-8?q?066)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/en/docs/deployment/server-workers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/deployment/server-workers.md b/docs/en/docs/deployment/server-workers.md index 4ccd9d9f6..2df9f3d43 100644 --- a/docs/en/docs/deployment/server-workers.md +++ b/docs/en/docs/deployment/server-workers.md @@ -90,7 +90,9 @@ Let's see what each of those options mean: ``` * 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: From 7802454131866a1af7b509702fb6de369f33c71d Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 16:56:04 +0000 Subject: [PATCH 1326/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 21b3be50e..624fc736b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). * ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). * ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). From 23511f1fdf5de1b47575c6d1e305c17a3851fbae Mon Sep 17 00:00:00 2001 From: Alex Rocha <62669972+LecoOliveira@users.noreply.github.com> Date: Sat, 2 Sep 2023 14:01:06 -0300 Subject: [PATCH 1327/1881] =?UTF-8?q?=F0=9F=8C=90=20Remove=20duplicate=20l?= =?UTF-8?q?ine=20in=20translation=20for=20`docs/pt/docs/tutorial/path-para?= =?UTF-8?q?ms.md`=20(#10126)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/path-params.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/pt/docs/tutorial/path-params.md b/docs/pt/docs/tutorial/path-params.md index 5de3756ed..cd8c18858 100644 --- a/docs/pt/docs/tutorial/path-params.md +++ b/docs/pt/docs/tutorial/path-params.md @@ -236,7 +236,6 @@ Então, você poderia usar ele com: Com o **FastAPI**, usando as declarações de tipo do Python, você obtém: * Suporte no editor: verificação de erros, e opção de autocompletar, etc. -* Parsing de dados * "Parsing" de dados * Validação de dados * Anotação da API e documentação automática From 7f1dedac2c8f57aa1e976aaa492f5cbd77d25ab1 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 17:01:44 +0000 Subject: [PATCH 1328/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 624fc736b..54a00d9ed 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). * ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). * ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). From c502197d7cffc6fe3f310fc2a72dd3148bcaa016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Dorr=C3=ADo=20V=C3=A1zquez?= <64154120+pablodorrio@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:02:26 +0200 Subject: [PATCH 1329/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20validation?= =?UTF-8?q?=20parameter=20name=20in=20docs,=20from=20`regex`=20to=20`patte?= =?UTF-8?q?rn`=20(#10085)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/query-params-str-validations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index f87adddcb..5d1c08add 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -932,7 +932,7 @@ Validations specific for strings: * `min_length` * `max_length` -* `regex` +* `pattern` In these examples you saw how to declare validations for `str` values. From e1a1a367a74a64c330e11fb63a870a2c77a29a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 2 Sep 2023 19:03:43 +0200 Subject: [PATCH 1330/1881] =?UTF-8?q?=F0=9F=93=8C=20Pin=20AnyIO=20to=20=0.27.0,<0.28.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.5.0", + # TODO: remove this pin after upgrading Starlette 0.31.1 + "anyio>=3.7.1,<4.0.0", ] dynamic = ["version"] From 8562cae44b18d0f1638bb6d338a85754468fc559 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 17:05:59 +0000 Subject: [PATCH 1331/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 54a00d9ed..24dd82085 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 📌 Pin AnyIO to < 4.0.0 to handle an incompatibility while upgrading to Starlette 0.31.1. PR [#10194](https://github.com/tiangolo/fastapi/pull/10194) by [@tiangolo](https://github.com/tiangolo). * 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). * ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). From 118010ad5ebb98f602deee7e69c3bff94aadf16a Mon Sep 17 00:00:00 2001 From: github-actions Date: Sat, 2 Sep 2023 17:06:22 +0000 Subject: [PATCH 1332/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 24dd82085..45d845f25 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ✏️ Fix validation parameter name in docs, from `regex` to `pattern`. PR [#10085](https://github.com/tiangolo/fastapi/pull/10085) by [@pablodorrio](https://github.com/pablodorrio). * 📌 Pin AnyIO to < 4.0.0 to handle an incompatibility while upgrading to Starlette 0.31.1. PR [#10194](https://github.com/tiangolo/fastapi/pull/10194) by [@tiangolo](https://github.com/tiangolo). * 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). * ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). From ce8ee1410ae4ba37744ed818be05e2cc187601e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 2 Sep 2023 19:09:47 +0200 Subject: [PATCH 1333/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 45d845f25..4f333119c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,25 +2,39 @@ ## Latest Changes -* ✏️ Fix validation parameter name in docs, from `regex` to `pattern`. PR [#10085](https://github.com/tiangolo/fastapi/pull/10085) by [@pablodorrio](https://github.com/pablodorrio). +### Fixes + * 📌 Pin AnyIO to < 4.0.0 to handle an incompatibility while upgrading to Starlette 0.31.1. PR [#10194](https://github.com/tiangolo/fastapi/pull/10194) by [@tiangolo](https://github.com/tiangolo). -* 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). + +### Docs + +* ✏️ Fix validation parameter name in docs, from `regex` to `pattern`. PR [#10085](https://github.com/tiangolo/fastapi/pull/10085) by [@pablodorrio](https://github.com/pablodorrio). * ✏️ Fix indent format in `docs/en/docs/deployment/server-workers.md`. PR [#10066](https://github.com/tiangolo/fastapi/pull/10066) by [@tamtam-fitness](https://github.com/tamtam-fitness). -* 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). * ✏️ Fix Pydantic examples in tutorial for Python types. PR [#9961](https://github.com/tiangolo/fastapi/pull/9961) by [@rahulsalgare](https://github.com/rahulsalgare). * ✏️ Fix link to Pydantic docs in `docs/en/docs/tutorial/extra-data-types.md`. PR [#10155](https://github.com/tiangolo/fastapi/pull/10155) by [@hasnatsajid](https://github.com/hasnatsajid). +* ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). +* ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). + +### Translations + +* 🌐 Remove duplicate line in translation for `docs/pt/docs/tutorial/path-params.md`. PR [#10126](https://github.com/tiangolo/fastapi/pull/10126) by [@LecoOliveira](https://github.com/LecoOliveira). +* 🌐 Add Yoruba translation for `docs/yo/docs/index.md`. PR [#10033](https://github.com/tiangolo/fastapi/pull/10033) by [@AfolabiOlaoluwa](https://github.com/AfolabiOlaoluwa). * 🌐 Add Ukrainian translation for `docs/uk/docs/python-types.md`. PR [#10080](https://github.com/tiangolo/fastapi/pull/10080) by [@rostik1410](https://github.com/rostik1410). * 🌐 Add Vietnamese translations for `docs/vi/docs/tutorial/first-steps.md` and `docs/vi/docs/tutorial/index.md`. PR [#10088](https://github.com/tiangolo/fastapi/pull/10088) by [@magiskboy](https://github.com/magiskboy). -* ✏️ Fix typo in `docs/en/docs/tutorial/handling-errors.md`. PR [#10170](https://github.com/tiangolo/fastapi/pull/10170) by [@poupapaa](https://github.com/poupapaa). -* ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). -* ✏️ Fix typo in `docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#10172](https://github.com/tiangolo/fastapi/pull/10172) by [@ragul-kachiappan](https://github.com/ragul-kachiappan). * 🌐 Add Ukrainian translation for `docs/uk/docs/alternatives.md`. PR [#10060](https://github.com/tiangolo/fastapi/pull/10060) by [@whysage](https://github.com/whysage). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/index.md`. PR [#10079](https://github.com/tiangolo/fastapi/pull/10079) by [@rostik1410](https://github.com/rostik1410). * ✏️ Fix typos in `docs/en/docs/how-to/separate-openapi-schemas.md` and `docs/en/docs/tutorial/schema-extra-example.md`. PR [#10189](https://github.com/tiangolo/fastapi/pull/10189) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/generate-clients.md`. PR [#9883](https://github.com/tiangolo/fastapi/pull/9883) by [@funny-cat-happy](https://github.com/funny-cat-happy). -* 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). + +### Refactors + +* ✏️ Fix typos in comment in `fastapi/applications.py`. PR [#10045](https://github.com/tiangolo/fastapi/pull/10045) by [@AhsanSheraz](https://github.com/AhsanSheraz). * ✅ Add missing test for OpenAPI examples, it was missing in coverage. PR [#10188](https://github.com/tiangolo/fastapi/pull/10188) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* 👥 Update FastAPI People. PR [#10186](https://github.com/tiangolo/fastapi/pull/10186) by [@tiangolo](https://github.com/tiangolo). + ## 0.103.0 ### Features From bfde8f3ef20dc338bbce8a0ff0f4b441c54d5b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 2 Sep 2023 19:10:19 +0200 Subject: [PATCH 1334/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?103.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 4f333119c..f03d54a9a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,9 @@ ## Latest Changes + +## 0.103.1 + ### Fixes * 📌 Pin AnyIO to < 4.0.0 to handle an incompatibility while upgrading to Starlette 0.31.1. PR [#10194](https://github.com/tiangolo/fastapi/pull/10194) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index ff8b98d3b..329477e41 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.103.0" +__version__ = "0.103.1" from starlette import status as status From 766dfb5b38a4751c0706b1ef4a73dc276e81572b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 10 Sep 2023 12:18:26 +0200 Subject: [PATCH 1335/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Bump.sh=20(#10227)?= 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/docs/img/sponsors/bump-sh-banner.png | Bin 0 -> 10139 bytes docs/en/docs/img/sponsors/bump-sh.png | Bin 0 -> 18609 bytes docs/en/overrides/main.html | 6 ++++++ 5 files changed, 10 insertions(+) create mode 100755 docs/en/docs/img/sponsors/bump-sh-banner.png create mode 100755 docs/en/docs/img/sponsors/bump-sh.png diff --git a/README.md b/README.md index 266213426..7aa9d4be6 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 0d9597f07..504c373a2 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -11,6 +11,9 @@ gold: - url: https://www.porter.run title: Deploy FastAPI on AWS with a few clicks img: https://fastapi.tiangolo.com/img/sponsors/porter.png + - url: https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor + title: Automate FastAPI documentation generation with Bump.sh + img: https://fastapi.tiangolo.com/img/sponsors/bump-sh.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/bump-sh-banner.png b/docs/en/docs/img/sponsors/bump-sh-banner.png new file mode 100755 index 0000000000000000000000000000000000000000..e75c0facd30acbee9121ae41cce236a99bea3646 GIT binary patch literal 10139 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 JPDHLkV1j@T&F26B literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/bump-sh.png b/docs/en/docs/img/sponsors/bump-sh.png new file mode 100755 index 0000000000000000000000000000000000000000..61817e86fa1064e2ea239bb45a504998aa6274c2 GIT binary patch literal 18609 zcmV)7K*zs{P)19 z`soK%I~JXk$;mFqYK4!{PN?d~jLFSOsIC)jnamh3cI+!V=W+q&x-}2SWKGA3tks&} zd95G^+f-0dM zG&nYZTCp6a9`tBjf4xcx*-5q$L7`m-(p-QM^iC;BoYF|&v2=hUUv^I`+yA%J0LdWo zK@=Gbh)v$utX>9(#+p-dgPw}SJQ`A1ZDFgRCk4G3dpVyw90fNr>g8A${t~_dOIO?i z^q(%E0Nc58sCkJt$#QG%@SOw4-1ceHmpvJ!P2}zf4wra^%#~prtx?=baV_x zysB^Lk3Eo~^T;7eb}mznX3(YnwCQ&KasZ=W)jgUyFwK3RSNFB%hHW`Rk`!z2dkWO5TcM+me&lwOCg^5-vi9y!(z z(U(6QtyYIHdkE1&pEot3&@OI)xGHRoUq5h$BTyn#2M#ps`)x}N zXcRo_hyhiFb}ucfQb3)+-Wl{X#|}1de-55EM8$4hrYEXVVJb#Fc*n?h^GygGAjp6b z#)L0y6&otkO-Tu{IrvJs&BSm^oL=j3uUFQ2{rOy7QOl(|iY@%~HIv5CsUnP~(j_v? zSa(<(+|sl)Cq46N;cy5r^bu9~Zx{t^oFVp_9I^c9YPLABb40Vef;RRH1zSd#27@b! z>VQjpQ#cj$KN@1%j|$$mD;v!%cyy!RWp>>2Vm$$!O>d2E1Ek0tU-lyW9Ic*Z&jQRE zl?9`s*G=aJJ5Gg*j!OriCdsSwBRwA(2i)hd$&&?jQ?2IvXmvT3p~)mDt8-CaQL?A$ z6{iYH$s;KMxd9B&m`@8d6_k~+Xo1nDlQMEa3v8+$LIEPvLeWXKJCv_RbUmc}v9Kn7 zIaeqi*n3}>*Sx8@8o*9>vofDODNoQy{AJik;B0D%0%}?Sx&ib|>M|7Cl{DO>6uc@! z3(U;2qGqLyPK9cVwvmjg$>{UnBI|!cvigIA#*+(cR0l<>=<=Ll&8Oz5!HXuHR#_9c z8mtzbVua{=+AU?ZJWdch$MiOiZ;E0^+)J>|?@^76xnK%94@{wRzV3v|0%8ug@>Hoy zPL5Hxl7td3V4coHeF~t;GVoKLv|e0JgjQR9?QpiTte!3>yCt(gGPHh&O@{GG;abe+ zUPCKVn$vO>+Xk4KO!8ic)aOwqz2Rmy*J#JuFj9v#FR(Z>s&mI^#h+W=|4gf~Kaphio0VH^_7SjZ7tGk1|jHULXsNBpjNVzhT+CnEqm3*A| zF&M!x1FrMM<%F!E{}TxZ=k>&$Wk%4hJ52K?=B;_Ji__59;AHjtcAJI}Sr~8ZXOU>J zirnn_SQ(RIEzxL$>tPu8)V-{?k~-(aaLO?d(|Za2cF~tXpqPS&IBW1?om>bi=I= zX04khn)Q2(f`jbA^oQ@>Mn^V$HH;$c2U5rDO5q>~eTP@<>HPzy$})T=VfD$Plqh$`*b#XRC0 zZ@(M`DIru>&9L1a-kIS*%x)p%id^j~mvWcN!^vBltv1h7w4GixTXjk{F)e)|Evr|FHZWo17{K)YN=8kd%Ih*~ z%)zuXO|54`D*+A62!So5iDr!IryLHrvQHbiI&|zpOt(HBO&ioug$sS%LDm~7YX5wp z;lD};1aGjsvaDB*J355qh2;hoznLM^05b|Eh#7Aq1b3i%V>)$RPo@^kcpkD|l-_8w zg~nJ}<5o9_N*c=?lyO_nBy2O<5*gqTg%0}dMi!Va{A!vCkjaoW!CP*MDSJP$9BW?u z92;WVfa0<&9h+^Qz0X1`MDLz#97a?bA7$^&XT|KO66>$xI+gM<)+yLehgWc?r z4V`gkA=lRqCS60LBYqQW-OGi2_Q=(Ug7hp1P>ab1gI$Ey-ql|7MU&`C@13)zpV6_D z_#tb%UgvRIz1|v-m>8%FlBMpgT$?#9RPP--+7C}iVsMNRmk*KKnbntb~tGG3y7sax7*<&F=XB?}m3>~1K;N-Mg1h1tNH{B>JHlsf=u zDO**e7|2;OKL!sza0e{C{i}7w)DeK%!|O6`SetDczYYeu(W-06{urTjW5LbCfw5fCS(RlE?!&50=N;71CyL>+z_^hqnpl*ya1!O9qpcOI*r{ zWV9YkW7N`5hJT^E|EJdp>J|6@2Iil!7*?)&CeXjM>EFVCykj4DeuK^6HIF?2n?C-} zF#6(uGM*jN*Nxi_$HCVcwHGehxz>~=J~Fg6ax{__R+ugxdJ3+pz9t$zR2(y5p>*<@p1_bp(hoqYbs zrcZbNHtVh`^@sX?D+ zp4+GO<6!gTl|TYlA2u`Dl-VTMU?In;1zwSlXqN*hFCGO0tTv}=`ns$-9;OR%Qc~Dl z{fq30q;Q<{nQ2!9`lxO%6gxdSXjti%T67?$^wQtF8P>gSGl2j4Q?@ZLyk>lX5nuDN zo}YKoHQwt!kdqG$EFR2KYp46IL0pK6 zw`*Mk1QRt$-P{0372~8Pw6ComoYlQ;X<$!zOAe!286W7C3k(3}b8OY&d1`}69iVEf z0&OYE=Vs4Qx5Q-3>gFmN-qmG#qV8QtmyFVJROMVhLB{aF`hZC~WLmlE+3>r;(@y}t z_&o5tR5pC+KV@q%mf(9p1YQcolQI*aFFx z;_KmavkwV0haE9L@Lzc9QK8<7`+fyeChq_{P2N6~e|O1`;PNlu7|M>Aae&X?q2yn2 zJ*-^$bQr479sg<1cWYR&;+LN0t+3s8ZwS%C!w>%fzH!~HaO;vEfVdBt_syp%zRvsJ z_jkU|I4|!Sxa+Qe3A}XLbFY8!bngyyD_1@Vixyq$f1iS)#*PPC@u&yHJPj!BXj-TM z7S9c+-@-NkRB#lBst*lO_$HO!6w?5+yrKpt2fX;L04>|YhKyA{vjT}vIJE+&0tM5J zpaOF8*Q45clws6E0?_ELNUK*r4|7huD3p!7_$S!p=ih;Uf7?ex*@k}Y`!|o@U|W2{ z$|r9#WpWo<|Mro`!|#L%n}x7Ctg~4a0#*WO>LAfgn6P=yPigNx-yc9rWjdeo4x4dG zINxsK8v-4HmnQ=9!Ue~|Uwhii7hMM&PAScrc`(eHb&#lc@WCI3FMQz&xc1ub?+%gLLHcvxNTeP#e>G;}^{Hig3(Z6es? zqsMk~#H=-I{shmhE{`RS`^67nqu=}l#x{Hnj6VM#f>yE0>Ed(e!0I&}k4^mM)+U>5 z1oz(mYnZoiQKP1*og)2ln0a9Mz1Mrx6)S!rTlXUEwbuu_dLFEI-St!0+k4r`Q{Ec9 zuuhM7k4|2fjL`8X%nZLXXB`alPrVSTn@$GwcHq2Oh)ZjDL}7|BDnswR(6j9E(!F=z1?*4`8yZn)SKR+Axc9z?0$?e0ULB3hUCV#w!|R7UuzmjG8v-~fy~&5o zJpE|dPvN}tznse#c_61yc&90E1C<|QH7RVfR!3geB;JDK=-8^Z0v2DK+4sJ z)}v$`9)9F8(LDkvc|V=V8|$cqKu>AH_{|~v09UsB$@9+gUX<3r^hdlYZ?X5ZzBWcnm35(Lv;yi?$QA|{&0!*y!j-TC40}Q`!0>FiW1-Pohk#Ca;A(JaJ$h2?~QAEm?}e;8m?1VPplO z79!}^@uI;){58rd@M19xh^5_97u#*{zD7^2jg2-K0AQHT?sud;J({@5ZM{;u#3Y$X zhJWpMhD!)emybS_%l_!SciOz_%69Ac`i-uSAGo{Vi$*iOqW2U|nX&_X?4Z3e(X3eq z`n+yqD}~YrAG9~@wdX(NIHw z#6{!CKG$Wa$cpOnh|yNvQiTWPd9w&BwhGdUKA)ipxFnM$O;?8ba!^-4h0B=3Wyo|= z0syR~N&rV~;%s0881e#`2-c$*LuhAGI99}ksNbeAd`h9QOj7$!pEuL2+g)DzH9YLs z*+(4yOaSobW`8pLfyq_bt-PM%Nq3m%OUe_DKiqFx9T<*Nn0@Va-wgn!4XYDQ_;e?y z54R~qzw*kn1Bhu|OXrirnil*T`04fZqI{E12Utq}Vdh25DVk;Ngl1lnsJ5Cd4r&%G z1w~0Ie$mcDHZ~}$##*|SG=oyNrOy)s>fVTF+lPQ3u!sRZwBPNgl%_Soo*Kz zjonCN+_?$~hU<~K+bpkWmZ$5-x`p39@!Bpc$7sleMw1BEVWLmd z&C-?;|YGP7Aub?^04L&*ftbqexpnu3&g}j?MS3aPB(Gzy9rb8jHkihx$|`Q z=NHNwIXbD>aq6)cLX7(;_ORMXLSG8f$a7YyTpw<-$!o*ma|v*cce*3O_?w@cW4x>Q zQ>{V2a>W_pvJ!#ya{ot(*1jvNon)82^6KZ-WEm)2-fp`$hH|=8wBp`h=%FwYe~6!_ z^r)o0DTUC8!l5S}I!(}&8O+CPuBX6v(mOLl| z51cIXAdj)qcEdn45`kCnibuiYD(iZm53lbFYh)))t(o}U+WIxM0C|U70Rn|AAj7eo z+e$;IpmlktMxn{pv<2+Q#5~F#@@{AV@$$Y)LfD|yZTDVQYPG9=1;eZla^mx2^)8ia zh^K46AXHOQVtp&6Ok%MF75uQLPJ^H9Mq*;$0j!*%DrmHTVBV_6J=$Q1mPD7CsLmhR zWgf{l{p*B5kna;=sP1b#BTq1|>y%I;6LQk5P|jMH%6j%O49IUXR1^yM%4pi)wTuPy zdf8O14|K=~_4`YZ7R)u!pXeM)DAZ)uBX6KMgi`>wL{lcw5~>R#vHIQu1z7hx1Bvf~eNXs(q!= zY@P?KnIS!{$brr0L?xwL>0cfu=X}@cY0it`bpC}DVtWtlEJd^83vMEK<}2Nf&Z*}K z9CJL}0B*9Pt>W?-%l`J_E}`o)QI(5?SCWPTZ<5jV%$2I(OeSC(-(K))B`|hfnM~N1 zhh`Ni2G-)0R->`YAe8*|QlmO&zdEm*b1R?oXEGSFX=Un`Gvz!zE||h({)NgMHR*{n z@`%Jbg`;fvjo^r3}_Nq77d99l@S_ zQ5U7m;mnD8vR}+_a^4O=jA(WJxRt_}8Jv|LYr7%XSOI@cF^EHz=LS)9`Ki_Hm;k`u zaF?&r%e4H(cep-A2|>lof<)WpKE`={j%HyoH2e*k`51Z~z`E?u6+n+MF{OsQVh z77i;{w?(x_gGDbJ$IY_JwD$$(JqZxVM=u{z=p zxtyv_rZK>qyd6rAw*H`T%mI%~df9i@W>W#{)JHYK7talN_7?~xayD7Ts4oX|L2vh} zkQGG;?U*ziD4pJOTI}W9mO}Scs$rjr;v9xqXmGxw9qg-O6{30AqDjhT$xswlswxk0 zwc(H(N}4MJY~rCnrGK>y8Kikb3kn}Z(T)H#T0m#NxlgM7m{EoD$opK)8ekRMs6M$` zE6j02u&x6aweM7*yz;|l7thlxV6j}wZ0bleImOc1KWVcihvbNAQY`hu4Rmdu$rzdTRN5tH>}pW*=`NOW?#~X zk_C+_4Xp((GJHqHs-9zMY#PP_%Dk4o*XvpoGgznmeP4ju z|3srvX(Q&Z~ew%Wa#ZL}cEs5CIeIf^`a%)lygp!VQs z15}Nm?X6kcR*w^SWp7<&)U6S!%zYf}+LN}DoX1p;*Bt4zq9;_%=jA4eTF?k~ZDwM< zDjWswsjcL)#cL*4bI8YiXtP)jbd_mMi#_!kn(#_fXwV4nhvA*vis2dgxs{7m0ah&X zn<=2K!1F2)=6nXnnti5Emuz@>wb_J2fEOF}V%~_N7v*TlhIt&CZ{GlD8im%mF3g~h zDMyiv`)Dtd9Lu)s3)bh&I>}OL>!U)?>bpi3T?cUWO6F1~tOTP-(Wv7l#2Mjw%{AmE zZ5ZZyZRBB&p3Pp;Dqv_i`WFoRtll!I6bT2YmZ61OJTA8er=)R}3>=(XLKBD;ZK@&f zvNCU%1gJKZuqi_=pEz%A01Ocj1B+i_z!EOMADTN+)=qgIIIn?QfRzFi8mGiWgGLA2 zZLZ;WAV9iKn>eU4SpaR0mGRV%$ry43g0(0nNX`7jXc!PTlC2`;Fe$Z|nTP;AOEt=B z#ca|-;mOE*scm{3IxM!!9Ms04+l>}aKCG^nH}n9|&THVHjqLE_s*?3dZICf(oJE_=yA!8Y z_lK7Wt4jR+jB=!mBW&~Sb<9mqCD*m#5K+*gVKbUwRrJU~A44ikbeunbb&%j!uCMg9 z$$u*wCyskG!9gRMEXf(csGK*&7kM4m-3!gm>l-NY%DV+r{aeCd(V5CX$WXVkK+WVT zj$#&N0|pR7*5w`fJ|3J#-L6m zI!(cJmqsi?@nl49sg(AxFFCF}P2`LgH16vVrPWp}%XO+Dkuy3>!a;V!HR$yes&O-q zD*LWYdhWUF6WFTDL6zFY#!ztZBL870nS_HCjdse-5th&vE4jNX%kv01C=$gDqr^}D z@w$0E%=#q3j`@ao&^~~rx#G0j)x1MtGU6FGf_?X*E3}i-TL9aj(Liu?eaUO{Xk?qbWtiza$%oz zXq-0avA>tC^!zdgRzke}ioBB9xifD3nNq8Q(Ds`Y< zkL*}?%6_fOgiZjtp>2%76=i2lqgxN~4t&@hpt&`V1FC_-k82YfFUIIMo8VfI>&;C` z8V6-6MB6x+HR6EQ6b%aKN+m{P9O^?TH*WCDJrax@JXKu{ZcyMg`El?jPLv$9KS5f; zX-<&%I48?W@WpFO*@wGv1FD*)LNP4Wuaz}a%xMx~c47~z_lWeUs%v>}Mbt=oCl1!5 zZXZ);7_$MDpt=MF`gJ-0bLJcd3xxV zC*#^NtAiD58~7YZT4V|A`VvsfOJj>~qb_gD%yaTh8=OY=(MYkHtPMs+N$VGN)(zHc zU?HvocKJjaSRkG_@%8Y9GrOzooH)tN^fOskBRF!A5fN=s!(=) zYy=;!c)H6j?+PDMyPv=t0i1#tdc)=Ai_SDzU3B4T(n>1;A zm^<(2aD3B^x514!-p;BaoOAByV4H2W5;dQE@)@}4rrY7i_pAu#ms~m@w%l?v`0ek1?>24M4#3PJjN5plT%Ho0@3`Yz;Il_BfCnG^ zHH`P*-O}gLyqVxlU{Slr!doJk9-8ye-c+CGx?qF4HYhyLsdlB#%Xg{)t5v7;1;Oej zBPWPtq{nUv<_^u3qLS|<9sP~k>&e<~7ng!2-FOe&J$BnA9Isxp)=%ojFlom(!Tk^X z2DD9U*&7NoC-3-XgMYUH^tw$trJ_%(*dGAv5EVX2>UpJa7STt{_<2X_V%f$pVarX! z=S->G<@}9c@}%vdmB@3_mlz*;^s&le(*b#0l5zEOTuuaSo%vXtaKc&eX#55;r8RyW z(|8j<8lP29J{|b-n>DBp1SIm1-D`GTpq(^n2PIf{NVL0q0Ead_uZa`44zv$E@Q@_* z>5e;=!I@`V=Er^W;BjgEcbhgf09bU`0B@48@^)fGT%kc5fB+J_l@WkCU*uW&LiV!x z#M`XTx!iV0Yv=dWmg~W;)od{a-BOH=HE`c;mEw8T@|%G7$fHldBfonhfO*EJ_JLUs zoRMwVihH^*5-d1tF)X>`Zcj56PCI!9tn`XE^XRi+$u}>`&D?s^#UX#M{f_kj+YIKP zJX3&s$ACz- zZ&1GcJy4$1KdDdT-!iLdy| zHv zZEb$8v`Z2w=Y8SxaLaeDfbYaV3a!V*x;Nag1a7#^ z;p9_}4BnT*XG(W^uiWxVgTdR4eFiUr1h4u_d^ot#q8|dhXvx?~kdaLmHw^p6gi!QjD}S20_}_Na{ix+!_TA{1VFwjKo?$=Ohy>y=s!&a?& zCWq0qF23UGZ^59nDtO_Au;Rzph4bT2zAyl9*Y{2d9+*7sL5H3kyd-(ZE4;@gQ0q^M z&iw2`c-Vt&$<3Drz%5z298Np)GB|MmX&$_v44|ei5TAn%IU(>Sy5)Co1P6M>p1Sip z;rxrf8p7AbUs}-dJaT^M@Nu(06~IEEPrJ$VyQ}wTd+hPP2=dN59sKb*ejJ_(9{Jj@ zU(|sZRZOF_foL??uk_yZcJKYSo48d7i^+Qu&5o0I@IdXnHHE=sFqSW0lEdhQ3%>-n z+;Vv+Kk3Bt!ko~2PTPIgAQPnr;#^T*iTCvBd%#uRnDIpOI)41ra7=C7dFS%*r)96b zs{~+I2CNKa9@ySj*X3mZ61=%B2aTO)3xF(4Mz%~;JJH$kMIK@q3&W?@f>pY*5;{94 zpJ#W9k9wFv4ywGTSwF;I==a=hYUZ-&l6j(q1NYwzuD|IHSiSanB{Khw$EYYAWWzv; zmOd*=9yS>W?cpB|o~APh1k9CBMkQaXI7P}0Qd^7I_(Tw-&p~*Xzb>sxKb4eRYyY4b2ht-tE1rWKgcIATU zdaJMtr74Xc-+4*`G3|6GAg&2OmLTSX(;9PH{}Onw^SiHCTy+y{=P#KgkE>G}qqK3h z%wO7}b#Lkm$v_4rc{chTKVdTu=x4$)Z4%Y|Kx!g%9*%Fc(TC9pyp(^$5%a?O_HrMh zcbijP#S48HPwQdIJNDR5c@Oi+{gzQ zAVdCj3b-1kHH0!nEV6W}y6hu(N?PGmUHNs;{wu=7uT%oPR+d3OU2YT&p%$e4lINQg z!E@h(522#so&HH{M^mS$6lmvMr!pff0J5(s#|c=7&ELj4 z`8^+YFTMC;%t#Yuyt`gqTBwfoj5fMJ%1Ac=tjgCcEt-vjHz4q?VKj8Dnd(wl-FZ;G zj;NDdxih=yx@dz!-8HM%6j{`D0DTP^BTWKNx-P5J#BH|l8r2Qw-4JpI zkPk7A$2fI~sd&R}V~GA_kfnTXmHtU}*M>0v`KmVeX6!%#wQ+++Jh6a{s@yUF!=CFr z=*eq)U9F-#T(&%Lf{aWVPEMYKvDs8St1c@aFH3YKFY%)9|CZ%jdOVlc9UFa>2k^T+ zfdBkLr(W26lMw5@1!6IyP$wJAcCzj-HX?)IrUsfXcErB73q#av(K*JeSs&H0Ru)cE zl09;b+aVbnRJBnV0H|$6SAvX2vCeb~z;f*>TSlX9dHM+3;OsdsZOL)~n}*8+ik}<< zIXA83^f6Ptx~zD~cpA>eB0=%KE}mOJPJpigTk3T5>pXx*Qe}u0h3ODIajhw3;j;iA7ddLb@ue!44t)oh; zhIda&$tS3WW5o=xM^#)^(-;?l3SJkDun`-Ysk9s<5K$bUI26FZp0X&c1ti`^A4n^3 zLc11yu;RC_W7=@;w258Nvk*uDflm>a8 zZm50MaWs(P>>AkQ#PSC47(~}LLDqN_DtT$kGF(PN&P`Du*7ejlO9`;XuLduIsp!af zi0=+z4YrOdO03J-F9)p!UUL&l=PCZ026S2Yw+$eK!5}VxWHBF8QH!a(7F=G=uKz?k z?0|z`*BQ;!$PXe|g*R5^mCCU^r+}QiJjULjsjf|PAWfWWjC7C3OoQ#Xj`w)tEI`j> zBeQe7Z<<$SWY83{yx0u-=}QN|9>s>RMxF+Lt?UdC$Zj|6qfcIqmouPozM4-3Mk#Kg zNva77sAvNgg=W37R3cS1mP#F^$8|!b`0lZJgNFmtYZn@&_z2xwDf=Y{5RVT!1}{EC zg;-SvsDaJ%G0`JR{_leFZY(Y zZv4mcz(Q-)qlT7bqqg|$xAkQPwCeCiEFj1H+)?`yPgQN0b*o6x3+P<)OH!^r?cNyH z(}Y@IJ9C6eVgPKt9?Uq+U=W3RxXp?(j{~9RKcFsazS?!xJW*bN%rxp4Roy}01*1I) zUhQS2oTYxf!y?NEtH;pAPL(@(t7HMalT}=2DEQ_wFXweBjb?>)7Fc6GfGTsvrJHMF zAv#E#sPB|Qkbo$IT;F>=D7i*%+eELE7Y4NAqPIVDE-g zsv!3vbO@6MYxwag=utS4KvjQv6+NzO??S>+`0IvWMTI5Cbu?DXI5ZYh^kaV23 z;)QixOD53#jC_3!y8tx(p^!)OvPROHWeL>i136d$#KxA>;A^zQf$mgW0_cLb;^#Xy zYCCFXrg;x4^UXZ55l5zbtA!-IeKGj59BF1GZQE6 zChALllXYB`m!wS3E(_(Slfx4l_-VJyW}LTJxe zQh)xMJxkj&os;0MfX&q_R&qJNv61CeTb5CP&RCVl2`ep7`M^9f@*z;=F@Mk9b`8I` z-TotWGk<++AQ~+W3kk5qZOk58b$~U9jdE7c^~xd`4WN@`Q+M7eyxD`&hxeHk#tgOV zQm5t^n%9T>JUa(q?Jb3etBgQYBTGAc z?=bk&TKoQbp4zP>~tJX2Pv=( z|L(l?QqLhEjjll|eXq1pt`?%AwhTd+J2AYy^6+pz^T<;aqk0^uWV?x5!<=Kf2k>Vd zH7`)DeDc}&@J9D+F%Gy^Zc2(=X6|88)<+JQWXqvJBg@e$j~k$+)$;MM%4-`uG)?MJ zQo+>D>)*H)d~C+Y!`aya9zoy!tlS0mm`=|mlRLkE>V{K#Vf;&M|VN<8SPpd@#CY|u)Z zgtusJ(LI&4^66{}=q)0;&ZCe10Y31-PB|I;VqeM|DZ|rQSSu(^1Ib46tcq_gk&%c5 zAwAr`){o7awa-`OiDqNZkKX(I^pnqoH*s)J%t5!s7Mq6h=hv=e*0k>C#5yHVR|%j0a(8m+EZc<@5BD?p4UoL}TEf_O6cAOWz9X1MgU(=HNBj*5PsiwWgFp zB^vuy%3J|KUVAB)s>~uiTK5Jx|D4a~_d#8M;}Rv53yL-Wl+T7|Yg3}PLmqe1IYBWg zP4K{`b@SvM%UdIsEWHb^xavk&a`U2aKI5oU!$b2k51S4%{2v`t-k#Im7wWEzudqn~ zxz5W%54aO8B`TkJ)Z73llK-ITd%}!E%Ud6*{J0a(3eWLUf0lgv%ORhZ0(*Uwvaa}-?}i8eFTD6l-`8XEga4_{tRv?Jx#rLPY|g*;C%3?DzRqpFpNqcO zd6jqm{YL{3=xrXSo^muyV&if9(q(Y*C0B($(X-fJx^zK!H1qDee-6|4?_NqkZ`3&R z>@NixZ49>Ab}KmR%;N(vDcyYYQrLIjY2kRzv8RXE#ctujed38nz}w$Zz;esYcfg{{ zz7~La+~;P&KYnm3{Ni6dtp~^!k=J$C+zi)W(>>3%@f~xNn9TbB@UQ*DSpr?(ubh z{DZsU^XGQ2aXQ2UnG%8a@Y%Ecy14N9v8TQEife)qp|JbYo)>xT-~Qs)@Ln%R7&lL| z_`=J9r{W!vdC)%?$o8H7o54xw6 z@PFKA4-Mc=G^Eh<%f5AR$AABLV7r+I->*HWzV(fp19<8DLm%D^p7+M+-#m|$e&cEW?Xf37 z>%*qraGZ3`yZ~MTD*==ef%MaJW+~duV8{1N3B1C%c^dk2QMJYHv2)P>kOEWybP9!4 zzO#Xvjf7Hlo}Y@05IX&`Y&K~+c-wX7cjeIe;6sj$Z;jjm7JuoCOhd|;yw`#=FNI}y z|19KP_q7W{UE07|6<(XudGMKjUHGu~fCM5^-jpxo=`k(`9Wp!ATlW3#J=BZ8a)Srb z4RE|)cOE!>uV6SRB}231_8)dHX!mL5v<_^$GQd*0;GeGaUigP_{&^?+KL1@X5(N6y z;q8@Yb#mu5Fv7u~JT}Nrbo9m!YHN}omj!2B66!39<8#DOo%fieLTUmojS)o?M;tvr zL@ATsu{~V&#nW>cPlp_OLU@1GSFb)Fw%z9S{%@-)tlZIqlE6!J#~!l)p7a2ryo)ZL zpTveY`nrGr-kpN?{`%K$4%xSSYbhM>J@QBP*$uw)?WLJ70r%*cC&C;3+Vqme=Y+hi z{V^GV5CVH1OTK+)IRCi^^44#BJ-pKcX6Y?=WHlm?E;wOf2$d=8^WF<@?q%H6hrjoY zTM@uJ`Q20C8;h?AVerY(+n#;m>4C?a-})A&8V=s%&k301UC%!0^zci0pFj6hnDp+Q z;I$je_ovN0@}wZc#b3QN)T4H_2jxJs!D|CF^KpePmpNYUBn)}m(7!Go@NFcWSBKN- zg>B?{QdoN51Ha5(hnk+ktKwASlfgyB;f6AF(Byj1u82g{ce zwp5qro{V0Q-Y#M%m+PdLv=h_tg#|{FUJyv0^!$tDCuO$SQnvXA?-4QnavlU;N*j9~ zXPiGz)Y&}nD0)Pm`I)D>9rw+)*c|4ZdK_%!fo{oP8EwOc|5a?8fctFhWAeh3{`L)9 z33_VVL1#!d-J+8%gH?lx0Ws$r;1+-JkazR~NYm+SF_Hlr%d@w?IlUgAlfpS2J%Fd@EzXqTPK{sFxcntNf_W|en**Th%qY~#Ib`V`B1-@B8bO*Hg-{OkoG zL=NG%SMI0cCR0jlvq6+&@^st`A`d+8_yxgpQau9iv*{a6u}ZhG)umZ}{Y&7z^E*r7 zTYjxfyUugZJ9UT=k1QSJ&8olkbqSCJ(8=#EeSG5gkA*Os*RlBUUKQ}x3r=ya0iMpC zt7uAnSIbiIDe%_fjg$h1JJ&Z$Bg@T@}{0TMItq0n^(WDYOn>EZBDI z__>p(;nt-;?0_D<_Tn!s%rsYAbwk(`OMsel>|tRosoM}rJ z)W)9EKH#H-pZbvb%TNa_wN>EW{`_-K@taXU$mb*@-I#Ik^u1-BC!fk^=be2LEWP7L z@Z4(o-fY^GT6Jg#(#!l>m;k(O{954BJMM~~IVk{6*G9E`sgx3ZQQ=dE>>GBpFZN+G zt%LO!2!H>($HUr}*1!uE9uMFD-ZFUm+qVzimo~8yI9rKY`jy|@BF{=8^GOSihr7PN z9Cq+)(#<@Nt1pi{3{jIopfGmIdv@~Sb6KAUaIy&Mn0d$f4XovAciF)`-(LnF+0zSl*MNj0&;rUkZ|hQi{-VUy`8*mL&}1Rzr)`E~f-@@{0DlJ}-{ zGwsOJE;R9+Jn5}rm-=D0(bd&?I=pQ%)!EhqU0?lPy=IMH`_2n%a{?-@ducrGzUSwD z7y9a2ka$I))mQo>z=pN22S2Ta>F;Y_{boh|g>x^1pZwo@186_~$^GDM9?VbqDC!R$ z+?G*8qrJ1|pYMVEPza}|e|%rq-fw6<U?g-(XiRN*CIf%S7 zrOBS=Z#*!yPohgtXPvkphs1Q*X(NAG>dsr1!q>g=D1c)<;Nm1t>zn`}2Wz>(qToS* zne%YG4aSS{FQK)oc9}VlT~*>Q{cS&GcQ-Qj0?34oUrxfjb37*HEpK83lV{r4p|Co< zWzi*3im(LIa2=RvR+ek7aRkC&MUQ0Y3WsKe;fFL`S6 zwkh-$-B?j|{Cc?zLaFkq7YPX&%5YD&4^$j#E}&dCPBEiQQsp;IRv4_&HSP0DL9y z#F=EvUtr&5BF8EY*@%+A9HlpFqE!slEfXkf3$9EcS*ujykMNmvv5dYd_)q_IS~&mS zoy)?t*AIPYY6z(*Y@T!MX%e6nPpt2#LIq$}uMgaU9*z2?0|T^5oBxullexBK`6(~y zjtp%jz#XXY+SOq1j=7<5sy?*A%Zvf=JGHoxc-3p!CKi8Bv3~ZqsnM~e^uRheIr3r{ zQ4A7R+}P8<6?i2#ga(GR31;taauJ=mC(el=qKM8yf~u811gEz6`>$H`tmjyN=CTH? z>*oQ^y!A5oCl+-(%q{@=lZWh=>HgL4Zl8C~W%a$#shP~I(-TAu7HrJNK<(*Z5DLm` z35B5zO`@0^*A!AVq=5+mc?gF08JN(4B3kVV&>+E<^IVZxf=-tYsIS@-UoQj=UZ}vE z`!qHNxWl9mI&?3^EL6ynwp=D_%Crk;P3xetVUx3SgH0)5P6L;^lwRuI#{;;SP95i8 zth7=?uN0tRv^rXBj2HK^`5g4r$Xg`4DhQfmDTDBd6$+&(jHXSi-}yi7A@lp3ne4et z&=tOdrj?I|Tp&F+KiB=>=k1x3fXK%V%zUS4THq42EsqPX_Vi*Gx?amV1Eicj=dd#R z8{69}5fX5<$u?LjZ@qj$jsn{y~-FTCiX?Y^d{l`X`h8)U& zOtk3f0|9!J6 zHmEXvX~ztK0nBi__Og&YI4>*ZuJWK9=&4MH&2@bzbU9IsZ5Tpm1}brnGZ#e<6*kIQ zo&yjjIFLET2L|wNwc&b;P|=~T^GZ^m$T_=hoV0@GeB*(U3=1)4tX>W&khPnrIG^sf zu)&HA<^j_kln4lM52S0q^D&G!{z77%o-G}bjzPHCnc3M^_k=B&_Vlp zZ#j1=-zoVLa0M_$xvTgkFiV-7dCoJ-o=qtm1@C)7wc}2`RkopimMYhbTtcXj#bRdeOKjN(;4InJFLnDUl5Q_s8n)G`Z`~eI3}Ka zIKGI}^~Ld}fs=MyOnm|yN)YFAke{=yM9MuapvlM3pDbYJbDa+fbXbMYz~~FGjWx6e zujBRArWW*rx89kCP6ao3HQ-Axz~~5!`OI}7c5R&lJDk?TYn=m_%E5cbP;}mg9>aQ`@^A9XgVBZ&}IM@#^zc%=ef+EpOtUJi_Ch%^sA;7wB z-zor@lw)k918}7xO$WmjZvbEimj9*64LBdB&ibSRj1F9hr2`$gbfd&TDFDvKFO$?` ziYzih4JZXx(QrhTV-nZRYrrP&-4SqbbBVPb65wm$Oo=GHujG?ykQXbo@a`IZMS@j< zh0$1f-QieZ*m>R*=*#wM;FfkIKW76q(`YK5r}~5=A$+DZnkH(*uZ72aTs^YxMWJw* z8q~WGdh!Q&5t%GNbLM?%7@A?){7Vvld7ELZVpp(b$l`@ev|d|e8UeQBT3v@lWkmou zf+RahlfLrK-KWfR0+><-;$EIRZ++Ig5smjY_Uz&MQDO zfM_s^^W>Fn+H~CVrrL;9JJqfRtORsvl>Qxi^~QjIem-*?E%w$A{@9c*OBQFD(WsSPTl0Sx3FZE4CW zuUqq$VquY2Q6jSyPEZ20f-&Cynu_HGth?fG08y#O#@m3 zInlO&+$!g72!p@|um-PVP$R7QW$wC{fTvMy6*~}*_+2i4X^HNU7NXUjM9z>V>8oqe zzz{U{Pu>rRy+em3(XyYwXqIV1&Zv2;M9kG7p?mc&nh_%yQ8ihuzO)wrXvv^YsWKqcN3H zqo%J7FlYxPHw5_r@D7r!lu8EjQ(6{Xat~kvS(x!wCn9=uxLau-CW#=N^0bHwq-{ZLhK5GVUF9$UCH7>!xv>R2-sD%&Ws%y0IqtNwrRkZmVoI7vnqQ}S{(V-DpWR>WHa(9nMT7^wu=YCJ|;4M5uV$X`mw|4L8^YDGPZ0$ znLM3R%7-T~%pzxlxzDn;YNp4CAxJ!O+DbfwrXD4fU2R@SR{gCDYI~1hc*t_1L8XD4 z=gVn8#=~63%wJLRa9Ds>bgC4t>wZZ$=n@e
+
{% endblock %} From a10c35673d586d1e33dd8189e863ef17602773c3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 10 Sep 2023 10:19:02 +0000 Subject: [PATCH 1336/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f03d54a9a..29793749d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). ## 0.103.1 From e0a99e24b8b96469b0a6ec51f3439a771d1767c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sun, 10 Sep 2023 12:36:28 +0200 Subject: [PATCH 1337/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Svix=20(#10228)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 - docs/en/data/sponsors.yml | 3 --- 2 files changed, 4 deletions(-) diff --git a/README.md b/README.md index 7aa9d4be6..73f70cd6a 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ The key features are: - diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 504c373a2..e700a5770 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -30,9 +30,6 @@ silver: - url: https://careers.powens.com/ title: Powens is hiring! img: https://fastapi.tiangolo.com/img/sponsors/powens.png - - url: https://www.svix.com/ - title: Svix - Webhooks as a service - img: https://fastapi.tiangolo.com/img/sponsors/svix.svg - url: https://databento.com/ title: Pay as you go for market data img: https://fastapi.tiangolo.com/img/sponsors/databento.svg From c6437d555d2c0b29a0e3ec54f2cc83753714099c Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 10 Sep 2023 10:37:04 +0000 Subject: [PATCH 1338/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 29793749d..45a87e654 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). ## 0.103.1 From 571c7a7aba8292fa8759b39f695daee01e6a636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 15 Sep 2023 10:38:48 +0200 Subject: [PATCH 1339/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20e?= =?UTF-8?q?nable=20Svix=20(revert=20#10228)=20(#10253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Update sponsors, remove Svix (revert #10228) This reverts commit e0a99e24b8b96469b0a6ec51f3439a771d1767c8. * 🔧 Tweak and update sponsors data --- README.md | 1 + docs/en/data/sponsors.yml | 3 +++ docs/en/data/sponsors_badge.yml | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 73f70cd6a..b86143f3d 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ The key features are: + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index e700a5770..5d7752d29 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -36,6 +36,9 @@ silver: - url: https://speakeasyapi.dev?utm_source=fastapi+repo&utm_medium=github+sponsorship title: SDKs for your API | Speakeasy img: https://fastapi.tiangolo.com/img/sponsors/speakeasy.png + - url: https://www.svix.com/ + title: Svix - Webhooks as a service + img: https://fastapi.tiangolo.com/img/sponsors/svix.svg bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 7b605e0ff..d67e27c87 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -12,7 +12,6 @@ logins: - ObliviousAI - Doist - nihpo - - svix - armand-sauzay - databento-bot - nanram22 @@ -20,3 +19,4 @@ logins: - porter-dev - fern-api - ndimares + - svixhq From 46d1da08da73d8b4100e709edab9d84aa6b6a203 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 15 Sep 2023 08:39:26 +0000 Subject: [PATCH 1340/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 45a87e654..5266e87c4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). From 2fcd8ce8ec981fdd8ea876ac5b346b3be8274372 Mon Sep 17 00:00:00 2001 From: xzmeng Date: Sat, 23 Sep 2023 07:30:46 +0800 Subject: [PATCH 1341/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/versions.md`=20(#10276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Big Yellow Dog --- docs/zh/docs/deployment/versions.md | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/zh/docs/deployment/versions.md diff --git a/docs/zh/docs/deployment/versions.md b/docs/zh/docs/deployment/versions.md new file mode 100644 index 000000000..75b870139 --- /dev/null +++ b/docs/zh/docs/deployment/versions.md @@ -0,0 +1,87 @@ +# 关于 FastAPI 版本 + +**FastAPI** 已在许多应用程序和系统的生产环境中使用。 并且测试覆盖率保持在100%。 但其开发进度仍在快速推进。 + +经常添加新功能,定期修复错误,并且代码仍在持续改进。 + +这就是为什么当前版本仍然是`0.x.x`,这反映出每个版本都可能有Breaking changes。 这遵循语义版本控制的约定。 + +你现在就可以使用 **FastAPI** 创建生产环境应用程序(你可能已经这样做了一段时间),你只需确保使用的版本可以与其余代码正确配合即可。 + +## 固定你的 `fastapi` 版本 + +你应该做的第一件事是将你正在使用的 **FastAPI** 版本“固定”到你知道适用于你的应用程序的特定最新版本。 + +例如,假设你在应用程序中使用版本`0.45.0`。 + +如果你使用`requirements.txt`文件,你可以使用以下命令指定版本: + +````txt +fastapi==0.45.0 +```` + +这意味着你将使用版本`0.45.0`。 + +或者你也可以将其固定为: + +````txt +fastapi>=0.45.0,<0.46.0 +```` + +这意味着你将使用`0.45.0`或更高版本,但低于`0.46.0`,例如,版本`0.45.2`仍会被接受。 + +如果你使用任何其他工具来管理你的安装,例如 Poetry、Pipenv 或其他工具,它们都有一种定义包的特定版本的方法。 + +## 可用版本 + +你可以在[发行说明](../release-notes.md){.internal-link target=_blank}中查看可用版本(例如查看当前最新版本)。 + +## 关于版本 + +遵循语义版本控制约定,任何低于`1.0.0`的版本都可能会添加 breaking changes。 + +FastAPI 还遵循这样的约定:任何`PATCH`版本更改都是为了bug修复和non-breaking changes。 + +!!! tip + "PATCH"是最后一个数字,例如,在`0.2.3`中,PATCH版本是`3`。 + +因此,你应该能够固定到如下版本: + +```txt +fastapi>=0.45.0,<0.46.0 +``` + +"MINOR"版本中会添加breaking changes和新功能。 + +!!! tip + "MINOR"是中间的数字,例如,在`0.2.3`中,MINOR版本是`2`。 + +## 升级FastAPI版本 + +你应该为你的应用程序添加测试。 + +使用 **FastAPI** 编写测试非常简单(感谢 Starlette),请参考文档:[测试](../tutorial/testing.md){.internal-link target=_blank} + +添加测试后,你可以将 **FastAPI** 版本升级到更新版本,并通过运行测试来确保所有代码都能正常工作。 + +如果一切正常,或者在进行必要的更改之后,并且所有测试都通过了,那么你可以将`fastapi`固定到新的版本。 + +## 关于Starlette + +你不应该固定`starlette`的版本。 + +不同版本的 **FastAPI** 将使用特定的较新版本的 Starlette。 + +因此,**FastAPI** 自己可以使用正确的 Starlette 版本。 + +## 关于 Pydantic + +Pydantic 包含针对 **FastAPI** 的测试及其自己的测试,因此 Pydantic 的新版本(`1.0.0`以上)始终与 FastAPI 兼容。 + +你可以将 Pydantic 固定到适合你的`1.0.0`以上和`2.0.0`以下的任何版本。 + +例如: + +````txt +pydantic>=1.2.0,<2.0.0 +```` From f4bc0d8205e6cdcda73d274350aff5928ed2e2ce Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Sep 2023 23:31:21 +0000 Subject: [PATCH 1342/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5266e87c4..e87ab47ae 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). * 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). From 84794221d98e963348aa10b9d4ccfeafa4bb470f Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov Date: Sat, 23 Sep 2023 02:36:59 +0300 Subject: [PATCH 1343/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/header-params.md`=20(#1022?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Artem Golicyn <86262613+AGolicyn@users.noreply.github.com> Co-authored-by: Nikita --- docs/ru/docs/tutorial/header-params.md | 227 +++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/ru/docs/tutorial/header-params.md diff --git a/docs/ru/docs/tutorial/header-params.md b/docs/ru/docs/tutorial/header-params.md new file mode 100644 index 000000000..0ff8ea489 --- /dev/null +++ b/docs/ru/docs/tutorial/header-params.md @@ -0,0 +1,227 @@ +# Header-параметры + +Вы можете определить параметры заголовка таким же образом, как вы определяете параметры `Query`, `Path` и `Cookie`. + +## Импорт `Header` + +Сперва импортируйте `Header`: + +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="3" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +## Объявление параметров `Header` + +Затем объявите параметры заголовка, используя ту же структуру, что и с `Path`, `Query` и `Cookie`. + +Первое значение является значением по умолчанию, вы можете передать все дополнительные параметры валидации или аннотации: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial001.py!} + ``` + +!!! note "Технические детали" + `Header` - это "родственный" класс `Path`, `Query` и `Cookie`. Он также наследуется от того же общего класса `Param`. + + Но помните, что когда вы импортируете `Query`, `Path`, `Header` и другие из `fastapi`, на самом деле это функции, которые возвращают специальные классы. + +!!! info "Дополнительная информация" + Чтобы объявить заголовки, важно использовать `Header`, иначе параметры интерпретируются как query-параметры. + +## Автоматическое преобразование + +`Header` обладает небольшой дополнительной функциональностью в дополнение к тому, что предоставляют `Path`, `Query` и `Cookie`. + +Большинство стандартных заголовков разделены символом "дефис", также известным как "минус" (`-`). + +Но переменная вроде `user-agent` недопустима в Python. + +По умолчанию `Header` преобразует символы имен параметров из символа подчеркивания (`_`) в дефис (`-`) для извлечения и документирования заголовков. + +Кроме того, HTTP-заголовки не чувствительны к регистру, поэтому вы можете объявить их в стандартном стиле Python (также известном как "snake_case"). + +Таким образом вы можете использовать `user_agent`, как обычно, в коде Python, вместо того, чтобы вводить заглавные буквы как `User_Agent` или что-то подобное. + +Если по какой-либо причине вам необходимо отключить автоматическое преобразование подчеркиваний в дефисы, установите для параметра `convert_underscores` в `Header` значение `False`: + +=== "Python 3.10+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/header_params/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/header_params/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial002.py!} + ``` + +!!! warning "Внимание" + Прежде чем установить для `convert_underscores` значение `False`, имейте в виду, что некоторые HTTP-прокси и серверы запрещают использование заголовков с подчеркиванием. + +## Повторяющиеся заголовки + +Есть возможность получать несколько заголовков с одним и тем же именем, но разными значениями. + +Вы можете определить эти случаи, используя список в объявлении типа. + +Вы получите все значения из повторяющегося заголовка в виде `list` Python. + +Например, чтобы объявить заголовок `X-Token`, который может появляться более одного раза, вы можете написать: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/header_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/header_params/tutorial003_py310.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/header_params/tutorial003.py!} + ``` + +Если вы взаимодействуете с этой *операцией пути*, отправляя два HTTP-заголовка, таких как: + +``` +X-Token: foo +X-Token: bar +``` + +Ответ был бы таким: + +```JSON +{ + "X-Token values": [ + "bar", + "foo" + ] +} +``` + +## Резюме + +Объявляйте заголовки с помощью `Header`, используя тот же общий шаблон, как при `Query`, `Path` и `Cookie`. + +И не беспокойтесь о символах подчеркивания в ваших переменных, **FastAPI** позаботится об их преобразовании. From 79399e43df61e05c92d93ac61c49ea5eb1080691 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Sep 2023 23:37:34 +0000 Subject: [PATCH 1344/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e87ab47ae..2c27faa81 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). * 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). From 89246313aad8c1d467cd08e88b1ec97030083945 Mon Sep 17 00:00:00 2001 From: Raman Bazhanau <67696229+romabozhanovgithub@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:38:53 +0200 Subject: [PATCH 1345/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Polish=20transla?= =?UTF-8?q?tion=20for=20`docs/pl/docs/help-fastapi.md`=20(#10121)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Igor Sulim <30448496+isulim@users.noreply.github.com> Co-authored-by: Sebastián Ramírez Co-authored-by: Antek S. --- docs/pl/docs/help-fastapi.md | 265 +++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/pl/docs/help-fastapi.md diff --git a/docs/pl/docs/help-fastapi.md b/docs/pl/docs/help-fastapi.md new file mode 100644 index 000000000..723df91d1 --- /dev/null +++ b/docs/pl/docs/help-fastapi.md @@ -0,0 +1,265 @@ +# Pomóż FastAPI - Uzyskaj pomoc + +Czy podoba Ci się **FastAPI**? + +Czy chciałbyś pomóc FastAPI, jego użytkownikom i autorowi? + +Może napotkałeś na trudności z **FastAPI** i potrzebujesz pomocy? + +Istnieje kilka bardzo łatwych sposobów, aby pomóc (czasami wystarczy jedno lub dwa kliknięcia). + +Istnieje również kilka sposobów uzyskania pomocy. + +## Zapisz się do newslettera + +Możesz zapisać się do rzadkiego [newslettera o **FastAPI i jego przyjaciołach**](/newsletter/){.internal-link target=_blank}, aby być na bieżąco z: + +* Aktualnościami o FastAPI i przyjaciołach 🚀 +* Przewodnikami 📝 +* Funkcjami ✨ +* Przełomowymi zmianami 🚨 +* Poradami i sztuczkami ✅ + +## Śledź FastAPI na Twitterze + +Śledź @fastapi na **Twitterze** aby być na bieżąco z najnowszymi wiadomościami o **FastAPI**. 🐦 + +## Dodaj gwiazdkę **FastAPI** na GitHubie + +Możesz "dodać gwiazdkę" FastAPI na GitHubie (klikając przycisk gwiazdki w prawym górnym rogu): https://github.com/tiangolo/fastapi. ⭐️ + +Dodając gwiazdkę, inni użytkownicy będą mogli łatwiej znaleźć projekt i zobaczyć, że był już przydatny dla innych. + +## Obserwuj repozytorium GitHub w poszukiwaniu nowych wydań + +Możesz "obserwować" FastAPI na GitHubie (klikając przycisk "obserwuj" w prawym górnym rogu): https://github.com/tiangolo/fastapi. 👀 + +Wybierz opcję "Tylko wydania". + +Dzięki temu będziesz otrzymywać powiadomienia (na swój adres e-mail) za każdym razem, gdy pojawi się nowe wydanie (nowa wersja) **FastAPI** z poprawkami błędów i nowymi funkcjami. + +## Skontaktuj się z autorem + +Możesz skontaktować się ze mną (Sebastián Ramírez / `tiangolo`), autorem. + +Możesz: + +* Śledzić mnie na **GitHubie**. + * Zobacz inne projekty open source, które stworzyłem, a mogą być dla Ciebie pomocne. + * Śledź mnie, aby dostać powiadomienie, gdy utworzę nowy projekt open source. +* Śledzić mnie na **Twitterze** lub na Mastodonie. + * Napisz mi, w jaki sposób korzystasz z FastAPI (uwielbiam o tym czytać). + * Dowiedz się, gdy ogłoszę coś nowego lub wypuszczę nowe narzędzia. + * Możesz także śledzić @fastapi na Twitterze (to oddzielne konto). +* Nawiąż ze mną kontakt na **Linkedinie**. + * Dowiedz się, gdy ogłoszę coś nowego lub wypuszczę nowe narzędzia (chociaż częściej korzystam z Twittera 🤷‍♂). +* Czytaj moje posty (lub śledź mnie) na **Dev.to** lub na **Medium**. + * Czytaj o innych pomysłach, artykułach i dowiedz się o narzędziach, które stworzyłem. + * Śledź mnie, by wiedzieć gdy opublikuję coś nowego. + +## Napisz tweeta o **FastAPI** + +Napisz tweeta o **FastAPI** i powiedz czemu Ci się podoba. 🎉 + +Uwielbiam czytać w jaki sposób **FastAPI** jest używane, co Ci się w nim podobało, w jakim projekcie/firmie go używasz itp. + +## Głosuj na FastAPI + +* Głosuj na **FastAPI** w Slant. +* Głosuj na **FastAPI** w AlternativeTo. +* Powiedz, że używasz **FastAPI** na StackShare. + +## Pomagaj innym, odpowiadając na ich pytania na GitHubie + +Możesz spróbować pomóc innym, odpowiadając w: + +* Dyskusjach na GitHubie +* Problemach na GitHubie + +W wielu przypadkach możesz już znać odpowiedź na te pytania. 🤓 + +Jeśli pomożesz wielu ludziom, możesz zostać oficjalnym [Ekspertem FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. 🎉 + +Pamiętaj tylko o najważniejszym: bądź życzliwy. Ludzie przychodzą sfrustrowani i w wielu przypadkach nie zadają pytań w najlepszy sposób, ale mimo to postaraj się być dla nich jak najbardziej życzliwy. 🤗 + +Chciałbym, by społeczność **FastAPI** była życzliwa i przyjazna. Nie akceptuj prześladowania ani braku szacunku wobec innych. Dbajmy o siebie nawzajem. + +--- + +Oto, jak pomóc innym z pytaniami (w dyskusjach lub problemach): + +### Zrozum pytanie + +* Upewnij się, czy rozumiesz **cel** i przypadek użycia osoby pytającej. + +* Następnie sprawdź, czy pytanie (większość to pytania) jest **jasne**. + +* W wielu przypadkach zadane pytanie dotyczy rozwiązania wymyślonego przez użytkownika, ale może istnieć **lepsze** rozwiązanie. Jeśli dokładnie zrozumiesz problem i przypadek użycia, być może będziesz mógł zaproponować lepsze **alternatywne rozwiązanie**. + +* Jeśli nie rozumiesz pytania, poproś o więcej **szczegółów**. + +### Odtwórz problem + +W większości przypadków problem wynika z **autorskiego kodu** osoby pytającej. + +Często pytający umieszczają tylko fragment kodu, niewystarczający do **odtworzenia problemu**. + +* Możesz poprosić ich o dostarczenie minimalnego, odtwarzalnego przykładu, który możesz **skopiować i wkleić** i uruchomić lokalnie, aby zobaczyć ten sam błąd lub zachowanie, które widzą, lub lepiej zrozumieć ich przypadki użycia. + +* Jeśli jesteś wyjątkowo pomocny, możesz spróbować **stworzyć taki przykład** samodzielnie, opierając się tylko na opisie problemu. Miej na uwadze, że może to zająć dużo czasu i lepiej może być najpierw poprosić ich o wyjaśnienie problemu. + +### Proponuj rozwiązania + +* Po zrozumieniu pytania możesz podać im możliwą **odpowiedź**. + +* W wielu przypadkach lepiej zrozumieć ich **podstawowy problem lub przypadek użycia**, ponieważ może istnieć lepszy sposób rozwiązania niż to, co próbują zrobić. + +### Poproś o zamknięcie + +Jeśli odpowiedzą, jest duża szansa, że rozwiązałeś ich problem, gratulacje, **jesteś bohaterem**! 🦸 + +* Jeśli Twoja odpowiedź rozwiązała problem, możesz poprosić o: + + * W Dyskusjach na GitHubie: oznaczenie komentarza jako **odpowiedź**. + * W Problemach na GitHubie: **zamknięcie** problemu. + +## Obserwuj repozytorium na GitHubie + +Możesz "obserwować" FastAPI na GitHubie (klikając przycisk "obserwuj" w prawym górnym rogu): https://github.com/tiangolo/fastapi. 👀 + +Jeśli wybierzesz "Obserwuj" zamiast "Tylko wydania", otrzymasz powiadomienia, gdy ktoś utworzy nowy problem lub pytanie. Możesz również określić, że chcesz być powiadamiany tylko o nowych problemach, dyskusjach, PR-ach itp. + +Następnie możesz spróbować pomóc rozwiązać te problemy. + +## Zadawaj pytania + +Możesz utworzyć nowe pytanie w repozytorium na GitHubie, na przykład aby: + +* Zadać **pytanie** lub zapytać o **problem**. +* Zaproponować nową **funkcję**. + +**Uwaga**: jeśli to zrobisz, poproszę Cię również o pomoc innym. 😉 + +## Przeglądaj Pull Requesty + +Możesz pomóc mi w przeglądaniu pull requestów autorstwa innych osób. + +Jak wcześniej wspomniałem, postaraj się być jak najbardziej życzliwy. 🤗 + +--- + +Oto, co warto mieć na uwadze podczas oceny pull requestu: + +### Zrozum problem + +* Najpierw upewnij się, że **rozumiesz problem**, który próbuje rozwiązać pull request. Może być osadzony w większym kontekście w GitHubowej dyskusji lub problemie. + +* Jest też duża szansa, że pull request nie jest konieczny, ponieważ problem można rozwiązać w **inny sposób**. Wtedy możesz to zasugerować lub o to zapytać. + +### Nie martw się stylem + +* Nie przejmuj się zbytnio rzeczami takimi jak style wiadomości commitów, przy wcielaniu pull requesta łączę commity i modyfikuję opis sumarycznego commita ręcznie. + +* Nie przejmuj się również stylem kodu, automatyczne narzędzia w repozytorium sprawdzają to samodzielnie. + +A jeśli istnieje jakaś konkretna potrzeba dotycząca stylu lub spójności, sam poproszę o zmiany lub dodam commity z takimi zmianami. + +### Sprawdź kod + +* Przeczytaj kod, zastanów się czy ma sens, **uruchom go lokalnie** i potwierdź czy faktycznie rozwiązuje problem. + +* Następnie dodaj **komentarz** z informacją o tym, że sprawdziłeś kod, dzięki temu będę miał pewność, że faktycznie go sprawdziłeś. + +!!! info + Niestety, nie mogę ślepo ufać PR-om, nawet jeśli mają kilka zatwierdzeń. + + Kilka razy zdarzyło się, że PR-y miały 3, 5 lub więcej zatwierdzeń (prawdopodobnie dlatego, że opis obiecuje rozwiązanie ważnego problemu), ale gdy sam sprawdziłem danego PR-a, okazał się być zbugowany lub nie rozwiązywał problemu, który rzekomo miał rozwiązywać. 😅 + + Dlatego tak ważne jest, abyś faktycznie przeczytał i uruchomił kod oraz napisał w komentarzu, że to zrobiłeś. 🤓 + +* Jeśli PR można uprościć w jakiś sposób, możesz o to poprosić, ale nie ma potrzeby być zbyt wybrednym, może być wiele subiektywnych punktów widzenia (a ja też będę miał swój 🙈), więc lepiej żebyś skupił się na kluczowych rzeczach. + +### Testy + +* Pomóż mi sprawdzić, czy PR ma **testy**. + +* Sprawdź, czy testy **nie przechodzą** przed PR. 🚨 + +* Następnie sprawdź, czy testy **przechodzą** po PR. ✅ + +* Wiele PR-ów nie ma testów, możesz **przypomnieć** im o dodaniu testów, a nawet **zaproponować** samemu jakieś testy. To jedna z rzeczy, które pochłaniają najwięcej czasu i możesz w tym bardzo pomóc. + +* Następnie skomentuj również to, czego spróbowałeś, wtedy będę wiedział, że to sprawdziłeś. 🤓 + +## Utwórz Pull Request + +Możesz [wnieść wkład](contributing.md){.internal-link target=_blank} do kodu źródłowego za pomocą Pull Requestu, na przykład: + +* Naprawić literówkę, którą znalazłeś w dokumentacji. +* Podzielić się artykułem, filmem lub podcastem, który stworzyłeś lub znalazłeś na temat FastAPI, edytując ten plik. + * Upewnij się, że dodajesz swój link na początku odpowiedniej sekcji. +* Pomóc w [tłumaczeniu dokumentacji](contributing.md#translations){.internal-link target=_blank} na Twój język. + * Możesz również pomóc w weryfikacji tłumaczeń stworzonych przez innych. +* Zaproponować nowe sekcje dokumentacji. +* Naprawić istniejący problem/błąd. + * Upewnij się, że dodajesz testy. +* Dodać nową funkcję. + * Upewnij się, że dodajesz testy. + * Upewnij się, że dodajesz dokumentację, jeśli jest to istotne. + +## Pomóż w utrzymaniu FastAPI + +Pomóż mi utrzymać **FastAPI**! 🤓 + +Jest wiele pracy do zrobienia, a w większości przypadków **TY** możesz to zrobić. + +Główne zadania, które możesz wykonać teraz to: + +* [Pomóc innym z pytaniami na GitHubie](#help-others-with-questions-in-github){.internal-link target=_blank} (zobacz sekcję powyżej). +* [Oceniać Pull Requesty](#review-pull-requests){.internal-link target=_blank} (zobacz sekcję powyżej). + +Te dwie czynności **zajmują najwięcej czasu**. To główna praca związana z utrzymaniem FastAPI. + +Jeśli możesz mi w tym pomóc, **pomożesz mi utrzymać FastAPI** i zapewnisz że będzie **rozwijać się szybciej i lepiej**. 🚀 + +## Dołącz do czatu + +Dołącz do 👥 serwera czatu na Discordzie 👥 i spędzaj czas z innymi w społeczności FastAPI. + +!!! wskazówka + Jeśli masz pytania, zadaj je w Dyskusjach na GitHubie, jest dużo większa szansa, że otrzymasz pomoc od [Ekspertów FastAPI](fastapi-people.md#experts){.internal-link target=_blank}. + + Używaj czatu tylko do innych ogólnych rozmów. + +Istnieje również poprzedni czat na Gitter, ale ponieważ nie ma tam kanałów i zaawansowanych funkcji, rozmowy są trudniejsze, dlatego teraz zalecany jest Discord. + +### Nie zadawaj pytań na czacie + +Miej na uwadze, że ponieważ czaty pozwalają na bardziej "swobodną rozmowę", łatwo jest zadawać pytania, które są zbyt ogólne i trudniejsze do odpowiedzi, więc możesz nie otrzymać odpowiedzi. + +Na GitHubie szablon poprowadzi Cię do napisania odpowiedniego pytania, dzięki czemu łatwiej uzyskasz dobrą odpowiedź, a nawet rozwiążesz problem samodzielnie, zanim zapytasz. Ponadto na GitHubie mogę się upewnić, że zawsze odpowiadam na wszystko, nawet jeśli zajmuje to trochę czasu. Osobiście nie mogę tego zrobić z systemami czatu. 😅 + +Rozmów w systemach czatu nie można tak łatwo przeszukiwać, jak na GitHubie, więc pytania i odpowiedzi mogą zaginąć w rozmowie. A tylko te na GitHubie liczą się do zostania [Ekspertem FastAPI](fastapi-people.md#experts){.internal-link target=_blank}, więc najprawdopodobniej otrzymasz więcej uwagi na GitHubie. + +Z drugiej strony w systemach czatu są tysiące użytkowników, więc jest duża szansa, że znajdziesz tam kogoś do rozmowy, prawie w każdej chwili. 😄 + +## Wspieraj autora + +Możesz również finansowo wesprzeć autora (mnie) poprzez sponsoring na GitHubie. + +Tam możesz postawić mi kawę ☕️ aby podziękować. 😄 + +Możesz także zostać srebrnym lub złotym sponsorem FastAPI. 🏅🎉 + +## Wspieraj narzędzia, które napędzają FastAPI + +Jak widziałeś w dokumentacji, FastAPI stoi na ramionach gigantów, Starlette i Pydantic. + +Możesz również wesprzeć: + +* Samuel Colvin (Pydantic) +* Encode (Starlette, Uvicorn) + +--- + +Dziękuję! 🚀 From 69a7c99b447c9ef103dc03e93d172cabd99ac832 Mon Sep 17 00:00:00 2001 From: github-actions Date: Fri, 22 Sep 2023 23:39:37 +0000 Subject: [PATCH 1346/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2c27faa81..b026929d1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). * 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). From cb410d301583ddf8cb3aadcb4cd57dc27ef47497 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov Date: Tue, 26 Sep 2023 02:00:09 +0300 Subject: [PATCH 1347/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typo=20in=20Russ?= =?UTF-8?q?ian=20translation=20for=20`docs/ru/docs/tutorial/body-fields.md?= =?UTF-8?q?`=20(#10224)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- docs/ru/docs/tutorial/body-fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md index 674b8bde4..2dc6c1e26 100644 --- a/docs/ru/docs/tutorial/body-fields.md +++ b/docs/ru/docs/tutorial/body-fields.md @@ -37,7 +37,7 @@ {!> ../../../docs_src/body_fields/tutorial001.py!} ``` -Функция `Field` работает так же, как `Query`, `Path` и `Body`, у ее такие же параметры и т.д. +Функция `Field` работает так же, как `Query`, `Path` и `Body`, у неё такие же параметры и т.д. !!! note "Технические детали" На самом деле, `Query`, `Path` и другие функции, которые вы увидите в дальнейшем, создают объекты подклассов общего класса `Param`, который сам по себе является подклассом `FieldInfo` из Pydantic. From c75cdc6d9ac210513b8832c01b8f5fe4e4d278ab Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 25 Sep 2023 23:00:56 +0000 Subject: [PATCH 1348/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b026929d1..033e73193 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typo in Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#10224](https://github.com/tiangolo/fastapi/pull/10224) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). From 3106a3a50e7e3f9ef3a3c62c041010e4d395a8d9 Mon Sep 17 00:00:00 2001 From: Yusuke Tamura <62091034+tamtam-fitness@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:01:57 +0900 Subject: [PATCH 1349/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/deployment/https.md`=20(#10298)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ja/docs/deployment/https.md | 200 +++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/ja/docs/deployment/https.md diff --git a/docs/ja/docs/deployment/https.md b/docs/ja/docs/deployment/https.md new file mode 100644 index 000000000..a291f870f --- /dev/null +++ b/docs/ja/docs/deployment/https.md @@ -0,0 +1,200 @@ +# HTTPS について + +HTTPSは単に「有効」か「無効」かで決まるものだと思いがちです。 + +しかし、それよりもはるかに複雑です。 + +!!! tip + もし急いでいたり、HTTPSの仕組みについて気にしないのであれば、次のセクションに進み、さまざまなテクニックを使ってすべてをセットアップするステップ・バイ・ステップの手順をご覧ください。 + +利用者の視点から **HTTPS の基本を学ぶ**に当たっては、次のリソースをオススメします: https://howhttps.works/. + +さて、**開発者の視点**から、HTTPSについて考える際に念頭に置くべきことをいくつかみていきましょう: + +* HTTPSの場合、**サーバ**は**第三者**によって生成された**「証明書」を持つ**必要があります。 + * これらの証明書は「生成」されたものではなく、実際には第三者から**取得**されたものです。 +* 証明書には**有効期限**があります。 + * つまりいずれ失効します。 + * そのため**更新**をし、第三者から**再度取得**する必要があります。 +* 接続の暗号化は**TCPレベル**で行われます。 + * それは**HTTPの1つ下**のレイヤーです。 + * つまり、**証明書と暗号化**の処理は、**HTTPの前**に行われます。 +* **TCPは "ドメイン "について知りません**。IPアドレスについてのみ知っています。 + * 要求された**特定のドメイン**に関する情報は、**HTTPデータ**に入ります。 +* **HTTPS証明書**は、**特定のドメイン**を「証明」しますが、プロトコルと暗号化はTCPレベルで行われ、どのドメインが扱われているかを**知る前**に行われます。 +* **デフォルトでは**、**IPアドレスごとに1つのHTTPS証明書**しか持てないことになります。 + * これは、サーバーの規模やアプリケーションの規模に寄りません。 + * しかし、これには**解決策**があります。 +* **TLS**プロトコル(HTTPの前に、TCPレベルで暗号化を処理するもの)には、**SNI**と呼ばれる**拡張**があります。 + * このSNI拡張機能により、1つのサーバー(**単一のIPアドレス**を持つ)が**複数のHTTPS証明書**を持ち、**複数のHTTPSドメイン/アプリケーション**にサービスを提供できるようになります。 + * これが機能するためには、**パブリックIPアドレス**でリッスンしている、サーバー上で動作している**単一の**コンポーネント(プログラム)が、サーバー内の**すべてのHTTPS証明書**を持っている必要があります。 + +* セキュアな接続を取得した**後**でも、通信プロトコルは**HTTPのまま**です。 + * コンテンツは**HTTPプロトコル**で送信されているにもかかわらず、**暗号化**されています。 + + +サーバー(マシン、ホストなど)上で**1つのプログラム/HTTPサーバー**を実行させ、**HTTPSに関する全てのこと**を管理するのが一般的です。 + +**暗号化された HTTPS リクエスト** を受信し、**復号化された HTTP リクエスト** を同じサーバーで実行されている実際の HTTP アプリケーション(この場合は **FastAPI** アプリケーション)に送信し、アプリケーションから **HTTP レスポンス** を受け取り、適切な **HTTPS 証明書** を使用して **暗号化** し、そして**HTTPS** を使用してクライアントに送り返します。 + +このサーバーはしばしば **TLS Termination Proxy**と呼ばれます。 + +TLS Termination Proxyとして使えるオプションには、以下のようなものがあります: + +* Traefik(証明書の更新も対応) +* Caddy (証明書の更新も対応) +* Nginx +* HAProxy + + +## Let's Encrypt + +Let's Encrypt以前は、これらの**HTTPS証明書**は信頼できる第三者によって販売されていました。 + +これらの証明書を取得するための手続きは面倒で、かなりの書類を必要とし、証明書はかなり高価なものでした。 + +しかしその後、**Let's Encrypt** が作られました。 + +これはLinux Foundationのプロジェクトから生まれたものです。 自動化された方法で、**HTTPS証明書を無料で**提供します。これらの証明書は、すべての標準的な暗号化セキュリティを使用し、また短命(約3ヶ月)ですが、こういった寿命の短さによって、**セキュリティは実際に優れています**。 + +ドメインは安全に検証され、証明書は自動的に生成されます。また、証明書の更新も自動化されます。 + +このアイデアは、これらの証明書の取得と更新を自動化することで、**安全なHTTPSを、無料で、永遠に**利用できるようにすることです。 + +## 開発者のための HTTPS + +ここでは、HTTPS APIがどのように見えるかの例を、主に開発者にとって重要なアイデアに注意を払いながら、ステップ・バイ・ステップで説明します。 + +### ドメイン名 + +ステップの初めは、**ドメイン名**を**取得すること**から始まるでしょう。その後、DNSサーバー(おそらく同じクラウドプロバイダー)に設定します。 + +おそらくクラウドサーバー(仮想マシン)かそれに類するものを手に入れ、固定の **パブリックIPアドレス**を持つことになるでしょう。 + +DNSサーバーでは、**取得したドメイン**をあなたのサーバーのパプリック**IPアドレス**に向けるレコード(「`Aレコード`」)を設定します。 + +これはおそらく、最初の1回だけあり、すべてをセットアップするときに行うでしょう。 + +!!! tip + ドメイン名の話はHTTPSに関する話のはるか前にありますが、すべてがドメインとIPアドレスに依存するため、ここで言及する価値があります。 + +### DNS + +では、実際のHTTPSの部分に注目してみよう。 + +まず、ブラウザは**DNSサーバー**に**ドメインに対するIP**が何であるかを確認します。今回は、`someapp.example.com`とします。 + +DNSサーバーは、ブラウザに特定の**IPアドレス**を使用するように指示します。このIPアドレスは、DNSサーバーで設定した、あなたのサーバーが使用するパブリックIPアドレスになります。 + + + +### TLS Handshake の開始 + +ブラウザはIPアドレスと**ポート443**(HTTPSポート)で通信します。 + +通信の最初の部分は、クライアントとサーバー間の接続を確立し、使用する暗号鍵などを決めるだけです。 + + + +TLS接続を確立するためのクライアントとサーバー間のこのやりとりは、**TLSハンドシェイク**と呼ばれます。 + +### SNI拡張機能付きのTLS + +サーバー内の**1つのプロセス**だけが、特定 の**IPアドレス**の特定の**ポート** で待ち受けることができます。 + +同じIPアドレスの他のポートで他のプロセスがリッスンしている可能性もありますが、IPアドレスとポートの組み合わせごとに1つだけです。 + +TLS(HTTPS)はデフォルトで`443`という特定のポートを使用する。つまり、これが必要なポートです。 + +このポートをリッスンできるのは1つのプロセスだけなので、これを実行するプロセスは**TLS Termination Proxy**となります。 + +TLS Termination Proxyは、1つ以上の**TLS証明書**(HTTPS証明書)にアクセスできます。 + +前述した**SNI拡張機能**を使用して、TLS Termination Proxy は、利用可能なTLS (HTTPS)証明書のどれを接続先として使用すべきかをチェックし、クライアントが期待するドメインに一致するものを使用します。 + +今回は、`someapp.example.com`の証明書を使うことになります。 + + + +クライアントは、そのTLS証明書を生成したエンティティ(この場合はLet's Encryptですが、これについては後述します)をすでに**信頼**しているため、その証明書が有効であることを**検証**することができます。 + +次に証明書を使用して、クライアントとTLS Termination Proxy は、 **TCP通信**の残りを**どのように暗号化するかを決定**します。これで**TLSハンドシェイク**の部分が完了します。 + +この後、クライアントとサーバーは**暗号化されたTCP接続**を持ちます。そして、その接続を使って実際の**HTTP通信**を開始することができます。 + +これが**HTTPS**であり、純粋な(暗号化されていない)TCP接続ではなく、**セキュアなTLS接続**の中に**HTTP**があるだけです。 + +!!! tip + 通信の暗号化は、HTTPレベルではなく、**TCPレベル**で行われることに注意してください。 + +### HTTPS リクエスト + +これでクライアントとサーバー(具体的にはブラウザとTLS Termination Proxy)は**暗号化されたTCP接続**を持つことになり、**HTTP通信**を開始することができます。 + +そこで、クライアントは**HTTPSリクエスト**を送信します。これは、暗号化されたTLSコネクションを介した単なるHTTPリクエストです。 + + + +### リクエストの復号化 + +TLS Termination Proxy は、合意が取れている暗号化を使用して、**リクエストを復号化**し、**プレーン (復号化された) HTTP リクエスト** をアプリケーションを実行しているプロセス (例えば、FastAPI アプリケーションを実行している Uvicorn を持つプロセス) に送信します。 + + + +### HTTP レスポンス + +アプリケーションはリクエストを処理し、**プレーン(暗号化されていない)HTTPレスポンス** をTLS Termination Proxyに送信します。 + + + +### HTTPS レスポンス + +TLS Termination Proxyは次に、事前に合意が取れている暗号(`someapp.example.com`の証明書から始まる)を使って**レスポンスを暗号化し**、ブラウザに送り返す。 + +その後ブラウザでは、レスポンスが有効で正しい暗号キーで暗号化されていることなどを検証します。そして、ブラウザはレスポンスを**復号化**して処理します。 + + + +クライアント(ブラウザ)は、レスポンスが正しいサーバーから来たことを知ることができます。 なぜなら、そのサーバーは、以前に**HTTPS証明書**を使って合意した暗号を使っているからです。 + +### 複数のアプリケーション + +同じサーバー(または複数のサーバー)に、例えば他のAPIプログラムやデータベースなど、**複数のアプリケーション**が存在する可能性があります。 + +特定のIPとポート(この例ではTLS Termination Proxy)を扱うことができるのは1つのプロセスだけですが、他のアプリケーション/プロセスも、同じ**パブリックIPとポート**の組み合わせを使用しようとしない限り、サーバー上で実行することができます。 + + + +そうすれば、TLS Termination Proxy は、**複数のドメイン**や複数のアプリケーションのHTTPSと証明書を処理し、それぞれのケースで適切なアプリケーションにリクエストを送信することができます。 + +### 証明書の更新 + +将来のある時点で、各証明書は(取得後約3ヶ月で)**失効**します。 + +その後、Let's Encryptと通信する別のプログラム(別のプログラムである場合もあれば、同じTLS Termination Proxyである場合もある)によって、証明書を更新します。 + + + +**TLS証明書**は、IPアドレスではなく、**ドメイン名に関連付けられて**います。 + +したがって、証明書を更新するために、更新プログラムは、認証局(Let's Encrypt)に対して、**そのドメインが本当に「所有」し、管理している**ことを**証明**する必要があります。 + +そのために、またさまざまなアプリケーションのニーズに対応するために、いくつかの方法があります。よく使われる方法としては: + +* **いくつかのDNSレコードを修正します。** + * これをするためには、更新プログラムはDNSプロバイダーのAPIをサポートする必要があります。したがって、使用しているDNSプロバイダーによっては、このオプションが使える場合もあれば、使えない場合もあります。 +* ドメインに関連付けられたパブリックIPアドレス上で、(少なくとも証明書取得プロセス中は)**サーバー**として実行します。 + * 上で述べたように、特定のIPとポートでリッスンできるプロセスは1つだけです。 + * これは、同じTLS Termination Proxyが証明書の更新処理も行う場合に非常に便利な理由の1つです。 + * そうでなければ、TLS Termination Proxyを一時的に停止し、証明書を取得するために更新プログラムを起動し、TLS Termination Proxyで証明書を設定し、TLS Termination Proxyを再起動しなければならないかもしれません。TLS Termination Proxyが停止している間はアプリが利用できなくなるため、これは理想的ではありません。 + + +アプリを提供しながらこのような更新処理を行うことは、アプリケーション・サーバー(Uvicornなど)でTLS証明書を直接使用するのではなく、TLS Termination Proxyを使用して**HTTPSを処理する別のシステム**を用意したくなる主な理由の1つです。 + +## まとめ + +**HTTPS**を持つことは非常に重要であり、ほとんどの場合、かなり**クリティカル**です。開発者として HTTPS に関わる労力のほとんどは、これらの**概念とその仕組みを理解する**ことです。 + +しかし、ひとたび**開発者向けHTTPS**の基本的な情報を知れば、簡単な方法ですべてを管理するために、さまざまなツールを組み合わせて設定することができます。 + +次の章では、**FastAPI** アプリケーションのために **HTTPS** をセットアップする方法について、いくつかの具体例を紹介します。🔒 From 14e0914fcf7275f8de40c8df586350349b30f443 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 25 Sep 2023 23:02:43 +0000 Subject: [PATCH 1350/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 033e73193..4c6d8e288 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Japanese translation for `docs/ja/docs/deployment/https.md`. PR [#10298](https://github.com/tiangolo/fastapi/pull/10298) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Fix typo in Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#10224](https://github.com/tiangolo/fastapi/pull/10224) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). From c89549c7037d6f9cc3a9535b22afbceaa0aae354 Mon Sep 17 00:00:00 2001 From: Sion Shin <82511301+Sion99@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:02:59 +0900 Subject: [PATCH 1351/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/deployment/cloud.md`=20(#10191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joona Yoon --- docs/ko/docs/deployment/cloud.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/ko/docs/deployment/cloud.md diff --git a/docs/ko/docs/deployment/cloud.md b/docs/ko/docs/deployment/cloud.md new file mode 100644 index 000000000..f2b965a91 --- /dev/null +++ b/docs/ko/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# FastAPI를 클라우드 제공업체에서 배포하기 + +사실상 거의 **모든 클라우드 제공업체**를 사용하여 여러분의 FastAPI 애플리케이션을 배포할 수 있습니다. + +대부분의 경우, 주요 클라우드 제공업체에서는 FastAPI를 배포할 수 있도록 가이드를 제공합니다. + +## 클라우드 제공업체 - 후원자들 + +몇몇 클라우드 제공업체들은 [**FastAPI를 후원하며**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨, 이를 통해 FastAPI와 FastAPI **생태계**가 지속적이고 건전한 **발전**을 할 수 있습니다. + +이는 FastAPI와 **커뮤니티** (여러분)에 대한 진정한 헌신을 보여줍니다. 그들은 여러분에게 **좋은 서비스**를 제공할 뿐 만이 아니라 여러분이 **훌륭하고 건강한 프레임워크인** FastAPI 를 사용하길 원하기 때문입니다. 🙇 + +아래와 같은 서비스를 사용해보고 각 서비스의 가이드를 따를 수도 있습니다: + +* Platform.sh +* Porter +* Deta From 255e743f98ecb89482705424a8f73079ace2a93a Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 25 Sep 2023 23:05:48 +0000 Subject: [PATCH 1352/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4c6d8e288..feb72f444 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Korean translation for `docs/ko/docs/deployment/cloud.md`. PR [#10191](https://github.com/tiangolo/fastapi/pull/10191) by [@Sion99](https://github.com/Sion99). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/https.md`. PR [#10298](https://github.com/tiangolo/fastapi/pull/10298) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Fix typo in Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#10224](https://github.com/tiangolo/fastapi/pull/10224) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). From 0e2ca1cacb0a6e7bf88476a85bd3c9bb1278f009 Mon Sep 17 00:00:00 2001 From: jaystone776 Date: Tue, 26 Sep 2023 07:06:09 +0800 Subject: [PATCH 1353/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Chinese=20tra?= =?UTF-8?q?nslation=20for=20`docs/tutorial/security/simple-oauth2.md`=20(#?= =?UTF-8?q?3844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../docs/tutorial/security/simple-oauth2.md | 214 +++++++++--------- 1 file changed, 111 insertions(+), 103 deletions(-) diff --git a/docs/zh/docs/tutorial/security/simple-oauth2.md b/docs/zh/docs/tutorial/security/simple-oauth2.md index 276f3d63b..c7f46177f 100644 --- a/docs/zh/docs/tutorial/security/simple-oauth2.md +++ b/docs/zh/docs/tutorial/security/simple-oauth2.md @@ -1,94 +1,98 @@ -# 使用密码和 Bearer 的简单 OAuth2 +# OAuth2 实现简单的 Password 和 Bearer 验证 -现在让我们接着上一章继续开发,并添加缺少的部分以实现一个完整的安全性流程。 +本章添加上一章示例中欠缺的部分,实现完整的安全流。 ## 获取 `username` 和 `password` -我们将使用 **FastAPI** 的安全性实用工具来获取 `username` 和 `password`。 +首先,使用 **FastAPI** 安全工具获取 `username` 和 `password`。 -OAuth2 规定在使用(我们打算用的)「password 流程」时,客户端/用户必须将 `username` 和 `password` 字段作为表单数据发送。 +OAuth2 规范要求使用**密码流**时,客户端或用户必须以表单数据形式发送 `username` 和 `password` 字段。 -而且规范明确了字段必须这样命名。因此 `user-name` 或 `email` 是行不通的。 +并且,这两个字段必须命名为 `username` 和 `password` ,不能使用 `user-name` 或 `email` 等其它名称。 -不过不用担心,你可以在前端按照你的想法将它展示给最终用户。 +不过也不用担心,前端仍可以显示终端用户所需的名称。 -而且你的数据库模型也可以使用你想用的任何其他名称。 +数据库模型也可以使用所需的名称。 -但是对于登录*路径操作*,我们需要使用这些名称来与规范兼容(以具备例如使用集成的 API 文档系统的能力)。 +但对于登录*路径操作*,则要使用兼容规范的 `username` 和 `password`,(例如,实现与 API 文档集成)。 -规范还写明了 `username` 和 `password` 必须作为表单数据发送(因此,此处不能使用 JSON)。 +该规范要求必须以表单数据形式发送 `username` 和 `password`,因此,不能使用 JSON 对象。 -### `scope` +### `Scope`(作用域) -规范还提到客户端可以发送另一个表单字段「`scope`」。 +OAuth2 还支持客户端发送**`scope`**表单字段。 -这个表单字段的名称为 `scope`(单数形式),但实际上它是一个由空格分隔的「作用域」组成的长字符串。 +虽然表单字段的名称是 `scope`(单数),但实际上,它是以空格分隔的,由多个**scope**组成的长字符串。 -每个「作用域」只是一个字符串(中间没有空格)。 +**作用域**只是不带空格的字符串。 -它们通常用于声明特定的安全权限,例如: +常用于声明指定安全权限,例如: -* `users:read` 或者 `users:write` 是常见的例子。 -* Facebook / Instagram 使用 `instagram_basic`。 -* Google 使用了 `https://www.googleapis.com/auth/drive` 。 +* 常见用例为,`users:read` 或 `users:write` +* 脸书和 Instagram 使用 `instagram_basic` +* 谷歌使用 `https://www.googleapis.com/auth/drive` -!!! info - 在 OAuth2 中「作用域」只是一个声明所需特定权限的字符串。 +!!! info "说明" - 它有没有 `:` 这样的其他字符或者是不是 URL 都没有关系。 + OAuth2 中,**作用域**只是声明指定权限的字符串。 - 这些细节是具体的实现。 + 是否使用冒号 `:` 等符号,或是不是 URL 并不重要。 - 对 OAuth2 来说它们就只是字符串而已。 + 这些细节只是特定的实现方式。 + + 对 OAuth2 来说,都只是字符串而已。 ## 获取 `username` 和 `password` 的代码 -现在,让我们使用 **FastAPI** 提供的实用工具来处理此问题。 +接下来,使用 **FastAPI** 工具获取用户名与密码。 ### `OAuth2PasswordRequestForm` -首先,导入 `OAuth2PasswordRequestForm`,然后在 `token` 的*路径操作*中通过 `Depends` 将其作为依赖项使用。 +首先,导入 `OAuth2PasswordRequestForm`,然后,在 `/token` *路径操作* 中,用 `Depends` 把该类作为依赖项。 ```Python hl_lines="4 76" {!../../../docs_src/security/tutorial003.py!} ``` -`OAuth2PasswordRequestForm` 是一个类依赖项,声明了如下的请求表单: +`OAuth2PasswordRequestForm` 是用以下几项内容声明表单请求体的类依赖项: -* `username`。 -* `password`。 -* 一个可选的 `scope` 字段,是一个由空格分隔的字符串组成的大字符串。 -* 一个可选的 `grant_type`. +* `username` +* `password` +* 可选的 `scope` 字段,由多个空格分隔的字符串组成的长字符串 +* 可选的 `grant_type` -!!! tip - OAuth2 规范实际上*要求* `grant_type` 字段使用一个固定的值 `password`,但是 `OAuth2PasswordRequestForm` 没有作强制约束。 +!!! tip "提示" - 如果你需要强制要求这一点,请使用 `OAuth2PasswordRequestFormStrict` 而不是 `OAuth2PasswordRequestForm`。 + 实际上,OAuth2 规范*要求* `grant_type` 字段使用固定值 `password`,但 `OAuth2PasswordRequestForm` 没有作强制约束。 -* 一个可选的 `client_id`(我们的示例不需要它)。 -* 一个可选的 `client_secret`(我们的示例不需要它)。 + 如需强制使用固定值 `password`,则不要用 `OAuth2PasswordRequestForm`,而是用 `OAuth2PasswordRequestFormStrict`。 -!!! info - `OAuth2PasswordRequestForm` 并不像 `OAuth2PasswordBearer` 一样是 FastAPI 的一个特殊的类。 +* 可选的 `client_id`(本例未使用) +* 可选的 `client_secret`(本例未使用) - `OAuth2PasswordBearer` 使得 **FastAPI** 明白它是一个安全方案。所以它得以通过这种方式添加到 OpenAPI 中。 +!!! info "说明" - 但 `OAuth2PasswordRequestForm` 只是一个你可以自己编写的类依赖项,或者你也可以直接声明 `Form` 参数。 + `OAuth2PasswordRequestForm` 与 `OAuth2PasswordBearer` 一样,都不是 FastAPI 的特殊类。 - 但是由于这是一种常见的使用场景,因此 FastAPI 出于简便直接提供了它。 + **FastAPI** 把 `OAuth2PasswordBearer` 识别为安全方案。因此,可以通过这种方式把它添加至 OpenAPI。 + + 但 `OAuth2PasswordRequestForm` 只是可以自行编写的类依赖项,也可以直接声明 `Form` 参数。 + + 但由于这种用例很常见,FastAPI 为了简便,就直接提供了对它的支持。 ### 使用表单数据 -!!! tip - 类依赖项 `OAuth2PasswordRequestForm` 的实例不会有用空格分隔的长字符串属性 `scope`,而是具有一个 `scopes` 属性,该属性将包含实际被发送的每个作用域字符串组成的列表。 +!!! tip "提示" - 在此示例中我们没有使用 `scopes`,但如果你需要的话可以使用该功能。 + `OAuth2PasswordRequestForm` 类依赖项的实例没有以空格分隔的长字符串属性 `scope`,但它支持 `scopes` 属性,由已发送的 scope 字符串列表组成。 -现在,使用表单字段中的 `username` 从(伪)数据库中获取用户数据。 + 本例没有使用 `scopes`,但开发者也可以根据需要使用该属性。 -如果没有这个用户,我们将返回一个错误消息,提示「用户名或密码错误」。 +现在,即可使用表单字段 `username`,从(伪)数据库中获取用户数据。 -对于这个错误,我们使用 `HTTPException` 异常: +如果不存在指定用户,则返回错误消息,提示**用户名或密码错误**。 + +本例使用 `HTTPException` 异常显示此错误: ```Python hl_lines="3 77-79" {!../../../docs_src/security/tutorial003.py!} @@ -96,27 +100,27 @@ OAuth2 规定在使用(我们打算用的)「password 流程」时,客户 ### 校验密码 -目前我们已经从数据库中获取了用户数据,但尚未校验密码。 +至此,我们已经从数据库中获取了用户数据,但尚未校验密码。 -让我们首先将这些数据放入 Pydantic `UserInDB` 模型中。 +接下来,首先将数据放入 Pydantic 的 `UserInDB` 模型。 -永远不要保存明文密码,因此,我们将使用(伪)哈希密码系统。 +注意:永远不要保存明文密码,本例暂时先使用(伪)哈希密码系统。 -如果密码不匹配,我们将返回同一个错误。 +如果密码不匹配,则返回与上面相同的错误。 -#### 哈希密码 +#### 密码哈希 -「哈希」的意思是:将某些内容(在本例中为密码)转换为看起来像乱码的字节序列(只是一个字符串)。 +**哈希**是指,将指定内容(本例中为密码)转换为形似乱码的字节序列(其实就是字符串)。 -每次你传入完全相同的内容(完全相同的密码)时,你都会得到完全相同的乱码。 +每次传入完全相同的内容(比如,完全相同的密码)时,得到的都是完全相同的乱码。 -但是你不能从乱码转换回密码。 +但这个乱码无法转换回传入的密码。 -##### 为什么使用哈希密码 +##### 为什么使用密码哈希 -如果你的数据库被盗,小偷将无法获得用户的明文密码,只有哈希值。 +原因很简单,假如数据库被盗,窃贼无法获取用户的明文密码,得到的只是哈希值。 -因此,小偷将无法尝试在另一个系统中使用这些相同的密码(由于许多用户在任何地方都使用相同的密码,因此这很危险)。 +这样一来,窃贼就无法在其它应用中使用窃取的密码,要知道,很多用户在所有系统中都使用相同的密码,风险超大。 ```Python hl_lines="80-83" {!../../../docs_src/security/tutorial003.py!} @@ -124,9 +128,9 @@ OAuth2 规定在使用(我们打算用的)「password 流程」时,客户 #### 关于 `**user_dict` -`UserInDB(**user_dict)` 表示: +`UserInDB(**user_dict)` 是指: -*直接将 `user_dict` 的键和值作为关键字参数传递,等同于:* +*直接把 `user_dict` 的键与值当作关键字参数传递,等效于:* ```Python UserInDB( @@ -138,75 +142,79 @@ UserInDB( ) ``` -!!! info - 有关 `user_dict` 的更完整说明,请参阅[**额外的模型**文档](../extra-models.md#about-user_indict){.internal-link target=_blank}。 +!!! info "说明" -## 返回令牌 + `user_dict` 的说明,详见[**更多模型**一章](../extra-models.md#about-user_indict){.internal-link target=_blank}。 -`token` 端点的响应必须是一个 JSON 对象。 +## 返回 Token -它应该有一个 `token_type`。在我们的例子中,由于我们使用的是「Bearer」令牌,因此令牌类型应为「`bearer`」。 +`token` 端点的响应必须是 JSON 对象。 -并且还应该有一个 `access_token` 字段,它是一个包含我们的访问令牌的字符串。 +响应返回的内容应该包含 `token_type`。本例中用的是**Bearer**Token,因此, Token 类型应为**`bearer`**。 -对于这个简单的示例,我们将极其不安全地返回相同的 `username` 作为令牌。 +返回内容还应包含 `access_token` 字段,它是包含权限 Token 的字符串。 -!!! tip - 在下一章中,你将看到一个真实的安全实现,使用了哈希密码和 JWT 令牌。 +本例只是简单的演示,返回的 Token 就是 `username`,但这种方式极不安全。 - 但现在,让我们仅关注我们需要的特定细节。 +!!! tip "提示" + + 下一章介绍使用哈希密码和 JWT Token 的真正安全机制。 + + 但现在,仅关注所需的特定细节。 ```Python hl_lines="85" {!../../../docs_src/security/tutorial003.py!} ``` -!!! tip - 根据规范,你应该像本示例一样,返回一个带有 `access_token` 和 `token_type` 的 JSON。 +!!! tip "提示" - 这是你必须在代码中自行完成的工作,并且要确保使用了这些 JSON 字段。 + 按规范的要求,应像本示例一样,返回带有 `access_token` 和 `token_type` 的 JSON 对象。 - 这几乎是唯一的你需要自己记住并正确地执行以符合规范的事情。 + 这是开发者必须在代码中自行完成的工作,并且要确保使用这些 JSON 的键。 - 其余的,**FastAPI** 都会为你处理。 + 这几乎是唯一需要开发者牢记在心,并按规范要求正确执行的事。 + + **FastAPI** 则负责处理其它的工作。 ## 更新依赖项 -现在我们将更新我们的依赖项。 +接下来,更新依赖项。 -我们想要仅当此用户处于启用状态时才能获取 `current_user`。 +使之仅在当前用户为激活状态时,才能获取 `current_user`。 -因此,我们创建了一个额外的依赖项 `get_current_active_user`,而该依赖项又以 `get_current_user` 作为依赖项。 +为此,要再创建一个依赖项 `get_current_active_user`,此依赖项以 `get_current_user` 依赖项为基础。 -如果用户不存在或处于未启用状态,则这两个依赖项都将仅返回 HTTP 错误。 +如果用户不存在,或状态为未激活,这两个依赖项都会返回 HTTP 错误。 -因此,在我们的端点中,只有当用户存在,身份认证通过且处于启用状态时,我们才能获得该用户: +因此,在端点中,只有当用户存在、通过身份验证、且状态为激活时,才能获得该用户: ```Python hl_lines="58-67 69-72 90" {!../../../docs_src/security/tutorial003.py!} ``` -!!! info - 我们在此处返回的值为 `Bearer` 的额外响应头 `WWW-Authenticate` 也是规范的一部分。 +!!! info "说明" - 任何的 401「未认证」HTTP(错误)状态码都应该返回 `WWW-Authenticate` 响应头。 + 此处返回值为 `Bearer` 的响应头 `WWW-Authenticate` 也是规范的一部分。 - 对于 bearer 令牌(我们的例子),该响应头的值应为 `Bearer`。 + 任何 401**UNAUTHORIZED**HTTP(错误)状态码都应返回 `WWW-Authenticate` 响应头。 - 实际上你可以忽略这个额外的响应头,不会有什么问题。 + 本例中,因为使用的是 Bearer Token,该响应头的值应为 `Bearer`。 - 但此处提供了它以符合规范。 + 实际上,忽略这个附加响应头,也不会有什么问题。 - 而且,(现在或将来)可能会有工具期望得到并使用它,然后对你或你的用户有用处。 + 之所以在此提供这个附加响应头,是为了符合规范的要求。 - 这就是遵循标准的好处... + 说不定什么时候,就有工具用得上它,而且,开发者或用户也可能用得上。 + + 这就是遵循标准的好处…… ## 实际效果 -打开交互式文档:http://127.0.0.1:8000/docs。 +打开 API 文档:http://127.0.0.1:8000/docs。 -### 身份认证 +### 身份验证 -点击「Authorize」按钮。 +点击**Authorize**按钮。 使用以下凭证: @@ -216,15 +224,15 @@ UserInDB( -在系统中进行身份认证后,你将看到: +通过身份验证后,显示下图所示的内容: -### 获取本人的用户数据 +### 获取当前用户数据 -现在执行 `/users/me` 路径的 `GET` 操作。 +使用 `/users/me` 路径的 `GET` 操作。 -你将获得你的用户数据,如: +可以提取如下当前用户数据: ```JSON { @@ -238,7 +246,7 @@ UserInDB( -如果你点击锁定图标并注销,然后再次尝试同一操作,则会得到 HTTP 401 错误: +点击小锁图标,注销后,再执行同样的操作,则会得到 HTTP 401 错误: ```JSON { @@ -246,17 +254,17 @@ UserInDB( } ``` -### 未启用的用户 +### 未激活用户 -现在尝试使用未启用的用户,并通过以下方式进行身份认证: +测试未激活用户,输入以下信息,进行身份验证: 用户名:`alice` 密码:`secret2` -然后尝试执行 `/users/me` 路径的 `GET` 操作。 +然后,执行 `/users/me` 路径的 `GET` 操作。 -你将得到一个「未启用的用户」错误,如: +显示下列**未激活用户**错误信息: ```JSON { @@ -264,12 +272,12 @@ UserInDB( } ``` -## 总结 +## 小结 -现在你掌握了为你的 API 实现一个基于 `username` 和 `password` 的完整安全系统的工具。 +使用本章的工具实现基于 `username` 和 `password` 的完整 API 安全系统。 -使用这些工具,你可以使安全系统与任何数据库以及任何用户或数据模型兼容。 +这些工具让安全系统兼容任何数据库、用户及数据模型。 -唯一缺少的细节是它实际上还并不「安全」。 +唯一欠缺的是,它仍然不是真的**安全**。 -在下一章中,你将看到如何使用一个安全的哈希密码库和 JWT 令牌。 +下一章,介绍使用密码哈希支持库与 JWT 令牌实现真正的安全机制。 From 073e7fc950f1af24634b304afbb34bf212756dff Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 25 Sep 2023 23:08:51 +0000 Subject: [PATCH 1354/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 feb72f444..58eb8ece0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Update Chinese translation for `docs/tutorial/security/simple-oauth2.md`. PR [#3844](https://github.com/tiangolo/fastapi/pull/3844) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Korean translation for `docs/ko/docs/deployment/cloud.md`. PR [#10191](https://github.com/tiangolo/fastapi/pull/10191) by [@Sion99](https://github.com/Sion99). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/https.md`. PR [#10298](https://github.com/tiangolo/fastapi/pull/10298) by [@tamtam-fitness](https://github.com/tamtam-fitness). * 🌐 Fix typo in Russian translation for `docs/ru/docs/tutorial/body-fields.md`. PR [#10224](https://github.com/tiangolo/fastapi/pull/10224) by [@AlertRED](https://github.com/AlertRED). From 1453cea4043c895cc6e9aff27762e72c64bc8619 Mon Sep 17 00:00:00 2001 From: mkdir700 Date: Thu, 28 Sep 2023 04:47:21 +0800 Subject: [PATCH 1355/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/async.md`=20(#5591)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jedore Co-authored-by: Sebastián Ramírez Co-authored-by: 吴定焕 <108172295+wdh99@users.noreply.github.com> --- docs/zh/docs/async.md | 430 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 docs/zh/docs/async.md diff --git a/docs/zh/docs/async.md b/docs/zh/docs/async.md new file mode 100644 index 000000000..7cc76fc86 --- /dev/null +++ b/docs/zh/docs/async.md @@ -0,0 +1,430 @@ +# 并发 async / await + +有关路径操作函数的 `async def` 语法以及异步代码、并发和并行的一些背景知识。 + +## 赶时间吗? + +TL;DR: + +如果你正在使用第三方库,它们会告诉你使用 `await` 关键字来调用它们,就像这样: + +```Python +results = await some_library() +``` + +然后,通过 `async def` 声明你的 *路径操作函数*: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! note + 你只能在被 `async def` 创建的函数内使用 `await` + +--- + +如果你正在使用一个第三方库和某些组件(比如:数据库、API、文件系统...)进行通信,第三方库又不支持使用 `await` (目前大多数数据库三方库都是这样),这种情况你可以像平常那样使用 `def` 声明一个路径操作函数,就像这样: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +如果你的应用程序不需要与其他任何东西通信而等待其响应,请使用 `async def`。 + +--- + +如果你不清楚,使用 `def` 就好. + +--- + +**注意**:你可以根据需要在路径操作函数中混合使用 `def` 和 `async def`,并使用最适合你的方式去定义每个函数。FastAPI 将为他们做正确的事情。 + +无论如何,在上述任何情况下,FastAPI 仍将异步工作,速度也非常快。 + +但是,通过遵循上述步骤,它将能够进行一些性能优化。 + +## 技术细节 + +Python 的现代版本支持通过一种叫**"协程"**——使用 `async` 和 `await` 语法的东西来写**”异步代码“**。 + +让我们在下面的部分中逐一介绍: + +* **异步代码** +* **`async` 和 `await`** +* **协程** + +## 异步代码 + +异步代码仅仅意味着编程语言 💬 有办法告诉计算机/程序 🤖 在代码中的某个点,它 🤖 将不得不等待在某些地方完成一些事情。让我们假设一些事情被称为 "慢文件"📝. + +所以,在等待"慢文件"📝完成的这段时间,计算机可以做一些其他工作。 + +然后计算机/程序 🤖 每次有机会都会回来,因为它又在等待,或者它 🤖 完成了当前所有的工作。而且它 🤖 将查看它等待的所有任务中是否有已经完成的,做它必须做的任何事情。 + +接下来,它 🤖 完成第一个任务(比如是我们的"慢文件"📝) 并继续与之相关的一切。 + +这个"等待其他事情"通常指的是一些相对较慢(与处理器和 RAM 存储器的速度相比)的 I/O 操作,比如说: + +* 通过网络发送来自客户端的数据 +* 客户端接收来自网络中的数据 +* 磁盘中要由系统读取并提供给程序的文件的内容 +* 程序提供给系统的要写入磁盘的内容 +* 一个 API 的远程调用 +* 一个数据库操作,直到完成 +* 一个数据库查询,直到返回结果 +* 等等. + +这个执行的时间大多是在等待 I/O 操作,因此它们被叫做 "I/O 密集型" 操作。 + +它被称为"异步"的原因是因为计算机/程序不必与慢任务"同步",去等待任务完成的确切时刻,而在此期间不做任何事情直到能够获取任务结果才继续工作。 + +相反,作为一个"异步"系统,一旦完成,任务就可以排队等待一段时间(几微秒),等待计算机程序完成它要做的任何事情,然后回来获取结果并继续处理它们。 + +对于"同步"(与"异步"相反),他们通常也使用"顺序"一词,因为计算机程序在切换到另一个任务之前是按顺序执行所有步骤,即使这些步骤涉及到等待。 + +### 并发与汉堡 + +上述异步代码的思想有时也被称为“并发”,它不同于“并行”。 + +并发和并行都与“不同的事情或多或少同时发生”有关。 + +但是并发和并行之间的细节是完全不同的。 + +要了解差异,请想象以下关于汉堡的故事: + +### 并发汉堡 + +你和你的恋人一起去快餐店,你排队在后面,收银员从你前面的人接单。😍 + + + +然后轮到你了,你为你的恋人和你选了两个非常豪华的汉堡。🍔🍔 + + + +收银员对厨房里的厨师说了一些话,让他们知道他们必须为你准备汉堡(尽管他们目前正在为之前的顾客准备汉堡)。 + + + +你付钱了。 💸 + +收银员给你轮到的号码。 + + + +当你在等待的时候,你和你的恋人一起去挑选一张桌子,然后你们坐下来聊了很长时间(因为汉堡很豪华,需要一些时间来准备)。 + +当你和你的恋人坐在桌子旁,等待汉堡的时候,你可以用这段时间来欣赏你的恋人是多么的棒、可爱和聪明✨😍✨。 + + + +在等待中和你的恋人交谈时,你会不时地查看柜台上显示的号码,看看是否已经轮到你了。 + +然后在某个时刻,终于轮到你了。你去柜台拿汉堡然后回到桌子上。 + + + +你们享用了汉堡,整个过程都很开心。✨ + + + +!!! info + 漂亮的插画来自 Ketrina Thompson. 🎨 + +--- + +在那个故事里,假设你是计算机程序 🤖 。 + +当你在排队时,你只是闲着😴, 轮到你前不做任何事情(仅排队)。但排队很快,因为收银员只接订单(不准备订单),所以这一切都还好。 + +然后,当轮到你时,需要你做一些实际性的工作,比如查看菜单,决定你想要什么,让你的恋人选择,支付,检查你是否提供了正确的账单或卡,检查你的收费是否正确,检查订单是否有正确的项目,等等。 + +此时,即使你仍然没有汉堡,你和收银员的工作也"暂停"了⏸, 因为你必须等待一段时间 🕙 让你的汉堡做好。 + +但是,当你离开柜台并坐在桌子旁,在轮到你的号码前的这段时间,你可以将焦点切换到 🔀 你的恋人上,并做一些"工作"⏯ 🤓。你可以做一些非常"有成效"的事情,比如和你的恋人调情😍. + +之后,收银员 💁 把号码显示在显示屏上,并说到 "汉堡做好了",而当显示的号码是你的号码时,你不会立刻疯狂地跳起来。因为你知道没有人会偷你的汉堡,因为你有你的号码,而其他人又有他们自己的号码。 + +所以你要等待你的恋人完成故事(完成当前的工作⏯ /正在做的事🤓), 轻轻微笑,说你要吃汉堡⏸. + +然后你去柜台🔀, 到现在初始任务已经完成⏯, 拿起汉堡,说声谢谢,然后把它们送到桌上。这就完成了与计数器交互的步骤/任务⏹. 这反过来又产生了一项新任务,即"吃汉堡"🔀 ⏯, 上一个"拿汉堡"的任务已经结束了⏹. + +### 并行汉堡 + +现在让我们假设不是"并发汉堡",而是"并行汉堡"。 + +你和你的恋人一起去吃并行快餐。 + +你站在队伍中,同时是厨师的几个收银员(比方说8个)从前面的人那里接单。 + +你之前的每个人都在等待他们的汉堡准备好后才离开柜台,因为8名收银员都会在下一份订单前马上准备好汉堡。 + + + +然后,终于轮到你了,你为你的恋人和你订购了两个非常精美的汉堡。 + +你付钱了 💸。 + + + +收银员去厨房。 + +你站在柜台前 🕙等待着,这样就不会有人在你之前抢走你的汉堡,因为没有轮流的号码。 + + + +当你和你的恋人忙于不让任何人出现在你面前,并且在他们到来的时候拿走你的汉堡时,你无法关注到你的恋人。😞 + +这是"同步"的工作,你被迫与服务员/厨师 👨‍🍳"同步"。你在此必须等待 🕙 ,在收银员/厨师 👨‍🍳 完成汉堡并将它们交给你的确切时间到达之前一直等待,否则其他人可能会拿走它们。 + + + +你经过长时间的等待 🕙 ,收银员/厨师 👨‍🍳终于带着汉堡回到了柜台。 + + + +你拿着汉堡,和你的情人一起上桌。 + +你们仅仅是吃了它们,就结束了。⏹ + + + +没有太多的交谈或调情,因为大部分时间 🕙 都在柜台前等待😞。 + +!!! info + 漂亮的插画来自 Ketrina Thompson. 🎨 + +--- + +在这个并行汉堡的场景中,你是一个计算机程序 🤖 且有两个处理器(你和你的恋人),都在等待 🕙 ,并投入他们的注意力 ⏯ 在柜台上等待了很长一段时间。 + +这家快餐店有 8 个处理器(收银员/厨师)。而并发汉堡店可能只有 2 个(一个收银员和一个厨师)。 + +但最终的体验仍然不是最好的。😞 + +--- + +这将是与汉堡的类似故事。🍔 + +一种更"贴近生活"的例子,想象一家银行。 + +直到最近,大多数银行都有多个出纳员 👨‍💼👨‍💼👨‍💼👨‍💼 还有一条长长排队队伍🕙🕙🕙🕙🕙🕙🕙🕙。 + +所有收银员都是一个接一个的在客户面前做完所有的工作👨‍💼⏯. + +你必须经过 🕙 较长时间排队,否则你就没机会了。 + +你可不会想带你的恋人 😍 和你一起去银行办事🏦. + +### 汉堡结论 + +在"你与恋人一起吃汉堡"的这个场景中,因为有很多人在等待🕙, 使用并发系统更有意义⏸🔀⏯. + +大多数 Web 应用都是这样的。 + +你的服务器正在等待很多很多用户通过他们不太好的网络发送来的请求。 + +然后再次等待 🕙 响应回来。 + +这个"等待" 🕙 是以微秒为单位测量的,但总的来说,最后还是等待很久。 + +这就是为什么使用异步对于 Web API 很有意义的原因 ⏸🔀⏯。 + +这种异步机制正是 NodeJS 受到欢迎的原因(尽管 NodeJS 不是并行的),以及 Go 作为编程语言的优势所在。 + +这与 **FastAPI** 的性能水平相同。 + +您可以同时拥有并行性和异步性,您可以获得比大多数经过测试的 NodeJS 框架更高的性能,并且与 Go 不相上下, Go 是一种更接近于 C 的编译语言(全部归功于 Starlette)。 + +### 并发比并行好吗? + +不!这不是故事的本意。 + +并发不同于并行。而是在需要大量等待的特定场景下效果更好。因此,在 Web 应用程序开发中,它通常比并行要好得多,但这并不意味着全部。 + +因此,为了平衡这一点,想象一下下面的短篇故事: + +> 你必须打扫一个又大又脏的房子。 + +*是的,这就是完整的故事。* + +--- + +在任何地方, 都不需要等待 🕙 ,只需要在房子的多个地方做着很多工作。 + +你可以像汉堡的例子那样轮流执行,先是客厅,然后是厨房,但因为你不需要等待 🕙 ,对于任何事情都是清洁,清洁,还是清洁,轮流不会影响任何事情。 + +无论是否轮流执行(并发),都需要相同的时间来完成,而你也会完成相同的工作量。 + +但在这种情况下,如果你能带上 8 名前收银员/厨师,现在是清洁工一起清扫,他们中的每一个人(加上你)都能占据房子的一个区域来清扫,你就可以在额外的帮助下并行的更快地完成所有工作。 + +在这个场景中,每个清洁工(包括您)都将是一个处理器,完成这个工作的一部分。 + +由于大多数执行时间是由实际工作(而不是等待)占用的,并且计算机中的工作是由 CPU 完成的,所以他们称这些问题为"CPU 密集型"。 + +--- + +CPU 密集型操作的常见示例是需要复杂的数学处理。 + +例如: + +* **音频**或**图像**处理; +* **计算机视觉**: 一幅图像由数百万像素组成,每个像素有3种颜色值,处理通常需要同时对这些像素进行计算; +* **机器学习**: 它通常需要大量的"矩阵"和"向量"乘法。想象一个包含数字的巨大电子表格,并同时将所有数字相乘; +* **深度学习**: 这是机器学习的一个子领域,同样适用。只是没有一个数字的电子表格可以相乘,而是一个庞大的数字集合,在很多情况下,你需要使用一个特殊的处理器来构建和使用这些模型。 + +### 并发 + 并行: Web + 机器学习 + +使用 **FastAPI**,您可以利用 Web 开发中常见的并发机制的优势(NodeJS 的主要吸引力)。 + +并且,您也可以利用并行和多进程(让多个进程并行运行)的优点来处理与机器学习系统中类似的 **CPU 密集型** 工作。 + +这一点,再加上 Python 是**数据科学**、机器学习(尤其是深度学习)的主要语言这一简单事实,使得 **FastAPI** 与数据科学/机器学习 Web API 和应用程序(以及其他许多应用程序)非常匹配。 + +了解如何在生产环境中实现这种并行性,可查看此文 [Deployment](deployment/index.md){.internal-link target=_blank}。 + +## `async` 和 `await` + +现代版本的 Python 有一种非常直观的方式来定义异步代码。这使它看起来就像正常的"顺序"代码,并在适当的时候"等待"。 + +当有一个操作需要等待才能给出结果,且支持这个新的 Python 特性时,您可以编写如下代码: + +```Python +burgers = await get_burgers(2) +``` + +这里的关键是 `await`。它告诉 Python 它必须等待 ⏸ `get_burgers(2)` 完成它的工作 🕙 ,然后将结果存储在 `burgers` 中。这样,Python 就会知道此时它可以去做其他事情 🔀 ⏯ (比如接收另一个请求)。 + +要使 `await` 工作,它必须位于支持这种异步机制的函数内。因此,只需使用 `async def` 声明它: + +```Python hl_lines="1" +async def get_burgers(number: int): + # Do some asynchronous stuff to create the burgers + return burgers +``` + +...而不是 `def`: + +```Python hl_lines="2" +# This is not asynchronous +def get_sequential_burgers(number: int): + # Do some sequential stuff to create the burgers + return burgers +``` + +使用 `async def`,Python 就知道在该函数中,它将遇上 `await`,并且它可以"暂停" ⏸ 执行该函数,直至执行其他操作 🔀 后回来。 + +当你想调用一个 `async def` 函数时,你必须"等待"它。因此,这不会起作用: + +```Python +# This won't work, because get_burgers was defined with: async def +burgers = get_burgers(2) +``` + +--- + +因此,如果您使用的库告诉您可以使用 `await` 调用它,则需要使用 `async def` 创建路径操作函数 ,如: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### 更多技术细节 + +您可能已经注意到,`await` 只能在 `async def` 定义的函数内部使用。 + +但与此同时,必须"等待"通过 `async def` 定义的函数。因此,带 `async def` 的函数也只能在 `async def` 定义的函数内部调用。 + +那么,这关于先有鸡还是先有蛋的问题,如何调用第一个 `async` 函数? + +如果您使用 **FastAPI**,你不必担心这一点,因为"第一个"函数将是你的路径操作函数,FastAPI 将知道如何做正确的事情。 + +但如果您想在没有 FastAPI 的情况下使用 `async` / `await`,则可以这样做。 + +### 编写自己的异步代码 + +Starlette (和 **FastAPI**) 是基于 AnyIO 实现的,这使得它们可以兼容 Python 的标准库 asyncioTrio。 + +特别是,你可以直接使用 AnyIO 来处理高级的并发用例,这些用例需要在自己的代码中使用更高级的模式。 + +即使您没有使用 **FastAPI**,您也可以使用 AnyIO 编写自己的异步程序,使其拥有较高的兼容性并获得一些好处(例如, 结构化并发)。 + +### 其他形式的异步代码 + +这种使用 `async` 和 `await` 的风格在语言中相对较新。 + +但它使处理异步代码变得容易很多。 + +这种相同的语法(或几乎相同)最近也包含在现代版本的 JavaScript 中(在浏览器和 NodeJS 中)。 + +但在此之前,处理异步代码非常复杂和困难。 + +在以前版本的 Python,你可以使用多线程或者 Gevent。但代码的理解、调试和思考都要复杂许多。 + +在以前版本的 NodeJS / 浏览器 JavaScript 中,你会使用"回调",因此也可能导致回调地狱。 + +## 协程 + +**协程**只是 `async def` 函数返回的一个非常奇特的东西的称呼。Python 知道它有点像一个函数,它可以启动,也会在某个时刻结束,而且它可能会在内部暂停 ⏸ ,只要内部有一个 `await`。 + +通过使用 `async` 和 `await` 的异步代码的所有功能大多数被概括为"协程"。它可以与 Go 的主要关键特性 "Goroutines" 相媲美。 + +## 结论 + +让我们再来回顾下上文所说的: + +> Python 的现代版本可以通过使用 `async` 和 `await` 语法创建**协程**,并用于支持**异步代码**。 + +现在应该能明白其含义了。✨ + +所有这些使得 FastAPI(通过 Starlette)如此强大,也是它拥有如此令人印象深刻的性能的原因。 + +## 非常技术性的细节 + +!!! warning + 你可以跳过这里。 + + 这些都是 FastAPI 如何在内部工作的技术细节。 + + 如果您有相当多的技术知识(协程、线程、阻塞等),并且对 FastAPI 如何处理 `async def` 与常规 `def` 感到好奇,请继续。 + +### 路径操作函数 + +当你使用 `def` 而不是 `async def` 来声明一个*路径操作函数*时,它运行在外部的线程池中并等待其结果,而不是直接调用(因为它会阻塞服务器)。 + +如果您使用过另一个不以上述方式工作的异步框架,并且您习惯于用普通的 `def` 定义普通的仅计算路径操作函数,以获得微小的性能增益(大约100纳秒),请注意,在 FastAPI 中,效果将完全相反。在这些情况下,最好使用 `async def`,除非路径操作函数内使用执行阻塞 I/O 的代码。 + +在这两种情况下,与您之前的框架相比,**FastAPI** 可能[仍然很快](/#performance){.internal-link target=_blank}。 + +### 依赖 + +这同样适用于[依赖](/tutorial/dependencies/index.md){.internal-link target=_blank}。如果一个依赖是标准的 `def` 函数而不是 `async def`,它将被运行在外部线程池中。 + +### 子依赖 + +你可以拥有多个相互依赖的依赖以及[子依赖](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作为函数的参数),它们中的一些可能是通过 `async def` 声明,也可能是通过 `def` 声明。它们仍然可以正常工作,这些通过 `def` 声明的函数将会在外部线程中调用(来自线程池),而不是"被等待"。 + +### 其他函数 + +您可直接调用通过 `def` 或 `async def` 创建的任何其他函数,FastAPI 不会影响您调用它们的方式。 + +这与 FastAPI 为您调用*路径操作函数*和依赖项的逻辑相反。 + +如果你的函数是通过 `def` 声明的,它将被直接调用(在代码中编写的地方),而不会在线程池中,如果这个函数通过 `async def` 声明,当在代码中调用时,你就应该使用 `await` 等待函数的结果。 + +--- + +再次提醒,这些是非常技术性的细节,如果你来搜索它可能对你有用。 + +否则,您最好应该遵守的指导原则赶时间吗?. From 27870e20f54e5cb938f5f030e767de41a4fef2b9 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Sep 2023 20:48:01 +0000 Subject: [PATCH 1356/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 58eb8ece0..f32565438 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). * 🌐 Update Chinese translation for `docs/tutorial/security/simple-oauth2.md`. PR [#3844](https://github.com/tiangolo/fastapi/pull/3844) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Korean translation for `docs/ko/docs/deployment/cloud.md`. PR [#10191](https://github.com/tiangolo/fastapi/pull/10191) by [@Sion99](https://github.com/Sion99). * 🌐 Add Japanese translation for `docs/ja/docs/deployment/https.md`. PR [#10298](https://github.com/tiangolo/fastapi/pull/10298) by [@tamtam-fitness](https://github.com/tamtam-fitness). From 69f82e5222ca6fff6c40db79c2b4518508a6c659 Mon Sep 17 00:00:00 2001 From: Samuel Rigaud <46346622+s-rigaud@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:52:31 -0400 Subject: [PATCH 1357/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typos=20in=20Fre?= =?UTF-8?q?nch=20translations=20for=20`docs/fr/docs/advanced/path-operatio?= =?UTF-8?q?n-advanced-configuration.md`,=20`docs/fr/docs/alternatives.md`,?= =?UTF-8?q?=20`docs/fr/docs/async.md`,=20`docs/fr/docs/features.md`,=20`do?= =?UTF-8?q?cs/fr/docs/help-fastapi.md`,=20`docs/fr/docs/index.md`,=20`docs?= =?UTF-8?q?/fr/docs/python-types.md`,=20`docs/fr/docs/tutorial/body.md`,?= =?UTF-8?q?=20`docs/fr/docs/tutorial/first-steps.md`,=20`docs/fr/docs/tuto?= =?UTF-8?q?rial/query-params.md`=20(#10154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez --- .../path-operation-advanced-configuration.md | 2 +- docs/fr/docs/alternatives.md | 2 +- docs/fr/docs/async.md | 2 +- docs/fr/docs/features.md | 14 +++++++------- docs/fr/docs/help-fastapi.md | 4 ++-- docs/fr/docs/index.md | 4 ++-- docs/fr/docs/python-types.md | 2 +- docs/fr/docs/tutorial/body.md | 2 +- docs/fr/docs/tutorial/first-steps.md | 2 +- docs/fr/docs/tutorial/query-params.md | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/fr/docs/advanced/path-operation-advanced-configuration.md b/docs/fr/docs/advanced/path-operation-advanced-configuration.md index ace9f19f9..7ded97ce1 100644 --- a/docs/fr/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/fr/docs/advanced/path-operation-advanced-configuration.md @@ -66,7 +66,7 @@ Il y a un chapitre entier ici dans la documentation à ce sujet, vous pouvez le Lorsque vous déclarez un *chemin* dans votre application, **FastAPI** génère automatiquement les métadonnées concernant ce *chemin* à inclure dans le schéma OpenAPI. !!! note "Détails techniques" - La spécification OpenAPI appelle ces métaonnées des Objets d'opération. + La spécification OpenAPI appelle ces métadonnées des Objets d'opération. Il contient toutes les informations sur le *chemin* et est utilisé pour générer automatiquement la documentation. diff --git a/docs/fr/docs/alternatives.md b/docs/fr/docs/alternatives.md index ee20438c3..8e58a3dfa 100644 --- a/docs/fr/docs/alternatives.md +++ b/docs/fr/docs/alternatives.md @@ -387,7 +387,7 @@ Gérer toute la validation des données, leur sérialisation et la documentation ### Starlette -Starlette est un framework/toolkit léger ASGI, qui est idéal pour construire des services asyncio performants. +Starlette est un framework/toolkit léger ASGI, qui est idéal pour construire des services asyncio performants. Il est très simple et intuitif. Il est conçu pour être facilement extensible et avoir des composants modulaires. diff --git a/docs/fr/docs/async.md b/docs/fr/docs/async.md index db88c4663..af4d6ca06 100644 --- a/docs/fr/docs/async.md +++ b/docs/fr/docs/async.md @@ -250,7 +250,7 @@ Par exemple : ### Concurrence + Parallélisme : Web + Machine Learning -Avec **FastAPI** vous pouvez bénéficier de la concurrence qui est très courante en developement web (c'est l'attrait principal de NodeJS). +Avec **FastAPI** vous pouvez bénéficier de la concurrence qui est très courante en développement web (c'est l'attrait principal de NodeJS). Mais vous pouvez aussi profiter du parallélisme et multiprocessing afin de gérer des charges **CPU bound** qui sont récurrentes dans les systèmes de *Machine Learning*. diff --git a/docs/fr/docs/features.md b/docs/fr/docs/features.md index dcc0e39ed..f5faa46b6 100644 --- a/docs/fr/docs/features.md +++ b/docs/fr/docs/features.md @@ -71,9 +71,9 @@ my_second_user: User = User(**second_user_data) Tout le framework a été conçu pour être facile et intuitif d'utilisation, toutes les décisions de design ont été testées sur de nombreux éditeurs avant même de commencer le développement final afin d'assurer la meilleure expérience de développement possible. -Dans le dernier sondage effectué auprès de développeurs python il était clair que la fonctionnalité la plus utilisée est "l'autocomplètion". +Dans le dernier sondage effectué auprès de développeurs python il était clair que la fonctionnalité la plus utilisée est "l’autocomplétion". -Tout le framwork **FastAPI** a été conçu avec cela en tête. L'autocomplétion fonctionne partout. +Tout le framework **FastAPI** a été conçu avec cela en tête. L'autocomplétion fonctionne partout. Vous devrez rarement revenir à la documentation. @@ -136,7 +136,7 @@ FastAPI contient un système simple mais extrêmement puissant d'Starlette. Le code utilisant Starlette que vous ajouterez fonctionnera donc aussi. -En fait, `FastAPI` est un sous compposant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. +En fait, `FastAPI` est un sous composant de `Starlette`. Donc, si vous savez déjà comment utiliser Starlette, la plupart des fonctionnalités fonctionneront de la même manière. Avec **FastAPI** vous aurez toutes les fonctionnalités de **Starlette** (FastAPI est juste Starlette sous stéroïdes): -* Des performances vraiments impressionnantes. C'est l'un des framework Python les plus rapide, à égalité avec **NodeJS** et **GO**. +* Des performances vraiment impressionnantes. C'est l'un des framework Python les plus rapide, à égalité avec **NodeJS** et **GO**. * Le support des **WebSockets**. * Le support de **GraphQL**. * Les tâches d'arrière-plan. @@ -180,7 +180,7 @@ Inclus des librairies externes basées, aussi, sur Pydantic, servent d' décorateur de validation * 100% de couverture de test. diff --git a/docs/fr/docs/help-fastapi.md b/docs/fr/docs/help-fastapi.md index 0995721e1..3bc3c3a8a 100644 --- a/docs/fr/docs/help-fastapi.md +++ b/docs/fr/docs/help-fastapi.md @@ -36,8 +36,8 @@ Vous pouvez : * Me suivre sur **Twitter**. * Dites-moi comment vous utilisez FastAPI (j'adore entendre ça). * Entendre quand je fais des annonces ou que je lance de nouveaux outils. -* Vous connectez à moi sur **Linkedin**. - * Etre notifié quand je fais des annonces ou que je lance de nouveaux outils (bien que j'utilise plus souvent Twitte 🤷‍♂). +* Vous connectez à moi sur **LinkedIn**. + * Etre notifié quand je fais des annonces ou que je lance de nouveaux outils (bien que j'utilise plus souvent Twitter 🤷‍♂). * Lire ce que j’écris (ou me suivre) sur **Dev.to** ou **Medium**. * Lire d'autres idées, articles, et sur les outils que j'ai créés. * Suivez-moi pour lire quand je publie quelque chose de nouveau. diff --git a/docs/fr/docs/index.md b/docs/fr/docs/index.md index 7c7547be1..4ac9864ec 100644 --- a/docs/fr/docs/index.md +++ b/docs/fr/docs/index.md @@ -424,7 +424,7 @@ Pour un exemple plus complet comprenant plus de fonctionnalités, voir le en-têtes.**, **cookies**, **champs de formulaire** et **fichiers**. * L'utilisation de **contraintes de validation** comme `maximum_length` ou `regex`. -* Un **systéme d'injection de dépendance ** très puissant et facile à utiliser . +* Un **système d'injection de dépendance ** très puissant et facile à utiliser . * Sécurité et authentification, y compris la prise en charge de **OAuth2** avec les **jetons JWT** et l'authentification **HTTP Basic**. * Des techniques plus avancées (mais tout aussi faciles) pour déclarer les **modèles JSON profondément imbriqués** (grâce à Pydantic). * Intégration de **GraphQL** avec Strawberry et d'autres bibliothèques. @@ -450,7 +450,7 @@ Utilisées par Pydantic: Utilisées par Starlette : * requests - Obligatoire si vous souhaitez utiliser `TestClient`. -* jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par defaut. +* jinja2 - Obligatoire si vous souhaitez utiliser la configuration de template par défaut. * python-multipart - Obligatoire si vous souhaitez supporter le "décodage" de formulaire avec `request.form()`. * itsdangerous - Obligatoire pour la prise en charge de `SessionMiddleware`. * pyyaml - Obligatoire pour le support `SchemaGenerator` de Starlette (vous n'en avez probablement pas besoin avec FastAPI). diff --git a/docs/fr/docs/python-types.md b/docs/fr/docs/python-types.md index 4008ed96f..f49fbafd3 100644 --- a/docs/fr/docs/python-types.md +++ b/docs/fr/docs/python-types.md @@ -119,7 +119,7 @@ Comme l'éditeur connaît le type des variables, vous n'avez pas seulement l'aut -Maintenant que vous avez connaissance du problème, convertissez `age` en chaine de caractères grâce à `str(age)` : +Maintenant que vous avez connaissance du problème, convertissez `age` en chaîne de caractères grâce à `str(age)` : ```Python hl_lines="2" {!../../../docs_src/python_types/tutorial004.py!} diff --git a/docs/fr/docs/tutorial/body.md b/docs/fr/docs/tutorial/body.md index 1e732d336..89720c973 100644 --- a/docs/fr/docs/tutorial/body.md +++ b/docs/fr/docs/tutorial/body.md @@ -98,7 +98,7 @@ Et vous obtenez aussi de la vérification d'erreur pour les opérations incorrec -Ce n'est pas un hasard, ce framework entier a été bati avec ce design comme objectif. +Ce n'est pas un hasard, ce framework entier a été bâti avec ce design comme objectif. Et cela a été rigoureusement testé durant la phase de design, avant toute implémentation, pour s'assurer que cela fonctionnerait avec tous les éditeurs. diff --git a/docs/fr/docs/tutorial/first-steps.md b/docs/fr/docs/tutorial/first-steps.md index 224c340c6..e98283f1e 100644 --- a/docs/fr/docs/tutorial/first-steps.md +++ b/docs/fr/docs/tutorial/first-steps.md @@ -170,7 +170,7 @@ Si vous créez votre app avec : {!../../../docs_src/first_steps/tutorial002.py!} ``` -Et la mettez dans un fichier `main.py`, alors vous appeleriez `uvicorn` avec : +Et la mettez dans un fichier `main.py`, alors vous appelleriez `uvicorn` avec :
diff --git a/docs/fr/docs/tutorial/query-params.md b/docs/fr/docs/tutorial/query-params.md index 7bf3b9e79..962135f63 100644 --- a/docs/fr/docs/tutorial/query-params.md +++ b/docs/fr/docs/tutorial/query-params.md @@ -75,7 +75,7 @@ Ici, le paramètre `q` sera optionnel, et aura `None` comme valeur par défaut. !!! note **FastAPI** saura que `q` est optionnel grâce au `=None`. - Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre editeur de texte pour détecter des erreurs dans votre code. + Le `Optional` dans `Optional[str]` n'est pas utilisé par **FastAPI** (**FastAPI** n'en utilisera que la partie `str`), mais il servira tout de même à votre éditeur de texte pour détecter des erreurs dans votre code. ## Conversion des types des paramètres de requête From 99ffbcdee0c9bd21855f259fb9983b5d823bed6e Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Sep 2023 20:53:18 +0000 Subject: [PATCH 1358/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f32565438..0ab8b5f17 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). * 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). * 🌐 Update Chinese translation for `docs/tutorial/security/simple-oauth2.md`. PR [#3844](https://github.com/tiangolo/fastapi/pull/3844) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Korean translation for `docs/ko/docs/deployment/cloud.md`. PR [#10191](https://github.com/tiangolo/fastapi/pull/10191) by [@Sion99](https://github.com/Sion99). From b2f8ac6a83d8938afbe1476462c694d8a6e0b6a7 Mon Sep 17 00:00:00 2001 From: ArtemKhymenko Date: Wed, 27 Sep 2023 23:53:36 +0300 Subject: [PATCH 1359/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/tutorial/extra-data-types.md`=20(?= =?UTF-8?q?#10132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ArtemKhymenko Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Rostyslav --- docs/uk/docs/tutorial/encoder.md | 42 +++++++ docs/uk/docs/tutorial/extra-data-types.md | 130 ++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 docs/uk/docs/tutorial/encoder.md create mode 100644 docs/uk/docs/tutorial/extra-data-types.md diff --git a/docs/uk/docs/tutorial/encoder.md b/docs/uk/docs/tutorial/encoder.md new file mode 100644 index 000000000..d660447b4 --- /dev/null +++ b/docs/uk/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# JSON Compatible Encoder + +Існують випадки, коли вам може знадобитися перетворити тип даних (наприклад, модель Pydantic) в щось сумісне з JSON (наприклад, `dict`, `list`, і т. д.). + +Наприклад, якщо вам потрібно зберегти це в базі даних. + +Для цього, **FastAPI** надає `jsonable_encoder()` функцію. + +## Використання `jsonable_encoder` + +Давайте уявимо, що у вас є база даних `fake_db`, яка приймає лише дані, сумісні з JSON. + +Наприклад, вона не приймає об'єкти типу `datetime`, оскільки вони не сумісні з JSON. + +Отже, об'єкт типу `datetime` потрібно перетворити в рядок `str`, який містить дані в ISO форматі. + +Тим самим способом ця база даних не прийматиме об'єкт типу Pydantic model (об'єкт з атрибутами), а лише `dict`. + +Ви можете використовувати `jsonable_encoder` для цього. + +Вона приймає об'єкт, такий як Pydantic model, і повертає його версію, сумісну з JSON: + +=== "Python 3.10+" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +У цьому прикладі вона конвертує Pydantic model у `dict`, а `datetime` у `str`. + +Результат виклику цієї функції - це щось, що можна кодувати з використанням стандарту Python `json.dumps()`. + +Вона не повертає велику строку `str`, яка містить дані у форматі JSON (як строка). Вона повертає стандартну структуру даних Python (наприклад `dict`) із значеннями та підзначеннями, які є сумісними з JSON. + +!!! Примітка + `jsonable_encoder` фактично використовується **FastAPI** внутрішньо для перетворення даних. Проте вона корисна в багатьох інших сценаріях. diff --git a/docs/uk/docs/tutorial/extra-data-types.md b/docs/uk/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..ba75ee627 --- /dev/null +++ b/docs/uk/docs/tutorial/extra-data-types.md @@ -0,0 +1,130 @@ +# Додаткові типи даних + +До цього часу, ви використовували загальнопоширені типи даних, такі як: + +* `int` +* `float` +* `str` +* `bool` + +Але можна також використовувати більш складні типи даних. + +І ви все ще матимете ті ж можливості, які були показані до цього: + +* Чудова підтримка редактора. +* Конвертація даних з вхідних запитів. +* Конвертація даних для відповіді. +* Валідація даних. +* Автоматична анотація та документація. + +## Інші типи даних + +Ось додаткові типи даних для використання: + +* `UUID`: + * Стандартний "Універсальний Унікальний Ідентифікатор", який часто використовується як ідентифікатор у багатьох базах даних та системах. + * У запитах та відповідях буде представлений як `str`. +* `datetime.datetime`: + * Пайтонівський `datetime.datetime`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15T15:53:00+05:00`. +* `datetime.date`: + * Пайтонівський `datetime.date`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `2008-09-15`. +* `datetime.time`: + * Пайтонівський `datetime.time`. + * У запитах та відповідях буде представлений як `str` в форматі ISO 8601, як: `14:23:55.003`. +* `datetime.timedelta`: + * Пайтонівський `datetime.timedelta`. + * У запитах та відповідях буде представлений як `float` загальної кількості секунд. + * Pydantic також дозволяє представляти це як "ISO 8601 time diff encoding", більше інформації дивись у документації. +* `frozenset`: + * У запитах і відповідях це буде оброблено так само, як і `set`: + * У запитах список буде зчитано, дублікати будуть видалені та він буде перетворений на `set`. + * У відповідях, `set` буде перетворений на `list`. + * Згенерована схема буде вказувати, що значення `set` є унікальними (з використанням JSON Schema's `uniqueItems`). +* `bytes`: + * Стандартний Пайтонівський `bytes`. + * У запитах і відповідях це буде оброблено як `str`. + * Згенерована схема буде вказувати, що це `str` з "форматом" `binary`. +* `Decimal`: + * Стандартний Пайтонівський `Decimal`. + * У запитах і відповідях це буде оброблено так само, як і `float`. +* Ви можете перевірити всі дійсні типи даних Pydantic тут: типи даних Pydantic. + +## Приклад + +Ось приклад *path operation* з параметрами, використовуючи деякі з вищезазначених типів. + +=== "Python 3.10+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="1 3 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1 3 13-17" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1 2 11-15" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="1 2 12-16" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` + +Зверніть увагу, що параметри всередині функції мають свій звичайний тип даних, і ви можете, наприклад, виконувати звичайні маніпуляції з датами, такі як: + +=== "Python 3.10+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="19-20" + {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="17-18" + {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ non-Annotated" + + !!! tip + Бажано використовувати `Annotated` версію, якщо це можливо. + + ```Python hl_lines="18-19" + {!> ../../../docs_src/extra_data_types/tutorial001.py!} + ``` From 1c4a9e91b67a370b35f793ab746d32f3fc026621 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Sep 2023 20:55:18 +0000 Subject: [PATCH 1360/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0ab8b5f17..ddd761240 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). * 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). * 🌐 Update Chinese translation for `docs/tutorial/security/simple-oauth2.md`. PR [#3844](https://github.com/tiangolo/fastapi/pull/3844) by [@jaystone776](https://github.com/jaystone776). From 74cf05117b21b90ac4da40bba8dad527220c465f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 27 Sep 2023 18:01:46 -0500 Subject: [PATCH 1361/1881] =?UTF-8?q?=F0=9F=94=A7=20Rename=20label=20"awai?= =?UTF-8?q?ting=20review"=20to=20"awaiting-review"=20to=20simplify=20searc?= =?UTF-8?q?h=20queries=20(#10343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/notify-translations/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/notify-translations/app/main.py b/.github/actions/notify-translations/app/main.py index 494fe6ad8..8ac1f233d 100644 --- a/.github/actions/notify-translations/app/main.py +++ b/.github/actions/notify-translations/app/main.py @@ -9,7 +9,7 @@ import httpx from github import Github from pydantic import BaseModel, BaseSettings, SecretStr -awaiting_label = "awaiting review" +awaiting_label = "awaiting-review" lang_all_label = "lang-all" approved_label = "approved-2" translations_path = Path(__file__).parent / "translations.yml" From b944b55dfc5e5cacccd9a7125fe883f80a45cba7 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Sep 2023 23:02:35 +0000 Subject: [PATCH 1362/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ddd761240..52ee4c7fd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). * 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). From bc935e08b6d9069280ed34625930a1461235db9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 27 Sep 2023 23:14:40 -0500 Subject: [PATCH 1363/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20compat?= =?UTF-8?q?ibility=20with=20Pydantic=20v2.4,=20new=20renamed=20functions?= =?UTF-8?q?=20and=20JSON=20Schema=20input/output=20models=20with=20default?= =?UTF-8?q?=20values=20(#10344)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚚 Refactor deprecated import general_plain_validator_function to with_info_plain_validator_function * 🚚 Rename deprecated FieldValidationInfo to ValidationInfo * ✅ Update tests with new defaults for JSON Schema for default values * ♻️ Add Pydantic v1 version of with_info_plain_validator_function * 👷 Invalidate cache * ✅ Fix tests for Pydantic v1 * ✅ Tweak tests coverage for older Pydantic v2 versions --- .github/workflows/test.yml | 4 +- fastapi/_compat.py | 14 +++++-- fastapi/datastructures.py | 4 +- fastapi/openapi/models.py | 4 +- tests/test_compat.py | 2 +- tests/test_filter_pydantic_sub_model_pv2.py | 4 +- ...t_openapi_separate_input_output_schemas.py | 6 ++- .../test_body_updates/test_tutorial001.py | 38 ++----------------- .../test_tutorial001_py310.py | 38 ++----------------- .../test_tutorial001_py39.py | 38 ++----------------- .../test_dataclasses/test_tutorial003.py | 22 ++--------- .../test_tutorial004.py | 32 ++-------------- .../test_tutorial005.py | 32 ++-------------- .../test_tutorial005_py310.py | 32 ++-------------- .../test_tutorial005_py39.py | 32 ++-------------- .../test_path_params/test_tutorial005.py | 4 +- .../test_tutorial001.py | 20 ++-------- .../test_tutorial001_py310.py | 20 ++-------- .../test_tutorial001_py39.py | 20 ++-------- 19 files changed, 63 insertions(+), 303 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c9723b25b..4ebc64a14 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -62,7 +62,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v05 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt diff --git a/fastapi/_compat.py b/fastapi/_compat.py index eb55b08f2..a4b305d42 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -58,9 +58,15 @@ if PYDANTIC_V2: from pydantic_core import CoreSchema as CoreSchema from pydantic_core import PydanticUndefined, PydanticUndefinedType from pydantic_core import Url as Url - from pydantic_core.core_schema import ( - general_plain_validator_function as general_plain_validator_function, - ) + + try: + from pydantic_core.core_schema import ( + with_info_plain_validator_function as with_info_plain_validator_function, + ) + except ImportError: # pragma: no cover + from pydantic_core.core_schema import ( + general_plain_validator_function as with_info_plain_validator_function, # noqa: F401 + ) Required = PydanticUndefined Undefined = PydanticUndefined @@ -345,7 +351,7 @@ else: class PydanticSchemaGenerationError(Exception): # type: ignore[no-redef] pass - def general_plain_validator_function( # type: ignore[misc] + def with_info_plain_validator_function( # type: ignore[misc] function: Callable[..., Any], *, ref: Union[str, None] = None, diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index 3c96c56c7..b2865cd40 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -5,7 +5,7 @@ from fastapi._compat import ( CoreSchema, GetJsonSchemaHandler, JsonSchemaValue, - general_plain_validator_function, + with_info_plain_validator_function, ) from starlette.datastructures import URL as URL # noqa: F401 from starlette.datastructures import Address as Address # noqa: F401 @@ -49,7 +49,7 @@ class UploadFile(StarletteUploadFile): def __get_pydantic_core_schema__( cls, source: Type[Any], handler: Callable[[Any], CoreSchema] ) -> CoreSchema: - return general_plain_validator_function(cls._validate) + return with_info_plain_validator_function(cls._validate) class DefaultPlaceholder: diff --git a/fastapi/openapi/models.py b/fastapi/openapi/models.py index 3d982eb9a..5f3bdbb20 100644 --- a/fastapi/openapi/models.py +++ b/fastapi/openapi/models.py @@ -7,7 +7,7 @@ from fastapi._compat import ( GetJsonSchemaHandler, JsonSchemaValue, _model_rebuild, - general_plain_validator_function, + with_info_plain_validator_function, ) from fastapi.logger import logger from pydantic import AnyUrl, BaseModel, Field @@ -52,7 +52,7 @@ except ImportError: # pragma: no cover def __get_pydantic_core_schema__( cls, source: Type[Any], handler: Callable[[Any], CoreSchema] ) -> CoreSchema: - return general_plain_validator_function(cls._validate) + return with_info_plain_validator_function(cls._validate) class Contact(BaseModel): diff --git a/tests/test_compat.py b/tests/test_compat.py index 47160ee76..bf268b860 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -24,7 +24,7 @@ def test_model_field_default_required(): @needs_pydanticv1 -def test_upload_file_dummy_general_plain_validator_function(): +def test_upload_file_dummy_with_info_plain_validator_function(): # For coverage assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {} diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py index 9f5e6b08f..9097d2ce5 100644 --- a/tests/test_filter_pydantic_sub_model_pv2.py +++ b/tests/test_filter_pydantic_sub_model_pv2.py @@ -12,7 +12,7 @@ from .utils import needs_pydanticv2 @pytest.fixture(name="client") def get_client(): - from pydantic import BaseModel, FieldValidationInfo, field_validator + from pydantic import BaseModel, ValidationInfo, field_validator app = FastAPI() @@ -28,7 +28,7 @@ def get_client(): foo: ModelB @field_validator("name") - def lower_username(cls, name: str, info: FieldValidationInfo): + def lower_username(cls, name: str, info: ValidationInfo): if not name.endswith("A"): raise ValueError("name must end in A") return name diff --git a/tests/test_openapi_separate_input_output_schemas.py b/tests/test_openapi_separate_input_output_schemas.py index 70f4b90d7..aeb85f735 100644 --- a/tests/test_openapi_separate_input_output_schemas.py +++ b/tests/test_openapi_separate_input_output_schemas.py @@ -4,19 +4,23 @@ from fastapi import FastAPI from fastapi.testclient import TestClient from pydantic import BaseModel -from .utils import needs_pydanticv2 +from .utils import PYDANTIC_V2, needs_pydanticv2 class SubItem(BaseModel): subname: str sub_description: Optional[str] = None tags: List[str] = [] + if PYDANTIC_V2: + model_config = {"json_schema_serialization_defaults_required": True} class Item(BaseModel): name: str description: Optional[str] = None sub: Optional[SubItem] = None + if PYDANTIC_V2: + model_config = {"json_schema_serialization_defaults_required": True} def get_app_client(separate_input_output_schemas: bool = True) -> TestClient: diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001.py b/tests/test_tutorial/test_body_updates/test_tutorial001.py index 58587885e..e586534a0 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001.py @@ -52,9 +52,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -86,9 +84,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -116,7 +112,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -126,35 +122,9 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "Item-Input": { - "title": "Item", + "Item": { "type": "object", - "properties": { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "Item-Output": { "title": "Item", - "type": "object", - "required": ["name", "description", "price", "tax", "tags"], "properties": { "name": { "anyOf": [{"type": "string"}, {"type": "null"}], diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py index d8a62502f..6bc969d43 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py310.py @@ -55,9 +55,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -89,9 +87,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -119,7 +115,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -129,35 +125,9 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "Item-Input": { - "title": "Item", + "Item": { "type": "object", - "properties": { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "Item-Output": { "title": "Item", - "type": "object", - "required": ["name", "description", "price", "tax", "tags"], "properties": { "name": { "anyOf": [{"type": "string"}, {"type": "null"}], diff --git a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py index c604df6ec..a1edb3370 100644 --- a/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_body_updates/test_tutorial001_py39.py @@ -55,9 +55,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -89,9 +87,7 @@ def test_openapi_schema(client: TestClient): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -119,7 +115,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -129,35 +125,9 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "Item-Input": { - "title": "Item", + "Item": { "type": "object", - "properties": { - "name": { - "title": "Name", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": { - "title": "Price", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tax": {"title": "Tax", "type": "number", "default": 10.5}, - "tags": { - "title": "Tags", - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, - "Item-Output": { "title": "Item", - "type": "object", - "required": ["name", "description", "price", "tax", "tags"], "properties": { "name": { "anyOf": [{"type": "string"}, {"type": "null"}], diff --git a/tests/test_tutorial/test_dataclasses/test_tutorial003.py b/tests/test_tutorial/test_dataclasses/test_tutorial003.py index f2ca85823..dd0e36735 100644 --- a/tests/test_tutorial/test_dataclasses/test_tutorial003.py +++ b/tests/test_tutorial/test_dataclasses/test_tutorial003.py @@ -79,9 +79,7 @@ def test_openapi_schema(): "schema": { "title": "Items", "type": "array", - "items": { - "$ref": "#/components/schemas/Item-Input" - }, + "items": {"$ref": "#/components/schemas/Item"}, } } }, @@ -136,14 +134,14 @@ def test_openapi_schema(): "schemas": { "Author": { "title": "Author", - "required": ["name", "items"], + "required": ["name"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, "items": { "title": "Items", "type": "array", - "items": {"$ref": "#/components/schemas/Item-Output"}, + "items": {"$ref": "#/components/schemas/Item"}, }, }, }, @@ -158,27 +156,15 @@ def test_openapi_schema(): } }, }, - "Item-Input": { + "Item": { "title": "Item", "required": ["name"], "type": "object", "properties": { "name": {"title": "Name", "type": "string"}, "description": { - "title": "Description", "anyOf": [{"type": "string"}, {"type": "null"}], - }, - }, - }, - "Item-Output": { - "title": "Item", - "required": ["name", "description"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], }, }, }, diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py index c5b2fb670..4f69e4646 100644 --- a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py @@ -34,9 +34,7 @@ def test_openapi_schema(): "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -57,7 +55,7 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -67,7 +65,7 @@ def test_openapi_schema(): }, "components": { "schemas": { - "Item-Input": { + "Item": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -91,30 +89,6 @@ def test_openapi_schema(): }, }, }, - "Item-Output": { - "title": "Item", - "required": ["name", "description", "price", "tax", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "title": "Description", - "anyOf": [{"type": "string"}, {"type": "null"}], - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py index 458923b5a..d3792e701 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py @@ -34,9 +34,7 @@ def test_openapi_schema(): "description": "The created item", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -57,7 +55,7 @@ def test_openapi_schema(): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -67,7 +65,7 @@ def test_openapi_schema(): }, "components": { "schemas": { - "Item-Input": { + "Item": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -91,30 +89,6 @@ def test_openapi_schema(): }, }, }, - "Item-Output": { - "title": "Item", - "required": ["name", "description", "price", "tax", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py index 1fcc5c4e0..a68deb3df 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py @@ -41,9 +41,7 @@ def test_openapi_schema(client: TestClient): "description": "The created item", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -64,7 +62,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -74,7 +72,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "Item-Input": { + "Item": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -98,30 +96,6 @@ def test_openapi_schema(client: TestClient): }, }, }, - "Item-Output": { - "title": "Item", - "required": ["name", "description", "price", "tax", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py index 470fe032b..e17f2592d 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py @@ -41,9 +41,7 @@ def test_openapi_schema(client: TestClient): "description": "The created item", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Item-Output" - } + "schema": {"$ref": "#/components/schemas/Item"} } }, }, @@ -64,7 +62,7 @@ def test_openapi_schema(client: TestClient): "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -74,7 +72,7 @@ def test_openapi_schema(client: TestClient): }, "components": { "schemas": { - "Item-Input": { + "Item": { "title": "Item", "required": ["name", "price"], "type": "object", @@ -98,30 +96,6 @@ def test_openapi_schema(client: TestClient): }, }, }, - "Item-Output": { - "title": "Item", - "required": ["name", "description", "price", "tax", "tags"], - "type": "object", - "properties": { - "name": {"title": "Name", "type": "string"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - "price": {"title": "Price", "type": "number"}, - "tax": { - "title": "Tax", - "anyOf": [{"type": "number"}, {"type": "null"}], - }, - "tags": { - "title": "Tags", - "uniqueItems": True, - "type": "array", - "items": {"type": "string"}, - "default": [], - }, - }, - }, "ValidationError": { "title": "ValidationError", "required": ["loc", "msg", "type"], diff --git a/tests/test_tutorial/test_path_params/test_tutorial005.py b/tests/test_tutorial/test_path_params/test_tutorial005.py index 90fa6adaf..2e4b0146b 100644 --- a/tests/test_tutorial/test_path_params/test_tutorial005.py +++ b/tests/test_tutorial/test_path_params/test_tutorial005.py @@ -33,9 +33,9 @@ def test_get_enums_invalid(): { "type": "enum", "loc": ["path", "model_name"], - "msg": "Input should be 'alexnet','resnet' or 'lenet'", + "msg": "Input should be 'alexnet', 'resnet' or 'lenet'", "input": "foo", - "ctx": {"expected": "'alexnet','resnet' or 'lenet'"}, + "ctx": {"expected": "'alexnet', 'resnet' or 'lenet'"}, } ] } diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py index 8079c1134..cdfae9f8c 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001.py @@ -48,9 +48,7 @@ def test_openapi_schema(client: TestClient) -> None: "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/Item-Output" - }, + "items": {"$ref": "#/components/schemas/Item"}, "type": "array", "title": "Response Read Items Items Get", } @@ -65,7 +63,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -102,7 +100,7 @@ def test_openapi_schema(client: TestClient) -> None: "type": "object", "title": "HTTPValidationError", }, - "Item-Input": { + "Item": { "properties": { "name": {"type": "string", "title": "Name"}, "description": { @@ -114,18 +112,6 @@ def test_openapi_schema(client: TestClient) -> None: "required": ["name"], "title": "Item", }, - "Item-Output": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name", "description"], - "title": "Item", - }, "ValidationError": { "properties": { "loc": { diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py index 4fa98ccbe..3b22146f6 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py310.py @@ -51,9 +51,7 @@ def test_openapi_schema(client: TestClient) -> None: "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/Item-Output" - }, + "items": {"$ref": "#/components/schemas/Item"}, "type": "array", "title": "Response Read Items Items Get", } @@ -68,7 +66,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -105,7 +103,7 @@ def test_openapi_schema(client: TestClient) -> None: "type": "object", "title": "HTTPValidationError", }, - "Item-Input": { + "Item": { "properties": { "name": {"type": "string", "title": "Name"}, "description": { @@ -117,18 +115,6 @@ def test_openapi_schema(client: TestClient) -> None: "required": ["name"], "title": "Item", }, - "Item-Output": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name", "description"], - "title": "Item", - }, "ValidationError": { "properties": { "loc": { diff --git a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py index ad36582ed..991abe811 100644 --- a/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py +++ b/tests/test_tutorial/test_separate_openapi_schemas/test_tutorial001_py39.py @@ -51,9 +51,7 @@ def test_openapi_schema(client: TestClient) -> None: "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/Item-Output" - }, + "items": {"$ref": "#/components/schemas/Item"}, "type": "array", "title": "Response Read Items Items Get", } @@ -68,7 +66,7 @@ def test_openapi_schema(client: TestClient) -> None: "requestBody": { "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/Item-Input"} + "schema": {"$ref": "#/components/schemas/Item"} } }, "required": True, @@ -105,7 +103,7 @@ def test_openapi_schema(client: TestClient) -> None: "type": "object", "title": "HTTPValidationError", }, - "Item-Input": { + "Item": { "properties": { "name": {"type": "string", "title": "Name"}, "description": { @@ -117,18 +115,6 @@ def test_openapi_schema(client: TestClient) -> None: "required": ["name"], "title": "Item", }, - "Item-Output": { - "properties": { - "name": {"type": "string", "title": "Name"}, - "description": { - "anyOf": [{"type": "string"}, {"type": "null"}], - "title": "Description", - }, - }, - "type": "object", - "required": ["name", "description"], - "title": "Item", - }, "ValidationError": { "properties": { "loc": { From 831b5d5402a65ee9f415670f4116522c8e874ed3 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 28 Sep 2023 04:15:17 +0000 Subject: [PATCH 1364/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 52ee4c7fd..a8b9bba03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values. PR [#10344](https://github.com/tiangolo/fastapi/pull/10344) by [@tiangolo](https://github.com/tiangolo). * 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). From 2f50ae882589e26aa725555a88adcb3cdd793137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 28 Sep 2023 14:41:17 -0500 Subject: [PATCH 1365/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Flint=20(#10349)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 5d7752d29..cea547a10 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -43,9 +43,6 @@ bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. img: https://fastapi.tiangolo.com/img/sponsors/exoflare.png - - url: https://www.flint.sh - title: IT expertise, consulting and development by passionate people - img: https://fastapi.tiangolo.com/img/sponsors/flint.png - url: https://bit.ly/3JJ7y5C title: Build cross-modal and multimodal applications on the cloud img: https://fastapi.tiangolo.com/img/sponsors/jina2.svg From d769da3c38a2b882799a69499b862a5e964e9d4e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 28 Sep 2023 19:42:38 +0000 Subject: [PATCH 1366/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a8b9bba03..ed5598551 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, remove Flint. PR [#10349](https://github.com/tiangolo/fastapi/pull/10349) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values. PR [#10344](https://github.com/tiangolo/fastapi/pull/10344) by [@tiangolo](https://github.com/tiangolo). * 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). From d0b17dd49c299404e60f78a349f547a460357a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 28 Sep 2023 14:51:39 -0500 Subject: [PATCH 1367/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Python?= =?UTF-8?q?=20version=20in=20Docker=20images=20for=20GitHub=20Actions=20(#?= =?UTF-8?q?10350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/comment-docs-preview-in-pr/Dockerfile | 2 +- .github/actions/notify-translations/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile index 4f20c5f10..14b0d0269 100644 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ b/.github/actions/comment-docs-preview-in-pr/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.9 RUN pip install httpx "pydantic==1.5.1" pygithub diff --git a/.github/actions/notify-translations/Dockerfile b/.github/actions/notify-translations/Dockerfile index fa4197e6a..b68b4bb1a 100644 --- a/.github/actions/notify-translations/Dockerfile +++ b/.github/actions/notify-translations/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.9 RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0" From fcda32d2311b79606f8a5c3f0556d0a30ea082a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 28 Sep 2023 14:56:50 -0500 Subject: [PATCH 1368/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ed5598551..1a74cc17f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,9 +2,12 @@ ## Latest Changes -* 🔧 Update sponsors, remove Flint. PR [#10349](https://github.com/tiangolo/fastapi/pull/10349) by [@tiangolo](https://github.com/tiangolo). +### Refactors + * ⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values. PR [#10344](https://github.com/tiangolo/fastapi/pull/10344) by [@tiangolo](https://github.com/tiangolo). -* 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). + +### Translations + * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/extra-data-types.md`. PR [#10132](https://github.com/tiangolo/fastapi/pull/10132) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Fix typos in French translations for `docs/fr/docs/advanced/path-operation-advanced-configuration.md`, `docs/fr/docs/alternatives.md`, `docs/fr/docs/async.md`, `docs/fr/docs/features.md`, `docs/fr/docs/help-fastapi.md`, `docs/fr/docs/index.md`, `docs/fr/docs/python-types.md`, `docs/fr/docs/tutorial/body.md`, `docs/fr/docs/tutorial/first-steps.md`, `docs/fr/docs/tutorial/query-params.md`. PR [#10154](https://github.com/tiangolo/fastapi/pull/10154) by [@s-rigaud](https://github.com/s-rigaud). * 🌐 Add Chinese translation for `docs/zh/docs/async.md`. PR [#5591](https://github.com/tiangolo/fastapi/pull/5591) by [@mkdir700](https://github.com/mkdir700). @@ -15,6 +18,11 @@ * 🌐 Add Polish translation for `docs/pl/docs/help-fastapi.md`. PR [#10121](https://github.com/tiangolo/fastapi/pull/10121) by [@romabozhanovgithub](https://github.com/romabozhanovgithub). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/header-params.md`. PR [#10226](https://github.com/tiangolo/fastapi/pull/10226) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/versions.md`. PR [#10276](https://github.com/tiangolo/fastapi/pull/10276) by [@xzmeng](https://github.com/xzmeng). + +### Internal + +* 🔧 Update sponsors, remove Flint. PR [#10349](https://github.com/tiangolo/fastapi/pull/10349) by [@tiangolo](https://github.com/tiangolo). +* 🔧 Rename label "awaiting review" to "awaiting-review" to simplify search queries. PR [#10343](https://github.com/tiangolo/fastapi/pull/10343) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, enable Svix (revert #10228). PR [#10253](https://github.com/tiangolo/fastapi/pull/10253) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Svix. PR [#10228](https://github.com/tiangolo/fastapi/pull/10228) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Bump.sh. PR [#10227](https://github.com/tiangolo/fastapi/pull/10227) by [@tiangolo](https://github.com/tiangolo). From 1bf5e7a10e33d6f65e0f701ec271785fbc9b463c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Thu, 28 Sep 2023 14:57:42 -0500 Subject: [PATCH 1369/1881] =?UTF-8?q?=F0=9F=94=96=20Release=200.103.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1a74cc17f..11333a712 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,8 @@ ## Latest Changes +## 0.103.2 + ### Refactors * ⬆️ Upgrade compatibility with Pydantic v2.4, new renamed functions and JSON Schema input/output models with default values. PR [#10344](https://github.com/tiangolo/fastapi/pull/10344) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 329477e41..981ca4945 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.103.1" +__version__ = "0.103.2" from starlette import status as status From 568b35f3dfbc4c4c5e02c292c672dc509d62b460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 2 Oct 2023 18:11:52 -0500 Subject: [PATCH 1370/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#10363)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions --- docs/en/data/github_sponsors.yml | 80 +++++++----------- docs/en/data/people.yml | 138 +++++++++++++++---------------- 2 files changed, 100 insertions(+), 118 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 1ec71dd49..b9d74ea7b 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,5 +1,8 @@ sponsors: -- - login: cryptapi +- - login: bump-sh + avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 + url: https://github.com/bump-sh + - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi - login: porter-dev @@ -29,15 +32,15 @@ sponsors: - login: deepset-ai avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 url: https://github.com/deepset-ai + - login: svix + avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 + url: https://github.com/svix - login: databento-bot avatarUrl: https://avatars.githubusercontent.com/u/98378480?u=494f679996e39427f7ddb1a7de8441b7c96fb670&v=4 url: https://github.com/databento-bot - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes -- - login: getsentry - avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 - url: https://github.com/getsentry - - login: acsone avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 url: https://github.com/acsone @@ -50,9 +53,6 @@ sponsors: - login: marvin-robot avatarUrl: https://avatars.githubusercontent.com/u/41086007?u=091c5cb75af363123d66f58194805a97220ee1a7&v=4 url: https://github.com/marvin-robot - - login: Flint-company - avatarUrl: https://avatars.githubusercontent.com/u/48908872?u=355cd3d8992d4be8173058e7000728757c55ad49&v=4 - url: https://github.com/Flint-company - login: BoostryJP avatarUrl: https://avatars.githubusercontent.com/u/57932412?v=4 url: https://github.com/BoostryJP @@ -77,9 +77,6 @@ sponsors: - login: AccentDesign avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 url: https://github.com/AccentDesign - - login: RodneyU215 - avatarUrl: https://avatars.githubusercontent.com/u/3329665?u=ec6a9adf8e7e8e306eed7d49687c398608d1604f&v=4 - url: https://github.com/RodneyU215 - login: americanair avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 url: https://github.com/americanair @@ -89,16 +86,10 @@ sponsors: - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - - login: mukulmantosh - avatarUrl: https://avatars.githubusercontent.com/u/15572034?u=006f0a33c0e15bb2de57474a2cf7d2f8ee05f5a0&v=4 - url: https://github.com/mukulmantosh - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: indeedeng - avatarUrl: https://avatars.githubusercontent.com/u/2905043?v=4 - url: https://github.com/indeedeng - - login: NateXVI +- - login: NateXVI avatarUrl: https://avatars.githubusercontent.com/u/48195620?u=4bc8751ae50cb087c40c1fe811764aa070b9eea6&v=4 url: https://github.com/NateXVI - - login: Kludex @@ -113,9 +104,6 @@ sponsors: - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden - - login: dekoza - avatarUrl: https://avatars.githubusercontent.com/u/210980?u=c03c78a8ae1039b500dfe343665536ebc51979b2&v=4 - url: https://github.com/dekoza - login: pamelafox avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 url: https://github.com/pamelafox @@ -143,9 +131,6 @@ sponsors: - login: mickaelandrieu avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 url: https://github.com/mickaelandrieu - - login: jonakoudijs - avatarUrl: https://avatars.githubusercontent.com/u/1906344?u=5ca0c9a1a89b6a2ba31abe35c66bdc07af60a632&v=4 - url: https://github.com/jonakoudijs - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 @@ -227,6 +212,9 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa + - login: rahulsalgare + avatarUrl: https://avatars.githubusercontent.com/u/21974430?u=ade6f182b94554ab8491d7421de5e78f711dcaf8&v=4 + url: https://github.com/rahulsalgare - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy @@ -245,9 +233,6 @@ sponsors: - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure - - login: askurihin - avatarUrl: https://avatars.githubusercontent.com/u/37978981?v=4 - url: https://github.com/askurihin - login: arleybri18 avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 url: https://github.com/arleybri18 @@ -266,27 +251,18 @@ sponsors: - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - - login: thisistheplace - avatarUrl: https://avatars.githubusercontent.com/u/57633545?u=a3f3a7f8ace8511c6c067753f6eb6aee0db11ac6&v=4 - url: https://github.com/thisistheplace - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia - - login: daverin - avatarUrl: https://avatars.githubusercontent.com/u/70378377?u=6d1814195c0de7162820eaad95a25b423a3869c0&v=4 - url: https://github.com/daverin - login: anthonycepeda avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - - login: hbakri - avatarUrl: https://avatars.githubusercontent.com/u/92298226?u=36eee6d75db1272264d3ee92f1871f70b8917bd8&v=4 - url: https://github.com/hbakri - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki @@ -305,6 +281,9 @@ sponsors: - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson + - login: yourkin + avatarUrl: https://avatars.githubusercontent.com/u/178984?u=b43a7e5f8818f7d9083d3b110118d9c27d48a794&v=4 + url: https://github.com/yourkin - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs @@ -320,9 +299,6 @@ sponsors: - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy - - login: hardbyte - avatarUrl: https://avatars.githubusercontent.com/u/855189?u=aa29e92f34708814d6b67fcd47ca4cf2ce1c04ed&v=4 - url: https://github.com/hardbyte - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke @@ -423,7 +399,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia - login: timzaz - avatarUrl: https://avatars.githubusercontent.com/u/19709244?u=e6658f6b0b188294ce2db20dad94a678fcea718d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/19709244?u=264d7db95c28156363760229c30ee1116efd4eeb&v=4 url: https://github.com/timzaz - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 @@ -443,6 +419,9 @@ sponsors: - login: joerambo avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 url: https://github.com/joerambo + - login: msniezynski + avatarUrl: https://avatars.githubusercontent.com/u/27588547?u=0e3be5ac57dcfdf124f470bcdf74b5bf79af1b6c&v=4 + url: https://github.com/msniezynski - login: rlnchow avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 url: https://github.com/rlnchow @@ -464,6 +443,12 @@ sponsors: - login: miraedbswo avatarUrl: https://avatars.githubusercontent.com/u/36796047?u=9e7a5b3e558edc61d35d0f9dfac37541bae7f56d&v=4 url: https://github.com/miraedbswo + - login: DSMilestone6538 + avatarUrl: https://avatars.githubusercontent.com/u/37230924?u=f299dce910366471523155e0cb213356d34aadc1&v=4 + url: https://github.com/DSMilestone6538 + - login: curegit + avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 + url: https://github.com/curegit - login: kristiangronberg avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 url: https://github.com/kristiangronberg @@ -491,11 +476,8 @@ sponsors: - login: romabozhanovgithub avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 url: https://github.com/romabozhanovgithub - - login: mnicolleUTC - avatarUrl: https://avatars.githubusercontent.com/u/68548924?u=1fdd7f436bb1f4857c3415e62aa8fbc055fc1a76&v=4 - url: https://github.com/mnicolleUTC - login: mbukeRepo - avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=e125069c6ac5c49355e2b3ca2aa66a445fc4cccd&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=d2eb23e2b222a3b316c4183b05a3236b32819dc2&v=4 url: https://github.com/mbukeRepo - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 @@ -515,12 +497,12 @@ sponsors: - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - - login: koalawangyang - avatarUrl: https://avatars.githubusercontent.com/u/40114367?u=3bb94010f0473b8d4c2edea5a8c1cab61e6a1b22&v=4 - url: https://github.com/koalawangyang - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: lodine-software - avatarUrl: https://avatars.githubusercontent.com/u/133536046?v=4 - url: https://github.com/lodine-software + - login: shywn-mrk + avatarUrl: https://avatars.githubusercontent.com/u/51455763?u=389e2608e4056fe5e1f23e9ad56a9415277504d3&v=4 + url: https://github.com/shywn-mrk + - login: almeida-matheus + avatarUrl: https://avatars.githubusercontent.com/u/66216198?u=54335eaa0ced626be5c1ff52fead1ebc032286ec&v=4 + url: https://github.com/almeida-matheus diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 8ebd2f84c..db06cbdaf 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1866 - prs: 486 + answers: 1868 + prs: 496 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 480 + count: 501 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -26,7 +26,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: jgould22 - count: 164 + count: 168 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: euri10 @@ -61,10 +61,10 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen -- login: yinziyan1206 +- login: acidjunk count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: insomnes count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 @@ -73,14 +73,14 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: acidjunk - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: adriangb - count: 44 + count: 45 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb +- login: yinziyan1206 + count: 44 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: odiseo0 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 @@ -93,18 +93,22 @@ experts: count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: chbndrhnns + count: 38 + avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 + url: https://github.com/chbndrhnns - login: STeveShary count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: chbndrhnns - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 - url: https://github.com/chbndrhnns - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt +- login: n8sty + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -117,10 +121,6 @@ experts: count: 26 avatarUrl: https://avatars.githubusercontent.com/u/43723790?u=9bcce836bbce55835291c5b2ac93a4e311f4b3c3&v=4 url: https://github.com/dbanty -- login: n8sty - count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: wshayes count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -149,6 +149,10 @@ experts: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: chrisK824 + count: 19 + avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 + url: https://github.com/chrisK824 - login: zoliknemet count: 18 avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 @@ -157,6 +161,10 @@ experts: count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt +- login: ebottos94 + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -173,55 +181,39 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 url: https://github.com/caeser1996 +- login: nymous + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: jonatasoli count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli -- login: ebottos94 - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny -- login: chrisK824 - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: nymous - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous - login: abhint count: 15 avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 url: https://github.com/abhint last_month_active: -- login: JavierSanchezCastro - count: 12 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro - login: Kludex - count: 10 + count: 8 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: jgould22 - count: 8 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 -- login: romabozhanovgithub - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 - url: https://github.com/romabozhanovgithub - login: n8sty - count: 5 + count: 7 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty - login: chrisK824 - count: 3 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 +- login: danielfcollier + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/38995330?u=5799be795fc310f75f3a5fe9242307d59b194520&v=4 + url: https://github.com/danielfcollier top_contributors: - login: waynerv count: 25 @@ -235,14 +227,14 @@ top_contributors: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: jaystone776 + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: dmontagu count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu -- login: jaystone776 - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 - login: Xewus count: 14 avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 @@ -307,6 +299,10 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: tamtam-fitness + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 + url: https://github.com/tamtam-fitness - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -347,21 +343,25 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 url: https://github.com/ivan-abc +- login: rostik1410 + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 + url: https://github.com/rostik1410 top_reviewers: - login: Kludex - count: 136 + count: 139 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: yezz123 + count: 80 + avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 + url: https://github.com/yezz123 - login: BilalAlpaslan count: 79 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan -- login: yezz123 - count: 78 - avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 - url: https://github.com/yezz123 - login: iudeen - count: 53 + count: 54 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: tokusumi @@ -380,14 +380,14 @@ top_reviewers: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd +- login: Xewus + count: 44 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay -- login: Xewus - count: 39 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: JarroVGIT count: 34 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 @@ -460,6 +460,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 url: https://github.com/axel584 +- login: Alexandrhub + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 + url: https://github.com/Alexandrhub - login: DevDae count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 @@ -472,10 +476,6 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: Alexandrhub - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 - url: https://github.com/Alexandrhub - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -484,6 +484,10 @@ top_reviewers: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 url: https://github.com/peidrao +- login: wdh99 + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 + url: https://github.com/wdh99 - login: r0b2g1t count: 13 avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 @@ -500,10 +504,6 @@ top_reviewers: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 url: https://github.com/solomein-sv -- login: wdh99 - count: 11 - avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 - url: https://github.com/wdh99 - login: mariacamilagl count: 10 avatarUrl: https://avatars.githubusercontent.com/u/11489395?u=4adb6986bf3debfc2b8216ae701f2bd47d73da7d&v=4 From cb4f0e57ce792b7538ad0f82f0772a57c86b80ca Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 2 Oct 2023 23:12:28 +0000 Subject: [PATCH 1371/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 11333a712..87476fd1e 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 [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). ## 0.103.2 ### Refactors From 89789c80aeb3743bd76486f3d28a174f1815c771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 4 Oct 2023 17:51:10 -0500 Subject: [PATCH 1372/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20B?= =?UTF-8?q?ump.sh=20images=20(#10381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 2 +- docs/en/docs/img/sponsors/bump-sh-banner.svg | 48 +++++++++++++++++ docs/en/docs/img/sponsors/bump-sh.svg | 56 ++++++++++++++++++++ docs/en/overrides/main.html | 2 +- 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/img/sponsors/bump-sh-banner.svg create mode 100644 docs/en/docs/img/sponsors/bump-sh.svg diff --git a/README.md b/README.md index b86143f3d..3f125e60e 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ The key features are: - + diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index cea547a10..dac47d2f0 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -13,7 +13,7 @@ gold: img: https://fastapi.tiangolo.com/img/sponsors/porter.png - url: https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor title: Automate FastAPI documentation generation with Bump.sh - img: https://fastapi.tiangolo.com/img/sponsors/bump-sh.png + img: https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/bump-sh-banner.svg b/docs/en/docs/img/sponsors/bump-sh-banner.svg new file mode 100644 index 000000000..c8ec7675a --- /dev/null +++ b/docs/en/docs/img/sponsors/bump-sh-banner.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/docs/img/sponsors/bump-sh.svg b/docs/en/docs/img/sponsors/bump-sh.svg new file mode 100644 index 000000000..053e54b1d --- /dev/null +++ b/docs/en/docs/img/sponsors/bump-sh.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index d867d2ee5..4c7f19fd4 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -49,7 +49,7 @@
From c1adce4fe93a0035e69988f7e051cfab97d8acef Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 4 Oct 2023 22:52:00 +0000 Subject: [PATCH 1373/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 87476fd1e..3c7ecad43 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). ## 0.103.2 From 2ba7586ff3f4dca379043489141c6d07cb2451bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 17 Oct 2023 09:59:11 +0400 Subject: [PATCH 1374/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Drop=20support?= =?UTF-8?q?=20for=20Python=203.7,=20require=20Python=203.8=20or=20above=20?= =?UTF-8?q?(#10442)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update docs, require Python 3.8+, drop 3.7 * 🔧 Update pyproject.toml, drop support for Python 3.7, require Python 3.8+ * 👷 Update CI GitHub Actions, drop support for Python 3.7, require 3.8+ * 📝 Update docs' references to Python 3.6 and 3.7, use Python 3.8 --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- README.md | 6 +- docs/de/docs/features.md | 2 +- .../docs/advanced/additional-status-codes.md | 4 +- .../en/docs/advanced/advanced-dependencies.md | 16 ++--- docs/en/docs/advanced/generate-clients.md | 6 +- .../docs/advanced/security/http-basic-auth.md | 12 ++-- .../docs/advanced/security/oauth2-scopes.md | 32 ++++----- docs/en/docs/advanced/settings.md | 12 ++-- docs/en/docs/advanced/testing-dependencies.md | 4 +- docs/en/docs/advanced/websockets.md | 6 +- .../docs/how-to/separate-openapi-schemas.md | 8 +-- docs/en/docs/index.md | 6 +- docs/en/docs/python-types.md | 24 +++---- docs/en/docs/tutorial/background-tasks.md | 4 +- docs/en/docs/tutorial/bigger-applications.md | 4 +- docs/en/docs/tutorial/body-fields.md | 8 +-- docs/en/docs/tutorial/body-multiple-params.md | 18 ++--- docs/en/docs/tutorial/body-nested-models.md | 20 +++--- docs/en/docs/tutorial/body-updates.md | 8 +-- docs/en/docs/tutorial/body.md | 12 ++-- docs/en/docs/tutorial/cookie-params.md | 8 +-- .../dependencies/classes-as-dependencies.md | 52 +++++++------- ...pendencies-in-path-operation-decorators.md | 16 ++--- .../dependencies/dependencies-with-yield.md | 8 +-- .../dependencies/global-dependencies.md | 4 +- docs/en/docs/tutorial/dependencies/index.md | 14 ++-- .../tutorial/dependencies/sub-dependencies.md | 16 ++--- docs/en/docs/tutorial/encoder.md | 2 +- docs/en/docs/tutorial/extra-data-types.md | 8 +-- docs/en/docs/tutorial/extra-models.md | 10 +-- docs/en/docs/tutorial/header-params.md | 16 ++--- .../tutorial/path-operation-configuration.md | 10 +-- .../path-params-numeric-validations.md | 26 +++---- .../tutorial/query-params-str-validations.md | 72 +++++++++---------- docs/en/docs/tutorial/query-params.md | 8 +-- docs/en/docs/tutorial/request-files.md | 28 ++++---- .../docs/tutorial/request-forms-and-files.md | 8 +-- docs/en/docs/tutorial/request-forms.md | 8 +-- docs/en/docs/tutorial/response-model.md | 28 ++++---- docs/en/docs/tutorial/schema-extra-example.md | 18 ++--- docs/en/docs/tutorial/security/first-steps.md | 12 ++-- .../tutorial/security/get-current-user.md | 24 +++---- docs/en/docs/tutorial/security/oauth2-jwt.md | 16 ++--- .../docs/tutorial/security/simple-oauth2.md | 20 +++--- docs/en/docs/tutorial/sql-databases.md | 20 +++--- docs/en/docs/tutorial/testing.md | 4 +- docs/es/docs/features.md | 2 +- docs/es/docs/index.md | 6 +- docs/fr/docs/features.md | 2 +- docs/fr/docs/index.md | 6 +- docs/ja/docs/features.md | 2 +- docs/ja/docs/index.md | 2 +- docs/ko/docs/index.md | 2 +- docs/pl/docs/features.md | 2 +- docs/pl/docs/index.md | 4 +- docs/pt/docs/features.md | 2 +- docs/pt/docs/index.md | 6 +- docs/pt/docs/tutorial/body-multiple-params.md | 10 +-- docs/pt/docs/tutorial/encoder.md | 2 +- docs/pt/docs/tutorial/extra-models.md | 10 +-- docs/pt/docs/tutorial/header-params.md | 8 +-- .../tutorial/path-operation-configuration.md | 10 +-- .../path-params-numeric-validations.md | 4 +- docs/pt/docs/tutorial/query-params.md | 8 +-- docs/ru/docs/features.md | 2 +- docs/ru/docs/index.md | 6 +- docs/ru/docs/tutorial/background-tasks.md | 2 +- docs/ru/docs/tutorial/body-fields.md | 4 +- docs/ru/docs/tutorial/body-multiple-params.md | 18 ++--- docs/ru/docs/tutorial/body-nested-models.md | 20 +++--- docs/ru/docs/tutorial/cookie-params.md | 4 +- .../dependencies/global-dependencies.md | 4 +- docs/ru/docs/tutorial/extra-data-types.md | 4 +- docs/ru/docs/tutorial/extra-models.md | 10 +-- docs/ru/docs/tutorial/header-params.md | 16 ++--- .../tutorial/path-operation-configuration.md | 10 +-- .../path-params-numeric-validations.md | 26 +++---- .../tutorial/query-params-str-validations.md | 72 +++++++++---------- docs/ru/docs/tutorial/query-params.md | 8 +-- docs/ru/docs/tutorial/request-forms.md | 8 +-- docs/ru/docs/tutorial/response-model.md | 28 ++++---- docs/ru/docs/tutorial/schema-extra-example.md | 12 ++-- docs/ru/docs/tutorial/testing.md | 4 +- docs/tr/docs/features.md | 2 +- docs/tr/docs/index.md | 6 +- docs/uk/docs/python-types.md | 22 +++--- docs/uk/docs/tutorial/body.md | 12 ++-- docs/uk/docs/tutorial/cookie-params.md | 8 +-- docs/uk/docs/tutorial/encoder.md | 2 +- docs/uk/docs/tutorial/extra-data-types.md | 8 +-- docs/vi/docs/features.md | 2 +- docs/vi/docs/index.md | 6 +- docs/vi/docs/python-types.md | 18 ++--- docs/yo/docs/index.md | 6 +- docs/zh/docs/advanced/generate-clients.md | 6 +- docs/zh/docs/advanced/settings.md | 12 ++-- docs/zh/docs/advanced/websockets.md | 6 +- docs/zh/docs/index.md | 6 +- docs/zh/docs/tutorial/background-tasks.md | 4 +- docs/zh/docs/tutorial/body-fields.md | 8 +-- docs/zh/docs/tutorial/body-multiple-params.md | 18 ++--- docs/zh/docs/tutorial/body-nested-models.md | 20 +++--- docs/zh/docs/tutorial/body.md | 12 ++-- docs/zh/docs/tutorial/cookie-params.md | 8 +-- .../dependencies/classes-as-dependencies.md | 4 +- docs/zh/docs/tutorial/encoder.md | 2 +- docs/zh/docs/tutorial/extra-data-types.md | 8 +-- docs/zh/docs/tutorial/extra-models.md | 10 +-- docs/zh/docs/tutorial/header-params.md | 16 ++--- .../path-params-numeric-validations.md | 10 +-- .../tutorial/query-params-str-validations.md | 2 +- docs/zh/docs/tutorial/request-files.md | 6 +- docs/zh/docs/tutorial/response-model.md | 8 +-- docs/zh/docs/tutorial/schema-extra-example.md | 8 +-- docs/zh/docs/tutorial/security/first-steps.md | 4 +- docs/zh/docs/tutorial/sql-databases.md | 20 +++--- docs/zh/docs/tutorial/testing.md | 4 +- pyproject.toml | 3 +- 120 files changed, 657 insertions(+), 658 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b84c5bf17..5ab0603b9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.10" # Issue ref: https://github.com/actions/setup-python/issues/436 # cache: "pip" cache-dependency-path: pyproject.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ebc64a14..769b52021 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: diff --git a/README.md b/README.md index 3f125e60e..aeb29b587 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features are: @@ -120,7 +120,7 @@ If you are building a CLI app to be ## Requirements -Python 3.7+ +Python 3.8+ FastAPI stands on the shoulders of giants: @@ -336,7 +336,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.7+**. +Just standard **Python 3.8+**. For example, for an `int`: diff --git a/docs/de/docs/features.md b/docs/de/docs/features.md index f281afd1e..64fa8092d 100644 --- a/docs/de/docs/features.md +++ b/docs/de/docs/features.md @@ -25,7 +25,7 @@ Mit einer interaktiven API-Dokumentation und explorativen webbasierten Benutzers ### Nur modernes Python -Alles basiert auf **Python 3.6 Typ**-Deklarationen (dank Pydantic). Es muss keine neue Syntax gelernt werden, nur standardisiertes modernes Python. +Alles basiert auf **Python 3.8 Typ**-Deklarationen (dank Pydantic). Es muss keine neue Syntax gelernt werden, nur standardisiertes modernes Python. diff --git a/docs/en/docs/advanced/additional-status-codes.md b/docs/en/docs/advanced/additional-status-codes.md index 416444d3b..0ce275343 100644 --- a/docs/en/docs/advanced/additional-status-codes.md +++ b/docs/en/docs/advanced/additional-status-codes.md @@ -26,7 +26,7 @@ To achieve that, import `JSONResponse`, and return your content there directly, {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 26" {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} @@ -41,7 +41,7 @@ To achieve that, import `JSONResponse`, and return your content there directly, {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/advanced-dependencies.md b/docs/en/docs/advanced/advanced-dependencies.md index 402c5d755..0cffab56d 100644 --- a/docs/en/docs/advanced/advanced-dependencies.md +++ b/docs/en/docs/advanced/advanced-dependencies.md @@ -24,13 +24,13 @@ To do that, we declare a method `__call__`: {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/dependencies/tutorial011_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -51,13 +51,13 @@ And now, we can use `__init__` to declare the parameters of the instance that we {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/dependencies/tutorial011_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -78,13 +78,13 @@ We could create an instance of this class with: {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17" {!> ../../../docs_src/dependencies/tutorial011_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -113,13 +113,13 @@ checker(q="somequery") {!> ../../../docs_src/dependencies/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/dependencies/tutorial011_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index f439ed93a..07a8f039f 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -35,7 +35,7 @@ Let's start with a simple FastAPI application: {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11 14-15 18 19 23" {!> ../../../docs_src/generate_clients/tutorial001.py!} @@ -147,7 +147,7 @@ For example, you could have a section for **items** and another section for **us {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23 28 36" {!> ../../../docs_src/generate_clients/tutorial002.py!} @@ -204,7 +204,7 @@ You can then pass that custom function to **FastAPI** as the `generate_unique_id {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8-9 12" {!> ../../../docs_src/generate_clients/tutorial003.py!} diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 8177a4b28..6f9002f60 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -26,13 +26,13 @@ Then, when you type that username and password, the browser sends them in the he {!> ../../../docs_src/security/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="2 7 11" {!> ../../../docs_src/security/tutorial006_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -65,13 +65,13 @@ Then we can use `secrets.compare_digest()` to ensure that `credentials.username` {!> ../../../docs_src/security/tutorial007_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 12-24" {!> ../../../docs_src/security/tutorial007_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -148,13 +148,13 @@ After detecting that the credentials are incorrect, return an `HTTPException` wi {!> ../../../docs_src/security/tutorial007_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="26-30" {!> ../../../docs_src/security/tutorial007_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index 41cd61683..304a46090 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -68,7 +68,7 @@ First, let's quickly see the parts that change from the examples in the main **T {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="2 4 8 12 47 65 106 108-116 122-125 129-135 140 156" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -92,7 +92,7 @@ First, let's quickly see the parts that change from the examples in the main **T {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -121,7 +121,7 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="63-66" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -146,7 +146,7 @@ The `scopes` parameter receives a `dict` with each scope as a key and the descri {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -188,7 +188,7 @@ And we return the scopes as part of the JWT token. {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="156" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -212,7 +212,7 @@ And we return the scopes as part of the JWT token. {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -254,7 +254,7 @@ In this case, it requires the scope `me` (it could require more than one scope). {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 140 171" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -278,7 +278,7 @@ In this case, it requires the scope `me` (it could require more than one scope). {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -320,7 +320,7 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8 106" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -344,7 +344,7 @@ This `SecurityScopes` class is similar to `Request` (`Request` was used to get t {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -377,7 +377,7 @@ In this exception, we include the scopes required (if any) as a string separated {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="106 108-116" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -401,7 +401,7 @@ In this exception, we include the scopes required (if any) as a string separated {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -436,7 +436,7 @@ We also verify that we have a user with that username, and if not, we raise that {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="47 117-128" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -460,7 +460,7 @@ We also verify that we have a user with that username, and if not, we raise that {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -487,7 +487,7 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these {!> ../../../docs_src/security/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="129-135" {!> ../../../docs_src/security/tutorial005_an.py!} @@ -511,7 +511,7 @@ For this, we use `security_scopes.scopes`, that contains a `list` with all these {!> ../../../docs_src/security/tutorial005_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 8f6c7da93..d39130777 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -260,13 +260,13 @@ Now we create a dependency that returns a new `config.Settings()`. {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="6 12-13" {!> ../../../docs_src/settings/app02_an/main.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -288,13 +288,13 @@ And then we can require it from the *path operation function* as a dependency an {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 19-21" {!> ../../../docs_src/settings/app02_an/main.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -396,13 +396,13 @@ But as we are using the `@lru_cache()` decorator on top, the `Settings` object w {!> ../../../docs_src/settings/app03_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 11" {!> ../../../docs_src/settings/app03_an/main.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/testing-dependencies.md b/docs/en/docs/advanced/testing-dependencies.md index ee48a735d..57dd87f56 100644 --- a/docs/en/docs/advanced/testing-dependencies.md +++ b/docs/en/docs/advanced/testing-dependencies.md @@ -40,7 +40,7 @@ And then **FastAPI** will call that override instead of the original dependency. {!> ../../../docs_src/dependency_testing/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="29-30 33" {!> ../../../docs_src/dependency_testing/tutorial001_an.py!} @@ -55,7 +55,7 @@ And then **FastAPI** will call that override instead of the original dependency. {!> ../../../docs_src/dependency_testing/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 94cf191d2..49b8fba89 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -124,7 +124,7 @@ They work the same way as for other FastAPI endpoints/*path operations*: {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="69-70 83" {!> ../../../docs_src/websockets/tutorial002_an.py!} @@ -139,7 +139,7 @@ They work the same way as for other FastAPI endpoints/*path operations*: {!> ../../../docs_src/websockets/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -191,7 +191,7 @@ When a WebSocket connection is closed, the `await websocket.receive_text()` will {!> ../../../docs_src/websockets/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="81-83" {!> ../../../docs_src/websockets/tutorial003.py!} diff --git a/docs/en/docs/how-to/separate-openapi-schemas.md b/docs/en/docs/how-to/separate-openapi-schemas.md index d38be3c59..10be1071a 100644 --- a/docs/en/docs/how-to/separate-openapi-schemas.md +++ b/docs/en/docs/how-to/separate-openapi-schemas.md @@ -44,7 +44,7 @@ Let's say you have a Pydantic model with default values, like this one: -=== "Python 3.7+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-9]!} @@ -99,7 +99,7 @@ If you use this model as an input like here: -=== "Python 3.7+" +=== "Python 3.8+" ```Python hl_lines="16" {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py[ln:1-17]!} @@ -142,7 +142,7 @@ But if you use the same model as an output, like here: {!> ../../../docs_src/separate_openapi_schemas/tutorial001_py39.py!} ``` -=== "Python 3.7+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/separate_openapi_schemas/tutorial001.py!} @@ -214,7 +214,7 @@ In that case, you can disable this feature in **FastAPI**, with the parameter `s {!> ../../../docs_src/separate_openapi_schemas/tutorial002_py39.py!} ``` -=== "Python 3.7+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/separate_openapi_schemas/tutorial002.py!} diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index ebd74bc8f..cd3f3e000 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.7+ based on standard Python type hints. +FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.8+ based on standard Python type hints. The key features are: @@ -115,7 +115,7 @@ If you are building a CLI app to be ## Requirements -Python 3.7+ +Python 3.8+ FastAPI stands on the shoulders of giants: @@ -331,7 +331,7 @@ You do that with standard modern Python types. You don't have to learn a new syntax, the methods or classes of a specific library, etc. -Just standard **Python 3.7+**. +Just standard **Python 3.8+**. For example, for an `int`: diff --git a/docs/en/docs/python-types.md b/docs/en/docs/python-types.md index 693613a36..cdd22ea4a 100644 --- a/docs/en/docs/python-types.md +++ b/docs/en/docs/python-types.md @@ -182,7 +182,7 @@ For example, let's define a variable to be a `list` of `str`. {!> ../../../docs_src/python_types/tutorial006_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" From `typing`, import `List` (with a capital `L`): @@ -230,7 +230,7 @@ You would do the same to declare `tuple`s and `set`s: {!> ../../../docs_src/python_types/tutorial007_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial007.py!} @@ -255,7 +255,7 @@ The second type parameter is for the values of the `dict`: {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008.py!} @@ -281,7 +281,7 @@ In Python 3.10 there's also a **new syntax** where you can put the possible type {!> ../../../docs_src/python_types/tutorial008b_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008b.py!} @@ -311,13 +311,13 @@ This also means that in Python 3.10, you can use `Something | None`: {!> ../../../docs_src/python_types/tutorial009_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.6+ alternative" +=== "Python 3.8+ alternative" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009b.py!} @@ -375,10 +375,10 @@ These types that take type parameters in square brackets are called **Generic ty * `set` * `dict` - And the same as with Python 3.6, from the `typing` module: + And the same as with Python 3.8, from the `typing` module: * `Union` - * `Optional` (the same as with Python 3.6) + * `Optional` (the same as with Python 3.8) * ...and others. In Python 3.10, as an alternative to using the generics `Union` and `Optional`, you can use the vertical bar (`|`) to declare unions of types, that's a lot better and simpler. @@ -392,13 +392,13 @@ These types that take type parameters in square brackets are called **Generic ty * `set` * `dict` - And the same as with Python 3.6, from the `typing` module: + And the same as with Python 3.8, from the `typing` module: * `Union` * `Optional` * ...and others. -=== "Python 3.6+" +=== "Python 3.8+" * `List` * `Tuple` @@ -458,7 +458,7 @@ An example from the official Pydantic docs: {!> ../../../docs_src/python_types/tutorial011_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/python_types/tutorial011.py!} @@ -486,7 +486,7 @@ Python also has a feature that allows putting **additional metadata** in these t {!> ../../../docs_src/python_types/tutorial013_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" In versions below Python 3.9, you import `Annotated` from `typing_extensions`. diff --git a/docs/en/docs/tutorial/background-tasks.md b/docs/en/docs/tutorial/background-tasks.md index 178297192..bc8e2af6a 100644 --- a/docs/en/docs/tutorial/background-tasks.md +++ b/docs/en/docs/tutorial/background-tasks.md @@ -69,7 +69,7 @@ Using `BackgroundTasks` also works with the dependency injection system, you can {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14 16 23 26" {!> ../../../docs_src/background_tasks/tutorial002_an.py!} @@ -84,7 +84,7 @@ Using `BackgroundTasks` also works with the dependency injection system, you can {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 26d26475f..1cf7e50e0 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -118,13 +118,13 @@ We will now use a simple dependency to read a custom `X-Token` header: {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 5-7" {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/body-fields.md b/docs/en/docs/tutorial/body-fields.md index 8966032ff..55e67fdd6 100644 --- a/docs/en/docs/tutorial/body-fields.md +++ b/docs/en/docs/tutorial/body-fields.md @@ -18,7 +18,7 @@ First, you have to import it: {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4" {!> ../../../docs_src/body_fields/tutorial001_an.py!} @@ -33,7 +33,7 @@ First, you have to import it: {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -61,7 +61,7 @@ You can then use `Field` with model attributes: {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12-15" {!> ../../../docs_src/body_fields/tutorial001_an.py!} @@ -76,7 +76,7 @@ You can then use `Field` with model attributes: {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/body-multiple-params.md b/docs/en/docs/tutorial/body-multiple-params.md index b214092c9..ebef8eeaa 100644 --- a/docs/en/docs/tutorial/body-multiple-params.md +++ b/docs/en/docs/tutorial/body-multiple-params.md @@ -20,7 +20,7 @@ And you can also declare body parameters as optional, by setting the default to {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-21" {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} @@ -35,7 +35,7 @@ And you can also declare body parameters as optional, by setting the default to {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -68,7 +68,7 @@ But you can also declare multiple body parameters, e.g. `item` and `user`: {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial002.py!} @@ -123,7 +123,7 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} @@ -138,7 +138,7 @@ But you can instruct **FastAPI** to treat it as another body key using `Body`: {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -197,7 +197,7 @@ For example: {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="28" {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} @@ -212,7 +212,7 @@ For example: {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -250,7 +250,7 @@ as in: {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} @@ -265,7 +265,7 @@ as in: {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index ffa0c0d0e..3a1052397 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -12,7 +12,7 @@ You can define an attribute to be a subtype. For example, a Python `list`: {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial001.py!} @@ -73,7 +73,7 @@ So, in our example, we can make `tags` be specifically a "list of strings": {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial002.py!} @@ -99,7 +99,7 @@ Then we can declare `tags` as a set of strings: {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 14" {!> ../../../docs_src/body_nested_models/tutorial003.py!} @@ -137,7 +137,7 @@ For example, we can define an `Image` model: {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -159,7 +159,7 @@ And then we can use it as the type of an attribute: {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -208,7 +208,7 @@ For example, as in the `Image` model we have a `url` field, we can declare it to {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 10" {!> ../../../docs_src/body_nested_models/tutorial005.py!} @@ -232,7 +232,7 @@ You can also use Pydantic models as subtypes of `list`, `set`, etc: {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial006.py!} @@ -283,7 +283,7 @@ You can define arbitrarily deeply nested models: {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 14 20 23 27" {!> ../../../docs_src/body_nested_models/tutorial007.py!} @@ -314,7 +314,7 @@ as in: {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15" {!> ../../../docs_src/body_nested_models/tutorial008.py!} @@ -354,7 +354,7 @@ In this case, you would accept any `dict` as long as it has `int` keys with `flo {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/body_nested_models/tutorial009.py!} diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index a32948db1..3341f2d5d 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -18,7 +18,7 @@ You can use the `jsonable_encoder` to convert the input data to data that can be {!> ../../../docs_src/body_updates/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="30-35" {!> ../../../docs_src/body_updates/tutorial001.py!} @@ -79,7 +79,7 @@ Then you can use this to generate a `dict` with only the data that was set (sent {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="34" {!> ../../../docs_src/body_updates/tutorial002.py!} @@ -103,7 +103,7 @@ Like `stored_item_model.copy(update=update_data)`: {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="35" {!> ../../../docs_src/body_updates/tutorial002.py!} @@ -136,7 +136,7 @@ In summary, to apply partial updates you would: {!> ../../../docs_src/body_updates/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="30-37" {!> ../../../docs_src/body_updates/tutorial002.py!} diff --git a/docs/en/docs/tutorial/body.md b/docs/en/docs/tutorial/body.md index 172b91fdf..67ba48f1e 100644 --- a/docs/en/docs/tutorial/body.md +++ b/docs/en/docs/tutorial/body.md @@ -25,7 +25,7 @@ First, you need to import `BaseModel` from `pydantic`: {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4" {!> ../../../docs_src/body/tutorial001.py!} @@ -43,7 +43,7 @@ Use standard Python types for all the attributes: {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="7-11" {!> ../../../docs_src/body/tutorial001.py!} @@ -81,7 +81,7 @@ To add it to your *path operation*, declare it the same way you declared path an {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial001.py!} @@ -155,7 +155,7 @@ Inside of the function, you can access all the attributes of the model object di {!> ../../../docs_src/body/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/body/tutorial002.py!} @@ -173,7 +173,7 @@ You can declare path parameters and request body at the same time. {!> ../../../docs_src/body/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17-18" {!> ../../../docs_src/body/tutorial003.py!} @@ -191,7 +191,7 @@ You can also declare **body**, **path** and **query** parameters, all at the sam {!> ../../../docs_src/body/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial004.py!} diff --git a/docs/en/docs/tutorial/cookie-params.md b/docs/en/docs/tutorial/cookie-params.md index 111e93458..3436a7df3 100644 --- a/docs/en/docs/tutorial/cookie-params.md +++ b/docs/en/docs/tutorial/cookie-params.md @@ -18,7 +18,7 @@ First import `Cookie`: {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ First import `Cookie`: {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -60,7 +60,7 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -75,7 +75,7 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md index 498d935fe..842f2adf6 100644 --- a/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/classes-as-dependencies.md @@ -18,7 +18,7 @@ In the previous example, we were returning a `dict` from our dependency ("depend {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/dependencies/tutorial001_an.py!} @@ -33,7 +33,7 @@ In the previous example, we were returning a `dict` from our dependency ("depend {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -115,7 +115,7 @@ Then, we can change the dependency "dependable" `common_parameters` from above t {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12-16" {!> ../../../docs_src/dependencies/tutorial002_an.py!} @@ -130,7 +130,7 @@ Then, we can change the dependency "dependable" `common_parameters` from above t {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -153,7 +153,7 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="13" {!> ../../../docs_src/dependencies/tutorial002_an.py!} @@ -168,7 +168,7 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -191,7 +191,7 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/dependencies/tutorial001_an.py!} @@ -206,7 +206,7 @@ Pay attention to the `__init__` method used to create the instance of the class: {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -241,7 +241,7 @@ Now you can declare your dependency using this class. {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/dependencies/tutorial002_an.py!} @@ -256,7 +256,7 @@ Now you can declare your dependency using this class. {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -271,7 +271,7 @@ Now you can declare your dependency using this class. Notice how we write `CommonQueryParams` twice in the above code: -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -280,7 +280,7 @@ Notice how we write `CommonQueryParams` twice in the above code: commons: CommonQueryParams = Depends(CommonQueryParams) ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -300,13 +300,13 @@ From it is that FastAPI will extract the declared parameters and that is what Fa In this case, the first `CommonQueryParams`, in: -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[CommonQueryParams, ... ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -319,13 +319,13 @@ In this case, the first `CommonQueryParams`, in: You could actually write just: -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[Any, Depends(CommonQueryParams)] ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -348,7 +348,7 @@ You could actually write just: {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/dependencies/tutorial003_an.py!} @@ -363,7 +363,7 @@ You could actually write just: {!> ../../../docs_src/dependencies/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -380,7 +380,7 @@ But declaring the type is encouraged as that way your editor will know what will But you see that we are having some code repetition here, writing `CommonQueryParams` twice: -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -389,7 +389,7 @@ But you see that we are having some code repetition here, writing `CommonQueryPa commons: CommonQueryParams = Depends(CommonQueryParams) ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] @@ -401,13 +401,13 @@ For those specific cases, you can do the following: Instead of writing: -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -418,13 +418,13 @@ Instead of writing: ...you write: -=== "Python 3.6+" +=== "Python 3.8+" ```Python commons: Annotated[CommonQueryParams, Depends()] ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -449,7 +449,7 @@ The same example would then look like: {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/dependencies/tutorial004_an.py!} @@ -464,7 +464,7 @@ The same example would then look like: {!> ../../../docs_src/dependencies/tutorial004_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md index ccef5aef4..eaab51d1b 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -20,13 +20,13 @@ It should be a `list` of `Depends()`: {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/dependencies/tutorial006_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -63,13 +63,13 @@ They can declare request requirements (like headers) or other sub-dependencies: {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="7 12" {!> ../../../docs_src/dependencies/tutorial006_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -88,13 +88,13 @@ These dependencies can `raise` exceptions, the same as normal dependencies: {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 14" {!> ../../../docs_src/dependencies/tutorial006_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -115,13 +115,13 @@ So, you can re-use a normal dependency (that returns a value) you already use so {!> ../../../docs_src/dependencies/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10 15" {!> ../../../docs_src/dependencies/tutorial006_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 8a5422ac8..fe18f1f1d 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -72,13 +72,13 @@ For example, `dependency_c` can have a dependency on `dependency_b`, and `depend {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="5 13 21" {!> ../../../docs_src/dependencies/tutorial008_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -99,13 +99,13 @@ And, in turn, `dependency_b` needs the value from `dependency_a` (here named `de {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17-18 25-26" {!> ../../../docs_src/dependencies/tutorial008_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/dependencies/global-dependencies.md b/docs/en/docs/tutorial/dependencies/global-dependencies.md index 0989b31d4..0dcf73176 100644 --- a/docs/en/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/global-dependencies.md @@ -12,13 +12,13 @@ In that case, they will be applied to all the *path operations* in the applicati {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="16" {!> ../../../docs_src/dependencies/tutorial012_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index f6f4bced0..bc98cb26e 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -43,7 +43,7 @@ It is just a function that can take all the same parameters that a *path operati {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-12" {!> ../../../docs_src/dependencies/tutorial001_an.py!} @@ -58,7 +58,7 @@ It is just a function that can take all the same parameters that a *path operati {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -106,7 +106,7 @@ And then it just returns a `dict` containing those values. {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/dependencies/tutorial001_an.py!} @@ -121,7 +121,7 @@ And then it just returns a `dict` containing those values. {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -146,7 +146,7 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="16 21" {!> ../../../docs_src/dependencies/tutorial001_an.py!} @@ -161,7 +161,7 @@ The same way you use `Body`, `Query`, etc. with your *path operation function* p {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -231,7 +231,7 @@ But because we are using `Annotated`, we can store that `Annotated` value in a v {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15 19 24" {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} diff --git a/docs/en/docs/tutorial/dependencies/sub-dependencies.md b/docs/en/docs/tutorial/dependencies/sub-dependencies.md index b50de1a46..1cb469a80 100644 --- a/docs/en/docs/tutorial/dependencies/sub-dependencies.md +++ b/docs/en/docs/tutorial/dependencies/sub-dependencies.md @@ -22,7 +22,7 @@ You could create a first dependency ("dependable") like: {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-10" {!> ../../../docs_src/dependencies/tutorial005_an.py!} @@ -37,7 +37,7 @@ You could create a first dependency ("dependable") like: {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -66,7 +66,7 @@ Then you can create another dependency function (a "dependable") that at the sam {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/dependencies/tutorial005_an.py!} @@ -81,7 +81,7 @@ Then you can create another dependency function (a "dependable") that at the sam {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -113,7 +113,7 @@ Then we can use the dependency with: {!> ../../../docs_src/dependencies/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/dependencies/tutorial005_an.py!} @@ -128,7 +128,7 @@ Then we can use the dependency with: {!> ../../../docs_src/dependencies/tutorial005_py310.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -161,14 +161,14 @@ And it will save the returned value in a ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="5 22" {!> ../../../docs_src/encoder/tutorial001.py!} diff --git a/docs/en/docs/tutorial/extra-data-types.md b/docs/en/docs/tutorial/extra-data-types.md index b34ccd26f..fd7a99af3 100644 --- a/docs/en/docs/tutorial/extra-data-types.md +++ b/docs/en/docs/tutorial/extra-data-types.md @@ -67,7 +67,7 @@ Here's an example *path operation* with parameters using some of the above types {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 3 13-17" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -82,7 +82,7 @@ Here's an example *path operation* with parameters using some of the above types {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -105,7 +105,7 @@ Note that the parameters inside the function have their natural data type, and y {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-20" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -120,7 +120,7 @@ Note that the parameters inside the function have their natural data type, and y {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index e91e879e4..590d095bd 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -23,7 +23,7 @@ Here's a general idea of how the models could look like with their password fiel {!> ../../../docs_src/extra_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" {!> ../../../docs_src/extra_models/tutorial001.py!} @@ -164,7 +164,7 @@ That way, we can declare just the differences between the models (with plaintext {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 15-16 19-20 23-24" {!> ../../../docs_src/extra_models/tutorial002.py!} @@ -187,7 +187,7 @@ To do that, use the standard Python type hint ../../../docs_src/extra_models/tutorial003.py!} @@ -219,7 +219,7 @@ For that, use the standard Python `typing.List` (or just `list` in Python 3.9 an {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 20" {!> ../../../docs_src/extra_models/tutorial004.py!} @@ -239,7 +239,7 @@ In this case, you can use `typing.Dict` (or just `dict` in Python 3.9 and above) {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 8" {!> ../../../docs_src/extra_models/tutorial005.py!} diff --git a/docs/en/docs/tutorial/header-params.md b/docs/en/docs/tutorial/header-params.md index 9e928cdc6..bbba90998 100644 --- a/docs/en/docs/tutorial/header-params.md +++ b/docs/en/docs/tutorial/header-params.md @@ -18,7 +18,7 @@ First import `Header`: {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ First import `Header`: {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -60,7 +60,7 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -75,7 +75,7 @@ The first value is the default value, you can pass all the extra validation or a {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -120,7 +120,7 @@ If for some reason you need to disable automatic conversion of underscores to hy {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/header_params/tutorial002_an.py!} @@ -135,7 +135,7 @@ If for some reason you need to disable automatic conversion of underscores to hy {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -169,7 +169,7 @@ For example, to declare a header of `X-Token` that can appear more than once, yo {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial003_an.py!} @@ -193,7 +193,7 @@ For example, to declare a header of `X-Token` that can appear more than once, yo {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/path-operation-configuration.md b/docs/en/docs/tutorial/path-operation-configuration.md index 7d4d4bcca..babf85acb 100644 --- a/docs/en/docs/tutorial/path-operation-configuration.md +++ b/docs/en/docs/tutorial/path-operation-configuration.md @@ -25,7 +25,7 @@ But if you don't remember what each number code is for, you can use the shortcut {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3 17" {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} @@ -54,7 +54,7 @@ You can add tags to your *path operation*, pass the parameter `tags` with a `lis {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 22 27" {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} @@ -92,7 +92,7 @@ You can add a `summary` and `description`: {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20-21" {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} @@ -116,7 +116,7 @@ You can write ../../../docs_src/path_operation_configuration/tutorial004.py!} @@ -142,7 +142,7 @@ You can specify the response description with the parameter `response_descriptio {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 9255875d6..57ad20b13 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -18,7 +18,7 @@ First, import `Path` from `fastapi`, and import `Annotated`: {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3-4" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -33,7 +33,7 @@ First, import `Path` from `fastapi`, and import `Annotated`: {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -67,7 +67,7 @@ For example, to declare a `title` metadata value for the path parameter `item_id {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -82,7 +82,7 @@ For example, to declare a `title` metadata value for the path parameter `item_id {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -117,7 +117,7 @@ It doesn't matter for **FastAPI**. It will detect the parameters by their names, So, you can declare your function as: -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -134,7 +134,7 @@ But have in mind that if you use `Annotated`, you won't have this problem, it wo {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} @@ -174,7 +174,7 @@ Have in mind that if you use `Annotated`, as you are not using function paramete {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} @@ -192,13 +192,13 @@ Here, with `ge=1`, `item_id` will need to be an integer number "`g`reater than o {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -220,13 +220,13 @@ The same applies for: {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -251,13 +251,13 @@ And the same for lt. {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 5d1c08add..0b2cf02a8 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -10,7 +10,7 @@ Let's take this application as example: {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} @@ -42,7 +42,7 @@ To achieve that, first import: {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" In versions of Python below Python 3.9 you import `Annotated` from `typing_extensions`. @@ -73,7 +73,7 @@ We had this type annotation: q: str | None = None ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python q: Union[str, None] = None @@ -87,7 +87,7 @@ What we will do is wrap that with `Annotated`, so it becomes: q: Annotated[str | None] = None ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python q: Annotated[Union[str, None]] = None @@ -107,7 +107,7 @@ Now that we have this `Annotated` where we can put more metadata, add `Query` to {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} @@ -138,7 +138,7 @@ This is how you would use `Query()` as the default value of your function parame {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} @@ -251,7 +251,7 @@ You can also add a parameter `min_length`: {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} @@ -266,7 +266,7 @@ You can also add a parameter `min_length`: {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -291,7 +291,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial004_an.py!} @@ -306,7 +306,7 @@ You can define a ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -405,13 +405,13 @@ So, when you need to declare a value as required while using `Query`, you can si {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -435,13 +435,13 @@ There's an alternative way to explicitly declare that a value is required. You c {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -475,7 +475,7 @@ To do that, you can declare that `None` is a valid type but still use `...` as t {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} @@ -490,7 +490,7 @@ To do that, you can declare that `None` is a valid type but still use `...` as t {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -512,13 +512,13 @@ If you feel uncomfortable using `...`, you can also import and use `Required` fr {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="2 9" {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -548,7 +548,7 @@ For example, to declare a query parameter `q` that can appear multiple times in {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} @@ -572,7 +572,7 @@ For example, to declare a query parameter `q` that can appear multiple times in {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -617,7 +617,7 @@ And you can also define a default `list` of values if none are provided: {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} @@ -632,7 +632,7 @@ And you can also define a default `list` of values if none are provided: {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -668,13 +668,13 @@ You can also use `list` directly instead of `List[str]` (or `list[str]` in Pytho {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -713,7 +713,7 @@ You can add a `title`: {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} @@ -728,7 +728,7 @@ You can add a `title`: {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -751,7 +751,7 @@ And a `description`: {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15" {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} @@ -766,7 +766,7 @@ And a `description`: {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -805,7 +805,7 @@ Then you can declare an `alias`, and that alias is what will be used to find the {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} @@ -820,7 +820,7 @@ Then you can declare an `alias`, and that alias is what will be used to find the {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -849,7 +849,7 @@ Then pass the parameter `deprecated=True` to `Query`: {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} @@ -864,7 +864,7 @@ Then pass the parameter `deprecated=True` to `Query`: {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -893,7 +893,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} @@ -908,7 +908,7 @@ To exclude a query parameter from the generated OpenAPI schema (and thus, from t {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 0b74b10f8..988ff4bf1 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -69,7 +69,7 @@ The same way, you can declare optional query parameters, by setting their defaul {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial002.py!} @@ -90,7 +90,7 @@ You can also declare `bool` types, and they will be converted: {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial003.py!} @@ -143,7 +143,7 @@ They will be detected by name: {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8 10" {!> ../../../docs_src/query_params/tutorial004.py!} @@ -209,7 +209,7 @@ And of course, you can define some parameters as required, some as having a defa {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params/tutorial006.py!} diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index 1fe1e7a33..c85a68ed6 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -19,13 +19,13 @@ Import `File` and `UploadFile` from `fastapi`: {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1" {!> ../../../docs_src/request_files/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -44,13 +44,13 @@ Create file parameters the same way you would for `Body` or `Form`: {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/request_files/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -85,13 +85,13 @@ Define a file parameter with a type of `UploadFile`: {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="13" {!> ../../../docs_src/request_files/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -181,7 +181,7 @@ You can make a file optional by using standard type annotations and setting a de {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10 18" {!> ../../../docs_src/request_files/tutorial001_02_an.py!} @@ -196,7 +196,7 @@ You can make a file optional by using standard type annotations and setting a de {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -215,13 +215,13 @@ You can also use `File()` with `UploadFile`, for example, to set additional meta {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8 14" {!> ../../../docs_src/request_files/tutorial001_03_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -244,7 +244,7 @@ To use that, declare a list of `bytes` or `UploadFile`: {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11 16" {!> ../../../docs_src/request_files/tutorial002_an.py!} @@ -259,7 +259,7 @@ To use that, declare a list of `bytes` or `UploadFile`: {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -285,7 +285,7 @@ And the same way as before, you can use `File()` to set additional parameters, e {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12 19-21" {!> ../../../docs_src/request_files/tutorial003_an.py!} @@ -300,7 +300,7 @@ And the same way as before, you can use `File()` to set additional parameters, e {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/request-forms-and-files.md b/docs/en/docs/tutorial/request-forms-and-files.md index 1818946c4..a58291dc8 100644 --- a/docs/en/docs/tutorial/request-forms-and-files.md +++ b/docs/en/docs/tutorial/request-forms-and-files.md @@ -15,13 +15,13 @@ You can define files and form fields at the same time using `File` and `Form`. {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1" {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -40,13 +40,13 @@ Create file and form parameters the same way you would for `Body` or `Query`: {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11" {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/request-forms.md b/docs/en/docs/tutorial/request-forms.md index 5d441a614..0e8ac5f4f 100644 --- a/docs/en/docs/tutorial/request-forms.md +++ b/docs/en/docs/tutorial/request-forms.md @@ -17,13 +17,13 @@ Import `Form` from `fastapi`: {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1" {!> ../../../docs_src/request_forms/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -42,13 +42,13 @@ Create form parameters the same way you would for `Body` or `Query`: {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/request_forms/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index 2181cfb5a..d6d3d61cb 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -16,7 +16,7 @@ You can use **type annotations** the same way you would for input data in functi {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18 23" {!> ../../../docs_src/response_model/tutorial001_01.py!} @@ -65,7 +65,7 @@ You can use the `response_model` parameter in any of the *path operations*: {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001.py!} @@ -101,7 +101,7 @@ Here we are declaring a `UserIn` model, it will contain a plaintext password: {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11" {!> ../../../docs_src/response_model/tutorial002.py!} @@ -121,7 +121,7 @@ And we are using this model to declare our input and the same model to declare o {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/response_model/tutorial002.py!} @@ -146,7 +146,7 @@ We can instead create an input model with the plaintext password and an output m {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -160,7 +160,7 @@ Here, even though our *path operation function* is returning the same input user {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -174,7 +174,7 @@ Here, even though our *path operation function* is returning the same input user {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -208,7 +208,7 @@ And in those cases, we can use classes and inheritance to take advantage of func {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-13 15-16 20" {!> ../../../docs_src/response_model/tutorial003_01.py!} @@ -284,7 +284,7 @@ The same would happen if you had something like a ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="83-86" {!> ../../../docs_src/security/tutorial003_an.py!} @@ -209,7 +209,7 @@ So, the thief won't be able to try to use those same passwords in another system {!> ../../../docs_src/security/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -264,7 +264,7 @@ For this simple example, we are going to just be completely insecure and return {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="88" {!> ../../../docs_src/security/tutorial003_an.py!} @@ -279,7 +279,7 @@ For this simple example, we are going to just be completely insecure and return {!> ../../../docs_src/security/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. @@ -321,7 +321,7 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a {!> ../../../docs_src/security/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="59-67 70-75 95" {!> ../../../docs_src/security/tutorial003_an.py!} @@ -336,7 +336,7 @@ So, in our endpoint, we will only get a user if the user exists, was correctly a {!> ../../../docs_src/security/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 6e0e5dc06..010244bbf 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -281,7 +281,7 @@ But for security, the `password` won't be in other Pydantic *models*, for exampl {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -325,7 +325,7 @@ Not only the IDs of those items, but all the data that we defined in the Pydanti {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -354,7 +354,7 @@ In the `Config` class, set the attribute `orm_mode = True`. {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -494,7 +494,7 @@ In a very simplistic way create the database tables: {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -528,7 +528,7 @@ Our dependency will create a new SQLAlchemy `SessionLocal` that will be used in {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-20" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -553,7 +553,7 @@ This will then give us better editor support inside the *path operation function {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24 32 38 47 53" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -574,7 +574,7 @@ Now, finally, here's the standard **FastAPI** *path operations* code. {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -673,7 +673,7 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -693,7 +693,7 @@ For example, in a background task worker with ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -752,7 +752,7 @@ The middleware we'll add (just a function) will create a new SQLAlchemy `Session {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14-22" {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} diff --git a/docs/en/docs/tutorial/testing.md b/docs/en/docs/tutorial/testing.md index ec133a4d0..3f8dd69a1 100644 --- a/docs/en/docs/tutorial/testing.md +++ b/docs/en/docs/tutorial/testing.md @@ -122,7 +122,7 @@ Both *path operations* require an `X-Token` header. {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/app_testing/app_b_an/main.py!} @@ -137,7 +137,7 @@ Both *path operations* require an `X-Token` header. {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index 5d6b6509a..d05c4f73e 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -25,7 +25,7 @@ Documentación interactiva de la API e interfaces web de exploración. Hay múlt ### Simplemente Python moderno -Todo está basado en las declaraciones de tipo de **Python 3.6** estándar (gracias a Pydantic). No necesitas aprender una sintáxis nueva, solo Python moderno. +Todo está basado en las declaraciones de tipo de **Python 3.8** estándar (gracias a Pydantic). No necesitas aprender una sintáxis nueva, solo Python moderno. Si necesitas un repaso de 2 minutos de cómo usar los tipos de Python (así no uses FastAPI) prueba el tutorial corto: [Python Types](python-types.md){.internal-link target=_blank}. diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 5b75880c0..30a575577 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -23,7 +23,7 @@ **Código Fuente**: https://github.com/tiangolo/fastapi --- -FastAPI es un web framework moderno y rápido (de alto rendimiento) para construir APIs con Python 3.6+ basado en las anotaciones de tipos estándar de Python. +FastAPI es un web framework moderno y rápido (de alto rendimiento) para construir APIs con Python 3.8+ basado en las anotaciones de tipos estándar de Python. Sus características principales son: @@ -106,7 +106,7 @@ Si estás construyendo un app de CLI< ## Wymagania -Python 3.7+ +Python 3.8+ FastAPI oparty jest na: @@ -321,7 +321,7 @@ Robisz to tak samo jak ze standardowymi typami w Pythonie. Nie musisz sie uczyć żadnej nowej składni, metod lub klas ze specyficznych bibliotek itp. -Po prostu standardowy **Python 3.6+**. +Po prostu standardowy **Python 3.8+**. Na przykład, dla danych typu `int`: diff --git a/docs/pt/docs/features.md b/docs/pt/docs/features.md index bd0db8e76..822992c5b 100644 --- a/docs/pt/docs/features.md +++ b/docs/pt/docs/features.md @@ -25,7 +25,7 @@ Documentação interativa da API e navegação _web_ da interface de usuário. C ### Apenas Python moderno -Tudo é baseado no padrão das declarações de **tipos do Python 3.6** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. +Tudo é baseado no padrão das declarações de **tipos do Python 3.8** (graças ao Pydantic). Nenhuma sintaxe nova para aprender. Apenas o padrão moderno do Python. Se você precisa refrescar a memória rapidamente sobre como usar tipos do Python (mesmo que você não use o FastAPI), confira esse rápido tutorial: [Tipos do Python](python-types.md){.internal-link target=_blank}. diff --git a/docs/pt/docs/index.md b/docs/pt/docs/index.md index 591e7f3d4..d1e64b3b9 100644 --- a/docs/pt/docs/index.md +++ b/docs/pt/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python 3.6 ou superior, baseado nos _type hints_ padrões do Python. +FastAPI é um moderno e rápido (alta performance) _framework web_ para construção de APIs com Python 3.8 ou superior, baseado nos _type hints_ padrões do Python. Os recursos chave são: @@ -100,7 +100,7 @@ Se você estiver construindo uma aplicação ../../../docs_src/body_multiple_params/tutorial001.py!} @@ -44,7 +44,7 @@ Mas você pode também declarar múltiplos parâmetros de corpo, por exemplo, `i {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial002.py!} @@ -87,7 +87,7 @@ Se você declará-lo como é, porque é um valor singular, o **FastAPI** assumir Mas você pode instruir o **FastAPI** para tratá-lo como outra chave do corpo usando `Body`: -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial003.py!} @@ -143,7 +143,7 @@ Por exemplo: {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="27" {!> ../../../docs_src/body_multiple_params/tutorial004.py!} @@ -172,7 +172,7 @@ como em: {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17" {!> ../../../docs_src/body_multiple_params/tutorial005.py!} diff --git a/docs/pt/docs/tutorial/encoder.md b/docs/pt/docs/tutorial/encoder.md index bb4483fdc..b9bfbf63b 100644 --- a/docs/pt/docs/tutorial/encoder.md +++ b/docs/pt/docs/tutorial/encoder.md @@ -26,7 +26,7 @@ A função recebe um objeto, como um modelo Pydantic e retorna uma versão compa {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="5 22" {!> ../../../docs_src/encoder/tutorial001.py!} diff --git a/docs/pt/docs/tutorial/extra-models.md b/docs/pt/docs/tutorial/extra-models.md index dd5407eb2..1343a3ae4 100644 --- a/docs/pt/docs/tutorial/extra-models.md +++ b/docs/pt/docs/tutorial/extra-models.md @@ -17,7 +17,7 @@ Isso é especialmente o caso para modelos de usuários, porque: Aqui está uma ideia geral de como os modelos poderiam parecer com seus campos de senha e os lugares onde são usados: -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" {!> ../../../docs_src/extra_models/tutorial001.py!} @@ -158,7 +158,7 @@ Toda conversão de dados, validação, documentação, etc. ainda funcionará no Dessa forma, podemos declarar apenas as diferenças entre os modelos (com `password` em texto claro, com `hashed_password` e sem senha): -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="9 15-16 19-20 23-24" {!> ../../../docs_src/extra_models/tutorial002.py!} @@ -181,7 +181,7 @@ Para fazer isso, use a dica de tipo padrão do Python `Union`, inclua o tipo mais específico primeiro, seguido pelo tipo menos específico. No exemplo abaixo, o tipo mais específico `PlaneItem` vem antes de `CarItem` em `Union[PlaneItem, CarItem]`. -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="1 14-15 18-20 33" {!> ../../../docs_src/extra_models/tutorial003.py!} @@ -213,7 +213,7 @@ Da mesma forma, você pode declarar respostas de listas de objetos. Para isso, use o padrão Python `typing.List` (ou simplesmente `list` no Python 3.9 e superior): -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="1 20" {!> ../../../docs_src/extra_models/tutorial004.py!} @@ -233,7 +233,7 @@ Isso é útil se você não souber os nomes de campo / atributo válidos (que se Neste caso, você pode usar `typing.Dict` (ou simplesmente dict no Python 3.9 e superior): -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="1 8" {!> ../../../docs_src/extra_models/tutorial005.py!} diff --git a/docs/pt/docs/tutorial/header-params.md b/docs/pt/docs/tutorial/header-params.md index bc8843327..4bdfb7e9c 100644 --- a/docs/pt/docs/tutorial/header-params.md +++ b/docs/pt/docs/tutorial/header-params.md @@ -12,7 +12,7 @@ Primeiro importe `Header`: {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001.py!} @@ -30,7 +30,7 @@ O primeiro valor é o valor padrão, você pode passar todas as validações adi {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial001.py!} @@ -66,7 +66,7 @@ Se por algum motivo você precisar desabilitar a conversão automática de subli {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial002.py!} @@ -97,7 +97,7 @@ Por exemplo, para declarar um cabeçalho de `X-Token` que pode aparecer mais de {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/header_params/tutorial003.py!} diff --git a/docs/pt/docs/tutorial/path-operation-configuration.md b/docs/pt/docs/tutorial/path-operation-configuration.md index e0a23f665..13a87240f 100644 --- a/docs/pt/docs/tutorial/path-operation-configuration.md +++ b/docs/pt/docs/tutorial/path-operation-configuration.md @@ -13,7 +13,7 @@ Você pode passar diretamente o código `int`, como `404`. Mas se você não se lembrar o que cada código numérico significa, pode usar as constantes de atalho em `status`: -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="3 17" {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} @@ -42,7 +42,7 @@ Esse código de status será usado na resposta e será adicionado ao esquema Ope Você pode adicionar tags para sua *operação de rota*, passe o parâmetro `tags` com uma `list` de `str` (comumente apenas um `str`): -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="17 22 27" {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} @@ -80,7 +80,7 @@ Nestes casos, pode fazer sentido armazenar as tags em um `Enum`. Você pode adicionar um `summary` e uma `description`: -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="20-21" {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} @@ -104,7 +104,7 @@ Como as descrições tendem a ser longas e cobrir várias linhas, você pode dec Você pode escrever Markdown na docstring, ele será interpretado e exibido corretamente (levando em conta a indentação da docstring). -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="19-27" {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} @@ -131,7 +131,7 @@ Ela será usada nas documentações interativas: Você pode especificar a descrição da resposta com o parâmetro `response_description`: -=== "Python 3.6 and above" +=== "Python 3.8 and above" ```Python hl_lines="21" {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} diff --git a/docs/pt/docs/tutorial/path-params-numeric-validations.md b/docs/pt/docs/tutorial/path-params-numeric-validations.md index ec9b74b30..eb0d31dc3 100644 --- a/docs/pt/docs/tutorial/path-params-numeric-validations.md +++ b/docs/pt/docs/tutorial/path-params-numeric-validations.md @@ -12,7 +12,7 @@ Primeiro, importe `Path` de `fastapi`: {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} @@ -30,7 +30,7 @@ Por exemplo para declarar um valor de metadado `title` para o parâmetro de rota {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/path_params_numeric_validations/tutorial001.py!} diff --git a/docs/pt/docs/tutorial/query-params.md b/docs/pt/docs/tutorial/query-params.md index 3ada4fd21..08bb99dbc 100644 --- a/docs/pt/docs/tutorial/query-params.md +++ b/docs/pt/docs/tutorial/query-params.md @@ -69,7 +69,7 @@ Da mesma forma, você pode declarar parâmetros de consulta opcionais, definindo {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial002.py!} @@ -91,7 +91,7 @@ Você também pode declarar tipos `bool`, e eles serão convertidos: {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial003.py!} @@ -143,7 +143,7 @@ Eles serão detectados pelo nome: {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8 10" {!> ../../../docs_src/query_params/tutorial004.py!} @@ -209,7 +209,7 @@ E claro, você pode definir alguns parâmetros como obrigatórios, alguns possui {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params/tutorial006.py!} diff --git a/docs/ru/docs/features.md b/docs/ru/docs/features.md index e18f7bc87..97841cc83 100644 --- a/docs/ru/docs/features.md +++ b/docs/ru/docs/features.md @@ -27,7 +27,7 @@ ### Только современный Python -Все эти возможности основаны на стандартных **аннотациях типов Python 3.6** (благодаря Pydantic). Не нужно изучать новый синтаксис. Только лишь стандартный современный Python. +Все эти возможности основаны на стандартных **аннотациях типов Python 3.8** (благодаря Pydantic). Не нужно изучать новый синтаксис. Только лишь стандартный современный Python. Если вам нужно освежить знания, как использовать аннотации типов в Python (даже если вы не используете FastAPI), выделите 2 минуты и просмотрите краткое руководство: [Введение в аннотации типов Python¶ ](python-types.md){.internal-link target=_blank}. diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 30c32e046..97a3947bd 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI — это современный, быстрый (высокопроизводительный) веб-фреймворк для создания API используя Python 3.6+, в основе которого лежит стандартная аннотация типов Python. +FastAPI — это современный, быстрый (высокопроизводительный) веб-фреймворк для создания API используя Python 3.8+, в основе которого лежит стандартная аннотация типов Python. Ключевые особенности: @@ -109,7 +109,7 @@ FastAPI — это современный, быстрый (высокопрои ## Зависимости -Python 3.7+ +Python 3.8+ FastAPI стоит на плечах гигантов: @@ -325,7 +325,7 @@ def update_item(item_id: int, item: Item): Вам не нужно изучать новый синтаксис, методы или классы конкретной библиотеки и т. д. -Только стандартный **Python 3.6+**. +Только стандартный **Python 3.8+**. Например, для `int`: diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index 81efda786..7a3cf6d83 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -63,7 +63,7 @@ {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="13 15 22 25" {!> ../../../docs_src/background_tasks/tutorial002.py!} diff --git a/docs/ru/docs/tutorial/body-fields.md b/docs/ru/docs/tutorial/body-fields.md index 2dc6c1e26..02a598004 100644 --- a/docs/ru/docs/tutorial/body-fields.md +++ b/docs/ru/docs/tutorial/body-fields.md @@ -12,7 +12,7 @@ {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4" {!> ../../../docs_src/body_fields/tutorial001.py!} @@ -31,7 +31,7 @@ {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11-14" {!> ../../../docs_src/body_fields/tutorial001.py!} diff --git a/docs/ru/docs/tutorial/body-multiple-params.md b/docs/ru/docs/tutorial/body-multiple-params.md index a20457092..e52ef6f6f 100644 --- a/docs/ru/docs/tutorial/body-multiple-params.md +++ b/docs/ru/docs/tutorial/body-multiple-params.md @@ -20,7 +20,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-21" {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} @@ -35,7 +35,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! Заметка Рекомендуется использовать версию с `Annotated`, если это возможно. @@ -68,7 +68,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial002.py!} @@ -123,7 +123,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} @@ -138,7 +138,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! Заметка Рекомендуется использовать `Annotated` версию, если это возможно. @@ -197,7 +197,7 @@ q: str | None = None {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="28" {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} @@ -212,7 +212,7 @@ q: str | None = None {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! Заметка Рекомендуется использовать `Annotated` версию, если это возможно. @@ -250,7 +250,7 @@ item: Item = Body(embed=True) {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} @@ -265,7 +265,7 @@ item: Item = Body(embed=True) {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! Заметка Рекомендуется использовать `Annotated` версию, если это возможно. diff --git a/docs/ru/docs/tutorial/body-nested-models.md b/docs/ru/docs/tutorial/body-nested-models.md index 6435e316f..a6d123d30 100644 --- a/docs/ru/docs/tutorial/body-nested-models.md +++ b/docs/ru/docs/tutorial/body-nested-models.md @@ -12,7 +12,7 @@ {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial001.py!} @@ -73,7 +73,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial002.py!} @@ -99,7 +99,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 14" {!> ../../../docs_src/body_nested_models/tutorial003.py!} @@ -137,7 +137,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -159,7 +159,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -208,7 +208,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 10" {!> ../../../docs_src/body_nested_models/tutorial005.py!} @@ -232,7 +232,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial006.py!} @@ -283,7 +283,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 14 20 23 27" {!> ../../../docs_src/body_nested_models/tutorial007.py!} @@ -314,7 +314,7 @@ images: list[Image] {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15" {!> ../../../docs_src/body_nested_models/tutorial008.py!} @@ -354,7 +354,7 @@ images: list[Image] {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/body_nested_models/tutorial009.py!} diff --git a/docs/ru/docs/tutorial/cookie-params.md b/docs/ru/docs/tutorial/cookie-params.md index a6f2caa26..5f99458b6 100644 --- a/docs/ru/docs/tutorial/cookie-params.md +++ b/docs/ru/docs/tutorial/cookie-params.md @@ -12,7 +12,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001.py!} @@ -30,7 +30,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/cookie_params/tutorial001.py!} diff --git a/docs/ru/docs/tutorial/dependencies/global-dependencies.md b/docs/ru/docs/tutorial/dependencies/global-dependencies.md index 870d42cf5..eb1b4d7c1 100644 --- a/docs/ru/docs/tutorial/dependencies/global-dependencies.md +++ b/docs/ru/docs/tutorial/dependencies/global-dependencies.md @@ -12,13 +12,13 @@ {!> ../../../docs_src/dependencies/tutorial012_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="16" {!> ../../../docs_src/dependencies/tutorial012_an.py!} ``` -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip "Подсказка" Рекомендуется использовать 'Annotated' версию, если это возможно. diff --git a/docs/ru/docs/tutorial/extra-data-types.md b/docs/ru/docs/tutorial/extra-data-types.md index efcbcb38a..0f613a6b2 100644 --- a/docs/ru/docs/tutorial/extra-data-types.md +++ b/docs/ru/docs/tutorial/extra-data-types.md @@ -55,7 +55,7 @@ Вот пример *операции пути* с параметрами, который демонстрирует некоторые из вышеперечисленных типов. -=== "Python 3.6 и выше" +=== "Python 3.8 и выше" ```Python hl_lines="1 3 12-16" {!> ../../../docs_src/extra_data_types/tutorial001.py!} @@ -69,7 +69,7 @@ Обратите внимание, что параметры внутри функции имеют свой естественный тип данных, и вы, например, можете выполнять обычные манипуляции с датами, такие как: -=== "Python 3.6 и выше" +=== "Python 3.8 и выше" ```Python hl_lines="18-19" {!> ../../../docs_src/extra_data_types/tutorial001.py!} diff --git a/docs/ru/docs/tutorial/extra-models.md b/docs/ru/docs/tutorial/extra-models.md index a346f7432..30176b4e3 100644 --- a/docs/ru/docs/tutorial/extra-models.md +++ b/docs/ru/docs/tutorial/extra-models.md @@ -23,7 +23,7 @@ {!> ../../../docs_src/extra_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" {!> ../../../docs_src/extra_models/tutorial001.py!} @@ -164,7 +164,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 15-16 19-20 23-24" {!> ../../../docs_src/extra_models/tutorial002.py!} @@ -187,7 +187,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 14-15 18-20 33" {!> ../../../docs_src/extra_models/tutorial003.py!} @@ -219,7 +219,7 @@ some_variable: PlaneItem | CarItem {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 20" {!> ../../../docs_src/extra_models/tutorial004.py!} @@ -239,7 +239,7 @@ some_variable: PlaneItem | CarItem {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 8" {!> ../../../docs_src/extra_models/tutorial005.py!} diff --git a/docs/ru/docs/tutorial/header-params.md b/docs/ru/docs/tutorial/header-params.md index 0ff8ea489..1be4ac707 100644 --- a/docs/ru/docs/tutorial/header-params.md +++ b/docs/ru/docs/tutorial/header-params.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Предпочтительнее использовать версию с аннотацией, если это возможно. @@ -60,7 +60,7 @@ {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -75,7 +75,7 @@ {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Предпочтительнее использовать версию с аннотацией, если это возможно. @@ -120,7 +120,7 @@ {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/header_params/tutorial002_an.py!} @@ -135,7 +135,7 @@ {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Предпочтительнее использовать версию с аннотацией, если это возможно. @@ -169,7 +169,7 @@ {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial003_an.py!} @@ -193,7 +193,7 @@ {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Предпочтительнее использовать версию с аннотацией, если это возможно. diff --git a/docs/ru/docs/tutorial/path-operation-configuration.md b/docs/ru/docs/tutorial/path-operation-configuration.md index 013903add..db99409f4 100644 --- a/docs/ru/docs/tutorial/path-operation-configuration.md +++ b/docs/ru/docs/tutorial/path-operation-configuration.md @@ -25,7 +25,7 @@ {!> ../../../docs_src/path_operation_configuration/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3 17" {!> ../../../docs_src/path_operation_configuration/tutorial001.py!} @@ -54,7 +54,7 @@ {!> ../../../docs_src/path_operation_configuration/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 22 27" {!> ../../../docs_src/path_operation_configuration/tutorial002.py!} @@ -92,7 +92,7 @@ {!> ../../../docs_src/path_operation_configuration/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20-21" {!> ../../../docs_src/path_operation_configuration/tutorial003.py!} @@ -116,7 +116,7 @@ {!> ../../../docs_src/path_operation_configuration/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-27" {!> ../../../docs_src/path_operation_configuration/tutorial004.py!} @@ -142,7 +142,7 @@ {!> ../../../docs_src/path_operation_configuration/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/path_operation_configuration/tutorial005.py!} diff --git a/docs/ru/docs/tutorial/path-params-numeric-validations.md b/docs/ru/docs/tutorial/path-params-numeric-validations.md index 0d034ef34..bd2c29d0a 100644 --- a/docs/ru/docs/tutorial/path-params-numeric-validations.md +++ b/docs/ru/docs/tutorial/path-params-numeric-validations.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3-4" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -67,7 +67,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -82,7 +82,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -117,7 +117,7 @@ Поэтому вы можете определить функцию так: -=== "Python 3.6 без Annotated" +=== "Python 3.8 без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -134,7 +134,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial002_an.py!} @@ -174,7 +174,7 @@ Python не будет ничего делать с `*`, но он будет з {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial003_an.py!} @@ -192,13 +192,13 @@ Python не будет ничего делать с `*`, но он будет з {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial004_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -220,13 +220,13 @@ Python не будет ничего делать с `*`, но он будет з {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/path_params_numeric_validations/tutorial005_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -251,13 +251,13 @@ Python не будет ничего делать с `*`, но он будет з {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/path_params_numeric_validations/tutorial006_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md index 68042db63..15be5dbf6 100644 --- a/docs/ru/docs/tutorial/query-params-str-validations.md +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -10,7 +10,7 @@ {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} @@ -42,7 +42,7 @@ Query-параметр `q` имеет тип `Union[str, None]` (или `str | N {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" В версиях Python ниже Python 3.9 `Annotation` импортируется из `typing_extensions`. @@ -66,7 +66,7 @@ Query-параметр `q` имеет тип `Union[str, None]` (или `str | N q: str | None = None ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python q: Union[str, None] = None @@ -80,7 +80,7 @@ Query-параметр `q` имеет тип `Union[str, None]` (или `str | N q: Annotated[str | None] = None ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python q: Annotated[Union[str, None]] = None @@ -100,7 +100,7 @@ Query-параметр `q` имеет тип `Union[str, None]` (или `str | N {!> ../../../docs_src/query_params_str_validations/tutorial002_an_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial002_an.py!} @@ -131,7 +131,7 @@ Query-параметр `q` имеет тип `Union[str, None]` (или `str | N {!> ../../../docs_src/query_params_str_validations/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial002.py!} @@ -244,7 +244,7 @@ q: str = Query(default="rick") {!> ../../../docs_src/query_params_str_validations/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial003_an.py!} @@ -259,7 +259,7 @@ q: str = Query(default="rick") {!> ../../../docs_src/query_params_str_validations/tutorial003_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -284,7 +284,7 @@ q: str = Query(default="rick") {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/query_params_str_validations/tutorial004_an.py!} @@ -299,7 +299,7 @@ q: str = Query(default="rick") {!> ../../../docs_src/query_params_str_validations/tutorial004_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -330,13 +330,13 @@ q: str = Query(default="rick") {!> ../../../docs_src/query_params_str_validations/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial005_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -384,13 +384,13 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial006_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial006_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -414,13 +414,13 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial006b_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial006b_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -454,7 +454,7 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial006c_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial006c_an.py!} @@ -469,7 +469,7 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial006c_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -491,13 +491,13 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="2 9" {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -527,7 +527,7 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial011_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial011_an.py!} @@ -551,7 +551,7 @@ q: Union[str, None] = None {!> ../../../docs_src/query_params_str_validations/tutorial011_py39.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -596,7 +596,7 @@ http://localhost:8000/items/?q=foo&q=bar {!> ../../../docs_src/query_params_str_validations/tutorial012_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial012_an.py!} @@ -611,7 +611,7 @@ http://localhost:8000/items/?q=foo&q=bar {!> ../../../docs_src/query_params_str_validations/tutorial012_py39.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -647,13 +647,13 @@ http://localhost:8000/items/ {!> ../../../docs_src/query_params_str_validations/tutorial013_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/query_params_str_validations/tutorial013_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -692,7 +692,7 @@ http://localhost:8000/items/ {!> ../../../docs_src/query_params_str_validations/tutorial007_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial007_an.py!} @@ -707,7 +707,7 @@ http://localhost:8000/items/ {!> ../../../docs_src/query_params_str_validations/tutorial007_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -730,7 +730,7 @@ http://localhost:8000/items/ {!> ../../../docs_src/query_params_str_validations/tutorial008_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15" {!> ../../../docs_src/query_params_str_validations/tutorial008_an.py!} @@ -745,7 +745,7 @@ http://localhost:8000/items/ {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -784,7 +784,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial009_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params_str_validations/tutorial009_an.py!} @@ -799,7 +799,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial009_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -828,7 +828,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial010_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/query_params_str_validations/tutorial010_an.py!} @@ -843,7 +843,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. @@ -872,7 +872,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial014_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial014_an.py!} @@ -887,7 +887,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems {!> ../../../docs_src/query_params_str_validations/tutorial014_py310.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. diff --git a/docs/ru/docs/tutorial/query-params.md b/docs/ru/docs/tutorial/query-params.md index 68333ec56..6e885cb65 100644 --- a/docs/ru/docs/tutorial/query-params.md +++ b/docs/ru/docs/tutorial/query-params.md @@ -69,7 +69,7 @@ http://127.0.0.1:8000/items/?skip=20 {!> ../../../docs_src/query_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial002.py!} @@ -90,7 +90,7 @@ http://127.0.0.1:8000/items/?skip=20 {!> ../../../docs_src/query_params/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params/tutorial003.py!} @@ -143,7 +143,7 @@ http://127.0.0.1:8000/items/foo?short=yes {!> ../../../docs_src/query_params/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8 10" {!> ../../../docs_src/query_params/tutorial004.py!} @@ -209,7 +209,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy {!> ../../../docs_src/query_params/tutorial006_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/query_params/tutorial006.py!} diff --git a/docs/ru/docs/tutorial/request-forms.md b/docs/ru/docs/tutorial/request-forms.md index a20cf78e0..0fc9e4eda 100644 --- a/docs/ru/docs/tutorial/request-forms.md +++ b/docs/ru/docs/tutorial/request-forms.md @@ -17,13 +17,13 @@ {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1" {!> ../../../docs_src/request_forms/tutorial001_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать 'Annotated' версию, если это возможно. @@ -42,13 +42,13 @@ {!> ../../../docs_src/request_forms/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8" {!> ../../../docs_src/request_forms/tutorial001_an.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" Рекомендуется использовать 'Annotated' версию, если это возможно. diff --git a/docs/ru/docs/tutorial/response-model.md b/docs/ru/docs/tutorial/response-model.md index c5e111790..38b45e2a5 100644 --- a/docs/ru/docs/tutorial/response-model.md +++ b/docs/ru/docs/tutorial/response-model.md @@ -16,7 +16,7 @@ FastAPI позволяет использовать **аннотации тип {!> ../../../docs_src/response_model/tutorial001_01_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18 23" {!> ../../../docs_src/response_model/tutorial001_01.py!} @@ -65,7 +65,7 @@ FastAPI будет использовать этот возвращаемый т {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001.py!} @@ -101,7 +101,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11" {!> ../../../docs_src/response_model/tutorial002.py!} @@ -120,7 +120,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/response_model/tutorial002.py!} @@ -145,7 +145,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -159,7 +159,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -173,7 +173,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -207,7 +207,7 @@ FastAPI будет использовать значение `response_model` д {!> ../../../docs_src/response_model/tutorial003_01_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-13 15-16 20" {!> ../../../docs_src/response_model/tutorial003_01.py!} @@ -283,7 +283,7 @@ FastAPI совместно с Pydantic выполнит некоторую ма {!> ../../../docs_src/response_model/tutorial003_04_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/response_model/tutorial003_04.py!} @@ -305,7 +305,7 @@ FastAPI совместно с Pydantic выполнит некоторую ма {!> ../../../docs_src/response_model/tutorial003_05_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/response_model/tutorial003_05.py!} @@ -329,7 +329,7 @@ FastAPI совместно с Pydantic выполнит некоторую ма {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11 13-14" {!> ../../../docs_src/response_model/tutorial004.py!} @@ -359,7 +359,7 @@ FastAPI совместно с Pydantic выполнит некоторую ма {!> ../../../docs_src/response_model/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial004.py!} @@ -446,7 +446,7 @@ FastAPI достаточно умен (на самом деле, это засл {!> ../../../docs_src/response_model/tutorial005_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="31 37" {!> ../../../docs_src/response_model/tutorial005.py!} @@ -467,7 +467,7 @@ FastAPI достаточно умен (на самом деле, это засл {!> ../../../docs_src/response_model/tutorial006_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="31 37" {!> ../../../docs_src/response_model/tutorial006.py!} diff --git a/docs/ru/docs/tutorial/schema-extra-example.md b/docs/ru/docs/tutorial/schema-extra-example.md index a0363b9ba..a13ab5935 100644 --- a/docs/ru/docs/tutorial/schema-extra-example.md +++ b/docs/ru/docs/tutorial/schema-extra-example.md @@ -14,7 +14,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-23" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} @@ -39,7 +39,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 10-13" {!> ../../../docs_src/schema_extra_example/tutorial002.py!} @@ -78,7 +78,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23-28" {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} @@ -93,7 +93,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Заметка Рекомендуется использовать версию с `Annotated`, если это возможно. @@ -133,7 +133,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24-50" {!> ../../../docs_src/schema_extra_example/tutorial004_an.py!} @@ -148,7 +148,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial004_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Заметка Рекомендуется использовать версию с `Annotated`, если это возможно. diff --git a/docs/ru/docs/tutorial/testing.md b/docs/ru/docs/tutorial/testing.md index 3f9005112..ca47a6f51 100644 --- a/docs/ru/docs/tutorial/testing.md +++ b/docs/ru/docs/tutorial/testing.md @@ -122,7 +122,7 @@ {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/app_testing/app_b_an/main.py!} @@ -137,7 +137,7 @@ {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` -=== "Python 3.6+ без Annotated" +=== "Python 3.8+ без Annotated" !!! tip "Подсказка" По возможности используйте версию с `Annotated`. diff --git a/docs/tr/docs/features.md b/docs/tr/docs/features.md index f8220fb58..8b143ffe7 100644 --- a/docs/tr/docs/features.md +++ b/docs/tr/docs/features.md @@ -27,7 +27,7 @@ OpenAPI standartlarına dayalı olan bir framework olarak, geliştiricilerin bir ### Sadece modern Python -Tamamiyle standartlar **Python 3.6**'nın type hintlerine dayanıyor (Pydantic'in sayesinde). Yeni bir syntax öğrenmene gerek yok. Sadece modern Python. +Tamamiyle standartlar **Python 3.8**'nın type hintlerine dayanıyor (Pydantic'in sayesinde). Yeni bir syntax öğrenmene gerek yok. Sadece modern Python. Eğer Python type hintlerini bilmiyorsan veya bir hatırlatmaya ihtiyacın var ise(FastAPI kullanmasan bile) şu iki dakikalık küçük bilgilendirici içeriğe bir göz at: [Python Types](python-types.md){.internal-link target=_blank}. diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index e74efbc2f..e61f5b82c 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI, Python 3.6+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. +FastAPI, Python 3.8+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. Ana özellikleri: @@ -115,7 +115,7 @@ Eğer API yerine komut satırı uygulaması ## Gereksinimler -Python 3.7+ +Python 3.8+ FastAPI iki devin omuzları üstünde duruyor: @@ -331,7 +331,7 @@ Type-hinting işlemini Python dilindeki standart veri tipleri ile yapabilirsin Yeni bir syntax'e alışmana gerek yok, metodlar ve classlar zaten spesifik kütüphanelere ait. -Sadece standart **Python 3.6+**. +Sadece standart **Python 3.8+**. Örnek olarak, `int` tanımlamak için: diff --git a/docs/uk/docs/python-types.md b/docs/uk/docs/python-types.md index f792e83a8..6c8e29016 100644 --- a/docs/uk/docs/python-types.md +++ b/docs/uk/docs/python-types.md @@ -164,7 +164,7 @@ John Doe Наприклад, давайте визначимо змінну, яка буде `list` із `str`. -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" З модуля `typing`, імпортуємо `List` (з великої літери `L`): @@ -218,7 +218,7 @@ John Doe Ви повинні зробити те ж саме, щоб оголосити `tuple` і `set`: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial007.py!} @@ -243,7 +243,7 @@ John Doe Другий параметр типу для значення у `dict`: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008.py!} @@ -269,7 +269,7 @@ John Doe У Python 3.10 також є **альтернативний синтаксис**, у якому ви можете розділити можливі типи за допомогою вертикальної смуги (`|`). -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008b.py!} @@ -299,13 +299,13 @@ John Doe Це також означає, що в Python 3.10 ви можете використовувати `Something | None`: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.6 і вище - альтернатива" +=== "Python 3.8 і вище - альтернатива" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009b.py!} @@ -321,7 +321,7 @@ John Doe Ці типи, які приймають параметри типу у квадратних дужках, називаються **Generic types** or **Generics**, наприклад: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" * `List` * `Tuple` @@ -340,7 +340,7 @@ John Doe * `set` * `dict` - І те саме, що й у Python 3.6, із модуля `typing`: + І те саме, що й у Python 3.8, із модуля `typing`: * `Union` * `Optional` @@ -355,10 +355,10 @@ John Doe * `set` * `dict` - І те саме, що й у Python 3.6, із модуля `typing`: + І те саме, що й у Python 3.8, із модуля `typing`: * `Union` - * `Optional` (так само як у Python 3.6) + * `Optional` (так само як у Python 3.8) * ...та інші. У Python 3.10, як альтернатива використанню `Union` та `Optional`, ви можете використовувати вертикальну смугу (`|`) щоб оголосити об'єднання типів. @@ -397,7 +397,7 @@ John Doe Приклад з документації Pydantic: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python {!> ../../../docs_src/python_types/tutorial011.py!} diff --git a/docs/uk/docs/tutorial/body.md b/docs/uk/docs/tutorial/body.md index e78c5de0e..9759e7f45 100644 --- a/docs/uk/docs/tutorial/body.md +++ b/docs/uk/docs/tutorial/body.md @@ -19,7 +19,7 @@ Спочатку вам потрібно імпортувати `BaseModel` з `pydantic`: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="4" {!> ../../../docs_src/body/tutorial001.py!} @@ -37,7 +37,7 @@ Використовуйте стандартні типи Python для всіх атрибутів: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="7-11" {!> ../../../docs_src/body/tutorial001.py!} @@ -75,7 +75,7 @@ Щоб додати модель даних до вашої *операції шляху*, оголосіть її так само, як ви оголосили параметри шляху та запиту: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial001.py!} @@ -149,7 +149,7 @@ Усередині функції ви можете отримати прямий доступ до всіх атрибутів об’єкта моделі: -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="21" {!> ../../../docs_src/body/tutorial002.py!} @@ -167,7 +167,7 @@ **FastAPI** розпізнає, що параметри функції, які відповідають параметрам шляху, мають бути **взяті з шляху**, а параметри функції, які оголошуються як моделі Pydantic, **взяті з тіла запиту**. -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="17-18" {!> ../../../docs_src/body/tutorial003.py!} @@ -185,7 +185,7 @@ **FastAPI** розпізнає кожен з них і візьме дані з потрібного місця. -=== "Python 3.6 і вище" +=== "Python 3.8 і вище" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial004.py!} diff --git a/docs/uk/docs/tutorial/cookie-params.md b/docs/uk/docs/tutorial/cookie-params.md index 2b0e8993c..199b93839 100644 --- a/docs/uk/docs/tutorial/cookie-params.md +++ b/docs/uk/docs/tutorial/cookie-params.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Бажано використовувати `Annotated` версію, якщо це можливо. @@ -60,7 +60,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -75,7 +75,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Бажано використовувати `Annotated` версію, якщо це можливо. diff --git a/docs/uk/docs/tutorial/encoder.md b/docs/uk/docs/tutorial/encoder.md index d660447b4..b6583341f 100644 --- a/docs/uk/docs/tutorial/encoder.md +++ b/docs/uk/docs/tutorial/encoder.md @@ -26,7 +26,7 @@ {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="5 22" {!> ../../../docs_src/encoder/tutorial001.py!} diff --git a/docs/uk/docs/tutorial/extra-data-types.md b/docs/uk/docs/tutorial/extra-data-types.md index ba75ee627..ec5ec0d18 100644 --- a/docs/uk/docs/tutorial/extra-data-types.md +++ b/docs/uk/docs/tutorial/extra-data-types.md @@ -67,7 +67,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 3 13-17" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -82,7 +82,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Бажано використовувати `Annotated` версію, якщо це можливо. @@ -105,7 +105,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-20" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -120,7 +120,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Бажано використовувати `Annotated` версію, якщо це можливо. diff --git a/docs/vi/docs/features.md b/docs/vi/docs/features.md index 0599530e8..306aeb359 100644 --- a/docs/vi/docs/features.md +++ b/docs/vi/docs/features.md @@ -26,7 +26,7 @@ Tài liệu tương tác API và web giao diện người dùng. Là một frame ### Chỉ cần phiên bản Python hiện đại -Tất cả được dựa trên khai báo kiểu dữ liệu chuẩn của **Python 3.6** (cảm ơn Pydantic). Bạn không cần học cú pháp mới, chỉ cần biết chuẩn Python hiện đại. +Tất cả được dựa trên khai báo kiểu dữ liệu chuẩn của **Python 3.8** (cảm ơn Pydantic). Bạn không cần học cú pháp mới, chỉ cần biết chuẩn Python hiện đại. Nếu bạn cần 2 phút để làm mới lại cách sử dụng các kiểu dữ liệu mới của Python (thậm chí nếu bạn không sử dụng FastAPI), xem hướng dẫn ngắn: [Kiểu dữ liệu Python](python-types.md){.internal-link target=_blank}. diff --git a/docs/vi/docs/index.md b/docs/vi/docs/index.md index 0e773a011..3f416dbec 100644 --- a/docs/vi/docs/index.md +++ b/docs/vi/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI là một web framework hiện đại, hiệu năng cao để xây dựng web APIs với Python 3.7+ dựa trên tiêu chuẩn Python type hints. +FastAPI là một web framework hiện đại, hiệu năng cao để xây dựng web APIs với Python 3.8+ dựa trên tiêu chuẩn Python type hints. Những tính năng như: @@ -116,7 +116,7 @@ Nếu bạn đang xây dựng một CLI ../../../docs_src/python_types/tutorial006_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" Từ `typing`, import `List` (với chữ cái `L` viết hoa): @@ -230,7 +230,7 @@ Bạn sẽ làm điều tương tự để khai báo các `tuple` và các `set {!> ../../../docs_src/python_types/tutorial007_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial007.py!} @@ -255,7 +255,7 @@ Tham số kiểu dữ liệu thứ hai dành cho giá trị của `dict`. {!> ../../../docs_src/python_types/tutorial008_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008.py!} @@ -284,7 +284,7 @@ Trong Python 3.10 cũng có một **cú pháp mới** mà bạn có thể đặt {!> ../../../docs_src/python_types/tutorial008b_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial008b.py!} @@ -314,13 +314,13 @@ Sử dụng `Optional[str]` thay cho `str` sẽ cho phép trình soạn thảo g {!> ../../../docs_src/python_types/tutorial009_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009.py!} ``` -=== "Python 3.6+ alternative" +=== "Python 3.8+ alternative" ```Python hl_lines="1 4" {!> ../../../docs_src/python_types/tutorial009b.py!} @@ -404,7 +404,7 @@ Những kiểu dữ liệu này lấy tham số kiểu dữ liệu trong dấu n * `Optional` * ...and others. -=== "Python 3.6+" +=== "Python 3.8+" * `List` * `Tuple` @@ -464,7 +464,7 @@ Một ví dụ từ tài liệu chính thức của Pydantic: {!> ../../../docs_src/python_types/tutorial011_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/python_types/tutorial011.py!} @@ -493,7 +493,7 @@ Python cũng có một tính năng cho phép đặt **metadata bổ sung** trong {!> ../../../docs_src/python_types/tutorial013_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" Ở phiên bản dưới Python 3.9, bạn import `Annotated` từ `typing_extensions`. diff --git a/docs/yo/docs/index.md b/docs/yo/docs/index.md index ca75a6b13..101e13b6b 100644 --- a/docs/yo/docs/index.md +++ b/docs/yo/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI jẹ́ ìgbàlódé, tí ó yára (iṣẹ-giga), ìlànà wẹ́ẹ́bù fún kikọ àwọn API pẹ̀lú Python 3.7+ èyí tí ó da lori àwọn ìtọ́kasí àmì irúfẹ́ Python. +FastAPI jẹ́ ìgbàlódé, tí ó yára (iṣẹ-giga), ìlànà wẹ́ẹ́bù fún kikọ àwọn API pẹ̀lú Python 3.8+ èyí tí ó da lori àwọn ìtọ́kasí àmì irúfẹ́ Python. Àwọn ẹya pàtàkì ni: @@ -115,7 +115,7 @@ Ti o ba n kọ ohun èlò CLI láti ## Èròjà -Python 3.7+ +Python 3.8+ FastAPI dúró lórí àwọn èjìká tí àwọn òmíràn: @@ -331,7 +331,7 @@ O ṣe ìyẹn pẹ̀lú irúfẹ́ àmì ìtọ́kasí ìgbàlódé Python. O ò nílò láti kọ́ síńtáàsì tuntun, ìlànà tàbí ọ̀wọ́ kíláàsì kan pàtó, abbl (i.e. àti bẹbẹ lọ). -Ìtọ́kasí **Python 3.7+** +Ìtọ́kasí **Python 3.8+** Fún àpẹẹrẹ, fún `int`: diff --git a/docs/zh/docs/advanced/generate-clients.md b/docs/zh/docs/advanced/generate-clients.md index f3a58c062..e222e479c 100644 --- a/docs/zh/docs/advanced/generate-clients.md +++ b/docs/zh/docs/advanced/generate-clients.md @@ -22,7 +22,7 @@ {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11 14-15 18 19 23" {!> ../../../docs_src/generate_clients/tutorial001.py!} @@ -134,7 +134,7 @@ frontend-app@1.0.0 generate-client /home/user/code/frontend-app {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23 28 36" {!> ../../../docs_src/generate_clients/tutorial002.py!} @@ -191,7 +191,7 @@ FastAPI为每个*路径操作*使用一个**唯一ID**,它用于**操作ID** {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="8-9 12" {!> ../../../docs_src/generate_clients/tutorial003.py!} diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md index 597e99a77..7f718acef 100644 --- a/docs/zh/docs/advanced/settings.md +++ b/docs/zh/docs/advanced/settings.md @@ -223,13 +223,13 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="6 12-13" {!> ../../../docs_src/settings/app02_an/main.py!} ``` -=== "Python 3.6+ 非注解版本" +=== "Python 3.8+ 非注解版本" !!! tip 如果可能,请尽量使用 `Annotated` 版本。 @@ -251,13 +251,13 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app {!> ../../../docs_src/settings/app02_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 19-21" {!> ../../../docs_src/settings/app02_an/main.py!} ``` -=== "Python 3.6+ 非注解版本" +=== "Python 3.8+ 非注解版本" !!! tip 如果可能,请尽量使用 `Annotated` 版本。 @@ -345,13 +345,13 @@ def get_settings(): {!> ../../../docs_src/settings/app03_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 11" {!> ../../../docs_src/settings/app03_an/main.py!} ``` -=== "Python 3.6+ 非注解版本" +=== "Python 3.8+ 非注解版本" !!! tip 如果可能,请尽量使用 `Annotated` 版本。 diff --git a/docs/zh/docs/advanced/websockets.md b/docs/zh/docs/advanced/websockets.md index a723487fd..a5cbdd965 100644 --- a/docs/zh/docs/advanced/websockets.md +++ b/docs/zh/docs/advanced/websockets.md @@ -118,7 +118,7 @@ $ uvicorn main:app --reload {!> ../../../docs_src/websockets/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="69-70 83" {!> ../../../docs_src/websockets/tutorial002_an.py!} @@ -133,7 +133,7 @@ $ uvicorn main:app --reload {!> ../../../docs_src/websockets/tutorial002_py310.py!} ``` -=== "Python 3.6+ 非带注解版本" +=== "Python 3.8+ 非带注解版本" !!! tip 如果可能,请尽量使用 `Annotated` 版本。 @@ -181,7 +181,7 @@ $ uvicorn main:app --reload {!> ../../../docs_src/websockets/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="81-83" {!> ../../../docs_src/websockets/tutorial003.py!} diff --git a/docs/zh/docs/index.md b/docs/zh/docs/index.md index 1de2a8d36..d776e5813 100644 --- a/docs/zh/docs/index.md +++ b/docs/zh/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。 +FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.8+ 并基于标准的 Python 类型提示。 关键特性: @@ -107,7 +107,7 @@ FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框 ## 依赖 -Python 3.6 及更高版本 +Python 3.8 及更高版本 FastAPI 站在以下巨人的肩膀之上: @@ -323,7 +323,7 @@ def update_item(item_id: int, item: Item): 你不需要去学习新的语法、了解特定库的方法或类,等等。 -只需要使用标准的 **Python 3.6 及更高版本**。 +只需要使用标准的 **Python 3.8 及更高版本**。 举个例子,比如声明 `int` 类型: diff --git a/docs/zh/docs/tutorial/background-tasks.md b/docs/zh/docs/tutorial/background-tasks.md index c8568298b..94b75d4fd 100644 --- a/docs/zh/docs/tutorial/background-tasks.md +++ b/docs/zh/docs/tutorial/background-tasks.md @@ -69,7 +69,7 @@ {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14 16 23 26" {!> ../../../docs_src/background_tasks/tutorial002_an.py!} @@ -84,7 +84,7 @@ {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} ``` -=== "Python 3.6+ 没Annotated" +=== "Python 3.8+ 没Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/body-fields.md b/docs/zh/docs/tutorial/body-fields.md index c153784dc..fb6c6d9b6 100644 --- a/docs/zh/docs/tutorial/body-fields.md +++ b/docs/zh/docs/tutorial/body-fields.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4" {!> ../../../docs_src/body_fields/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -61,7 +61,7 @@ {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12-15" {!> ../../../docs_src/body_fields/tutorial001_an.py!} @@ -76,7 +76,7 @@ {!> ../../../docs_src/body_fields/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/docs/zh/docs/tutorial/body-multiple-params.md b/docs/zh/docs/tutorial/body-multiple-params.md index ee2cba6df..c93ef2f5c 100644 --- a/docs/zh/docs/tutorial/body-multiple-params.md +++ b/docs/zh/docs/tutorial/body-multiple-params.md @@ -20,7 +20,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-21" {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} @@ -35,7 +35,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -68,7 +68,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/body_multiple_params/tutorial002.py!} @@ -124,7 +124,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} @@ -139,7 +139,7 @@ {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -193,7 +193,7 @@ q: str = None {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="28" {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} @@ -208,7 +208,7 @@ q: str = None {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -247,7 +247,7 @@ item: Item = Body(embed=True) {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} @@ -262,7 +262,7 @@ item: Item = Body(embed=True) {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/body-nested-models.md b/docs/zh/docs/tutorial/body-nested-models.md index 7704d2624..c65308bef 100644 --- a/docs/zh/docs/tutorial/body-nested-models.md +++ b/docs/zh/docs/tutorial/body-nested-models.md @@ -12,7 +12,7 @@ {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial001.py!} @@ -63,7 +63,7 @@ my_list: List[str] {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14" {!> ../../../docs_src/body_nested_models/tutorial002.py!} @@ -89,7 +89,7 @@ Python 具有一种特殊的数据类型来保存一组唯一的元素,即 `se {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 14" {!> ../../../docs_src/body_nested_models/tutorial003.py!} @@ -127,7 +127,7 @@ Pydantic 模型的每个属性都具有类型。 {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9-11" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -149,7 +149,7 @@ Pydantic 模型的每个属性都具有类型。 {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial004.py!} @@ -198,7 +198,7 @@ Pydantic 模型的每个属性都具有类型。 {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 10" {!> ../../../docs_src/body_nested_models/tutorial005.py!} @@ -222,7 +222,7 @@ Pydantic 模型的每个属性都具有类型。 {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="20" {!> ../../../docs_src/body_nested_models/tutorial006.py!} @@ -273,7 +273,7 @@ Pydantic 模型的每个属性都具有类型。 {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 14 20 23 27" {!> ../../../docs_src/body_nested_models/tutorial007.py!} @@ -298,7 +298,7 @@ images: List[Image] {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15" {!> ../../../docs_src/body_nested_models/tutorial008.py!} @@ -338,7 +338,7 @@ images: List[Image] {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/body_nested_models/tutorial009.py!} diff --git a/docs/zh/docs/tutorial/body.md b/docs/zh/docs/tutorial/body.md index d00c96dc3..5cf53c0c2 100644 --- a/docs/zh/docs/tutorial/body.md +++ b/docs/zh/docs/tutorial/body.md @@ -23,7 +23,7 @@ {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4" {!> ../../../docs_src/body/tutorial001.py!} @@ -41,7 +41,7 @@ {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="7-11" {!> ../../../docs_src/body/tutorial001.py!} @@ -79,7 +79,7 @@ {!> ../../../docs_src/body/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial001.py!} @@ -142,7 +142,7 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 {!> ../../../docs_src/body/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="21" {!> ../../../docs_src/body/tutorial002.py!} @@ -160,7 +160,7 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 {!> ../../../docs_src/body/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17-18" {!> ../../../docs_src/body/tutorial003.py!} @@ -178,7 +178,7 @@ Pydantic 本身甚至也进行了一些更改以支持此功能。 {!> ../../../docs_src/body/tutorial004_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/body/tutorial004.py!} diff --git a/docs/zh/docs/tutorial/cookie-params.md b/docs/zh/docs/tutorial/cookie-params.md index 470fd8e82..f115f9677 100644 --- a/docs/zh/docs/tutorial/cookie-params.md +++ b/docs/zh/docs/tutorial/cookie-params.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -61,7 +61,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/cookie_params/tutorial001_an.py!} @@ -76,7 +76,7 @@ {!> ../../../docs_src/cookie_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md index f404820df..1866da298 100644 --- a/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md +++ b/docs/zh/docs/tutorial/dependencies/classes-as-dependencies.md @@ -12,7 +12,7 @@ {!> ../../../docs_src/dependencies/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/dependencies/tutorial001.py!} @@ -85,7 +85,7 @@ fluffy = Cat(name="Mr Fluffy") {!> ../../../docs_src/dependencies/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11-15" {!> ../../../docs_src/dependencies/tutorial002.py!} diff --git a/docs/zh/docs/tutorial/encoder.md b/docs/zh/docs/tutorial/encoder.md index 76ed846ce..859ebc2e8 100644 --- a/docs/zh/docs/tutorial/encoder.md +++ b/docs/zh/docs/tutorial/encoder.md @@ -26,7 +26,7 @@ {!> ../../../docs_src/encoder/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="5 22" {!> ../../../docs_src/encoder/tutorial001.py!} diff --git a/docs/zh/docs/tutorial/extra-data-types.md b/docs/zh/docs/tutorial/extra-data-types.md index 76d606903..a74efa61b 100644 --- a/docs/zh/docs/tutorial/extra-data-types.md +++ b/docs/zh/docs/tutorial/extra-data-types.md @@ -67,7 +67,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 3 13-17" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -82,7 +82,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -105,7 +105,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="19-20" {!> ../../../docs_src/extra_data_types/tutorial001_an.py!} @@ -120,7 +120,7 @@ {!> ../../../docs_src/extra_data_types/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/extra-models.md b/docs/zh/docs/tutorial/extra-models.md index 32f8f9df1..06427a73d 100644 --- a/docs/zh/docs/tutorial/extra-models.md +++ b/docs/zh/docs/tutorial/extra-models.md @@ -23,7 +23,7 @@ {!> ../../../docs_src/extra_models/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" {!> ../../../docs_src/extra_models/tutorial001.py!} @@ -164,7 +164,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 15-16 19-20 23-24" {!> ../../../docs_src/extra_models/tutorial002.py!} @@ -188,7 +188,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 14-15 18-20 33" {!> ../../../docs_src/extra_models/tutorial003.py!} @@ -206,7 +206,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial004_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 20" {!> ../../../docs_src/extra_models/tutorial004.py!} @@ -226,7 +226,7 @@ UserInDB( {!> ../../../docs_src/extra_models/tutorial005_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="1 8" {!> ../../../docs_src/extra_models/tutorial005.py!} diff --git a/docs/zh/docs/tutorial/header-params.md b/docs/zh/docs/tutorial/header-params.md index 22ff6dc27..2701167b3 100644 --- a/docs/zh/docs/tutorial/header-params.md +++ b/docs/zh/docs/tutorial/header-params.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -60,7 +60,7 @@ {!> ../../../docs_src/header_params/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial001_an.py!} @@ -75,7 +75,7 @@ {!> ../../../docs_src/header_params/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -120,7 +120,7 @@ {!> ../../../docs_src/header_params/tutorial002_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="12" {!> ../../../docs_src/header_params/tutorial002_an.py!} @@ -135,7 +135,7 @@ {!> ../../../docs_src/header_params/tutorial002_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -170,7 +170,7 @@ {!> ../../../docs_src/header_params/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10" {!> ../../../docs_src/header_params/tutorial003_an.py!} @@ -194,7 +194,7 @@ {!> ../../../docs_src/header_params/tutorial003_py39.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/path-params-numeric-validations.md b/docs/zh/docs/tutorial/path-params-numeric-validations.md index 78fa922b4..9b41ad7cf 100644 --- a/docs/zh/docs/tutorial/path-params-numeric-validations.md +++ b/docs/zh/docs/tutorial/path-params-numeric-validations.md @@ -18,7 +18,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3-4" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -33,7 +33,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -60,7 +60,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="11" {!> ../../../docs_src/path_params_numeric_validations/tutorial001_an.py!} @@ -75,7 +75,7 @@ {!> ../../../docs_src/path_params_numeric_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 @@ -107,7 +107,7 @@ 因此,你可以将函数声明为: -=== "Python 3.6 non-Annotated" +=== "Python 3.8 non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/query-params-str-validations.md b/docs/zh/docs/tutorial/query-params-str-validations.md index 7244aeade..39253eb0d 100644 --- a/docs/zh/docs/tutorial/query-params-str-validations.md +++ b/docs/zh/docs/tutorial/query-params-str-validations.md @@ -10,7 +10,7 @@ {!> ../../../docs_src/query_params_str_validations/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/query_params_str_validations/tutorial001.py!} diff --git a/docs/zh/docs/tutorial/request-files.md b/docs/zh/docs/tutorial/request-files.md index 03474907e..2c48f33ca 100644 --- a/docs/zh/docs/tutorial/request-files.md +++ b/docs/zh/docs/tutorial/request-files.md @@ -130,7 +130,7 @@ contents = myfile.file.read() {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 17" {!> ../../../docs_src/request_files/tutorial001_02.py!} @@ -158,7 +158,7 @@ FastAPI 支持同时上传多个文件。 {!> ../../../docs_src/request_files/tutorial002_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="10 15" {!> ../../../docs_src/request_files/tutorial002.py!} @@ -183,7 +183,7 @@ FastAPI 支持同时上传多个文件。 {!> ../../../docs_src/request_files/tutorial003_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="18" {!> ../../../docs_src/request_files/tutorial003.py!} diff --git a/docs/zh/docs/tutorial/response-model.md b/docs/zh/docs/tutorial/response-model.md index f529cb0d8..e731b6989 100644 --- a/docs/zh/docs/tutorial/response-model.md +++ b/docs/zh/docs/tutorial/response-model.md @@ -20,7 +20,7 @@ {!> ../../../docs_src/response_model/tutorial001_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="17 22 24-27" {!> ../../../docs_src/response_model/tutorial001.py!} @@ -78,7 +78,7 @@ FastAPI 将使用此 `response_model` 来: {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9 11 16" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -92,7 +92,7 @@ FastAPI 将使用此 `response_model` 来: {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24" {!> ../../../docs_src/response_model/tutorial003.py!} @@ -106,7 +106,7 @@ FastAPI 将使用此 `response_model` 来: {!> ../../../docs_src/response_model/tutorial003_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="22" {!> ../../../docs_src/response_model/tutorial003.py!} diff --git a/docs/zh/docs/tutorial/schema-extra-example.md b/docs/zh/docs/tutorial/schema-extra-example.md index 816e8f68e..ebc04da8b 100644 --- a/docs/zh/docs/tutorial/schema-extra-example.md +++ b/docs/zh/docs/tutorial/schema-extra-example.md @@ -16,7 +16,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-23" {!> ../../../docs_src/schema_extra_example/tutorial001.py!} @@ -34,7 +34,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial002_py310.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="4 10-13" {!> ../../../docs_src/schema_extra_example/tutorial002.py!} @@ -61,7 +61,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial003_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23-28" {!> ../../../docs_src/schema_extra_example/tutorial003_an.py!} @@ -76,7 +76,7 @@ {!> ../../../docs_src/schema_extra_example/tutorial003_py310.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/security/first-steps.md b/docs/zh/docs/tutorial/security/first-steps.md index 7b1052e12..dda956417 100644 --- a/docs/zh/docs/tutorial/security/first-steps.md +++ b/docs/zh/docs/tutorial/security/first-steps.md @@ -26,13 +26,13 @@ {!> ../../../docs_src/security/tutorial001_an_py39.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/security/tutorial001_an.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip 尽可能选择使用 `Annotated` 的版本。 diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index 482588f94..8b09dc677 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -258,7 +258,7 @@ connect_args={"check_same_thread": False} {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="3 6-8 11-12 23-24 27-28" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -302,7 +302,7 @@ name: str {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-17 31-34" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -331,7 +331,7 @@ name: str {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15 19-20 31 36-37" {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -471,7 +471,7 @@ current_user.items {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="9" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -505,7 +505,7 @@ current_user.items {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="15-20" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -530,7 +530,7 @@ current_user.items {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="24 32 38 47 53" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -551,7 +551,7 @@ current_user.items {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="23-28 31-34 37-42 45-49 52-55" {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -650,7 +650,7 @@ def read_user(user_id: int, db: Session = Depends(get_db)): {!> ../../../docs_src/sql_databases/sql_app_py39/schemas.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/sql_databases/sql_app/schemas.py!} @@ -670,7 +670,7 @@ def read_user(user_id: int, db: Session = Depends(get_db)): {!> ../../../docs_src/sql_databases/sql_app_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/sql_databases/sql_app/main.py!} @@ -729,7 +729,7 @@ $ uvicorn sql_app.main:app --reload {!> ../../../docs_src/sql_databases/sql_app_py39/alt_main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python hl_lines="14-22" {!> ../../../docs_src/sql_databases/sql_app/alt_main.py!} diff --git a/docs/zh/docs/tutorial/testing.md b/docs/zh/docs/tutorial/testing.md index 41f01f8d8..77fff7596 100644 --- a/docs/zh/docs/tutorial/testing.md +++ b/docs/zh/docs/tutorial/testing.md @@ -122,7 +122,7 @@ {!> ../../../docs_src/app_testing/app_b_an_py39/main.py!} ``` -=== "Python 3.6+" +=== "Python 3.8+" ```Python {!> ../../../docs_src/app_testing/app_b_an/main.py!} @@ -137,7 +137,7 @@ {!> ../../../docs_src/app_testing/app_b_py310/main.py!} ``` -=== "Python 3.6+ non-Annotated" +=== "Python 3.8+ non-Annotated" !!! tip Prefer to use the `Annotated` version if possible. diff --git a/pyproject.toml b/pyproject.toml index 2870b31a5..b49f472d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "fastapi" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.8" license = "MIT" authors = [ { name = "Sebastián Ramírez", email = "tiangolo@gmail.com" }, @@ -32,7 +32,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", From 4ef8c3286dca34ef5821eadd6e692044169beb80 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Oct 2023 05:59:55 +0000 Subject: [PATCH 1375/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3c7ecad43..b7fe5a640 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). ## 0.103.2 From 6b0c77e554ff6462696159899b9fe710a5d6a8fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:18:40 +0400 Subject: [PATCH 1376/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.8.6=20to=201.8.10=20(#10061)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.6 to 1.8.10. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.6...v1.8.10) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5ab0603b9..faa46d751 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.8.6 + uses: pypa/gh-action-pypi-publish@v1.8.10 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From e5fd92a7aba2ce9e3eebce1d683f10858cbbfb34 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Oct 2023 07:19:26 +0000 Subject: [PATCH 1377/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b7fe5a640..f4d546900 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). From d03373f3e803e637017fe4807cbefd08d218f8e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:19:41 +0400 Subject: [PATCH 1378/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/checkout?= =?UTF-8?q?=20from=203=20to=204=20(#10208)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 6 +++--- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/latest-changes.yml | 2 +- .github/workflows/notify-translations.yml | 2 +- .github/workflows/people.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 6 +++--- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index dedf23fb9..a1ce0860a 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -17,7 +17,7 @@ jobs: outputs: docs: ${{ steps.filter.outputs.docs }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # For pull requests it's not necessary to checkout the code but for master it is - uses: dorny/paths-filter@v2 id: filter @@ -35,7 +35,7 @@ jobs: outputs: langs: ${{ steps.show-langs.outputs.langs }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: @@ -71,7 +71,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index dcd6d7107..8af8ba827 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -14,7 +14,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Clean site run: | rm -rf ./site diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index e38870f46..ffec5ee5e 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -24,7 +24,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # To allow latest-changes to commit to the main branch token: ${{ secrets.FASTAPI_LATEST_CHANGES }} diff --git a/.github/workflows/notify-translations.yml b/.github/workflows/notify-translations.yml index 44ee83ec0..c0904ce48 100644 --- a/.github/workflows/notify-translations.yml +++ b/.github/workflows/notify-translations.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Allow debugging with tmate - name: Setup tmate session uses: mxschmitt/action-tmate@v3 diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 4480a1427..b0868771d 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Ref: https://github.com/actions/runner/issues/2033 - name: Fix git safe.directory in container run: mkdir -p /home/runner/work/_temp/_github_home && printf "[safe]\n\tdirectory = /github/workspace" > /home/runner/work/_temp/_github_home/.gitconfig diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index faa46d751..8cbd01b92 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 769b52021..7eccaa0ac 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: @@ -50,7 +50,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: @@ -92,7 +92,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.8' From 89e741765250b907b846192069e05f91d50bcaaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 11:20:09 +0400 Subject: [PATCH 1379/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.27.0=20to=202.28.0=20(#10268)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.27.0 to 2.28.0. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.27.0...v2.28.0) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 8af8ba827..155ebd0a8 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -21,7 +21,7 @@ jobs: mkdir ./site - name: Download Artifact Docs id: download - uses: dawidd6/action-download-artifact@v2.27.0 + uses: dawidd6/action-download-artifact@v2.28.0 with: if_no_artifact_found: ignore github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 4e689d95c..38b44c413 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -24,7 +24,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.27.0 + - uses: dawidd6/action-download-artifact@v2.28.0 with: github_token: ${{ secrets.FASTAPI_SMOKESHOW_DOWNLOAD_ARTIFACTS }} workflow: test.yml From 912e4bb90603860fd5d7dd759c3db02dbfd1addc Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Oct 2023 07:20:20 +0000 Subject: [PATCH 1380/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f4d546900..7be1eda35 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump actions/checkout from 3 to 4. PR [#10208](https://github.com/tiangolo/fastapi/pull/10208) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). From 3fa44aabe37362d4640403d9571897ea7dd5cf9b Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 17 Oct 2023 07:20:59 +0000 Subject: [PATCH 1381/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7be1eda35..4222c4400 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -2,6 +2,7 @@ ## Latest Changes +* ⬆ Bump dawidd6/action-download-artifact from 2.27.0 to 2.28.0. PR [#10268](https://github.com/tiangolo/fastapi/pull/10268) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 3 to 4. PR [#10208](https://github.com/tiangolo/fastapi/pull/10208) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). From 05ca41cfd1c56157cf5bd2b3c1958263bdd71611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 18 Oct 2023 16:36:40 +0400 Subject: [PATCH 1382/1881] =?UTF-8?q?=E2=9C=A8=20Add=20reference=20(code?= =?UTF-8?q?=20API)=20docs=20with=20PEP=20727,=20add=20subclass=20with=20cu?= =?UTF-8?q?stom=20docstrings=20for=20`BackgroundTasks`,=20refactor=20docs?= =?UTF-8?q?=20structure=20(#10392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ➕ Add mkdocstrings and griffe-typingdoc to dependencies * 🔧 Add mkdocstrings configs to MkDocs * 📝 Add first WIP reference page * ⬆️ Upgrade typing-extensions to the minimum version including Doc() * 📝 Add docs to FastAPI parameters * 📝 Add docstrings for OpenAPI docs utils * 📝 Add docstrings for security utils * 📝 Add docstrings for UploadFile * 📝 Update docstrings in FastAPI class * 📝 Add docstrings for path operation methods * 📝 Add docstring for jsonable_encoder * 📝 Add docstrings for exceptions * 📝 Add docstsrings for parameter functions * 📝 Add docstrings for responses * 📝 Add docstrings for APIRouter * ♻️ Sub-class BackgroundTasks to document it with docstrings * 📝 Update usage of background tasks in dependencies * ✅ Update tests with new deprecation warnings * 📝 Add new reference docs * 🔧 Update MkDocs with new reference docs * ✅ Update pytest fixture, deprecation is raised only once * 🎨 Update format for types in exceptions.py * ♻️ Update annotations in BackgroundTask, `Annotated` can't take ParamSpec's P.args or P.kwargs * ✏️ Fix typos caught by @pawamoy * 🔧 Update and fix MkDocstrings configs from @pawamoy tips * 📝 Update reference docs * ✏️ Fix typos found by @pawamoy * ➕ Add HTTPX as a dependency for docs, for the TestClient * 🔧 Update MkDocs config, rename websockets reference * 🔇 Add type-ignores for Doc as the stubs haven't been released for mypy * 🔥 Remove duplicated deprecated notice * 🔇 Remove typing error for unreleased stub in openapi/docs.py * ✅ Add tests for UploadFile for coverage * ⬆️ Upgrade griffe-typingdoc==0.2.2 * 📝 Refactor docs structure * 🔨 Update README generation with new index frontmatter and style * 🔨 Update generation of languages, remove from top menu, keep in lang menu * 📝 Add OpenAPI Pydantic models * 🔨 Update docs script to not translate Reference and Release Notes * 🔧 Add reference for OpenAPI models * 🔧 Update MkDocs config for mkdocstrings insiders * 👷 Install mkdocstring insiders in CI for docs * 🐛 Fix MkDocstrings insiders install URL * ➕ Move dependencies shared by docs and tests to its own requirements file * 👷 Update cache keys for test and docs dependencies * 📝 Remove no longer needed __init__ placeholder docstrings * 📝 Move docstring for APIRouter to the class level (not __init__ level) * 🔥 Remove no longer needed dummy placeholder __init__ docstring --- .github/workflows/build-docs.yml | 14 +- .github/workflows/test.yml | 4 +- docs/en/docs/about/index.md | 3 + docs/en/docs/fastapi-people.md | 5 + docs/en/docs/features.md | 5 + docs/en/docs/help/index.md | 3 + docs/en/docs/index.md | 9 + docs/en/docs/learn/index.md | 5 + docs/en/docs/reference/apirouter.md | 25 + docs/en/docs/reference/background.md | 13 + docs/en/docs/reference/dependencies.md | 32 + docs/en/docs/reference/encoders.md | 3 + docs/en/docs/reference/exceptions.md | 22 + docs/en/docs/reference/fastapi.md | 32 + docs/en/docs/reference/httpconnection.md | 13 + docs/en/docs/reference/index.md | 7 + docs/en/docs/reference/middleware.md | 46 + docs/en/docs/reference/openapi/docs.md | 11 + docs/en/docs/reference/openapi/index.md | 5 + docs/en/docs/reference/openapi/models.md | 5 + docs/en/docs/reference/parameters.md | 36 + docs/en/docs/reference/request.md | 18 + docs/en/docs/reference/response.md | 15 + docs/en/docs/reference/responses.md | 166 + docs/en/docs/reference/security/index.md | 76 + docs/en/docs/reference/staticfiles.md | 14 + docs/en/docs/reference/status.md | 39 + docs/en/docs/reference/templating.md | 14 + docs/en/docs/reference/testclient.md | 14 + docs/en/docs/reference/uploadfile.md | 23 + docs/en/docs/reference/websockets.md | 70 + docs/en/docs/release-notes.md | 5 + docs/en/docs/resources/index.md | 3 + docs/en/mkdocs.yml | 297 +- fastapi/applications.py | 4229 ++++++++++++++++- fastapi/background.py | 60 +- fastapi/datastructures.py | 123 +- fastapi/dependencies/utils.py | 18 +- fastapi/encoders.py | 110 +- fastapi/exceptions.py | 131 +- fastapi/openapi/docs.py | 169 +- fastapi/param_functions.py | 2179 ++++++++- fastapi/responses.py | 14 + fastapi/routing.py | 3483 +++++++++++++- fastapi/security/api_key.py | 227 +- fastapi/security/http.py | 285 +- fastapi/security/oauth2.py | 549 ++- fastapi/security/open_id_connect_url.py | 58 +- pyproject.toml | 2 +- requirements-docs-tests.txt | 3 + requirements-docs.txt | 3 + requirements-tests.txt | 3 +- scripts/docs.py | 7 +- scripts/mkdocs_hooks.py | 8 + tests/test_datastructures.py | 18 + tests/test_router_events.py | 3 + .../test_tutorial001.py | 15 +- .../test_events/test_tutorial001.py | 13 +- .../test_events/test_tutorial002.py | 13 +- .../test_testing/test_tutorial003.py | 4 +- 60 files changed, 11791 insertions(+), 988 deletions(-) create mode 100644 docs/en/docs/about/index.md create mode 100644 docs/en/docs/help/index.md create mode 100644 docs/en/docs/learn/index.md create mode 100644 docs/en/docs/reference/apirouter.md create mode 100644 docs/en/docs/reference/background.md create mode 100644 docs/en/docs/reference/dependencies.md create mode 100644 docs/en/docs/reference/encoders.md create mode 100644 docs/en/docs/reference/exceptions.md create mode 100644 docs/en/docs/reference/fastapi.md create mode 100644 docs/en/docs/reference/httpconnection.md create mode 100644 docs/en/docs/reference/index.md create mode 100644 docs/en/docs/reference/middleware.md create mode 100644 docs/en/docs/reference/openapi/docs.md create mode 100644 docs/en/docs/reference/openapi/index.md create mode 100644 docs/en/docs/reference/openapi/models.md create mode 100644 docs/en/docs/reference/parameters.md create mode 100644 docs/en/docs/reference/request.md create mode 100644 docs/en/docs/reference/response.md create mode 100644 docs/en/docs/reference/responses.md create mode 100644 docs/en/docs/reference/security/index.md create mode 100644 docs/en/docs/reference/staticfiles.md create mode 100644 docs/en/docs/reference/status.md create mode 100644 docs/en/docs/reference/templating.md create mode 100644 docs/en/docs/reference/testclient.md create mode 100644 docs/en/docs/reference/uploadfile.md create mode 100644 docs/en/docs/reference/websockets.md create mode 100644 docs/en/docs/resources/index.md create mode 100644 requirements-docs-tests.txt diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index a1ce0860a..4100781c5 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -44,14 +44,17 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v06 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-tests.txt') }}-v06 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + run: | + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git - name: Export Language Codes id: show-langs run: | @@ -80,13 +83,16 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt') }}-v06 + key: ${{ runner.os }}-python-docs-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-docs.txt', 'requirements-docs-tests.txt') }}-v06 - name: Install docs extras if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' - run: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + run: | + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git + pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git - name: Update Languages run: python ./scripts/docs.py update-languages - uses: actions/cache@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7eccaa0ac..59754525d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v06 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -62,7 +62,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v06 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v06 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt diff --git a/docs/en/docs/about/index.md b/docs/en/docs/about/index.md new file mode 100644 index 000000000..27b78696b --- /dev/null +++ b/docs/en/docs/about/index.md @@ -0,0 +1,3 @@ +# About + +About FastAPI, its design, inspiration and more. 🤓 diff --git a/docs/en/docs/fastapi-people.md b/docs/en/docs/fastapi-people.md index 20caaa1ee..7e26358d8 100644 --- a/docs/en/docs/fastapi-people.md +++ b/docs/en/docs/fastapi-people.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # FastAPI People FastAPI has an amazing community that welcomes people from all backgrounds. diff --git a/docs/en/docs/features.md b/docs/en/docs/features.md index 98f37b534..6f13b03bb 100644 --- a/docs/en/docs/features.md +++ b/docs/en/docs/features.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # Features ## FastAPI features diff --git a/docs/en/docs/help/index.md b/docs/en/docs/help/index.md new file mode 100644 index 000000000..5ee7df2fe --- /dev/null +++ b/docs/en/docs/help/index.md @@ -0,0 +1,3 @@ +# Help + +Help and get help, contribute, get involved. 🤝 diff --git a/docs/en/docs/index.md b/docs/en/docs/index.md index cd3f3e000..3660e74e3 100644 --- a/docs/en/docs/index.md +++ b/docs/en/docs/index.md @@ -1,3 +1,12 @@ +--- +hide: + - navigation +--- + + +

FastAPI

diff --git a/docs/en/docs/learn/index.md b/docs/en/docs/learn/index.md new file mode 100644 index 000000000..d056fb320 --- /dev/null +++ b/docs/en/docs/learn/index.md @@ -0,0 +1,5 @@ +# Learn + +Here are the introductory sections and the tutorials to learn **FastAPI**. + +You could consider this a **book**, a **course**, the **official** and recommended way to learn FastAPI. 😎 diff --git a/docs/en/docs/reference/apirouter.md b/docs/en/docs/reference/apirouter.md new file mode 100644 index 000000000..b779ad291 --- /dev/null +++ b/docs/en/docs/reference/apirouter.md @@ -0,0 +1,25 @@ +# `APIRouter` class + +Here's the reference information for the `APIRouter` class, with all its parameters, +attributes and methods. + +You can import the `APIRouter` class directly from `fastapi`: + +```python +from fastapi import APIRouter +``` + +::: fastapi.APIRouter + options: + members: + - websocket + - include_router + - get + - put + - post + - delete + - options + - head + - patch + - trace + - on_event diff --git a/docs/en/docs/reference/background.md b/docs/en/docs/reference/background.md new file mode 100644 index 000000000..e0c0be899 --- /dev/null +++ b/docs/en/docs/reference/background.md @@ -0,0 +1,13 @@ +# Background Tasks - `BackgroundTasks` + +You can declare a parameter in a *path operation function* or dependency function +with the type `BackgroundTasks`, and then you can use it to schedule the execution +of background tasks after the response is sent. + +You can import it directly from `fastapi`: + +```python +from fastapi import BackgroundTasks +``` + +::: fastapi.BackgroundTasks diff --git a/docs/en/docs/reference/dependencies.md b/docs/en/docs/reference/dependencies.md new file mode 100644 index 000000000..95cd101e4 --- /dev/null +++ b/docs/en/docs/reference/dependencies.md @@ -0,0 +1,32 @@ +# Dependencies - `Depends()` and `Security()` + +## `Depends()` + +Dependencies are handled mainly with the special function `Depends()` that takes a +callable. + +Here is the reference for it and its parameters. + +You can import it directly from `fastapi`: + +```python +from fastapi import Depends +``` + +::: fastapi.Depends + +## `Security()` + +For many scenarios, you can handle security (authorization, authentication, etc.) with +dependendencies, using `Depends()`. + +But when you want to also declare OAuth2 scopes, you can use `Security()` instead of +`Depends()`. + +You can import `Security()` directly from `fastapi`: + +```python +from fastapi import Security +``` + +::: fastapi.Security diff --git a/docs/en/docs/reference/encoders.md b/docs/en/docs/reference/encoders.md new file mode 100644 index 000000000..28df2e43a --- /dev/null +++ b/docs/en/docs/reference/encoders.md @@ -0,0 +1,3 @@ +# Encoders - `jsonable_encoder` + +::: fastapi.encoders.jsonable_encoder diff --git a/docs/en/docs/reference/exceptions.md b/docs/en/docs/reference/exceptions.md new file mode 100644 index 000000000..adc9b91ce --- /dev/null +++ b/docs/en/docs/reference/exceptions.md @@ -0,0 +1,22 @@ +# Exceptions - `HTTPException` and `WebSocketException` + +These are the exceptions that you can raise to show errors to the client. + +When you raise an exception, as would happen with normal Python, the rest of the +excecution is aborted. This way you can raise these exceptions from anywhere in the +code to abort a request and show the error to the client. + +You can use: + +* `HTTPException` +* `WebSocketException` + +These exceptions can be imported directly from `fastapi`: + +```python +from fastapi import HTTPException, WebSocketException +``` + +::: fastapi.HTTPException + +::: fastapi.WebSocketException diff --git a/docs/en/docs/reference/fastapi.md b/docs/en/docs/reference/fastapi.md new file mode 100644 index 000000000..8b87664cb --- /dev/null +++ b/docs/en/docs/reference/fastapi.md @@ -0,0 +1,32 @@ +# `FastAPI` class + +Here's the reference information for the `FastAPI` class, with all its parameters, +attributes and methods. + +You can import the `FastAPI` class directly from `fastapi`: + +```python +from fastapi import FastAPI +``` + +::: fastapi.FastAPI + options: + members: + - openapi_version + - webhooks + - state + - dependency_overrides + - openapi + - websocket + - include_router + - get + - put + - post + - delete + - options + - head + - patch + - trace + - on_event + - middleware + - exception_handler diff --git a/docs/en/docs/reference/httpconnection.md b/docs/en/docs/reference/httpconnection.md new file mode 100644 index 000000000..43dfc46f9 --- /dev/null +++ b/docs/en/docs/reference/httpconnection.md @@ -0,0 +1,13 @@ +# `HTTPConnection` class + +When you want to define dependencies that should be compatible with both HTTP and +WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a +`Request` or a `WebSocket`. + +You can import it from `fastapi.requests`: + +```python +from fastapi.requests import HTTPConnection +``` + +::: fastapi.requests.HTTPConnection diff --git a/docs/en/docs/reference/index.md b/docs/en/docs/reference/index.md new file mode 100644 index 000000000..994b3c38c --- /dev/null +++ b/docs/en/docs/reference/index.md @@ -0,0 +1,7 @@ +# Reference - Code API + +Here's the reference or code API, the classes, functions, parameters, attributes, and +all the FastAPI parts you can use in you applications. + +If you want to **learn FastAPI** you are much better off reading the +[FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/). diff --git a/docs/en/docs/reference/middleware.md b/docs/en/docs/reference/middleware.md new file mode 100644 index 000000000..89704d3c8 --- /dev/null +++ b/docs/en/docs/reference/middleware.md @@ -0,0 +1,46 @@ +# Middleware + +There are several middlewares available provided by Starlette directly. + +Read more about them in the +[FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/). + +::: fastapi.middleware.cors.CORSMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.cors import CORSMiddleware +``` + +::: fastapi.middleware.gzip.GZipMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.gzip import GZipMiddleware +``` + +::: fastapi.middleware.httpsredirect.HTTPSRedirectMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware +``` + +::: fastapi.middleware.trustedhost.TrustedHostMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.trustedhost import TrustedHostMiddleware +``` + +::: fastapi.middleware.wsgi.WSGIMiddleware + +It can be imported from `fastapi`: + +```python +from fastapi.middleware.wsgi import WSGIMiddleware +``` diff --git a/docs/en/docs/reference/openapi/docs.md b/docs/en/docs/reference/openapi/docs.md new file mode 100644 index 000000000..ab620833e --- /dev/null +++ b/docs/en/docs/reference/openapi/docs.md @@ -0,0 +1,11 @@ +# OpenAPI `docs` + +Utilities to handle OpenAPI automatic UI documentation, including Swagger UI (by default at `/docs`) and ReDoc (by default at `/redoc`). + +::: fastapi.openapi.docs.get_swagger_ui_html + +::: fastapi.openapi.docs.get_redoc_html + +::: fastapi.openapi.docs.get_swagger_ui_oauth2_redirect_html + +::: fastapi.openapi.docs.swagger_ui_default_parameters diff --git a/docs/en/docs/reference/openapi/index.md b/docs/en/docs/reference/openapi/index.md new file mode 100644 index 000000000..e2b313f15 --- /dev/null +++ b/docs/en/docs/reference/openapi/index.md @@ -0,0 +1,5 @@ +# OpenAPI + +There are several utilities to handle OpenAPI. + +You normally don't need to use them unless you have a specific advanced use case that requires it. diff --git a/docs/en/docs/reference/openapi/models.md b/docs/en/docs/reference/openapi/models.md new file mode 100644 index 000000000..4a6b0770e --- /dev/null +++ b/docs/en/docs/reference/openapi/models.md @@ -0,0 +1,5 @@ +# OpenAPI `models` + +OpenAPI Pydantic models used to generate and validate the generated OpenAPI. + +::: fastapi.openapi.models diff --git a/docs/en/docs/reference/parameters.md b/docs/en/docs/reference/parameters.md new file mode 100644 index 000000000..8f77f0161 --- /dev/null +++ b/docs/en/docs/reference/parameters.md @@ -0,0 +1,36 @@ +# Request Parameters + +Here's the reference information for the request parameters. + +These are the special functions that you can put in *path operation function* +parameters or dependency functions with `Annotated` to get data from the request. + +It includes: + +* `Query()` +* `Path()` +* `Body()` +* `Cookie()` +* `Header()` +* `Form()` +* `File()` + +You can import them all directly from `fastapi`: + +```python +from fastapi import Body, Cookie, File, Form, Header, Path, Query +``` + +::: fastapi.Query + +::: fastapi.Path + +::: fastapi.Body + +::: fastapi.Cookie + +::: fastapi.Header + +::: fastapi.Form + +::: fastapi.File diff --git a/docs/en/docs/reference/request.md b/docs/en/docs/reference/request.md new file mode 100644 index 000000000..91ec7d37b --- /dev/null +++ b/docs/en/docs/reference/request.md @@ -0,0 +1,18 @@ +# `Request` class + +You can declare a parameter in a *path operation function* or dependency to be of type +`Request` and then you can access the raw request object directly, without any +validation, etc. + +You can import it directly from `fastapi`: + +```python +from fastapi import Request +``` + +!!! tip + When you want to define dependencies that should be compatible with both HTTP and + WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a + `Request` or a `WebSocket`. + +::: fastapi.Request diff --git a/docs/en/docs/reference/response.md b/docs/en/docs/reference/response.md new file mode 100644 index 000000000..916254583 --- /dev/null +++ b/docs/en/docs/reference/response.md @@ -0,0 +1,15 @@ +# `Response` class + +You can declare a parameter in a *path operation function* or dependency to be of type +`Response` and then you can set data for the response like headers or cookies. + +You can also use it directly to create an instance of it and return it from your *path +operations*. + +You can import it directly from `fastapi`: + +```python +from fastapi import Response +``` + +::: fastapi.Response diff --git a/docs/en/docs/reference/responses.md b/docs/en/docs/reference/responses.md new file mode 100644 index 000000000..2cbbd8963 --- /dev/null +++ b/docs/en/docs/reference/responses.md @@ -0,0 +1,166 @@ +# Custom Response Classes - File, HTML, Redirect, Streaming, etc. + +There are several custom response classes you can use to create an instance and return +them directly from your *path operations*. + +Read more about it in the +[FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + +You can import them directly from `fastapi.responses`: + +```python +from fastapi.responses import ( + FileResponse, + HTMLResponse, + JSONResponse, + ORJSONResponse, + PlainTextResponse, + RedirectResponse, + Response, + StreamingResponse, + UJSONResponse, +) +``` + +## FastAPI Responses + +There are a couple of custom FastAPI response classes, you can use them to optimize JSON performance. + +::: fastapi.responses.UJSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.ORJSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +## Starlette Responses + +::: fastapi.responses.FileResponse + options: + members: + - chunk_size + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.HTMLResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.JSONResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.PlainTextResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.RedirectResponse + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.Response + options: + members: + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie + +::: fastapi.responses.StreamingResponse + options: + members: + - body_iterator + - charset + - status_code + - media_type + - body + - background + - raw_headers + - render + - init_headers + - headers + - set_cookie + - delete_cookie diff --git a/docs/en/docs/reference/security/index.md b/docs/en/docs/reference/security/index.md new file mode 100644 index 000000000..ff86e9e30 --- /dev/null +++ b/docs/en/docs/reference/security/index.md @@ -0,0 +1,76 @@ +# Security Tools + +When you need to declare dependencies with OAuth2 scopes you use `Security()`. + +But you still need to define what is the dependable, the callable that you pass as +a parameter to `Depends()` or `Security()`. + +There are multiple tools that you can use to create those dependables, and they get +integrated into OpenAPI so they are shown in the automatic docs UI, they can be used +by automatically generated clients and SDKs, etc. + +You can import them from `fastapi.security`: + +```python +from fastapi.security import ( + APIKeyCookie, + APIKeyHeader, + APIKeyQuery, + HTTPAuthorizationCredentials, + HTTPBasic, + HTTPBasicCredentials, + HTTPBearer, + HTTPDigest, + OAuth2, + OAuth2AuthorizationCodeBearer, + OAuth2PasswordBearer, + OAuth2PasswordRequestForm, + OAuth2PasswordRequestFormStrict, + OpenIdConnect, + SecurityScopes, +) +``` + +## API Key Security Schemes + +::: fastapi.security.APIKeyCookie + +::: fastapi.security.APIKeyHeader + +::: fastapi.security.APIKeyQuery + +## HTTP Authentication Schemes + +::: fastapi.security.HTTPBasic + +::: fastapi.security.HTTPBearer + +::: fastapi.security.HTTPDigest + +## HTTP Credentials + +::: fastapi.security.HTTPAuthorizationCredentials + +::: fastapi.security.HTTPBasicCredentials + +## OAuth2 Authentication + +::: fastapi.security.OAuth2 + +::: fastapi.security.OAuth2AuthorizationCodeBearer + +::: fastapi.security.OAuth2PasswordBearer + +## OAuth2 Password Form + +::: fastapi.security.OAuth2PasswordRequestForm + +::: fastapi.security.OAuth2PasswordRequestFormStrict + +## OAuth2 Security Scopes in Dependencies + +::: fastapi.security.SecurityScopes + +## OpenID Connect + +::: fastapi.security.OpenIdConnect diff --git a/docs/en/docs/reference/staticfiles.md b/docs/en/docs/reference/staticfiles.md new file mode 100644 index 000000000..ce66f17b3 --- /dev/null +++ b/docs/en/docs/reference/staticfiles.md @@ -0,0 +1,14 @@ +# Static Files - `StaticFiles` + +You can use the `StaticFiles` class to serve static files, like JavaScript, CSS, images, etc. + +Read more about it in the +[FastAPI docs for Static Files](https://fastapi.tiangolo.com/tutorial/static-files/). + +You can import it directly from `fastapi.staticfiles`: + +```python +from fastapi.staticfiles import StaticFiles +``` + +::: fastapi.staticfiles.StaticFiles diff --git a/docs/en/docs/reference/status.md b/docs/en/docs/reference/status.md new file mode 100644 index 000000000..54fba9387 --- /dev/null +++ b/docs/en/docs/reference/status.md @@ -0,0 +1,39 @@ +# Status Codes + +You can import the `status` module from `fastapi`: + +```python +from fastapi import status +``` + +`status` is provided directly by Starlette. + +It containes a group of named constants (variables) with integer status codes. + +For example: + +* 200: `status.HTTP_200_OK` +* 403: `status.HTTP_403_FORBIDDEN` +* etc. + +It can be convenient to quickly access HTTP (and WebSocket) status codes in your app, +using autocompletion for the name without having to remember the integer status codes +by memory. + +Read more about it in the +[FastAPI docs about Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + +## Example + +```python +from fastapi import FastAPI, status + +app = FastAPI() + + +@app.get("/items/", status_code=status.HTTP_418_IM_A_TEAPOT) +def read_items(): + return [{"name": "Plumbus"}, {"name": "Portal Gun"}] +``` + +::: fastapi.status diff --git a/docs/en/docs/reference/templating.md b/docs/en/docs/reference/templating.md new file mode 100644 index 000000000..c865badfc --- /dev/null +++ b/docs/en/docs/reference/templating.md @@ -0,0 +1,14 @@ +# Templating - `Jinja2Templates` + +You can use the `Jinja2Templates` class to render Jinja templates. + +Read more about it in the +[FastAPI docs for Templates](https://fastapi.tiangolo.com/advanced/templates/). + +You can import it directly from `fastapi.templating`: + +```python +from fastapi.templating import Jinja2Templates +``` + +::: fastapi.templating.Jinja2Templates diff --git a/docs/en/docs/reference/testclient.md b/docs/en/docs/reference/testclient.md new file mode 100644 index 000000000..e391d964a --- /dev/null +++ b/docs/en/docs/reference/testclient.md @@ -0,0 +1,14 @@ +# Test Client - `TestClient` + +You can use the `TestClient` class to test FastAPI applications without creating an actual HTTP and socket connection, just communicating directly with the FastAPI code. + +Read more about it in the +[FastAPI docs for Testing](https://fastapi.tiangolo.com/tutorial/testing/). + +You can import it directly from `fastapi.testclient`: + +```python +from fastapi.testclient import TestClient +``` + +::: fastapi.testclient.TestClient diff --git a/docs/en/docs/reference/uploadfile.md b/docs/en/docs/reference/uploadfile.md new file mode 100644 index 000000000..45c644b18 --- /dev/null +++ b/docs/en/docs/reference/uploadfile.md @@ -0,0 +1,23 @@ +# `UploadFile` class + +You can define *path operation function* parameters to be of the type `UploadFile` +to receive files from the request. + +You can import it directly from `fastapi`: + +```python +from fastapi import UploadFile +``` + +::: fastapi.UploadFile + options: + members: + - file + - filename + - size + - headers + - content_type + - read + - write + - seek + - close diff --git a/docs/en/docs/reference/websockets.md b/docs/en/docs/reference/websockets.md new file mode 100644 index 000000000..2a0469467 --- /dev/null +++ b/docs/en/docs/reference/websockets.md @@ -0,0 +1,70 @@ +# WebSockets + +When defining WebSockets, you normally declare a parameter of type `WebSocket` and +with it you can read data from the client and send data to it. + +It is provided directly by Starlette, but you can import it from `fastapi`: + +```python +from fastapi import WebSocket +``` + +!!! tip + When you want to define dependencies that should be compatible with both HTTP and + WebSockets, you can define a parameter that takes an `HTTPConnection` instead of a + `Request` or a `WebSocket`. + +::: fastapi.WebSocket + options: + members: + - scope + - app + - url + - base_url + - headers + - query_params + - path_params + - cookies + - client + - state + - url_for + - client_state + - application_state + - receive + - send + - accept + - receive_text + - receive_bytes + - receive_json + - iter_text + - iter_bytes + - iter_json + - send_text + - send_bytes + - send_json + - close + +When a client disconnects, a `WebSocketDisconnect` exception is raised, you can catch +it. + +You can import it directly form `fastapi`: + +```python +from fastapi import WebSocketDisconnect +``` + +::: fastapi.WebSocketDisconnect + +## WebSockets - additional classes + +Additional classes for handling WebSockets. + +Provided directly by Starlette, but you can import it from `fastapi`: + +```python +from fastapi.websockets import WebSocketDisconnect, WebSocketState +``` + +::: fastapi.websockets.WebSocketDisconnect + +::: fastapi.websockets.WebSocketState diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 4222c4400..62ffd4b35 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # Release Notes ## Latest Changes diff --git a/docs/en/docs/resources/index.md b/docs/en/docs/resources/index.md new file mode 100644 index 000000000..8c7cac43b --- /dev/null +++ b/docs/en/docs/resources/index.md @@ -0,0 +1,3 @@ +# Resources + +Additional resources, external links, articles and more. ✈️ diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index ba1ac7924..a64eff269 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -30,6 +30,7 @@ theme: - content.code.annotate - content.code.copy - content.code.select + - navigation.tabs icon: repo: fontawesome/brands/github-alt logo: img/icon-white.svg @@ -52,142 +53,174 @@ plugins: advanced/custom-request-and-route.md: how-to/custom-request-and-route.md advanced/conditional-openapi.md: how-to/conditional-openapi.md advanced/extending-openapi.md: how-to/extending-openapi.md + mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_typingdoc + show_root_heading: true + show_if_no_docstring: true + preload_modules: [httpx, starlette] + inherited_members: true + members_order: source + separate_signature: true + unwrap_annotated: true + filters: ["!^_"] + merge_init_into_class: true + docstring_section_style: spacy + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true nav: - FastAPI: index.md -- Languages: - - en: / - - de: /de/ - - em: /em/ - - es: /es/ - - fa: /fa/ - - fr: /fr/ - - he: /he/ - - id: /id/ - - ja: /ja/ - - ko: /ko/ - - pl: /pl/ - - pt: /pt/ - - ru: /ru/ - - tr: /tr/ - - uk: /uk/ - - ur: /ur/ - - vi: /vi/ - - yo: /yo/ - - zh: /zh/ - features.md +- Learn: + - learn/index.md + - python-types.md + - async.md + - Tutorial - User Guide: + - tutorial/index.md + - tutorial/first-steps.md + - tutorial/path-params.md + - tutorial/query-params.md + - tutorial/body.md + - tutorial/query-params-str-validations.md + - tutorial/path-params-numeric-validations.md + - tutorial/body-multiple-params.md + - tutorial/body-fields.md + - tutorial/body-nested-models.md + - tutorial/schema-extra-example.md + - tutorial/extra-data-types.md + - tutorial/cookie-params.md + - tutorial/header-params.md + - tutorial/response-model.md + - tutorial/extra-models.md + - tutorial/response-status-code.md + - tutorial/request-forms.md + - tutorial/request-files.md + - tutorial/request-forms-and-files.md + - tutorial/handling-errors.md + - tutorial/path-operation-configuration.md + - tutorial/encoder.md + - tutorial/body-updates.md + - Dependencies: + - tutorial/dependencies/index.md + - tutorial/dependencies/classes-as-dependencies.md + - tutorial/dependencies/sub-dependencies.md + - tutorial/dependencies/dependencies-in-path-operation-decorators.md + - tutorial/dependencies/global-dependencies.md + - tutorial/dependencies/dependencies-with-yield.md + - Security: + - tutorial/security/index.md + - tutorial/security/first-steps.md + - tutorial/security/get-current-user.md + - tutorial/security/simple-oauth2.md + - tutorial/security/oauth2-jwt.md + - tutorial/middleware.md + - tutorial/cors.md + - tutorial/sql-databases.md + - tutorial/bigger-applications.md + - tutorial/background-tasks.md + - tutorial/metadata.md + - tutorial/static-files.md + - tutorial/testing.md + - tutorial/debugging.md + - Advanced User Guide: + - advanced/index.md + - advanced/path-operation-advanced-configuration.md + - advanced/additional-status-codes.md + - advanced/response-directly.md + - advanced/custom-response.md + - advanced/additional-responses.md + - advanced/response-cookies.md + - advanced/response-headers.md + - advanced/response-change-status-code.md + - advanced/advanced-dependencies.md + - Advanced Security: + - advanced/security/index.md + - advanced/security/oauth2-scopes.md + - advanced/security/http-basic-auth.md + - advanced/using-request-directly.md + - advanced/dataclasses.md + - advanced/middleware.md + - advanced/sub-applications.md + - advanced/behind-a-proxy.md + - advanced/templates.md + - advanced/websockets.md + - advanced/events.md + - advanced/testing-websockets.md + - advanced/testing-events.md + - advanced/testing-dependencies.md + - advanced/testing-database.md + - advanced/async-tests.md + - advanced/settings.md + - advanced/openapi-callbacks.md + - advanced/openapi-webhooks.md + - advanced/wsgi.md + - advanced/generate-clients.md + - Deployment: + - deployment/index.md + - deployment/versions.md + - deployment/https.md + - deployment/manually.md + - deployment/concepts.md + - deployment/cloud.md + - deployment/server-workers.md + - deployment/docker.md + - How To - Recipes: + - how-to/index.md + - how-to/general.md + - how-to/sql-databases-peewee.md + - how-to/async-sql-encode-databases.md + - how-to/nosql-databases-couchbase.md + - how-to/graphql.md + - how-to/custom-request-and-route.md + - how-to/conditional-openapi.md + - how-to/extending-openapi.md + - how-to/separate-openapi-schemas.md + - how-to/custom-docs-ui-assets.md + - how-to/configure-swagger-ui.md +- Reference (Code API): + - reference/index.md + - reference/fastapi.md + - reference/parameters.md + - reference/status.md + - reference/uploadfile.md + - reference/exceptions.md + - reference/dependencies.md + - reference/apirouter.md + - reference/background.md + - reference/request.md + - reference/websockets.md + - reference/httpconnection.md + - reference/response.md + - reference/responses.md + - reference/middleware.md + - OpenAPI: + - reference/openapi/index.md + - reference/openapi/docs.md + - reference/openapi/models.md + - reference/security/index.md + - reference/encoders.md + - reference/staticfiles.md + - reference/templating.md + - reference/testclient.md - fastapi-people.md -- python-types.md -- Tutorial - User Guide: - - tutorial/index.md - - tutorial/first-steps.md - - tutorial/path-params.md - - tutorial/query-params.md - - tutorial/body.md - - tutorial/query-params-str-validations.md - - tutorial/path-params-numeric-validations.md - - tutorial/body-multiple-params.md - - tutorial/body-fields.md - - tutorial/body-nested-models.md - - tutorial/schema-extra-example.md - - tutorial/extra-data-types.md - - tutorial/cookie-params.md - - tutorial/header-params.md - - tutorial/response-model.md - - tutorial/extra-models.md - - tutorial/response-status-code.md - - tutorial/request-forms.md - - tutorial/request-files.md - - tutorial/request-forms-and-files.md - - tutorial/handling-errors.md - - tutorial/path-operation-configuration.md - - tutorial/encoder.md - - tutorial/body-updates.md - - Dependencies: - - tutorial/dependencies/index.md - - tutorial/dependencies/classes-as-dependencies.md - - tutorial/dependencies/sub-dependencies.md - - tutorial/dependencies/dependencies-in-path-operation-decorators.md - - tutorial/dependencies/global-dependencies.md - - tutorial/dependencies/dependencies-with-yield.md - - Security: - - tutorial/security/index.md - - tutorial/security/first-steps.md - - tutorial/security/get-current-user.md - - tutorial/security/simple-oauth2.md - - tutorial/security/oauth2-jwt.md - - tutorial/middleware.md - - tutorial/cors.md - - tutorial/sql-databases.md - - tutorial/bigger-applications.md - - tutorial/background-tasks.md - - tutorial/metadata.md - - tutorial/static-files.md - - tutorial/testing.md - - tutorial/debugging.md -- Advanced User Guide: - - advanced/index.md - - advanced/path-operation-advanced-configuration.md - - advanced/additional-status-codes.md - - advanced/response-directly.md - - advanced/custom-response.md - - advanced/additional-responses.md - - advanced/response-cookies.md - - advanced/response-headers.md - - advanced/response-change-status-code.md - - advanced/advanced-dependencies.md - - Advanced Security: - - advanced/security/index.md - - advanced/security/oauth2-scopes.md - - advanced/security/http-basic-auth.md - - advanced/using-request-directly.md - - advanced/dataclasses.md - - advanced/middleware.md - - advanced/sub-applications.md - - advanced/behind-a-proxy.md - - advanced/templates.md - - advanced/websockets.md - - advanced/events.md - - advanced/testing-websockets.md - - advanced/testing-events.md - - advanced/testing-dependencies.md - - advanced/testing-database.md - - advanced/async-tests.md - - advanced/settings.md - - advanced/openapi-callbacks.md - - advanced/openapi-webhooks.md - - advanced/wsgi.md - - advanced/generate-clients.md -- async.md -- Deployment: - - deployment/index.md - - deployment/versions.md - - deployment/https.md - - deployment/manually.md - - deployment/concepts.md - - deployment/cloud.md - - deployment/server-workers.md - - deployment/docker.md -- How To - Recipes: - - how-to/index.md - - how-to/general.md - - how-to/sql-databases-peewee.md - - how-to/async-sql-encode-databases.md - - how-to/nosql-databases-couchbase.md - - how-to/graphql.md - - how-to/custom-request-and-route.md - - how-to/conditional-openapi.md - - how-to/extending-openapi.md - - how-to/separate-openapi-schemas.md - - how-to/custom-docs-ui-assets.md - - how-to/configure-swagger-ui.md -- project-generation.md -- alternatives.md -- history-design-future.md -- external-links.md -- benchmarks.md -- help-fastapi.md -- newsletter.md -- contributing.md +- Resources: + - resources/index.md + - project-generation.md + - external-links.md + - newsletter.md +- About: + - about/index.md + - alternatives.md + - history-design-future.md + - benchmarks.md +- Help: + - help/index.md + - help-fastapi.md + - contributing.md - release-notes.md markdown_extensions: toc: diff --git a/fastapi/applications.py b/fastapi/applications.py index 5cc568292..46b7ae81b 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -43,57 +43,792 @@ from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] AppType = TypeVar("AppType", bound="FastAPI") class FastAPI(Starlette): + """ + `FastAPI` app class, the main entrypoint to use FastAPI. + + Read more in the + [FastAPI docs for First Steps](https://fastapi.tiangolo.com/tutorial/first-steps/). + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + ``` + """ + def __init__( self: AppType, *, - debug: bool = False, - routes: Optional[List[BaseRoute]] = None, - title: str = "FastAPI", - summary: Optional[str] = None, - description: str = "", - version: str = "0.1.0", - openapi_url: Optional[str] = "/openapi.json", - openapi_tags: Optional[List[Dict[str, Any]]] = None, - servers: Optional[List[Dict[str, Union[str, Any]]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - redirect_slashes: bool = True, - docs_url: Optional[str] = "/docs", - redoc_url: Optional[str] = "/redoc", - swagger_ui_oauth2_redirect_url: Optional[str] = "/docs/oauth2-redirect", - swagger_ui_init_oauth: Optional[Dict[str, Any]] = None, - middleware: Optional[Sequence[Middleware]] = None, - exception_handlers: Optional[ - Dict[ - Union[int, Type[Exception]], - Callable[[Request, Any], Coroutine[Any, Any, Response]], - ] + debug: Annotated[ + bool, + Doc( + """ + Boolean indicating if debug tracebacks should be returned on server + errors. + + Read more in the + [Starlette docs for Applications](https://www.starlette.io/applications/#instantiating-the-application). + """ + ), + ] = False, + routes: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + **Note**: you probably shouldn't use this parameter, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation* decorators, + like: + + * `app.get()` + * `app.post()` + * etc. + + --- + + A list of routes to serve incoming HTTP and WebSocket requests. + """ + ), + deprecated( + """ + You normally wouldn't use this parameter with FastAPI, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation methods*, + like `app.get()`, `app.post()`, etc. + """ + ), ] = None, - on_startup: Optional[Sequence[Callable[[], Any]]] = None, - on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, - lifespan: Optional[Lifespan[AppType]] = None, - terms_of_service: Optional[str] = None, - contact: Optional[Dict[str, Union[str, Any]]] = None, - license_info: Optional[Dict[str, Union[str, Any]]] = None, - openapi_prefix: str = "", - root_path: str = "", - root_path_in_servers: bool = True, - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - webhooks: Optional[routing.APIRouter] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - swagger_ui_parameters: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), - separate_input_output_schemas: bool = True, - **extra: Any, + title: Annotated[ + str, + Doc( + """ + The title of the API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(title="ChimichangApp") + ``` + """ + ), + ] = "FastAPI", + summary: Annotated[ + Optional[str], + Doc( + """ + A short summary of the API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(summary="Deadpond's favorite app. Nuff said.") + ``` + """ + ), + ] = None, + description: Annotated[ + str, + Doc( + ''' + A description of the API. Supports Markdown (using + [CommonMark syntax](https://commonmark.org/)). + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI( + description=""" + ChimichangApp API helps you do awesome stuff. 🚀 + + ## Items + + You can **read items**. + + ## Users + + You will be able to: + + * **Create users** (_not implemented_). + * **Read users** (_not implemented_). + + """ + ) + ``` + ''' + ), + ] = "", + version: Annotated[ + str, + Doc( + """ + The version of the API. + + **Note** This is the version of your application, not the version of + the OpenAPI specification nor the version of FastAPI being used. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(version="0.0.1") + ``` + """ + ), + ] = "0.1.0", + openapi_url: Annotated[ + Optional[str], + Doc( + """ + The URL where the OpenAPI schema will be served from. + + If you set it to `None`, no OpenAPI schema will be served publicly, and + the default automatic endpoints `/docs` and `/redoc` will also be + disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#openapi-url). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(openapi_url="/api/v1/openapi.json") + ``` + """ + ), + ] = "/openapi.json", + openapi_tags: Annotated[ + Optional[List[Dict[str, Any]]], + Doc( + """ + A list of tags used by OpenAPI, these are the same `tags` you can set + in the *path operations*, like: + + * `@app.get("/users/", tags=["users"])` + * `@app.get("/items/", tags=["items"])` + + The order of the tags can be used to specify the order shown in + tools like Swagger UI, used in the automatic path `/docs`. + + It's not required to specify all the tags used. + + The tags that are not declared MAY be organized randomly or based + on the tools' logic. Each tag name in the list MUST be unique. + + The value of each item is a `dict` containing: + + * `name`: The name of the tag. + * `description`: A short description of the tag. + [CommonMark syntax](https://commonmark.org/) MAY be used for rich + text representation. + * `externalDocs`: Additional external documentation for this tag. If + provided, it would contain a `dict` with: + * `description`: A short description of the target documentation. + [CommonMark syntax](https://commonmark.org/) MAY be used for + rich text representation. + * `url`: The URL for the target documentation. Value MUST be in + the form of a URL. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-tags). + + **Example** + + ```python + from fastapi import FastAPI + + tags_metadata = [ + { + "name": "users", + "description": "Operations with users. The **login** logic is also here.", + }, + { + "name": "items", + "description": "Manage items. So _fancy_ they have their own docs.", + "externalDocs": { + "description": "Items external docs", + "url": "https://fastapi.tiangolo.com/", + }, + }, + ] + + app = FastAPI(openapi_tags=tags_metadata) + ``` + """ + ), + ] = None, + servers: Annotated[ + Optional[List[Dict[str, Union[str, Any]]]], + Doc( + """ + A `list` of `dict`s with connectivity information to a target server. + + You would use it, for example, if your application is served from + different domains and you want to use the same Swagger UI in the + browser to interact with each of them (instead of having multiple + browser tabs open). Or if you want to leave fixed the possible URLs. + + If the servers `list` is not provided, or is an empty `list`, the + default value would be a a `dict` with a `url` value of `/`. + + Each item in the `list` is a `dict` containing: + + * `url`: A URL to the target host. This URL supports Server Variables + and MAY be relative, to indicate that the host location is relative + to the location where the OpenAPI document is being served. Variable + substitutions will be made when a variable is named in `{`brackets`}`. + * `description`: An optional string describing the host designated by + the URL. [CommonMark syntax](https://commonmark.org/) MAY be used for + rich text representation. + * `variables`: A `dict` between a variable name and its value. The value + is used for substitution in the server's URL template. + + Read more in the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#additional-servers). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI( + servers=[ + {"url": "https://stag.example.com", "description": "Staging environment"}, + {"url": "https://prod.example.com", "description": "Production environment"}, + ] + ) + ``` + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of global dependencies, they will be applied to each + *path operation*, including in sub-routers. + + Read more about it in the + [FastAPI docs for Global Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/global-dependencies/). + + **Example** + + ```python + from fastapi import Depends, FastAPI + + from .dependencies import func_dep_1, func_dep_2 + + app = FastAPI(dependencies=[Depends(func_dep_1), Depends(func_dep_2)]) + ``` + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + + **Example** + + ```python + from fastapi import FastAPI + from fastapi.responses import ORJSONResponse + + app = FastAPI(default_response_class=ORJSONResponse) + ``` + """ + ), + ] = Default(JSONResponse), + redirect_slashes: Annotated[ + bool, + Doc( + """ + Whether to detect and redirect slashes in URLs when the client doesn't + use the same format. + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(redirect_slashes=True) # the default + + @app.get("/items/") + async def read_items(): + return [{"item_id": "Foo"}] + ``` + + With this app, if a client goes to `/items` (without a trailing slash), + they will be automatically redirected with an HTTP status code of 307 + to `/items/`. + """ + ), + ] = True, + docs_url: Annotated[ + Optional[str], + Doc( + """ + The path to the automatic interactive API documentation. + It is handled in the browser by Swagger UI. + + The default URL is `/docs`. You can disable it by setting it to `None`. + + If `openapi_url` is set to `None`, this will be automatically disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#docs-urls). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(docs_url="/documentation", redoc_url=None) + ``` + """ + ), + ] = "/docs", + redoc_url: Annotated[ + Optional[str], + Doc( + """ + The path to the alternative automatic interactive API documentation + provided by ReDoc. + + The default URL is `/redoc`. You can disable it by setting it to `None`. + + If `openapi_url` is set to `None`, this will be automatically disabled. + + Read more in the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#docs-urls). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(docs_url="/documentation", redoc_url="redocumentation") + ``` + """ + ), + ] = "/redoc", + swagger_ui_oauth2_redirect_url: Annotated[ + Optional[str], + Doc( + """ + The OAuth2 redirect endpoint for the Swagger UI. + + By default it is `/docs/oauth2-redirect`. + + This is only used if you use OAuth2 (with the "Authorize" button) + with Swagger UI. + """ + ), + ] = "/docs/oauth2-redirect", + swagger_ui_init_oauth: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + OAuth2 configuration for the Swagger UI, by default shown at `/docs`. + + Read more about the available configuration options in the + [Swagger UI docs](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). + """ + ), + ] = None, + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to be added when creating the application. + + In FastAPI you would normally do this with `app.add_middleware()` + instead. + + Read more in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/tutorial/middleware/). + """ + ), + ] = None, + exception_handlers: Annotated[ + Optional[ + Dict[ + Union[int, Type[Exception]], + Callable[[Request, Any], Coroutine[Any, Any, Response]], + ] + ], + Doc( + """ + A dictionary with handlers for exceptions. + + In FastAPI, you would normally use the decorator + `@app.exception_handler()`. + + Read more in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + """ + ), + ] = None, + on_startup: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of startup event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + on_shutdown: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of shutdown event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + lifespan: Annotated[ + Optional[Lifespan[AppType]], + Doc( + """ + A `Lifespan` context manager handler. This replaces `startup` and + `shutdown` functions with a single context manager. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + terms_of_service: Annotated[ + Optional[str], + Doc( + """ + A URL to the Terms of Service for your API. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI(terms_of_service="http://example.com/terms/") + ``` + """ + ), + ] = None, + contact: Annotated[ + Optional[Dict[str, Union[str, Any]]], + Doc( + """ + A dictionary with the contact information for the exposed API. + + It can contain several fields. + + * `name`: (`str`) The name of the contact person/organization. + * `url`: (`str`) A URL pointing to the contact information. MUST be in + the format of a URL. + * `email`: (`str`) The email address of the contact person/organization. + MUST be in the format of an email address. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI( + contact={ + "name": "Deadpoolio the Amazing", + "url": "http://x-force.example.com/contact/", + "email": "dp@x-force.example.com", + } + ) + ``` + """ + ), + ] = None, + license_info: Annotated[ + Optional[Dict[str, Union[str, Any]]], + Doc( + """ + A dictionary with the license information for the exposed API. + + It can contain several fields. + + * `name`: (`str`) **REQUIRED** (if a `license_info` is set). The + license name used for the API. + * `identifier`: (`str`) An [SPDX](https://spdx.dev/) license expression + for the API. The `identifier` field is mutually exclusive of the `url` + field. Available since OpenAPI 3.1.0, FastAPI 0.99.0. + * `url`: (`str`) A URL to the license used for the API. This MUST be + the format of a URL. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more at the + [FastAPI docs for Metadata and Docs URLs](https://fastapi.tiangolo.com/tutorial/metadata/#metadata-for-api). + + **Example** + + ```python + app = FastAPI( + license_info={ + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html", + } + ) + ``` + """ + ), + ] = None, + openapi_prefix: Annotated[ + str, + Doc( + """ + A URL prefix for the OpenAPI URL. + """ + ), + deprecated( + """ + "openapi_prefix" has been deprecated in favor of "root_path", which + follows more closely the ASGI standard, is simpler, and more + automatic. + """ + ), + ] = "", + root_path: Annotated[ + str, + Doc( + """ + A path prefix handled by a proxy that is not seen by the application + but is seen by external clients, which affects things like Swagger UI. + + Read more about it at the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(root_path="/api/v1") + ``` + """ + ), + ] = "", + root_path_in_servers: Annotated[ + bool, + Doc( + """ + To disable automatically generating the URLs in the `servers` field + in the autogenerated OpenAPI using the `root_path`. + + Read more about it in the + [FastAPI docs for Behind a Proxy](https://fastapi.tiangolo.com/advanced/behind-a-proxy/#disable-automatic-server-from-root_path). + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI(root_path_in_servers=False) + ``` + """ + ), + ] = True, + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + webhooks: Annotated[ + Optional[routing.APIRouter], + Doc( + """ + Add OpenAPI webhooks. This is similar to `callbacks` but it doesn't + depend on specific *path operations*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + **Note**: This is available since OpenAPI 3.1.0, FastAPI 0.99.0. + + Read more about it in the + [FastAPI docs for OpenAPI Webhooks](https://fastapi.tiangolo.com/advanced/openapi-webhooks/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* as deprecated. You probably don't need it, + but it's available. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) all the *path operations* in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + swagger_ui_parameters: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Parameters to configure Swagger UI, the autogenerated interactive API + documentation (by default at `/docs`). + + Read more about it in the + [FastAPI docs about how to Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), + separate_input_output_schemas: Annotated[ + bool, + Doc( + """ + Whether to generate separate OpenAPI schemas for request body and + response body when the results would be more precise. + + This is particularly useful when automatically generating clients. + + For example, if you have a model like: + + ```python + from pydantic import BaseModel + + class Item(BaseModel): + name: str + tags: list[str] = [] + ``` + + When `Item` is used for input, a request body, `tags` is not required, + the client doesn't have to provide it. + + But when using `Item` for output, for a response body, `tags` is always + available because it has a default value, even if it's just an empty + list. So, the client should be able to always expect it. + + In this case, there would be two different schemas, one for input and + another one for output. + """ + ), + ] = True, + **extra: Annotated[ + Any, + Doc( + """ + Extra keyword arguments to be stored in the app, not used by FastAPI + anywhere. + """ + ), + ], ) -> None: self.debug = debug self.title = title @@ -114,7 +849,37 @@ class FastAPI(Starlette): self.servers = servers or [] self.separate_input_output_schemas = separate_input_output_schemas self.extra = extra - self.openapi_version = "3.1.0" + self.openapi_version: Annotated[ + str, + Doc( + """ + The version string of OpenAPI. + + FastAPI will generate OpenAPI version 3.1.0, and will output that as + the OpenAPI version. But some tools, even though they might be + compatible with OpenAPI 3.1.0, might not recognize it as a valid. + + So you could override this value to trick those tools into using + the generated OpenAPI. Have in mind that this is a hack. But if you + avoid using features added in OpenAPI 3.1.0, it might work for your + use case. + + This is not passed as a parameter to the `FastAPI` class to avoid + giving the false idea that FastAPI would generate a different OpenAPI + schema. It is only available as an attribute. + + **Example** + + ```python + from fastapi import FastAPI + + app = FastAPI() + + app.openapi_version = "3.0.2" + ``` + """ + ), + ] = "3.1.0" self.openapi_schema: Optional[Dict[str, Any]] = None if self.openapi_url: assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'" @@ -127,10 +892,55 @@ class FastAPI(Starlette): "automatic. Check the docs at " "https://fastapi.tiangolo.com/advanced/sub-applications/" ) - self.webhooks = webhooks or routing.APIRouter() + self.webhooks: Annotated[ + routing.APIRouter, + Doc( + """ + The `app.webhooks` attribute is an `APIRouter` with the *path + operations* that will be used just for documentation of webhooks. + + Read more about it in the + [FastAPI docs for OpenAPI Webhooks](https://fastapi.tiangolo.com/advanced/openapi-webhooks/). + """ + ), + ] = ( + webhooks or routing.APIRouter() + ) self.root_path = root_path or openapi_prefix - self.state: State = State() - self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {} + self.state: Annotated[ + State, + Doc( + """ + A state object for the application. This is the same object for the + entire application, it doesn't change from request to request. + + You normally woudln't use this in FastAPI, for most of the cases you + would instead use FastAPI dependencies. + + This is simply inherited from Starlette. + + Read more about it in the + [Starlette docs for Applications](https://www.starlette.io/applications/#storing-state-on-the-app-instance). + """ + ), + ] = State() + self.dependency_overrides: Annotated[ + Dict[Callable[..., Any], Callable[..., Any]], + Doc( + """ + A dictionary with overrides for the dependencies. + + Each key is the original dependency callable, and the value is the + actual dependency that should be called. + + This is for testing, to replace expensive dependencies with testing + versions. + + Read more about it in the + [FastAPI docs for Testing Dependencies with Overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/). + """ + ), + ] = {} self.router: routing.APIRouter = routing.APIRouter( routes=routes, redirect_slashes=redirect_slashes, @@ -215,6 +1025,19 @@ class FastAPI(Starlette): return app def openapi(self) -> Dict[str, Any]: + """ + Generate the OpenAPI schema of the application. This is called by FastAPI + internally. + + The first time it is called it stores the result in the attribute + `app.openapi_schema`, and next times it is called, it just returns that same + result. To avoid the cost of generating the schema every time. + + If you need to modify the generated OpenAPI schema, you could modify it. + + Read more in the + [FastAPI docs for OpenAPI](https://fastapi.tiangolo.com/how-to/extending-openapi/). + """ if not self.openapi_schema: self.openapi_schema = get_openapi( title=self.title, @@ -427,11 +1250,58 @@ class FastAPI(Starlette): def websocket( self, - path: str, - name: Optional[str] = None, + path: Annotated[ + str, + Doc( + """ + WebSocket path. + """ + ), + ], + name: Annotated[ + Optional[str], + Doc( + """ + A name for the WebSocket. Only used internally. + """ + ), + ] = None, *, - dependencies: Optional[Sequence[Depends]] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be used for this + WebSocket. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Decorate a WebSocket function. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + **Example** + + ```python + from fastapi import FastAPI, WebSocket + + app = FastAPI() + + @app.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + ``` + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_api_websocket_route( path, @@ -445,20 +1315,196 @@ class FastAPI(Starlette): def include_router( self, - router: routing.APIRouter, + router: Annotated[routing.APIRouter, Doc("The `APIRouter` to include.")], *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - default_response_class: Type[Response] = Default(JSONResponse), - callbacks: Optional[List[BaseRoute]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + + **Example** + + ```python + from fastapi import Depends, FastAPI + + from .dependencies import get_token_header + from .internal import admin + + app = FastAPI() + + app.include_router( + admin.router, + dependencies=[Depends(get_token_header)], + ) + ``` + """ + ), + ] = None, + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all the *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + **Example** + + ```python + from fastapi import FastAPI + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + deprecated=True, + ) + ``` + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include (or not) all the *path operations* in this router in the + generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + **Example** + + ```python + from fastapi import FastAPI + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + include_in_schema=False, + ) + ``` + """ + ), + ] = True, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + Default response class to be used for the *path operations* in this + router. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + + **Example** + + ```python + from fastapi import FastAPI + from fastapi.responses import ORJSONResponse + + from .internal import old_api + + app = FastAPI() + + app.include_router( + old_api.router, + default_response_class=ORJSONResponse, + ) + ``` + """ + ), + ] = Default(JSONResponse), + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: + """ + Include an `APIRouter` in the same app. + + Read more about it in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import FastAPI + + from .users import users_router + + app = FastAPI() + + app.include_router(users_router) + ``` + """ self.router.include_router( router, prefix=prefix, @@ -474,33 +1520,351 @@ class FastAPI(Starlette): def get( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP GET operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.get("/items/") + def read_items(): + return [{"name": "Empanada"}, {"name": "Arepa"}] + ``` + """ return self.router.get( path, response_model=response_model, @@ -529,33 +1893,356 @@ class FastAPI(Starlette): def put( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PUT operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.put("/items/{item_id}") + def replace_item(item_id: str, item: Item): + return {"message": "Item replaced", "id": item_id} + ``` + """ return self.router.put( path, response_model=response_model, @@ -584,33 +2271,356 @@ class FastAPI(Starlette): def post( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP POST operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.post("/items/") + def create_item(item: Item): + return {"message": "Item created"} + ``` + """ return self.router.post( path, response_model=response_model, @@ -639,33 +2649,351 @@ class FastAPI(Starlette): def delete( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP DELETE operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.delete("/items/{item_id}") + def delete_item(item_id: str): + return {"message": "Item deleted"} + ``` + """ return self.router.delete( path, response_model=response_model, @@ -694,33 +3022,351 @@ class FastAPI(Starlette): def options( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP OPTIONS operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.options("/items/") + def get_item_options(): + return {"additions": ["Aji", "Guacamole"]} + ``` + """ return self.router.options( path, response_model=response_model, @@ -749,33 +3395,351 @@ class FastAPI(Starlette): def head( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP HEAD operation. + + ## Example + + ```python + from fastapi import FastAPI, Response + + app = FastAPI() + + @app.head("/items/", status_code=204) + def get_items_headers(response: Response): + response.headers["X-Cat-Dog"] = "Alone in the world" + ``` + """ return self.router.head( path, response_model=response_model, @@ -804,33 +3768,356 @@ class FastAPI(Starlette): def patch( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PATCH operation. + + ## Example + + ```python + from fastapi import FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + + @app.patch("/items/") + def update_item(item: Item): + return {"message": "Item updated in place"} + ``` + """ return self.router.patch( path, response_model=response_model, @@ -859,33 +4146,351 @@ class FastAPI(Starlette): def trace( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[routing.APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP TRACE operation. + + ## Example + + ```python + from fastapi import FastAPI + + app = FastAPI() + + @app.put("/items/{item_id}") + def trace_item(item_id: str): + return None + ``` + """ return self.router.trace( path, response_model=response_model, @@ -921,14 +4526,72 @@ class FastAPI(Starlette): return decorator + @deprecated( + """ + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + """ + ) def on_event( - self, event_type: str + self, + event_type: Annotated[ + str, + Doc( + """ + The type of event. `startup` or `shutdown`. + """ + ), + ], ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an event handler for the application. + + `on_event` is deprecated, use `lifespan` event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/#alternative-events-deprecated). + """ return self.router.on_event(event_type) def middleware( - self, middleware_type: str + self, + middleware_type: Annotated[ + str, + Doc( + """ + The type of middleware. Currently only supports `http`. + """ + ), + ], ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a middleware to the application. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/tutorial/middleware/). + + ## Example + + ```python + import time + + from fastapi import FastAPI, Request + + app = FastAPI() + + + @app.middleware("http") + async def add_process_time_header(request: Request, call_next): + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + ``` + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_middleware(BaseHTTPMiddleware, dispatch=func) return func @@ -936,8 +4599,46 @@ class FastAPI(Starlette): return decorator def exception_handler( - self, exc_class_or_status_code: Union[int, Type[Exception]] + self, + exc_class_or_status_code: Annotated[ + Union[int, Type[Exception]], + Doc( + """ + The Exception class this would handle, or a status code. + """ + ), + ], ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an exception handler to the app. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + + ## Example + + ```python + from fastapi import FastAPI, Request + from fastapi.responses import JSONResponse + + + class UnicornException(Exception): + def __init__(self, name: str): + self.name = name + + + app = FastAPI() + + + @app.exception_handler(UnicornException) + async def unicorn_exception_handler(request: Request, exc: UnicornException): + return JSONResponse( + status_code=418, + content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."}, + ) + ``` + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_exception_handler(exc_class_or_status_code, func) return func diff --git a/fastapi/background.py b/fastapi/background.py index dd3bbe249..35ab1b227 100644 --- a/fastapi/background.py +++ b/fastapi/background.py @@ -1 +1,59 @@ -from starlette.background import BackgroundTasks as BackgroundTasks # noqa +from typing import Any, Callable + +from starlette.background import BackgroundTasks as StarletteBackgroundTasks +from typing_extensions import Annotated, Doc, ParamSpec # type: ignore [attr-defined] + +P = ParamSpec("P") + + +class BackgroundTasks(StarletteBackgroundTasks): + """ + A collection of background tasks that will be called after a response has been + sent to the client. + + Read more about it in the + [FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/). + + ## Example + + ```python + from fastapi import BackgroundTasks, FastAPI + + app = FastAPI() + + + def write_notification(email: str, message=""): + with open("log.txt", mode="w") as email_file: + content = f"notification for {email}: {message}" + email_file.write(content) + + + @app.post("/send-notification/{email}") + async def send_notification(email: str, background_tasks: BackgroundTasks): + background_tasks.add_task(write_notification, email, message="some notification") + return {"message": "Notification sent in the background"} + ``` + """ + + def add_task( + self, + func: Annotated[ + Callable[P, Any], + Doc( + """ + The function to call after the response is sent. + + It can be a regular `def` function or an `async def` function. + """ + ), + ], + *args: P.args, + **kwargs: P.kwargs, + ) -> None: + """ + Add a function to be called in the background after the response is sent. + + Read more about it in the + [FastAPI docs for Background Tasks](https://fastapi.tiangolo.com/tutorial/background-tasks/). + """ + return super().add_task(func, *args, **kwargs) diff --git a/fastapi/datastructures.py b/fastapi/datastructures.py index b2865cd40..ce03e3ce4 100644 --- a/fastapi/datastructures.py +++ b/fastapi/datastructures.py @@ -1,4 +1,14 @@ -from typing import Any, Callable, Dict, Iterable, Type, TypeVar, cast +from typing import ( + Any, + BinaryIO, + Callable, + Dict, + Iterable, + Optional, + Type, + TypeVar, + cast, +) from fastapi._compat import ( PYDANTIC_V2, @@ -14,9 +24,120 @@ from starlette.datastructures import Headers as Headers # noqa: F401 from starlette.datastructures import QueryParams as QueryParams # noqa: F401 from starlette.datastructures import State as State # noqa: F401 from starlette.datastructures import UploadFile as StarletteUploadFile +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class UploadFile(StarletteUploadFile): + """ + A file uploaded in a request. + + Define it as a *path operation function* (or dependency) parameter. + + If you are using a regular `def` function, you can use the `upload_file.file` + attribute to access the raw standard Python file (blocking, not async), useful and + needed for non-async code. + + Read more about it in the + [FastAPI docs for Request Files](https://fastapi.tiangolo.com/tutorial/request-files/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import FastAPI, File, UploadFile + + app = FastAPI() + + + @app.post("/files/") + async def create_file(file: Annotated[bytes, File()]): + return {"file_size": len(file)} + + + @app.post("/uploadfile/") + async def create_upload_file(file: UploadFile): + return {"filename": file.filename} + ``` + """ + + file: Annotated[ + BinaryIO, + Doc("The standard Python file object (non-async)."), + ] + filename: Annotated[Optional[str], Doc("The original file name.")] + size: Annotated[Optional[int], Doc("The size of the file in bytes.")] + headers: Annotated[Headers, Doc("The headers of the request.")] + content_type: Annotated[ + Optional[str], Doc("The content type of the request, from the headers.") + ] + + async def write( + self, + data: Annotated[ + bytes, + Doc( + """ + The bytes to write to the file. + """ + ), + ], + ) -> None: + """ + Write some bytes to the file. + + You normally wouldn't use this from a file you read in a request. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().write(data) + + async def read( + self, + size: Annotated[ + int, + Doc( + """ + The number of bytes to read from the file. + """ + ), + ] = -1, + ) -> bytes: + """ + Read some bytes from the file. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().read(size) + + async def seek( + self, + offset: Annotated[ + int, + Doc( + """ + The position in bytes to seek to in the file. + """ + ), + ], + ) -> None: + """ + Move to a position in the file. + + Any next read or write will be done from that position. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().seek(offset) + + async def close(self) -> None: + """ + Close the file. + + To be awaitable, compatible with async, this is run in threadpool. + """ + return await super().close() + @classmethod def __get_validators__(cls: Type["UploadFile"]) -> Iterable[Callable[..., Any]]: yield cls.validate diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index e2915268c..96e07a45c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -44,6 +44,7 @@ from fastapi._compat import ( serialize_sequence_value, value_is_sequence, ) +from fastapi.background import BackgroundTasks from fastapi.concurrency import ( AsyncExitStack, asynccontextmanager, @@ -56,7 +57,7 @@ from fastapi.security.oauth2 import OAuth2, SecurityScopes from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.utils import create_response_field, get_path_param_names from pydantic.fields import FieldInfo -from starlette.background import BackgroundTasks +from starlette.background import BackgroundTasks as StarletteBackgroundTasks from starlette.concurrency import run_in_threadpool from starlette.datastructures import FormData, Headers, QueryParams, UploadFile from starlette.requests import HTTPConnection, Request @@ -305,7 +306,7 @@ def add_non_field_param_to_dependency( elif lenient_issubclass(type_annotation, Response): dependant.response_param_name = param_name return True - elif lenient_issubclass(type_annotation, BackgroundTasks): + elif lenient_issubclass(type_annotation, StarletteBackgroundTasks): dependant.background_tasks_param_name = param_name return True elif lenient_issubclass(type_annotation, SecurityScopes): @@ -382,7 +383,14 @@ def analyze_param( if lenient_issubclass( type_annotation, - (Request, WebSocket, HTTPConnection, Response, BackgroundTasks, SecurityScopes), + ( + Request, + WebSocket, + HTTPConnection, + Response, + StarletteBackgroundTasks, + SecurityScopes, + ), ): assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}" assert ( @@ -510,14 +518,14 @@ async def solve_dependencies( request: Union[Request, WebSocket], dependant: Dependant, body: Optional[Union[Dict[str, Any], FormData]] = None, - background_tasks: Optional[BackgroundTasks] = None, + background_tasks: Optional[StarletteBackgroundTasks] = None, response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, ) -> Tuple[ Dict[str, Any], List[Any], - Optional[BackgroundTasks], + Optional[StarletteBackgroundTasks], Response, Dict[Tuple[Callable[..., Any], Tuple[str]], Any], ]: diff --git a/fastapi/encoders.py b/fastapi/encoders.py index 30493697e..e50171393 100644 --- a/fastapi/encoders.py +++ b/fastapi/encoders.py @@ -22,6 +22,7 @@ from pydantic import BaseModel from pydantic.color import Color from pydantic.networks import AnyUrl, NameEmail from pydantic.types import SecretBytes, SecretStr +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] from ._compat import PYDANTIC_V2, Url, _model_dump @@ -99,16 +100,107 @@ encoders_by_class_tuples = generate_encoders_by_class_tuples(ENCODERS_BY_TYPE) def jsonable_encoder( - obj: Any, - include: Optional[IncEx] = None, - exclude: Optional[IncEx] = None, - by_alias: bool = True, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None, - sqlalchemy_safe: bool = True, + obj: Annotated[ + Any, + Doc( + """ + The input object to convert to JSON. + """ + ), + ], + include: Annotated[ + Optional[IncEx], + Doc( + """ + Pydantic's `include` parameter, passed to Pydantic models to set the + fields to include. + """ + ), + ] = None, + exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Pydantic's `exclude` parameter, passed to Pydantic models to set the + fields to exclude. + """ + ), + ] = None, + by_alias: Annotated[ + bool, + Doc( + """ + Pydantic's `by_alias` parameter, passed to Pydantic models to define if + the output should use the alias names (when provided) or the Python + attribute names. In an API, if you set an alias, it's probably because you + want to use it in the result, so you probably want to leave this set to + `True`. + """ + ), + ] = True, + exclude_unset: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_unset` parameter, passed to Pydantic models to define + if it should exclude from the output the fields that were not explicitly + set (and that only had their default values). + """ + ), + ] = False, + exclude_defaults: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_defaults` parameter, passed to Pydantic models to define + if it should exclude from the output the fields that had the same default + value, even when they were explicitly set. + """ + ), + ] = False, + exclude_none: Annotated[ + bool, + Doc( + """ + Pydantic's `exclude_none` parameter, passed to Pydantic models to define + if it should exclude from the output any fields that have a `None` value. + """ + ), + ] = False, + custom_encoder: Annotated[ + Optional[Dict[Any, Callable[[Any], Any]]], + Doc( + """ + Pydantic's `custom_encoder` parameter, passed to Pydantic models to define + a custom encoder. + """ + ), + ] = None, + sqlalchemy_safe: Annotated[ + bool, + Doc( + """ + Exclude from the output any fields that start with the name `_sa`. + + This is mainly a hack for compatibility with SQLAlchemy objects, they + store internal SQLAlchemy-specific state in attributes named with `_sa`, + and those objects can't (and shouldn't be) serialized to JSON. + """ + ), + ] = True, ) -> Any: + """ + Convert any object to something that can be encoded in JSON. + + This is used internally by FastAPI to make sure anything you return can be + encoded as JSON before it is sent to the client. + + You can also use it yourself, for example to convert objects before saving them + in a database that supports only JSON. + + Read more about it in the + [FastAPI docs for JSON Compatible Encoder](https://fastapi.tiangolo.com/tutorial/encoder/). + """ custom_encoder = custom_encoder or {} if custom_encoder: if type(obj) in custom_encoder: diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 42f4709fb..680d288e4 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -1,20 +1,141 @@ -from typing import Any, Dict, Optional, Sequence, Type +from typing import Any, Dict, Optional, Sequence, Type, Union from pydantic import BaseModel, create_model from starlette.exceptions import HTTPException as StarletteHTTPException -from starlette.exceptions import WebSocketException as WebSocketException # noqa: F401 +from starlette.exceptions import WebSocketException as StarletteWebSocketException +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class HTTPException(StarletteHTTPException): + """ + An HTTP exception you can raise in your own code to show errors to the client. + + This is for client errors, invalid authentication, invalid data, etc. Not for server + errors in your code. + + Read more about it in the + [FastAPI docs for Handling Errors](https://fastapi.tiangolo.com/tutorial/handling-errors/). + + ## Example + + ```python + from fastapi import FastAPI, HTTPException + + app = FastAPI() + + items = {"foo": "The Foo Wrestlers"} + + + @app.get("/items/{item_id}") + async def read_item(item_id: str): + if item_id not in items: + raise HTTPException(status_code=404, detail="Item not found") + return {"item": items[item_id]} + ``` + """ + def __init__( self, - status_code: int, - detail: Any = None, - headers: Optional[Dict[str, str]] = None, + status_code: Annotated[ + int, + Doc( + """ + HTTP status code to send to the client. + """ + ), + ], + detail: Annotated[ + Any, + Doc( + """ + Any data to be sent to the client in the `detail` key of the JSON + response. + """ + ), + ] = None, + headers: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + Any headers to send to the client in the response. + """ + ), + ] = None, ) -> None: super().__init__(status_code=status_code, detail=detail, headers=headers) +class WebSocketException(StarletteWebSocketException): + """ + A WebSocket exception you can raise in your own code to show errors to the client. + + This is for client errors, invalid authentication, invalid data, etc. Not for server + errors in your code. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import ( + Cookie, + FastAPI, + WebSocket, + WebSocketException, + status, + ) + + app = FastAPI() + + @app.websocket("/items/{item_id}/ws") + async def websocket_endpoint( + *, + websocket: WebSocket, + session: Annotated[str | None, Cookie()] = None, + item_id: str, + ): + if session is None: + raise WebSocketException(code=status.WS_1008_POLICY_VIOLATION) + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Session cookie is: {session}") + await websocket.send_text(f"Message text was: {data}, for item ID: {item_id}") + ``` + """ + + def __init__( + self, + code: Annotated[ + int, + Doc( + """ + A closing code from the + [valid codes defined in the specification](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1). + """ + ), + ], + reason: Annotated[ + Union[str, None], + Doc( + """ + The reason to close the WebSocket connection. + + It is UTF-8-encoded data. The interpretation of the reason is up to the + application, it is not specified by the WebSocket specification. + + It could contain text that could be human-readable or interpretable + by the client code, etc. + """ + ), + ] = None, + ) -> None: + super().__init__(code=code, reason=reason) + + RequestErrorModel: Type[BaseModel] = create_model("Request") WebSocketErrorModel: Type[BaseModel] = create_model("WebSocket") diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index 81f67dcc5..8cf0d17a1 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -3,8 +3,18 @@ from typing import Any, Dict, Optional from fastapi.encoders import jsonable_encoder from starlette.responses import HTMLResponse +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] -swagger_ui_default_parameters = { +swagger_ui_default_parameters: Annotated[ + Dict[str, Any], + Doc( + """ + Default configurations for Swagger UI. + + You can use it as a template to add any other configurations needed. + """ + ), +] = { "dom_id": "#swagger-ui", "layout": "BaseLayout", "deepLinking": True, @@ -15,15 +25,91 @@ swagger_ui_default_parameters = { def get_swagger_ui_html( *, - openapi_url: str, - title: str, - swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", - swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", - swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", - oauth2_redirect_url: Optional[str] = None, - init_oauth: Optional[Dict[str, Any]] = None, - swagger_ui_parameters: Optional[Dict[str, Any]] = None, + openapi_url: Annotated[ + str, + Doc( + """ + The OpenAPI URL that Swagger UI should load and use. + + This is normally done automatically by FastAPI using the default URL + `/openapi.json`. + """ + ), + ], + title: Annotated[ + str, + Doc( + """ + The HTML `` content, normally shown in the browser tab. + """ + ), + ], + swagger_js_url: Annotated[ + str, + Doc( + """ + The URL to use to load the Swagger UI JavaScript. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", + swagger_css_url: Annotated[ + str, + Doc( + """ + The URL to use to load the Swagger UI CSS. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", + swagger_favicon_url: Annotated[ + str, + Doc( + """ + The URL of the favicon to use. It is normally shown in the browser tab. + """ + ), + ] = "https://fastapi.tiangolo.com/img/favicon.png", + oauth2_redirect_url: Annotated[ + Optional[str], + Doc( + """ + The OAuth2 redirect URL, it is normally automatically handled by FastAPI. + """ + ), + ] = None, + init_oauth: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + A dictionary with Swagger UI OAuth2 initialization configurations. + """ + ), + ] = None, + swagger_ui_parameters: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Configuration parameters for Swagger UI. + + It defaults to [swagger_ui_default_parameters][fastapi.openapi.docs.swagger_ui_default_parameters]. + """ + ), + ] = None, ) -> HTMLResponse: + """ + Generate and return the HTML that loads Swagger UI for the interactive + API docs (normally served at `/docs`). + + You would only call this function yourself if you needed to override some parts, + for example the URLs to use to load Swagger UI's JavaScript and CSS. + + Read more about it in the + [FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/) + and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). + """ current_swagger_ui_parameters = swagger_ui_default_parameters.copy() if swagger_ui_parameters: current_swagger_ui_parameters.update(swagger_ui_parameters) @@ -74,12 +160,62 @@ def get_swagger_ui_html( def get_redoc_html( *, - openapi_url: str, - title: str, - redoc_js_url: str = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js", - redoc_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png", - with_google_fonts: bool = True, + openapi_url: Annotated[ + str, + Doc( + """ + The OpenAPI URL that ReDoc should load and use. + + This is normally done automatically by FastAPI using the default URL + `/openapi.json`. + """ + ), + ], + title: Annotated[ + str, + Doc( + """ + The HTML `<title>` content, normally shown in the browser tab. + """ + ), + ], + redoc_js_url: Annotated[ + str, + Doc( + """ + The URL to use to load the ReDoc JavaScript. + + It is normally set to a CDN URL. + """ + ), + ] = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js", + redoc_favicon_url: Annotated[ + str, + Doc( + """ + The URL of the favicon to use. It is normally shown in the browser tab. + """ + ), + ] = "https://fastapi.tiangolo.com/img/favicon.png", + with_google_fonts: Annotated[ + bool, + Doc( + """ + Load and use Google Fonts. + """ + ), + ] = True, ) -> HTMLResponse: + """ + Generate and return the HTML response that loads ReDoc for the alternative + API docs (normally served at `/redoc`). + + You would only call this function yourself if you needed to override some parts, + for example the URLs to use to load ReDoc's JavaScript and CSS. + + Read more about it in the + [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/). + """ html = f""" <!DOCTYPE html> <html> @@ -118,6 +254,11 @@ def get_redoc_html( def get_swagger_ui_oauth2_redirect_html() -> HTMLResponse: + """ + Generate the HTML response with the OAuth2 redirection for Swagger UI. + + You normally don't need to use or change this. + """ # copied from https://github.com/swagger-api/swagger-ui/blob/v4.14.0/dist/oauth2-redirect.html html = """ <!doctype html> diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 63914d1d6..3f6dbc959 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -3,43 +3,218 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Union from fastapi import params from fastapi._compat import Undefined from fastapi.openapi.models import Example -from typing_extensions import Annotated, deprecated +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] _Unset: Any = Undefined def Path( # noqa: N802 - default: Any = ..., + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = ..., *, - default_factory: Union[Callable[[], Any], None] = _Unset, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -47,12 +222,87 @@ def Path( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: + """ + Declare a path parameter for a *path operation*. + + Read more about it in the + [FastAPI docs for Path Parameters and Numeric Validations](https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/). + + ```python + from typing import Annotated + + from fastapi import FastAPI, Path + + app = FastAPI() + + + @app.get("/items/{item_id}") + async def read_items( + item_id: Annotated[int, Path(title="The ID of the item to get")], + ): + return {"item_id": item_id} + ``` + """ return params.Path( default=default, default_factory=default_factory, @@ -87,37 +337,209 @@ def Path( # noqa: N802 def Query( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -125,11 +547,65 @@ def Query( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Query( default=default, @@ -165,38 +641,220 @@ def Query( # noqa: N802 def Header( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - convert_underscores: bool = True, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + convert_underscores: Annotated[ + bool, + Doc( + """ + Automatically convert underscores to hyphens in the parameter field name. + + Read more about it in the + [FastAPI docs for Header Parameters](https://fastapi.tiangolo.com/tutorial/header-params/#automatic-conversion) + """ + ), + ] = True, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -204,11 +862,65 @@ def Header( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Header( default=default, @@ -245,37 +957,209 @@ def Header( # noqa: N802 def Cookie( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -283,11 +1167,65 @@ def Cookie( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Cookie( default=default, @@ -323,39 +1261,232 @@ def Cookie( # noqa: N802 def Body( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - embed: bool = False, - media_type: str = "application/json", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + embed: Annotated[ + bool, + Doc( + """ + When `embed` is `True`, the parameter will be expected in a JSON body as a + key instead of being the JSON body itself. + + This happens automatically when more than one `Body` parameter is declared. + + Read more about it in the + [FastAPI docs for Body - Multiple Parameters](https://fastapi.tiangolo.com/tutorial/body-multiple-params/#embed-a-single-body-parameter). + """ + ), + ] = False, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "application/json", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -363,11 +1494,65 @@ def Body( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Body( default=default, @@ -405,38 +1590,218 @@ def Body( # noqa: N802 def Form( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - media_type: str = "application/x-www-form-urlencoded", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "application/x-www-form-urlencoded", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -444,11 +1809,65 @@ def Form( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.Form( default=default, @@ -485,38 +1904,218 @@ def Form( # noqa: N802 def File( # noqa: N802 - default: Any = Undefined, + default: Annotated[ + Any, + Doc( + """ + Default value if the parameter field is not set. + """ + ), + ] = Undefined, *, - default_factory: Union[Callable[[], Any], None] = _Unset, - media_type: str = "multipart/form-data", - alias: Optional[str] = None, - alias_priority: Union[int, None] = _Unset, + default_factory: Annotated[ + Union[Callable[[], Any], None], + Doc( + """ + A callable to generate the default value. + + This doesn't affect `Path` parameters as the value is always required. + The parameter is available only for compatibility. + """ + ), + ] = _Unset, + media_type: Annotated[ + str, + Doc( + """ + The media type of this parameter field. Changing it would affect the + generated OpenAPI, but currently it doesn't affect the parsing of the data. + """ + ), + ] = "multipart/form-data", + alias: Annotated[ + Optional[str], + Doc( + """ + An alternative name for the parameter field. + + This will be used to extract the data and for the generated OpenAPI. + It is particularly useful when you can't use the name you want because it + is a Python reserved keyword or similar. + """ + ), + ] = None, + alias_priority: Annotated[ + Union[int, None], + Doc( + """ + Priority of the alias. This affects whether an alias generator is used. + """ + ), + ] = _Unset, # TODO: update when deprecating Pydantic v1, import these types # validation_alias: str | AliasPath | AliasChoices | None - validation_alias: Union[str, None] = None, - serialization_alias: Union[str, None] = None, - title: Optional[str] = None, - description: Optional[str] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, - pattern: Optional[str] = None, + validation_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Whitelist' validation step. The parameter field will be the single one + allowed by the alias or set of aliases defined. + """ + ), + ] = None, + serialization_alias: Annotated[ + Union[str, None], + Doc( + """ + 'Blacklist' validation step. The vanilla parameter field will be the + single one of the alias' or set of aliases' fields and all the other + fields will be ignored at serialization time. + """ + ), + ] = None, + title: Annotated[ + Optional[str], + Doc( + """ + Human-readable title. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Human-readable description. + """ + ), + ] = None, + gt: Annotated[ + Optional[float], + Doc( + """ + Greater than. If set, value must be greater than this. Only applicable to + numbers. + """ + ), + ] = None, + ge: Annotated[ + Optional[float], + Doc( + """ + Greater than or equal. If set, value must be greater than or equal to + this. Only applicable to numbers. + """ + ), + ] = None, + lt: Annotated[ + Optional[float], + Doc( + """ + Less than. If set, value must be less than this. Only applicable to numbers. + """ + ), + ] = None, + le: Annotated[ + Optional[float], + Doc( + """ + Less than or equal. If set, value must be less than or equal to this. + Only applicable to numbers. + """ + ), + ] = None, + min_length: Annotated[ + Optional[int], + Doc( + """ + Minimum length for strings. + """ + ), + ] = None, + max_length: Annotated[ + Optional[int], + Doc( + """ + Maximum length for strings. + """ + ), + ] = None, + pattern: Annotated[ + Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), + ] = None, regex: Annotated[ Optional[str], + Doc( + """ + RegEx pattern for strings. + """ + ), deprecated( "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead." ), ] = None, - discriminator: Union[str, None] = None, - strict: Union[bool, None] = _Unset, - multiple_of: Union[float, None] = _Unset, - allow_inf_nan: Union[bool, None] = _Unset, - max_digits: Union[int, None] = _Unset, - decimal_places: Union[int, None] = _Unset, - examples: Optional[List[Any]] = None, + discriminator: Annotated[ + Union[str, None], + Doc( + """ + Parameter field name for discriminating the type in a tagged union. + """ + ), + ] = None, + strict: Annotated[ + Union[bool, None], + Doc( + """ + If `True`, strict validation is applied to the field. + """ + ), + ] = _Unset, + multiple_of: Annotated[ + Union[float, None], + Doc( + """ + Value must be a multiple of this. Only applicable to numbers. + """ + ), + ] = _Unset, + allow_inf_nan: Annotated[ + Union[bool, None], + Doc( + """ + Allow `inf`, `-inf`, `nan`. Only applicable to numbers. + """ + ), + ] = _Unset, + max_digits: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of allow digits for strings. + """ + ), + ] = _Unset, + decimal_places: Annotated[ + Union[int, None], + Doc( + """ + Maximum number of decimal places allowed for numbers. + """ + ), + ] = _Unset, + examples: Annotated[ + Optional[List[Any]], + Doc( + """ + Example values for this field. + """ + ), + ] = None, example: Annotated[ Optional[Any], deprecated( @@ -524,11 +2123,65 @@ def File( # noqa: N802 "although still supported. Use examples instead." ), ] = _Unset, - openapi_examples: Optional[Dict[str, Example]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - json_schema_extra: Union[Dict[str, Any], None] = None, - **extra: Any, + openapi_examples: Annotated[ + Optional[Dict[str, Example]], + Doc( + """ + OpenAPI-specific examples. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Swagger UI (that provides the `/docs` interface) has better support for the + OpenAPI-specific examples than the JSON Schema `examples`, that's the main + use case for this. + + Read more about it in the + [FastAPI docs for Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/#using-the-openapi_examples-parameter). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this parameter field as deprecated. + + It will affect the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) this parameter field in the generated OpenAPI. + You probably don't need it, but it's available. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + json_schema_extra: Annotated[ + Union[Dict[str, Any], None], + Doc( + """ + Any additional JSON schema data. + """ + ), + ] = None, + **extra: Annotated[ + Any, + Doc( + """ + Include extra fields used by the JSON Schema. + """ + ), + deprecated( + """ + The `extra` kwargs is deprecated. Use `json_schema_extra` instead. + """ + ), + ], ) -> Any: return params.File( default=default, @@ -565,15 +2218,143 @@ def File( # noqa: N802 def Depends( # noqa: N802 - dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True + dependency: Annotated[ + Optional[Callable[..., Any]], + Doc( + """ + A "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you, just pass the object + directly. + """ + ), + ] = None, + *, + use_cache: Annotated[ + bool, + Doc( + """ + By default, after a dependency is called the first time in a request, if + the dependency is declared again for the rest of the request (for example + if the dependency is needed by several dependencies), the value will be + re-used for the rest of the request. + + Set `use_cache` to `False` to disable this behavior and ensure the + dependency is called again (if declared more than once) in the same request. + """ + ), + ] = True, ) -> Any: + """ + Declare a FastAPI dependency. + + It takes a single "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you. + + Read more about it in the + [FastAPI docs for Dependencies](https://fastapi.tiangolo.com/tutorial/dependencies/). + + **Example** + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + + app = FastAPI() + + + async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100): + return {"q": q, "skip": skip, "limit": limit} + + + @app.get("/items/") + async def read_items(commons: Annotated[dict, Depends(common_parameters)]): + return commons + ``` + """ return params.Depends(dependency=dependency, use_cache=use_cache) def Security( # noqa: N802 - dependency: Optional[Callable[..., Any]] = None, + dependency: Annotated[ + Optional[Callable[..., Any]], + Doc( + """ + A "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you, just pass the object + directly. + """ + ), + ] = None, *, - scopes: Optional[Sequence[str]] = None, - use_cache: bool = True, + scopes: Annotated[ + Optional[Sequence[str]], + Doc( + """ + OAuth2 scopes required for the *path operation* that uses this Security + dependency. + + The term "scope" comes from the OAuth2 specification, it seems to be + intentionaly vague and interpretable. It normally refers to permissions, + in cases to roles. + + These scopes are integrated with OpenAPI (and the API docs at `/docs`). + So they are visible in the OpenAPI specification. + ) + """ + ), + ] = None, + use_cache: Annotated[ + bool, + Doc( + """ + By default, after a dependency is called the first time in a request, if + the dependency is declared again for the rest of the request (for example + if the dependency is needed by several dependencies), the value will be + re-used for the rest of the request. + + Set `use_cache` to `False` to disable this behavior and ensure the + dependency is called again (if declared more than once) in the same request. + """ + ), + ] = True, ) -> Any: + """ + Declare a FastAPI Security dependency. + + The only difference with a regular dependency is that it can declare OAuth2 + scopes that will be integrated with OpenAPI and the automatic UI docs (by default + at `/docs`). + + It takes a single "dependable" callable (like a function). + + Don't call it directly, FastAPI will call it for you. + + Read more about it in the + [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/) and + in the + [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/). + + **Example** + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + + from .db import User + from .security import get_current_active_user + + app = FastAPI() + + @app.get("/users/me/items/") + async def read_own_items( + current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] + ): + return [{"item_id": "Foo", "owner": current_user.username}] + ``` + """ return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) diff --git a/fastapi/responses.py b/fastapi/responses.py index c0a13b755..6c8db6f33 100644 --- a/fastapi/responses.py +++ b/fastapi/responses.py @@ -21,12 +21,26 @@ except ImportError: # pragma: nocover class UJSONResponse(JSONResponse): + """ + JSON response using the high-performance ujson library to serialize data to JSON. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ + def render(self, content: Any) -> bytes: assert ujson is not None, "ujson must be installed to use UJSONResponse" return ujson.dumps(content, ensure_ascii=False).encode("utf-8") class ORJSONResponse(JSONResponse): + """ + JSON response using the high-performance orjson library to serialize data to JSON. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/). + """ + def render(self, content: Any) -> bytes: assert orjson is not None, "orjson must be installed to use ORJSONResponse" return orjson.dumps( diff --git a/fastapi/routing.py b/fastapi/routing.py index 1e3dfb4d5..e33e1d832 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -69,6 +69,7 @@ from starlette.routing import ( from starlette.routing import Mount as Mount # noqa from starlette.types import ASGIApp, Lifespan, Scope from starlette.websockets import WebSocket +from typing_extensions import Annotated, Doc, deprecated # type: ignore [attr-defined] def _prepare_response_content( @@ -519,30 +520,253 @@ class APIRoute(routing.Route): class APIRouter(routing.Router): + """ + `APIRouter` class, used to group *path operations*, for example to structure + an app in multiple files. It would then be included in the `FastAPI` app, or + in another `APIRouter` (ultimately included in the app). + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + + @router.get("/users/", tags=["users"]) + async def read_users(): + return [{"username": "Rick"}, {"username": "Morty"}] + + + app.include_router(router) + ``` + """ + def __init__( self, *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - routes: Optional[List[routing.BaseRoute]] = None, - redirect_slashes: bool = True, - default: Optional[ASGIApp] = None, - dependency_overrides_provider: Optional[Any] = None, - route_class: Type[APIRoute] = APIRoute, - on_startup: Optional[Sequence[Callable[[], Any]]] = None, - on_shutdown: Optional[Sequence[Callable[[], Any]]] = None, + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + """ + ), + ] = Default(JSONResponse), + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + routes: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + **Note**: you probably shouldn't use this parameter, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation* decorators, + like: + + * `router.get()` + * `router.post()` + * etc. + + --- + + A list of routes to serve incoming HTTP and WebSocket requests. + """ + ), + deprecated( + """ + You normally wouldn't use this parameter with FastAPI, it is inherited + from Starlette and supported for compatibility. + + In FastAPI, you normally would use the *path operation methods*, + like `router.get()`, `router.post()`, etc. + """ + ), + ] = None, + redirect_slashes: Annotated[ + bool, + Doc( + """ + Whether to detect and redirect slashes in URLs when the client doesn't + use the same format. + """ + ), + ] = True, + default: Annotated[ + Optional[ASGIApp], + Doc( + """ + Default function handler for this router. Used to handle + 404 Not Found errors. + """ + ), + ] = None, + dependency_overrides_provider: Annotated[ + Optional[Any], + Doc( + """ + Only used internally by FastAPI to handle dependency overrides. + + You shouldn't need to use it. It normally points to the `FastAPI` app + object. + """ + ), + ] = None, + route_class: Annotated[ + Type[APIRoute], + Doc( + """ + Custom route (*path operation*) class to be used by this router. + + Read more about it in the + [FastAPI docs for Custom Request and APIRoute class](https://fastapi.tiangolo.com/how-to/custom-request-and-route/#custom-apiroute-class-in-a-router). + """ + ), + ] = APIRoute, + on_startup: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of startup event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + on_shutdown: Annotated[ + Optional[Sequence[Callable[[], Any]]], + Doc( + """ + A list of shutdown event handler functions. + + You should instead use the `lifespan` handlers. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, # the generic to Lifespan[AppType] is the type of the top level application # which the router cannot know statically, so we use typing.Any - lifespan: Optional[Lifespan[Any]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + lifespan: Annotated[ + Optional[Lifespan[Any]], + Doc( + """ + A `Lifespan` context manager handler. This replaces `startup` and + `shutdown` functions with a single context manager. + + Read more in the + [FastAPI docs for `lifespan`](https://fastapi.tiangolo.com/advanced/events/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + To include (or not) all the *path operations* in this router in the + generated OpenAPI. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: super().__init__( routes=routes, @@ -755,11 +979,63 @@ class APIRouter(routing.Router): def websocket( self, - path: str, - name: Optional[str] = None, + path: Annotated[ + str, + Doc( + """ + WebSocket path. + """ + ), + ], + name: Annotated[ + Optional[str], + Doc( + """ + A name for the WebSocket. Only used internally. + """ + ), + ] = None, *, - dependencies: Optional[Sequence[params.Depends]] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be used for this + WebSocket. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Decorate a WebSocket function. + + Read more about it in the + [FastAPI docs for WebSockets](https://fastapi.tiangolo.com/advanced/websockets/). + + **Example** + + ## Example + + ```python + from fastapi import APIRouter, FastAPI, WebSocket + + app = FastAPI() + router = APIRouter() + + @router.websocket("/ws") + async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + while True: + data = await websocket.receive_text() + await websocket.send_text(f"Message text was: {data}") + + app.include_router(router) + ``` + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_api_websocket_route( path, func, name=name, dependencies=dependencies @@ -779,20 +1055,139 @@ class APIRouter(routing.Router): def include_router( self, - router: "APIRouter", + router: Annotated["APIRouter", Doc("The `APIRouter` to include.")], *, - prefix: str = "", - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - default_response_class: Type[Response] = Default(JSONResponse), - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - callbacks: Optional[List[BaseRoute]] = None, - deprecated: Optional[bool] = None, - include_in_schema: bool = True, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + prefix: Annotated[str, Doc("An optional path prefix for the router.")] = "", + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to all the *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to all the + *path operations* in this router. + + Read more about it in the + [FastAPI docs for Bigger Applications - Multiple Files](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + default_response_class: Annotated[ + Type[Response], + Doc( + """ + The default response class to be used. + + Read more in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#default-response-class). + """ + ), + ] = Default(JSONResponse), + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses to be shown in OpenAPI. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Additional Responses in OpenAPI](https://fastapi.tiangolo.com/advanced/additional-responses/). + + And in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/#include-an-apirouter-with-a-custom-prefix-tags-responses-and-dependencies). + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + OpenAPI callbacks that should apply to all *path operations* in this + router. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark all *path operations* in this router as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include (or not) all the *path operations* in this router in the + generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = True, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> None: + """ + Include another `APIRouter` in the same current `APIRouter`. + + Read more about it in the + [FastAPI docs for Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/). + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + internal_router = APIRouter() + users_router = APIRouter() + + @users_router.get("/users/") + def read_users(): + return [{"name": "Rick"}, {"name": "Morty"}] + + internal_router.include_router(users_router) + app.include_router(internal_router) + ``` + """ if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith( @@ -900,33 +1295,354 @@ class APIRouter(routing.Router): def get( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP GET operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.get("/items/") + def read_items(): + return [{"name": "Empanada"}, {"name": "Arepa"}] + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -956,33 +1672,359 @@ class APIRouter(routing.Router): def put( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PUT operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.put("/items/{item_id}") + def replace_item(item_id: str, item: Item): + return {"message": "Item replaced", "id": item_id} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1012,33 +2054,359 @@ class APIRouter(routing.Router): def post( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP POST operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.post("/items/") + def create_item(item: Item): + return {"message": "Item created"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1068,33 +2436,354 @@ class APIRouter(routing.Router): def delete( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP DELETE operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.delete("/items/{item_id}") + def delete_item(item_id: str): + return {"message": "Item deleted"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1124,33 +2813,354 @@ class APIRouter(routing.Router): def options( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP OPTIONS operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + + app = FastAPI() + router = APIRouter() + + @router.options("/items/") + def get_item_options(): + return {"additions": ["Aji", "Guacamole"]} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1180,33 +3190,359 @@ class APIRouter(routing.Router): def head( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP HEAD operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.head("/items/", status_code=204) + def get_items_headers(response: Response): + response.headers["X-Cat-Dog"] = "Alone in the world" + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1236,33 +3572,359 @@ class APIRouter(routing.Router): def patch( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP PATCH operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.patch("/items/") + def update_item(item: Item): + return {"message": "Item updated in place"} + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1292,33 +3954,359 @@ class APIRouter(routing.Router): def trace( self, - path: str, + path: Annotated[ + str, + Doc( + """ + The URL path to be used for this *path operation*. + + For example, in `http://example.com/items`, the path is `/items`. + """ + ), + ], *, - response_model: Any = Default(None), - status_code: Optional[int] = None, - tags: Optional[List[Union[str, Enum]]] = None, - dependencies: Optional[Sequence[params.Depends]] = None, - summary: Optional[str] = None, - description: Optional[str] = None, - response_description: str = "Successful Response", - responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None, - deprecated: Optional[bool] = None, - operation_id: Optional[str] = None, - response_model_include: Optional[IncEx] = None, - response_model_exclude: Optional[IncEx] = None, - response_model_by_alias: bool = True, - response_model_exclude_unset: bool = False, - response_model_exclude_defaults: bool = False, - response_model_exclude_none: bool = False, - include_in_schema: bool = True, - response_class: Type[Response] = Default(JSONResponse), - name: Optional[str] = None, - callbacks: Optional[List[BaseRoute]] = None, - openapi_extra: Optional[Dict[str, Any]] = None, - generate_unique_id_function: Callable[[APIRoute], str] = Default( - generate_unique_id - ), + response_model: Annotated[ + Any, + Doc( + """ + The type to use for the response. + + It could be any valid Pydantic *field* type. So, it doesn't have to + be a Pydantic model, it could be other things, like a `list`, `dict`, + etc. + + It will be used for: + + * Documentation: the generated OpenAPI (and the UI at `/docs`) will + show it as the response (JSON Schema). + * Serialization: you could return an arbitrary object and the + `response_model` would be used to serialize that object into the + corresponding JSON. + * Filtering: the JSON sent to the client will only contain the data + (fields) defined in the `response_model`. If you returned an object + that contains an attribute `password` but the `response_model` does + not include that field, the JSON sent to the client would not have + that `password`. + * Validation: whatever you return will be serialized with the + `response_model`, converting any data as necessary to generate the + corresponding JSON. But if the data in the object returned is not + valid, that would mean a violation of the contract with the client, + so it's an error from the API developer. So, FastAPI will raise an + error and return a 500 error code (Internal Server Error). + + Read more about it in the + [FastAPI docs for Response Model](https://fastapi.tiangolo.com/tutorial/response-model/). + """ + ), + ] = Default(None), + status_code: Annotated[ + Optional[int], + Doc( + """ + The default status code to be used for the response. + + You could override the status code by returning a response directly. + + Read more about it in the + [FastAPI docs for Response Status Code](https://fastapi.tiangolo.com/tutorial/response-status-code/). + """ + ), + ] = None, + tags: Annotated[ + Optional[List[Union[str, Enum]]], + Doc( + """ + A list of tags to be applied to the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/#tags). + """ + ), + ] = None, + dependencies: Annotated[ + Optional[Sequence[params.Depends]], + Doc( + """ + A list of dependencies (using `Depends()`) to be applied to the + *path operation*. + + Read more about it in the + [FastAPI docs for Dependencies in path operation decorators](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/). + """ + ), + ] = None, + summary: Annotated[ + Optional[str], + Doc( + """ + A summary for the *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + A description for the *path operation*. + + If not provided, it will be extracted automatically from the docstring + of the *path operation function*. + + It can contain Markdown. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Path Operation Configuration](https://fastapi.tiangolo.com/tutorial/path-operation-configuration/). + """ + ), + ] = None, + response_description: Annotated[ + str, + Doc( + """ + The description for the default response. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = "Successful Response", + responses: Annotated[ + Optional[Dict[Union[int, str], Dict[str, Any]]], + Doc( + """ + Additional responses that could be returned by this *path operation*. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + deprecated: Annotated[ + Optional[bool], + Doc( + """ + Mark this *path operation* as deprecated. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + operation_id: Annotated[ + Optional[str], + Doc( + """ + Custom operation ID to be used by this *path operation*. + + By default, it is generated automatically. + + If you provide a custom operation ID, you need to make sure it is + unique for the whole API. + + You can customize the + operation ID generation with the parameter + `generate_unique_id_function` in the `FastAPI` class. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = None, + response_model_include: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to include only certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_exclude: Annotated[ + Optional[IncEx], + Doc( + """ + Configuration passed to Pydantic to exclude certain fields in the + response data. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = None, + response_model_by_alias: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response model + should be serialized by alias when an alias is used. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_include-and-response_model_exclude). + """ + ), + ] = True, + response_model_exclude_unset: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that were not set and + have their default values. This is different from + `response_model_exclude_defaults` in that if the fields are set, + they will be included in the response, even if the value is the same + as the default. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_defaults: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data + should have all the fields, including the ones that have the same value + as the default. This is different from `response_model_exclude_unset` + in that if the fields are set but contain the same default values, + they will be excluded from the response. + + When `True`, default values are omitted from the response. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#use-the-response_model_exclude_unset-parameter). + """ + ), + ] = False, + response_model_exclude_none: Annotated[ + bool, + Doc( + """ + Configuration passed to Pydantic to define if the response data should + exclude fields set to `None`. + + This is much simpler (less smart) than `response_model_exclude_unset` + and `response_model_exclude_defaults`. You probably want to use one of + those two instead of this one, as those allow returning `None` values + when it makes sense. + + Read more about it in the + [FastAPI docs for Response Model - Return Type](https://fastapi.tiangolo.com/tutorial/response-model/#response_model_exclude_none). + """ + ), + ] = False, + include_in_schema: Annotated[ + bool, + Doc( + """ + Include this *path operation* in the generated OpenAPI schema. + + This affects the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for Query Parameters and String Validations](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#exclude-from-openapi). + """ + ), + ] = True, + response_class: Annotated[ + Type[Response], + Doc( + """ + Response class to be used for this *path operation*. + + This will not be used if you return a response directly. + + Read more about it in the + [FastAPI docs for Custom Response - HTML, Stream, File, others](https://fastapi.tiangolo.com/advanced/custom-response/#redirectresponse). + """ + ), + ] = Default(JSONResponse), + name: Annotated[ + Optional[str], + Doc( + """ + Name for this *path operation*. Only used internally. + """ + ), + ] = None, + callbacks: Annotated[ + Optional[List[BaseRoute]], + Doc( + """ + List of *path operations* that will be used as OpenAPI callbacks. + + This is only for OpenAPI documentation, the callbacks won't be used + directly. + + It will be added to the generated OpenAPI (e.g. visible at `/docs`). + + Read more about it in the + [FastAPI docs for OpenAPI Callbacks](https://fastapi.tiangolo.com/advanced/openapi-callbacks/). + """ + ), + ] = None, + openapi_extra: Annotated[ + Optional[Dict[str, Any]], + Doc( + """ + Extra metadata to be included in the OpenAPI schema for this *path + operation*. + + Read more about it in the + [FastAPI docs for Path Operation Advanced Configuration](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#custom-openapi-path-operation-schema). + """ + ), + ] = None, + generate_unique_id_function: Annotated[ + Callable[[APIRoute], str], + Doc( + """ + Customize the function used to generate unique IDs for the *path + operations* shown in the generated OpenAPI. + + This is particularly useful when automatically generating clients or + SDKs for your API. + + Read more about it in the + [FastAPI docs about how to Generate Clients](https://fastapi.tiangolo.com/advanced/generate-clients/#custom-generate-unique-id-function). + """ + ), + ] = Default(generate_unique_id), ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add a *path operation* using an HTTP TRACE operation. + + ## Example + + ```python + from fastapi import APIRouter, FastAPI + from pydantic import BaseModel + + class Item(BaseModel): + name: str + description: str | None = None + + app = FastAPI() + router = APIRouter() + + @router.put("/items/{item_id}") + def trace_item(item_id: str): + return None + + app.include_router(router) + ``` + """ return self.api_route( path=path, response_model=response_model, @@ -1346,9 +4334,34 @@ class APIRouter(routing.Router): generate_unique_id_function=generate_unique_id_function, ) + @deprecated( + """ + on_event is deprecated, use lifespan event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/). + """ + ) def on_event( - self, event_type: str + self, + event_type: Annotated[ + str, + Doc( + """ + The type of event. `startup` or `shutdown`. + """ + ), + ], ) -> Callable[[DecoratedCallable], DecoratedCallable]: + """ + Add an event handler for the router. + + `on_event` is deprecated, use `lifespan` event handlers instead. + + Read more about it in the + [FastAPI docs for Lifespan Events](https://fastapi.tiangolo.com/advanced/events/#alternative-events-deprecated). + """ + def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_event_handler(event_type, func) return func diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 8b2c5c080..b1a6b4f94 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -5,6 +5,7 @@ from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class APIKeyBase(SecurityBase): @@ -12,13 +13,83 @@ class APIKeyBase(SecurityBase): class APIKeyQuery(APIKeyBase): + """ + API key authentication using a query parameter. + + This defines the name of the query parameter that should be provided in the request + with the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the query parameter automatically and provides it as the + dependency result. But it doesn't define how to send that API key to the client. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyQuery + + app = FastAPI() + + query_scheme = APIKeyQuery(name="api_key") + + + @app.get("/items/") + async def read_items(api_key: str = Depends(query_scheme)): + return {"api_key": api_key} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + name: Annotated[ + str, + Doc("Query parameter name."), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the query parameter is not provided, `APIKeyQuery` will + automatically cancel the request and sebd the client an error. + + If `auto_error` is set to `False`, when the query parameter is not + available, instead of erroring out, the dependency result will be + `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a query + parameter or in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.query}, # type: ignore[arg-type] @@ -41,13 +112,79 @@ class APIKeyQuery(APIKeyBase): class APIKeyHeader(APIKeyBase): + """ + API key authentication using a header. + + This defines the name of the header that should be provided in the request with + the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the header automatically and provides it as the dependency + result. But it doesn't define how to send that key to the client. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyHeader + + app = FastAPI() + + header_scheme = APIKeyHeader(name="x-key") + + + @app.get("/items/") + async def read_items(key: str = Depends(header_scheme)): + return {"key": key} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + name: Annotated[str, Doc("Header name.")], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the header is not provided, `APIKeyHeader` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the header is not available, + instead of erroring out, the dependency result will be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a header or + in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.header}, # type: ignore[arg-type] @@ -70,13 +207,79 @@ class APIKeyHeader(APIKeyBase): class APIKeyCookie(APIKeyBase): + """ + API key authentication using a cookie. + + This defines the name of the cookie that should be provided in the request with + the API key and integrates that into the OpenAPI documentation. It extracts + the key value sent in the cookie automatically and provides it as the dependency + result. But it doesn't define how to set that cookie. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be a string containing the key value. + + ## Example + + ```python + from fastapi import Depends, FastAPI + from fastapi.security import APIKeyCookie + + app = FastAPI() + + cookie_scheme = APIKeyCookie(name="session") + + + @app.get("/items/") + async def read_items(session: str = Depends(cookie_scheme)): + return {"session": session} + ``` + """ + def __init__( self, *, - name: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + name: Annotated[str, Doc("Cookie name.")], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the cookie is not provided, `APIKeyCookie` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the cookie is not available, + instead of erroring out, the dependency result will be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in a cookie or + in an HTTP Bearer token). + """ + ), + ] = True, ): self.model: APIKey = APIKey( **{"in": APIKeyIn.cookie}, # type: ignore[arg-type] diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 8fc0aafd9..3627777d6 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -10,16 +10,60 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class HTTPBasicCredentials(BaseModel): - username: str - password: str + """ + The HTTP Basic credendials given as the result of using `HTTPBasic` in a + dependency. + + Read more about it in the + [FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/). + """ + + username: Annotated[str, Doc("The HTTP Basic username.")] + password: Annotated[str, Doc("The HTTP Basic password.")] class HTTPAuthorizationCredentials(BaseModel): - scheme: str - credentials: str + """ + The HTTP authorization credentials in the result of using `HTTPBearer` or + `HTTPDigest` in a dependency. + + The HTTP authorization header value is split by the first space. + + The first part is the `scheme`, the second part is the `credentials`. + + For example, in an HTTP Bearer token scheme, the client will send a header + like: + + ``` + Authorization: Bearer deadbeef12346 + ``` + + In this case: + + * `scheme` will have the value `"Bearer"` + * `credentials` will have the value `"deadbeef12346"` + """ + + scheme: Annotated[ + str, + Doc( + """ + The HTTP authorization scheme extracted from the header value. + """ + ), + ] + credentials: Annotated[ + str, + Doc( + """ + The HTTP authorization credentials extracted from the header value. + """ + ), + ] class HTTPBase(SecurityBase): @@ -51,13 +95,89 @@ class HTTPBase(SecurityBase): class HTTPBasic(HTTPBase): + """ + HTTP Basic authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPBasicCredentials` object containing the + `username` and the `password`. + + Read more about it in the + [FastAPI docs for HTTP Basic Auth](https://fastapi.tiangolo.com/advanced/security/http-basic-auth/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPBasic, HTTPBasicCredentials + + app = FastAPI() + + security = HTTPBasic() + + + @app.get("/users/me") + def read_current_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]): + return {"username": credentials.username, "password": credentials.password} + ``` + """ + def __init__( self, *, - scheme_name: Optional[str] = None, - realm: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + realm: Annotated[ + Optional[str], + Doc( + """ + HTTP Basic authentication realm. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Basic authentication is not provided (a + header), `HTTPBasic` will automatically cancel the request and send the + client an error. + + If `auto_error` is set to `False`, when the HTTP Basic authentication + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in HTTP Basic + authentication or in an HTTP Bearer token). + """ + ), + ] = True, ): self.model = HTTPBaseModel(scheme="basic", description=description) self.scheme_name = scheme_name or self.__class__.__name__ @@ -98,13 +218,81 @@ class HTTPBasic(HTTPBase): class HTTPBearer(HTTPBase): + """ + HTTP Bearer token authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPAuthorizationCredentials` object containing + the `scheme` and the `credentials`. + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + + app = FastAPI() + + security = HTTPBearer() + + + @app.get("/users/me") + def read_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)] + ): + return {"scheme": credentials.scheme, "credentials": credentials.credentials} + ``` + """ + def __init__( self, *, - bearerFormat: Optional[str] = None, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + bearerFormat: Annotated[Optional[str], Doc("Bearer token format.")] = None, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Bearer token not provided (in an + `Authorization` header), `HTTPBearer` will automatically cancel the + request and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Bearer token + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in an HTTP + Bearer token or in a cookie). + """ + ), + ] = True, ): self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description) self.scheme_name = scheme_name or self.__class__.__name__ @@ -134,12 +322,79 @@ class HTTPBearer(HTTPBase): class HTTPDigest(HTTPBase): + """ + HTTP Digest authentication. + + ## Usage + + Create an instance object and use that object as the dependency in `Depends()`. + + The dependency result will be an `HTTPAuthorizationCredentials` object containing + the `scheme` and the `credentials`. + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import HTTPAuthorizationCredentials, HTTPDigest + + app = FastAPI() + + security = HTTPDigest() + + + @app.get("/users/me") + def read_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)] + ): + return {"scheme": credentials.scheme, "credentials": credentials.credentials} + ``` + """ + def __init__( self, *, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if the HTTP Digest not provided, `HTTPDigest` will + automatically cancel the request and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Digest is not + available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, in HTTP + Digest or in a cookie). + """ + ), + ] = True, ): self.model = HTTPBaseModel(scheme="digest", description=description) self.scheme_name = scheme_name or self.__class__.__name__ diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index e4c4357e7..d427783ad 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -10,51 +10,136 @@ from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN # TODO: import from typing when deprecating Python 3.9 -from typing_extensions import Annotated +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class OAuth2PasswordRequestForm: """ - This is a dependency class, use it like: + This is a dependency class to collect the `username` and `password` as form data + for an OAuth2 password flow. - @app.post("/login") - def login(form_data: OAuth2PasswordRequestForm = Depends()): - data = form_data.parse() - print(data.username) - print(data.password) - for scope in data.scopes: - print(scope) - if data.client_id: - print(data.client_id) - if data.client_secret: - print(data.client_secret) - return data + The OAuth2 specification dictates that for a password flow the data should be + collected using form data (instead of JSON) and that it should have the specific + fields `username` and `password`. + + All the initialization parameters are extracted from the request. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import OAuth2PasswordRequestForm + + app = FastAPI() - It creates the following Form request parameters in your endpoint: + @app.post("/login") + def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]): + data = {} + data["scopes"] = [] + for scope in form_data.scopes: + data["scopes"].append(scope) + if form_data.client_id: + data["client_id"] = form_data.client_id + if form_data.client_secret: + data["client_secret"] = form_data.client_secret + return data + ``` - grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". - Nevertheless, this dependency class is permissive and allows not passing it. If you want to enforce it, - use instead the OAuth2PasswordRequestFormStrict dependency. - username: username string. The OAuth2 spec requires the exact field name "username". - password: password string. The OAuth2 spec requires the exact field name "password". - scope: Optional string. Several scopes (each one a string) separated by spaces. E.g. - "items:read items:write users:read profile openid" - client_id: optional string. OAuth2 recommends sending the client_id and client_secret (if any) - using HTTP Basic auth, as: client_id:client_secret - client_secret: optional string. OAuth2 recommends sending the client_id and client_secret (if any) - using HTTP Basic auth, as: client_id:client_secret + Note that for OAuth2 the scope `items:read` is a single scope in an opaque string. + You could have custom internal logic to separate it by colon caracters (`:`) or + similar, and get the two parts `items` and `read`. Many applications do that to + group and organize permisions, you could do it as well in your application, just + know that that it is application specific, it's not part of the specification. """ def __init__( self, *, - grant_type: Annotated[Union[str, None], Form(pattern="password")] = None, - username: Annotated[str, Form()], - password: Annotated[str, Form()], - scope: Annotated[str, Form()] = "", - client_id: Annotated[Union[str, None], Form()] = None, - client_secret: Annotated[Union[str, None], Form()] = None, + grant_type: Annotated[ + Union[str, None], + Form(pattern="password"), + Doc( + """ + The OAuth2 spec says it is required and MUST be the fixed string + "password". Nevertheless, this dependency class is permissive and + allows not passing it. If you want to enforce it, use instead the + `OAuth2PasswordRequestFormStrict` dependency. + """ + ), + ] = None, + username: Annotated[ + str, + Form(), + Doc( + """ + `username` string. The OAuth2 spec requires the exact field name + `username`. + """ + ), + ], + password: Annotated[ + str, + Form(), + Doc( + """ + `password` string. The OAuth2 spec requires the exact field name + `password". + """ + ), + ], + scope: Annotated[ + str, + Form(), + Doc( + """ + A single string with actually several scopes separated by spaces. Each + scope is also a string. + + For example, a single string with: + + ```python + "items:read items:write users:read profile openid" + ```` + + would represent the scopes: + + * `items:read` + * `items:write` + * `users:read` + * `profile` + * `openid` + """ + ), + ] = "", + client_id: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_id`, it can be sent as part of the form fields. + But the OAuth2 specification recommends sending the `client_id` and + `client_secret` (if any) using HTTP Basic auth. + """ + ), + ] = None, + client_secret: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_password` (and a `client_id`), they can be sent + as part of the form fields. But the OAuth2 specification recommends + sending the `client_id` and `client_secret` (if any) using HTTP Basic + auth. + """ + ), + ] = None, ): self.grant_type = grant_type self.username = username @@ -66,23 +151,54 @@ class OAuth2PasswordRequestForm: class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): """ - This is a dependency class, use it like: + This is a dependency class to collect the `username` and `password` as form data + for an OAuth2 password flow. - @app.post("/login") - def login(form_data: OAuth2PasswordRequestFormStrict = Depends()): - data = form_data.parse() - print(data.username) - print(data.password) - for scope in data.scopes: - print(scope) - if data.client_id: - print(data.client_id) - if data.client_secret: - print(data.client_secret) - return data + The OAuth2 specification dictates that for a password flow the data should be + collected using form data (instead of JSON) and that it should have the specific + fields `username` and `password`. + + All the initialization parameters are extracted from the request. + + The only difference between `OAuth2PasswordRequestFormStrict` and + `OAuth2PasswordRequestForm` is that `OAuth2PasswordRequestFormStrict` requires the + client to send the form field `grant_type` with the value `"password"`, which + is required in the OAuth2 specification (it seems that for no particular reason), + while for `OAuth2PasswordRequestForm` `grant_type` is optional. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + + ## Example + + ```python + from typing import Annotated + + from fastapi import Depends, FastAPI + from fastapi.security import OAuth2PasswordRequestForm + + app = FastAPI() - It creates the following Form request parameters in your endpoint: + @app.post("/login") + def login(form_data: Annotated[OAuth2PasswordRequestFormStrict, Depends()]): + data = {} + data["scopes"] = [] + for scope in form_data.scopes: + data["scopes"].append(scope) + if form_data.client_id: + data["client_id"] = form_data.client_id + if form_data.client_secret: + data["client_secret"] = form_data.client_secret + return data + ``` + + Note that for OAuth2 the scope `items:read` is a single scope in an opaque string. + You could have custom internal logic to separate it by colon caracters (`:`) or + similar, and get the two parts `items` and `read`. Many applications do that to + group and organize permisions, you could do it as well in your application, just + know that that it is application specific, it's not part of the specification. + grant_type: the OAuth2 spec says it is required and MUST be the fixed string "password". This dependency is strict about it. If you want to be permissive, use instead the @@ -99,12 +215,85 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): def __init__( self, - grant_type: Annotated[str, Form(pattern="password")], - username: Annotated[str, Form()], - password: Annotated[str, Form()], - scope: Annotated[str, Form()] = "", - client_id: Annotated[Union[str, None], Form()] = None, - client_secret: Annotated[Union[str, None], Form()] = None, + grant_type: Annotated[ + str, + Form(pattern="password"), + Doc( + """ + The OAuth2 spec says it is required and MUST be the fixed string + "password". This dependency is strict about it. If you want to be + permissive, use instead the `OAuth2PasswordRequestForm` dependency + class. + """ + ), + ], + username: Annotated[ + str, + Form(), + Doc( + """ + `username` string. The OAuth2 spec requires the exact field name + `username`. + """ + ), + ], + password: Annotated[ + str, + Form(), + Doc( + """ + `password` string. The OAuth2 spec requires the exact field name + `password". + """ + ), + ], + scope: Annotated[ + str, + Form(), + Doc( + """ + A single string with actually several scopes separated by spaces. Each + scope is also a string. + + For example, a single string with: + + ```python + "items:read items:write users:read profile openid" + ```` + + would represent the scopes: + + * `items:read` + * `items:write` + * `users:read` + * `profile` + * `openid` + """ + ), + ] = "", + client_id: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_id`, it can be sent as part of the form fields. + But the OAuth2 specification recommends sending the `client_id` and + `client_secret` (if any) using HTTP Basic auth. + """ + ), + ] = None, + client_secret: Annotated[ + Union[str, None], + Form(), + Doc( + """ + If there's a `client_password` (and a `client_id`), they can be sent + as part of the form fields. But the OAuth2 specification recommends + sending the `client_id` and `client_secret` (if any) using HTTP Basic + auth. + """ + ), + ] = None, ): super().__init__( grant_type=grant_type, @@ -117,13 +306,69 @@ class OAuth2PasswordRequestFormStrict(OAuth2PasswordRequestForm): class OAuth2(SecurityBase): + """ + This is the base class for OAuth2 authentication, an instance of it would be used + as a dependency. All other OAuth2 classes inherit from it and customize it for + each OAuth2 flow. + + You normally would not create a new class inheriting from it but use one of the + existing subclasses, and maybe compose them if you want to support multiple flows. + + Read more about it in the + [FastAPI docs for Security](https://fastapi.tiangolo.com/tutorial/security/). + """ + def __init__( self, *, - flows: Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]] = OAuthFlowsModel(), - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + flows: Annotated[ + Union[OAuthFlowsModel, Dict[str, Dict[str, Any]]], + Doc( + """ + The dictionary of OAuth2 flows. + """ + ), + ] = OAuthFlowsModel(), + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): self.model = OAuth2Model( flows=cast(OAuthFlowsModel, flows), description=description @@ -144,13 +389,74 @@ class OAuth2(SecurityBase): class OAuth2PasswordBearer(OAuth2): + """ + OAuth2 flow for authentication using a bearer token obtained with a password. + An instance of it would be used as a dependency. + + Read more about it in the + [FastAPI docs for Simple OAuth2 with Password and Bearer](https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/). + """ + def __init__( self, - tokenUrl: str, - scheme_name: Optional[str] = None, - scopes: Optional[Dict[str, str]] = None, - description: Optional[str] = None, - auto_error: bool = True, + tokenUrl: Annotated[ + str, + Doc( + """ + The URL to obtain the OAuth2 token. This would be the *path operation* + that has `OAuth2PasswordRequestForm` as a dependency. + """ + ), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + scopes: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + The OAuth2 scopes that would be required by the *path operations* that + use this dependency. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): if not scopes: scopes = {} @@ -180,15 +486,79 @@ class OAuth2PasswordBearer(OAuth2): class OAuth2AuthorizationCodeBearer(OAuth2): + """ + OAuth2 flow for authentication using a bearer token obtained with an OAuth2 code + flow. An instance of it would be used as a dependency. + """ + def __init__( self, authorizationUrl: str, - tokenUrl: str, - refreshUrl: Optional[str] = None, - scheme_name: Optional[str] = None, - scopes: Optional[Dict[str, str]] = None, - description: Optional[str] = None, - auto_error: bool = True, + tokenUrl: Annotated[ + str, + Doc( + """ + The URL to obtain the OAuth2 token. + """ + ), + ], + refreshUrl: Annotated[ + Optional[str], + Doc( + """ + The URL to refresh the token and obtain a new one. + """ + ), + ] = None, + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + scopes: Annotated[ + Optional[Dict[str, str]], + Doc( + """ + The OAuth2 scopes that would be required by the *path operations* that + use this dependency. + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OAuth2 authentication, it will automatically cancel the request and + send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OAuth2 + or in a cookie). + """ + ), + ] = True, ): if not scopes: scopes = {} @@ -226,6 +596,45 @@ class OAuth2AuthorizationCodeBearer(OAuth2): class SecurityScopes: - def __init__(self, scopes: Optional[List[str]] = None): - self.scopes = scopes or [] - self.scope_str = " ".join(self.scopes) + """ + This is a special class that you can define in a parameter in a dependency to + obtain the OAuth2 scopes required by all the dependencies in the same chain. + + This way, multiple dependencies can have different scopes, even when used in the + same *path operation*. And with this, you can access all the scopes required in + all those dependencies in a single place. + + Read more about it in the + [FastAPI docs for OAuth2 scopes](https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/). + """ + + def __init__( + self, + scopes: Annotated[ + Optional[List[str]], + Doc( + """ + This will be filled by FastAPI. + """ + ), + ] = None, + ): + self.scopes: Annotated[ + List[str], + Doc( + """ + The list of all the scopes required by dependencies. + """ + ), + ] = ( + scopes or [] + ) + self.scope_str: Annotated[ + str, + Doc( + """ + All the scopes required by all the dependencies in a single string + separated by spaces, as defined in the OAuth2 specification. + """ + ), + ] = " ".join(self.scopes) diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 4e65f1f6c..c612b475d 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -5,16 +5,66 @@ from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc # type: ignore [attr-defined] class OpenIdConnect(SecurityBase): + """ + OpenID Connect authentication class. An instance of it would be used as a + dependency. + """ + def __init__( self, *, - openIdConnectUrl: str, - scheme_name: Optional[str] = None, - description: Optional[str] = None, - auto_error: bool = True, + openIdConnectUrl: Annotated[ + str, + Doc( + """ + The OpenID Connect URL. + """ + ), + ], + scheme_name: Annotated[ + Optional[str], + Doc( + """ + Security scheme name. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + description: Annotated[ + Optional[str], + Doc( + """ + Security scheme description. + + It will be included in the generated OpenAPI (e.g. visible at `/docs`). + """ + ), + ] = None, + auto_error: Annotated[ + bool, + Doc( + """ + By default, if no HTTP Auhtorization header is provided, required for + OpenID Connect authentication, it will automatically cancel the request + and send the client an error. + + If `auto_error` is set to `False`, when the HTTP Authorization header + is not available, instead of erroring out, the dependency result will + be `None`. + + This is useful when you want to have optional authentication. + + It is also useful when you want to have authentication that can be + provided in one of multiple optional ways (for example, with OpenID + Connect or in a cookie). + """ + ), + ] = True, ): self.model = OpenIdConnectModel( openIdConnectUrl=openIdConnectUrl, description=description diff --git a/pyproject.toml b/pyproject.toml index b49f472d5..addde1d33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ classifiers = [ dependencies = [ "starlette>=0.27.0,<0.28.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", - "typing-extensions>=4.5.0", + "typing-extensions>=4.8.0", # TODO: remove this pin after upgrading Starlette 0.31.1 "anyio>=3.7.1,<4.0.0", ] diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt new file mode 100644 index 000000000..1a4a57267 --- /dev/null +++ b/requirements-docs-tests.txt @@ -0,0 +1,3 @@ +# For mkdocstrings and tests +httpx >=0.23.0,<0.25.0 +black == 23.3.0 diff --git a/requirements-docs.txt b/requirements-docs.txt index 2e667720e..3e0df6483 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,4 +1,5 @@ -e . +-r requirements-docs-tests.txt mkdocs-material==9.1.21 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 @@ -12,3 +13,5 @@ jieba==0.42.1 pillow==9.5.0 # For image processing by Material for MkDocs cairosvg==2.7.0 +mkdocstrings[python]==0.23.0 +griffe-typingdoc==0.2.2 diff --git a/requirements-tests.txt b/requirements-tests.txt index 6f7f4ac23..de8d3f26c 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,11 +1,10 @@ -e . +-r requirements-docs-tests.txt pydantic-settings >=2.0.0 pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.1 ruff ==0.0.275 -black == 23.3.0 -httpx >=0.23.0,<0.25.0 email_validator >=1.1.1,<3.0.0 dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy diff --git a/scripts/docs.py b/scripts/docs.py index 968dd9a3d..0023c670c 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -153,17 +153,21 @@ index_sponsors_template = """ def generate_readme_content() -> str: en_index = en_docs_path / "docs" / "index.md" content = en_index.read_text("utf-8") + match_pre = re.search(r"</style>\n\n", content) match_start = re.search(r"<!-- sponsors -->", content) match_end = re.search(r"<!-- /sponsors -->", content) sponsors_data_path = en_docs_path / "data" / "sponsors.yml" sponsors = mkdocs.utils.yaml_load(sponsors_data_path.read_text(encoding="utf-8")) if not (match_start and match_end): raise RuntimeError("Couldn't auto-generate sponsors section") + if not match_pre: + raise RuntimeError("Couldn't find pre section (<style>) in index.md") + frontmatter_end = match_pre.end() pre_end = match_start.end() post_start = match_end.start() template = Template(index_sponsors_template) message = template.render(sponsors=sponsors) - pre_content = content[:pre_end] + pre_content = content[frontmatter_end:pre_end] post_content = content[post_start:] new_content = pre_content + message + post_content return new_content @@ -286,7 +290,6 @@ def update_config() -> None: else: use_name = alternate_dict[url] new_alternate.append({"link": url, "name": use_name}) - config["nav"][1] = {"Languages": languages} config["extra"]["alternate"] = new_alternate en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index 008751f8a..b12487b50 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -8,6 +8,11 @@ from mkdocs.structure.files import File, Files from mkdocs.structure.nav import Link, Navigation, Section from mkdocs.structure.pages import Page +non_traslated_sections = [ + "reference/", + "release-notes.md", +] + @lru_cache() def get_missing_translation_content(docs_dir: str) -> str: @@ -123,6 +128,9 @@ def on_page_markdown( markdown: str, *, page: Page, config: MkDocsConfig, files: Files ) -> str: if isinstance(page.file, EnFile): + for excluded_section in non_traslated_sections: + if page.file.src_path.startswith(excluded_section): + return markdown missing_translation_content = get_missing_translation_content(config.docs_dir) header = "" body = markdown diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index b91467265..7e57d525c 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,3 +1,4 @@ +import io from pathlib import Path from typing import List @@ -52,3 +53,20 @@ def test_upload_file_is_closed(tmp_path: Path): assert testing_file_store assert testing_file_store[0].file.closed + + +# For UploadFile coverage, segments copied from Starlette tests + + +@pytest.mark.anyio +async def test_upload_file(): + stream = io.BytesIO(b"data") + file = UploadFile(filename="file", file=stream, size=4) + assert await file.read() == b"data" + assert file.size == 4 + await file.write(b" and more data!") + assert await file.read() == b"" + assert file.size == 19 + await file.seek(0) + assert await file.read() == b"data and more data!" + await file.close() diff --git a/tests/test_router_events.py b/tests/test_router_events.py index ba6b76382..1b9de18ae 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -21,6 +21,9 @@ def state() -> State: return State() +@pytest.mark.filterwarnings( + r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning" +) def test_router_events(state: State) -> None: app = FastAPI() diff --git a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py index 25d6df3e9..13568a532 100644 --- a/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py +++ b/tests/test_tutorial/test_async_sql_databases/test_tutorial001.py @@ -1,13 +1,20 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.async_sql_databases.tutorial001 import app - from ...utils import needs_pydanticv1 +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.async_sql_databases.tutorial001 import app + yield app + + # TODO: pv2 add version with Pydantic v2 @needs_pydanticv1 -def test_create_read(): +def test_create_read(app: FastAPI): with TestClient(app) as client: note = {"text": "Foo bar", "completed": False} response = client.post("/notes/", json=note) @@ -21,7 +28,7 @@ def test_create_read(): assert data in response.json() -def test_openapi_schema(): +def test_openapi_schema(app: FastAPI): with TestClient(app) as client: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_events/test_tutorial001.py b/tests/test_tutorial/test_events/test_tutorial001.py index a5bb299ac..f65b92d12 100644 --- a/tests/test_tutorial/test_events/test_tutorial001.py +++ b/tests/test_tutorial/test_events/test_tutorial001.py @@ -1,16 +1,23 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.events.tutorial001 import app + +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.events.tutorial001 import app + yield app -def test_events(): +def test_events(app: FastAPI): with TestClient(app) as client: response = client.get("/items/foo") assert response.status_code == 200, response.text assert response.json() == {"name": "Fighters"} -def test_openapi_schema(): +def test_openapi_schema(app: FastAPI): with TestClient(app) as client: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_events/test_tutorial002.py b/tests/test_tutorial/test_events/test_tutorial002.py index 81cbf4ab6..137294d73 100644 --- a/tests/test_tutorial/test_events/test_tutorial002.py +++ b/tests/test_tutorial/test_events/test_tutorial002.py @@ -1,9 +1,16 @@ +import pytest +from fastapi import FastAPI from fastapi.testclient import TestClient -from docs_src.events.tutorial002 import app + +@pytest.fixture(name="app", scope="module") +def get_app(): + with pytest.warns(DeprecationWarning): + from docs_src.events.tutorial002 import app + yield app -def test_events(): +def test_events(app: FastAPI): with TestClient(app) as client: response = client.get("/items/") assert response.status_code == 200, response.text @@ -12,7 +19,7 @@ def test_events(): assert "Application shutdown" in log.read() -def test_openapi_schema(): +def test_openapi_schema(app: FastAPI): with TestClient(app) as client: response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_tutorial/test_testing/test_tutorial003.py b/tests/test_tutorial/test_testing/test_tutorial003.py index d9e16390e..2a5d67071 100644 --- a/tests/test_tutorial/test_testing/test_tutorial003.py +++ b/tests/test_tutorial/test_testing/test_tutorial003.py @@ -1,5 +1,7 @@ -from docs_src.app_testing.tutorial003 import test_read_items +import pytest def test_main(): + with pytest.warns(DeprecationWarning): + from docs_src.app_testing.tutorial003 import test_read_items test_read_items() From f056d001e5fe2b29f00cdc8e68f3c01feb40ce89 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 18 Oct 2023 12:37:29 +0000 Subject: [PATCH 1383/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 62ffd4b35..c3485050f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✨ Add reference (code API) docs with PEP 727, add subclass with custom docstrings for `BackgroundTasks`, refactor docs structure. PR [#10392](https://github.com/tiangolo/fastapi/pull/10392) by [@tiangolo](https://github.com/tiangolo). * ⬆ Bump dawidd6/action-download-artifact from 2.27.0 to 2.28.0. PR [#10268](https://github.com/tiangolo/fastapi/pull/10268) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 3 to 4. PR [#10208](https://github.com/tiangolo/fastapi/pull/10208) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). From 76e547f254f92a06d9b5a103dfa32693c3d38451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 18 Oct 2023 16:50:22 +0400 Subject: [PATCH 1384/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c3485050f..e49394e72 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,13 +7,22 @@ hide: ## Latest Changes -* ✨ Add reference (code API) docs with PEP 727, add subclass with custom docstrings for `BackgroundTasks`, refactor docs structure. PR [#10392](https://github.com/tiangolo/fastapi/pull/10392) by [@tiangolo](https://github.com/tiangolo). +## Features + +* ✨ Add reference (code API) docs with PEP 727, add subclass with custom docstrings for `BackgroundTasks`, refactor docs structure. PR [#10392](https://github.com/tiangolo/fastapi/pull/10392) by [@tiangolo](https://github.com/tiangolo). New docs at [FastAPI Reference - Code API](https://fastapi.tiangolo.com/reference/). + +## Upgrades + +* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * ⬆ Bump dawidd6/action-download-artifact from 2.27.0 to 2.28.0. PR [#10268](https://github.com/tiangolo/fastapi/pull/10268) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump actions/checkout from 3 to 4. PR [#10208](https://github.com/tiangolo/fastapi/pull/10208) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.6 to 1.8.10. PR [#10061](https://github.com/tiangolo/fastapi/pull/10061) by [@dependabot[bot]](https://github.com/apps/dependabot). -* ⬆️ Drop support for Python 3.7, require Python 3.8 or above. PR [#10442](https://github.com/tiangolo/fastapi/pull/10442) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, Bump.sh images. PR [#10381](https://github.com/tiangolo/fastapi/pull/10381) by [@tiangolo](https://github.com/tiangolo). * 👥 Update FastAPI People. PR [#10363](https://github.com/tiangolo/fastapi/pull/10363) by [@tiangolo](https://github.com/tiangolo). + ## 0.103.2 ### Refactors From 38f191dcd30c7e7b7d422b4406f8eb8c6805b09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 18 Oct 2023 16:51:07 +0400 Subject: [PATCH 1385/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?104.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index e49394e72..b78b2cb5a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.104.0 + ## Features * ✨ Add reference (code API) docs with PEP 727, add subclass with custom docstrings for `BackgroundTasks`, refactor docs structure. PR [#10392](https://github.com/tiangolo/fastapi/pull/10392) by [@tiangolo](https://github.com/tiangolo). New docs at [FastAPI Reference - Code API](https://fastapi.tiangolo.com/reference/). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 981ca4945..4fdb155c2 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.103.2" +__version__ = "0.104.0" from starlette import status as status From c13aa9ed5f19feccdb41a8af465cbaa1de218630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 20 Oct 2023 12:27:26 +0400 Subject: [PATCH 1386/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unnecessary?= =?UTF-8?q?=20duplicated=20docstrings=20(#10484)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 7 ------- fastapi/routing.py | 7 ------- 2 files changed, 14 deletions(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 46b7ae81b..8ca374a54 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -86,13 +86,6 @@ class FastAPI(Starlette): **Note**: you probably shouldn't use this parameter, it is inherited from Starlette and supported for compatibility. - In FastAPI, you normally would use the *path operation* decorators, - like: - - * `app.get()` - * `app.post()` - * etc. - --- A list of routes to serve incoming HTTP and WebSocket requests. diff --git a/fastapi/routing.py b/fastapi/routing.py index e33e1d832..54d53bbbf 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -624,13 +624,6 @@ class APIRouter(routing.Router): **Note**: you probably shouldn't use this parameter, it is inherited from Starlette and supported for compatibility. - In FastAPI, you normally would use the *path operation* decorators, - like: - - * `router.get()` - * `router.post()` - * etc. - --- A list of routes to serve incoming HTTP and WebSocket requests. From 7670a132b3ce3374ed52c02659989a07da406985 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 08:28:05 +0000 Subject: [PATCH 1387/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b78b2cb5a..90ad69c28 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). ## 0.104.0 ## Features From dc7838eec310454d9b1a41712e75d220e18e2bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 20 Oct 2023 12:39:03 +0400 Subject: [PATCH 1388/1881] =?UTF-8?q?=F0=9F=94=A5=20Drop/close=20Gitter=20?= =?UTF-8?q?chat.=20Questions=20should=20go=20to=20GitHub=20Discussions,=20?= =?UTF-8?q?free=20conversations=20to=20Discord.=20(#10485)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/help-fastapi.md | 2 - docs/en/docs/help-fastapi.md | 2 - docs/en/docs/js/chat.js | 3 -- docs/fr/docs/help-fastapi.md | 18 --------- docs/ja/docs/help-fastapi.md | 14 ------- docs/pl/docs/help-fastapi.md | 2 - docs/pt/docs/help-fastapi.md | 2 - docs/ru/docs/help-fastapi.md | 2 - docs/zh/docs/help-fastapi.md | 2 - scripts/gitter_releases_bot.py | 67 ---------------------------------- scripts/notify.sh | 5 --- 11 files changed, 119 deletions(-) delete mode 100644 docs/en/docs/js/chat.js delete mode 100644 scripts/gitter_releases_bot.py delete mode 100755 scripts/notify.sh diff --git a/docs/em/docs/help-fastapi.md b/docs/em/docs/help-fastapi.md index d7b66185d..b998ade42 100644 --- a/docs/em/docs/help-fastapi.md +++ b/docs/em/docs/help-fastapi.md @@ -231,8 +231,6 @@ ⚙️ 💬 🕴 🎏 🏢 💬. -📤 ⏮️ <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">🥊 💬</a>, ✋️ ⚫️ 🚫 ✔️ 📻 & 🏧 ⚒, 💬 🌖 ⚠, 😧 🔜 👍 ⚙️. - ### 🚫 ⚙️ 💬 ❔ ✔️ 🤯 👈 💬 ✔ 🌅 "🆓 💬", ⚫️ ⏩ 💭 ❔ 👈 💁‍♂️ 🏢 & 🌅 ⚠ ❔,, 👆 💪 🚫 📨 ❔. diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index e977dba20..8199c9b9a 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -231,8 +231,6 @@ Join the 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" targ Use the chat only for other general conversations. -There is also the previous <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">Gitter chat</a>, but as it doesn't have channels and advanced features, conversations are more difficult, so Discord is now the recommended system. - ### Don't use the chat for questions Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. diff --git a/docs/en/docs/js/chat.js b/docs/en/docs/js/chat.js deleted file mode 100644 index debdef4da..000000000 --- a/docs/en/docs/js/chat.js +++ /dev/null @@ -1,3 +0,0 @@ -((window.gitter = {}).chat = {}).options = { - room: 'tiangolo/fastapi' -}; diff --git a/docs/fr/docs/help-fastapi.md b/docs/fr/docs/help-fastapi.md index 3bc3c3a8a..525c699f5 100644 --- a/docs/fr/docs/help-fastapi.md +++ b/docs/fr/docs/help-fastapi.md @@ -84,24 +84,6 @@ Vous pouvez <a href="https://github.com/tiangolo/fastapi" class="external-link" * Pour corriger une Issue/Bug existant. * Pour ajouter une nouvelle fonctionnalité. -## Rejoindre le chat - -<a href="https://gitter.im/tiangolo/fastapi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank"> - <img src="https://badges.gitter.im/tiangolo/fastapi.svg" alt="Rejoindre le chat à https://gitter.im/tiangolo/fastapi"> -</a> - -Rejoignez le chat sur Gitter: <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">https://gitter.im/tiangolo/fastapi</a>. - -Vous pouvez y avoir des conversations rapides avec d'autres personnes, aider les autres, partager des idées, etc. - -Mais gardez à l'esprit que, comme il permet une "conversation plus libre", il est facile de poser des questions trop générales et plus difficiles à répondre, de sorte que vous risquez de ne pas recevoir de réponses. - -Dans les Issues de GitHub, le modèle vous guidera pour écrire la bonne question afin que vous puissiez plus facilement obtenir une bonne réponse, ou même résoudre le problème vous-même avant même de le poser. Et dans GitHub, je peux m'assurer que je réponds toujours à tout, même si cela prend du temps. Je ne peux pas faire cela personnellement avec le chat Gitter. 😅 - -Les conversations dans Gitter ne sont pas non plus aussi facilement consultables que dans GitHub, de sorte que les questions et les réponses peuvent se perdre dans la conversation. - -De l'autre côté, il y a plus de 1000 personnes dans le chat, il y a donc de fortes chances que vous y trouviez quelqu'un à qui parler, presque tout le temps. 😄 - ## Parrainer l'auteur Vous pouvez également soutenir financièrement l'auteur (moi) via <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a>. diff --git a/docs/ja/docs/help-fastapi.md b/docs/ja/docs/help-fastapi.md index 166acb586..e753b7ce3 100644 --- a/docs/ja/docs/help-fastapi.md +++ b/docs/ja/docs/help-fastapi.md @@ -82,20 +82,6 @@ GitHubレポジトリで<a href="https://github.com/tiangolo/fastapi/issues/new/ * 既存のissue/バグを修正。 * 新機能を追加。 -## チャットに参加 - -Gitterでチャットに参加: <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">https://gitter.im/tiangolo/fastapi</a>. - -そこで、他の人と手早く会話したり、手助けやアイデアの共有などができます。 - -しかし、「自由な会話」が許容されているので一般的すぎて回答が難しい質問もしやすくなります。そのせいで回答を得られないかもしれません。 - -GitHub issuesでは良い回答を得やすい質問ができるように、もしくは、質問する前に自身で解決できるようにテンプレートがガイドしてくれます。そして、GitHubではたとえ時間がかかっても全てに答えているか確認できます。個人的にはGitterチャットでは同じことはできないです。😅 - -Gitterでの会話はGitHubほど簡単に検索できないので、質問と回答が会話の中に埋もれてしまいます。 - -一方、チャットには1000人以上いるので、いつでも話し相手が見つかる可能性が高いです。😄 - ## 開発者のスポンサーになる <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub sponsors</a>を通して開発者を経済的にサポートできます。 diff --git a/docs/pl/docs/help-fastapi.md b/docs/pl/docs/help-fastapi.md index 723df91d1..3d02a8741 100644 --- a/docs/pl/docs/help-fastapi.md +++ b/docs/pl/docs/help-fastapi.md @@ -231,8 +231,6 @@ Dołącz do 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" t Używaj czatu tylko do innych ogólnych rozmów. -Istnieje również poprzedni <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">czat na Gitter</a>, ale ponieważ nie ma tam kanałów i zaawansowanych funkcji, rozmowy są trudniejsze, dlatego teraz zalecany jest Discord. - ### Nie zadawaj pytań na czacie Miej na uwadze, że ponieważ czaty pozwalają na bardziej "swobodną rozmowę", łatwo jest zadawać pytania, które są zbyt ogólne i trudniejsze do odpowiedzi, więc możesz nie otrzymać odpowiedzi. diff --git a/docs/pt/docs/help-fastapi.md b/docs/pt/docs/help-fastapi.md index d82ce3414..d04905197 100644 --- a/docs/pt/docs/help-fastapi.md +++ b/docs/pt/docs/help-fastapi.md @@ -114,8 +114,6 @@ do FastAPI. Use o chat apenas para outro tipo de assunto. -Também existe o <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">chat do Gitter</a>, porém ele não possuí canais e recursos avançados, conversas são mais engessadas, por isso o Discord é mais recomendado. - ### Não faça perguntas no chat Tenha em mente que os chats permitem uma "conversa mais livre", dessa forma é muito fácil fazer perguntas que são muito genéricas e dificeís de responder, assim você pode acabar não sendo respondido. diff --git a/docs/ru/docs/help-fastapi.md b/docs/ru/docs/help-fastapi.md index a69e37bd8..65ff768d1 100644 --- a/docs/ru/docs/help-fastapi.md +++ b/docs/ru/docs/help-fastapi.md @@ -223,8 +223,6 @@ Используйте этот чат только для бесед на отвлечённые темы. -Существует также <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">чат в Gitter</a>, но поскольку в нем нет каналов и расширенных функций, общение в нём сложнее, потому рекомендуемой системой является Discord. - ### Не использовать чаты для вопросов Имейте в виду, что чаты позволяют больше "свободного общения", потому там легко задавать вопросы, которые слишком общие и на которые труднее ответить, так что Вы можете не получить нужные Вам ответы. diff --git a/docs/zh/docs/help-fastapi.md b/docs/zh/docs/help-fastapi.md index 2a99950e3..9b70d115a 100644 --- a/docs/zh/docs/help-fastapi.md +++ b/docs/zh/docs/help-fastapi.md @@ -114,8 +114,6 @@ 聊天室仅供闲聊。 -我们之前还使用过 <a href="https://gitter.im/tiangolo/fastapi" class="external-link" target="_blank">Gitter chat</a>,但它不支持频道等高级功能,聊天也比较麻烦,所以现在推荐使用 Discord。 - ### 别在聊天室里提问 注意,聊天室更倾向于“闲聊”,经常有人会提出一些笼统得让人难以回答的问题,所以在这里提问一般没人回答。 diff --git a/scripts/gitter_releases_bot.py b/scripts/gitter_releases_bot.py deleted file mode 100644 index a033d0d69..000000000 --- a/scripts/gitter_releases_bot.py +++ /dev/null @@ -1,67 +0,0 @@ -import inspect -import os - -import requests - -room_id = "5c9c9540d73408ce4fbc1403" # FastAPI -# room_id = "5cc46398d73408ce4fbed233" # Gitter development - -gitter_token = os.getenv("GITTER_TOKEN") -assert gitter_token -github_token = os.getenv("GITHUB_TOKEN") -assert github_token -tag_name = os.getenv("TAG") -assert tag_name - - -def get_github_graphql(tag_name: str): - github_graphql = """ - { - repository(owner: "tiangolo", name: "fastapi") { - release (tagName: "{{tag_name}}" ) { - description - } - } - } - """ - github_graphql = github_graphql.replace("{{tag_name}}", tag_name) - return github_graphql - - -def get_github_release_text(tag_name: str): - url = "https://api.github.com/graphql" - headers = {"Authorization": f"Bearer {github_token}"} - github_graphql = get_github_graphql(tag_name=tag_name) - response = requests.post(url, json={"query": github_graphql}, headers=headers) - assert response.status_code == 200 - data = response.json() - return data["data"]["repository"]["release"]["description"] - - -def get_gitter_message(release_text: str): - text = f""" - New release! :tada: :rocket: - (by FastAPI bot) - - ## {tag_name} - """ - text = inspect.cleandoc(text) + "\n\n" + release_text - return text - - -def send_gitter_message(text: str): - headers = {"Authorization": f"Bearer {gitter_token}"} - url = f"https://api.gitter.im/v1/rooms/{room_id}/chatMessages" - data = {"text": text} - response = requests.post(url, headers=headers, json=data) - assert response.status_code == 200 - - -def main(): - release_text = get_github_release_text(tag_name=tag_name) - text = get_gitter_message(release_text=release_text) - send_gitter_message(text=text) - - -if __name__ == "__main__": - main() diff --git a/scripts/notify.sh b/scripts/notify.sh deleted file mode 100755 index 8ce550026..000000000 --- a/scripts/notify.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -e - -python scripts/gitter_releases_bot.py From dcbe7f7ac0a6503254c8ac90924950b9cfb8fbf0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 08:39:45 +0000 Subject: [PATCH 1389/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 90ad69c28..0cfb193e3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). ## 0.104.0 From 6eb30959bc5486ec19b31675d92744d10d11c9ba Mon Sep 17 00:00:00 2001 From: Tiago Silva <tiago.arasilva@gmail.com> Date: Fri, 20 Oct 2023 09:52:59 +0100 Subject: [PATCH 1390/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/en/docs/reference/index.md`=20(#10467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix small typo in reference/index.md --- docs/en/docs/reference/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/reference/index.md b/docs/en/docs/reference/index.md index 994b3c38c..512d5c25c 100644 --- a/docs/en/docs/reference/index.md +++ b/docs/en/docs/reference/index.md @@ -1,7 +1,7 @@ # Reference - Code API Here's the reference or code API, the classes, functions, parameters, attributes, and -all the FastAPI parts you can use in you applications. +all the FastAPI parts you can use in your applications. If you want to **learn FastAPI** you are much better off reading the [FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/). From 968afca0581f24b9a0e6e58bc21d03bd181b8a59 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 08:53:37 +0000 Subject: [PATCH 1391/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0cfb193e3..00e369236 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). * 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). ## 0.104.0 From 57a030175e7027ba0200c78521efc44800b6fd52 Mon Sep 17 00:00:00 2001 From: yogabonito <yogabonito@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:55:30 +0200 Subject: [PATCH 1392/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs,=20remov?= =?UTF-8?q?e=20references=20to=20removed=20`pydantic.Required`=20in=20`doc?= =?UTF-8?q?s/en/docs/tutorial/query-params-str-validations.md`=20(#10469)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/query-params-str-validations.md | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 0b2cf02a8..0e777f6a0 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -502,33 +502,8 @@ To do that, you can declare that `None` is a valid type but still use `...` as t !!! tip Pydantic, which is what powers all the data validation and serialization in FastAPI, has a special behavior when you use `Optional` or `Union[Something, None]` without a default value, you can read more about it in the Pydantic docs about <a href="https://pydantic-docs.helpmanual.io/usage/models/#required-optional-fields" class="external-link" target="_blank">Required Optional fields</a>. -### Use Pydantic's `Required` instead of Ellipsis (`...`) - -If you feel uncomfortable using `...`, you can also import and use `Required` from Pydantic: - -=== "Python 3.9+" - - ```Python hl_lines="4 10" - {!> ../../../docs_src/query_params_str_validations/tutorial006d_an_py39.py!} - ``` - -=== "Python 3.8+" - - ```Python hl_lines="2 9" - {!> ../../../docs_src/query_params_str_validations/tutorial006d_an.py!} - ``` - -=== "Python 3.8+ non-Annotated" - - !!! tip - Prefer to use the `Annotated` version if possible. - - ```Python hl_lines="2 8" - {!> ../../../docs_src/query_params_str_validations/tutorial006d.py!} - ``` - !!! tip - Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...` nor `Required`. + Remember that in most of the cases, when something is required, you can simply omit the default, so you normally don't have to use `...`. ## Query parameter list / multiple values From cda5e770abed7efc92dbc96d4a21007ea485b4a6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 08:56:08 +0000 Subject: [PATCH 1393/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 00e369236..6832da44e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). * ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). * 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). * 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). From 4bd14306771cab5c1bff022dd2b1827cdd0ad186 Mon Sep 17 00:00:00 2001 From: yogabonito <yogabonito@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:58:03 +0200 Subject: [PATCH 1394/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20an?= =?UTF-8?q?d=20rewordings=20in=20`docs/en/docs/tutorial/body-nested-models?= =?UTF-8?q?.md`=20(#10468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/body-nested-models.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index 3a1052397..387f0de9a 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -183,18 +183,18 @@ This would mean that **FastAPI** would expect a body similar to: Again, doing just that declaration, with **FastAPI** you get: -* Editor support (completion, etc), even for nested models +* Editor support (completion, etc.), even for nested models * Data conversion * Data validation * Automatic documentation ## Special types and validation -Apart from normal singular types like `str`, `int`, `float`, etc. You can use more complex singular types that inherit from `str`. +Apart from normal singular types like `str`, `int`, `float`, etc. you can use more complex singular types that inherit from `str`. To see all the options you have, checkout the docs for <a href="https://pydantic-docs.helpmanual.io/usage/types/" class="external-link" target="_blank">Pydantic's exotic types</a>. You will see some examples in the next chapter. -For example, as in the `Image` model we have a `url` field, we can declare it to be instead of a `str`, a Pydantic's `HttpUrl`: +For example, as in the `Image` model we have a `url` field, we can declare it to be an instance of Pydantic's `HttpUrl` instead of a `str`: === "Python 3.10+" @@ -218,7 +218,7 @@ The string will be checked to be a valid URL, and documented in JSON Schema / Op ## Attributes with lists of submodels -You can also use Pydantic models as subtypes of `list`, `set`, etc: +You can also use Pydantic models as subtypes of `list`, `set`, etc.: === "Python 3.10+" @@ -238,7 +238,7 @@ You can also use Pydantic models as subtypes of `list`, `set`, etc: {!> ../../../docs_src/body_nested_models/tutorial006.py!} ``` -This will expect (convert, validate, document, etc) a JSON body like: +This will expect (convert, validate, document, etc.) a JSON body like: ```JSON hl_lines="11" { @@ -334,15 +334,15 @@ But you don't have to worry about them either, incoming dicts are converted auto ## Bodies of arbitrary `dict`s -You can also declare a body as a `dict` with keys of some type and values of other type. +You can also declare a body as a `dict` with keys of some type and values of some other type. -Without having to know beforehand what are the valid field/attribute names (as would be the case with Pydantic models). +This way, you don't have to know beforehand what the valid field/attribute names are (as would be the case with Pydantic models). This would be useful if you want to receive keys that you don't already know. --- -Other useful case is when you want to have keys of other type, e.g. `int`. +Another useful case is when you want to have keys of another type (e.g., `int`). That's what we are going to see here. From 6dac39dbcabbb8ceade5b2b96cd19b6902affbaa Mon Sep 17 00:00:00 2001 From: Surav Shrestha <98219089+suravshresth@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:43:51 +0545 Subject: [PATCH 1395/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/en/docs/reference/dependencies.md`=20(#10465)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/reference/dependencies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/reference/dependencies.md b/docs/en/docs/reference/dependencies.md index 95cd101e4..099968267 100644 --- a/docs/en/docs/reference/dependencies.md +++ b/docs/en/docs/reference/dependencies.md @@ -18,7 +18,7 @@ from fastapi import Depends ## `Security()` For many scenarios, you can handle security (authorization, authentication, etc.) with -dependendencies, using `Depends()`. +dependencies, using `Depends()`. But when you want to also declare OAuth2 scopes, you can use `Security()` instead of `Depends()`. From f785a6ce90bb1938779bb8e7d6124229528f9f06 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 09:00:11 +0000 Subject: [PATCH 1396/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6832da44e..d75f9a08c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). * 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). * ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). * 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). From ae84ff6e44d45fe75e96fb12ae225046d99c29e8 Mon Sep 17 00:00:00 2001 From: Heinz-Alexander Fuetterer <35225576+afuetterer@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:00:44 +0200 Subject: [PATCH 1397/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20emoji=20docs=20and=20in=20some=20source=20examples=20(#10438?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/deployment/concepts.md | 14 +++++----- docs/em/docs/deployment/docker.md | 26 +++++++++---------- docs/em/docs/deployment/server-workers.md | 8 +++--- docs/em/docs/project-generation.md | 2 +- docs/en/docs/how-to/sql-databases-peewee.md | 2 +- docs_src/openapi_webhooks/tutorial001.py | 2 +- fastapi/utils.py | 2 +- .../test_openapi_webhooks/test_tutorial001.py | 4 +-- tests/test_webhooks_security.py | 6 ++--- 9 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/em/docs/deployment/concepts.md b/docs/em/docs/deployment/concepts.md index 8ce775411..162b68615 100644 --- a/docs/em/docs/deployment/concepts.md +++ b/docs/em/docs/deployment/concepts.md @@ -43,7 +43,7 @@ * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 * ✳ * ⏮️ 🔢 🦲 💖 Certbot 📄 🔕 -* Kubernete ⏮️ 🚧 🕹 💖 👌 +* Kubernetes ⏮️ 🚧 🕹 💖 👌 * ⏮️ 🔢 🦲 💖 🛂-👨‍💼 📄 🔕 * 🍵 🔘 ☁ 🐕‍🦺 🍕 👫 🐕‍🦺 (✍ 🔛 👶) @@ -115,7 +115,7 @@ 🖼 🧰 👈 💪 👉 👨‍🏭: * ☁ -* Kubernete +* Kubernetes * ☁ ✍ * ☁ 🐝 📳 * ✳ @@ -165,7 +165,7 @@ 🖼, 👉 💪 🍵: * ☁ -* Kubernete +* Kubernetes * ☁ ✍ * ☁ 🐝 📳 * ✳ @@ -233,15 +233,15 @@ * 🐁 🔜 **🛠️ 👨‍💼** 👂 🔛 **📢** & **⛴**, 🧬 🔜 ✔️ **💗 Uvicorn 👨‍🏭 🛠️** * **Uvicorn** 🛠️ **Uvicorn 👨‍🏭** * 1️⃣ Uvicorn **🛠️ 👨‍💼** 🔜 👂 🔛 **📢** & **⛴**, & ⚫️ 🔜 ▶️ **💗 Uvicorn 👨‍🏭 🛠️** -* **Kubernete** & 🎏 📎 **📦 ⚙️** +* **Kubernetes** & 🎏 📎 **📦 ⚙️** * 🕳 **☁** 🧽 🔜 👂 🔛 **📢** & **⛴**. 🧬 🔜 ✔️ **💗 📦**, 🔠 ⏮️ **1️⃣ Uvicorn 🛠️** 🏃‍♂ * **☁ 🐕‍🦺** 👈 🍵 👉 👆 * ☁ 🐕‍🦺 🔜 🎲 **🍵 🧬 👆**. ⚫️ 🔜 🎲 ➡️ 👆 🔬 **🛠️ 🏃**, ⚖️ **📦 🖼** ⚙️, 🙆 💼, ⚫️ 🔜 🌅 🎲 **👁 Uvicorn 🛠️**, & ☁ 🐕‍🦺 🔜 🈚 🔁 ⚫️. !!! tip - 🚫 😟 🚥 👫 🏬 🔃 **📦**, ☁, ⚖️ Kubernete 🚫 ⚒ 📚 🔑. + 🚫 😟 🚥 👫 🏬 🔃 **📦**, ☁, ⚖️ Kubernetes 🚫 ⚒ 📚 🔑. - 👤 🔜 💬 👆 🌅 🔃 📦 🖼, ☁, Kubernete, ♒️. 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + 👤 🔜 💬 👆 🌅 🔃 📦 🖼, ☁, Kubernetes, ♒️. 🔮 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. ## ⏮️ 🔁 ⏭ ▶️ @@ -268,7 +268,7 @@ 📥 💪 💭: -* "🕑 📦" Kubernete 👈 🏃 ⏭ 👆 📱 📦 +* "🕑 📦" Kubernetes 👈 🏃 ⏭ 👆 📱 📦 * 🎉 ✍ 👈 🏃 ⏮️ 🔁 & ⤴️ ▶️ 👆 🈸 * 👆 🔜 💪 🌌 ▶️/⏏ *👈* 🎉 ✍, 🔍 ❌, ♒️. diff --git a/docs/em/docs/deployment/docker.md b/docs/em/docs/deployment/docker.md index 51ece5599..f28735ed7 100644 --- a/docs/em/docs/deployment/docker.md +++ b/docs/em/docs/deployment/docker.md @@ -74,7 +74,7 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] , 👆 🔜 🏃 **💗 📦** ⏮️ 🎏 👜, 💖 💽, 🐍 🈸, 🕸 💽 ⏮️ 😥 🕸 🈸, & 🔗 👫 👯‍♂️ 📨 👫 🔗 🕸. -🌐 📦 🧾 ⚙️ (💖 ☁ ⚖️ Kubernete) ✔️ 👫 🕸 ⚒ 🛠️ 🔘 👫. +🌐 📦 🧾 ⚙️ (💖 ☁ ⚖️ Kubernetes) ✔️ 👫 🕸 ⚒ 🛠️ 🔘 👫. ## 📦 & 🛠️ @@ -96,7 +96,7 @@ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] 👉 ⚫️❔ 👆 🔜 💚 **🏆 💼**, 🖼: -* ⚙️ **Kubernete** ⚖️ 🎏 🧰 +* ⚙️ **Kubernetes** ⚖️ 🎏 🧰 * 🕐❔ 🏃‍♂ 🔛 **🍓 👲** * ⚙️ ☁ 🐕‍🦺 👈 🔜 🏃 📦 🖼 👆, ♒️. @@ -395,7 +395,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ⚫️ 💪 ➕1️⃣ 📦, 🖼 ⏮️ <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>, 🚚 **🇺🇸🔍** & **🏧** 🛠️ **📄**. !!! tip - Traefik ✔️ 🛠️ ⏮️ ☁, Kubernete, & 🎏, ⚫️ 📶 ⏩ ⚒ 🆙 & 🔗 🇺🇸🔍 👆 📦 ⏮️ ⚫️. + Traefik ✔️ 🛠️ ⏮️ ☁, Kubernetes, & 🎏, ⚫️ 📶 ⏩ ⚒ 🆙 & 🔗 🇺🇸🔍 👆 📦 ⏮️ ⚫️. 👐, 🇺🇸🔍 💪 🍵 ☁ 🐕‍🦺 1️⃣ 👫 🐕‍🦺 (⏪ 🏃 🈸 📦). @@ -403,7 +403,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] 📤 🛎 ➕1️⃣ 🧰 🈚 **▶️ & 🏃‍♂** 👆 📦. -⚫️ 💪 **☁** 🔗, **☁ ✍**, **Kubernete**, **☁ 🐕‍🦺**, ♒️. +⚫️ 💪 **☁** 🔗, **☁ ✍**, **Kubernetes**, **☁ 🐕‍🦺**, ♒️. 🌅 (⚖️ 🌐) 💼, 📤 🙅 🎛 🛠️ 🏃 📦 🔛 🕴 & 🛠️ ⏏ 🔛 ❌. 🖼, ☁, ⚫️ 📋 ⏸ 🎛 `--restart`. @@ -413,7 +413,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] 🚥 👆 ✔️ <abbr title="A group of machines that are configured to be connected and work together in some way.">🌑</abbr> 🎰 ⏮️ **☁**, ☁ 🐝 📳, 🖖, ⚖️ ➕1️⃣ 🎏 🏗 ⚙️ 🛠️ 📎 📦 🔛 💗 🎰, ⤴️ 👆 🔜 🎲 💚 **🍵 🧬** **🌑 🎚** ↩️ ⚙️ **🛠️ 👨‍💼** (💖 🐁 ⏮️ 👨‍🏭) 🔠 📦. -1️⃣ 📚 📎 📦 🧾 ⚙️ 💖 Kubernete 🛎 ✔️ 🛠️ 🌌 🚚 **🧬 📦** ⏪ 🔗 **📐 ⚖** 📨 📨. 🌐 **🌑 🎚**. +1️⃣ 📚 📎 📦 🧾 ⚙️ 💖 Kubernetes 🛎 ✔️ 🛠️ 🌌 🚚 **🧬 📦** ⏪ 🔗 **📐 ⚖** 📨 📨. 🌐 **🌑 🎚**. 📚 💼, 👆 🔜 🎲 💚 🏗 **☁ 🖼 ⚪️➡️ 🖌** [🔬 🔛](#dockerfile), ❎ 👆 🔗, & 🏃‍♂ **👁 Uvicorn 🛠️** ↩️ 🏃‍♂ 🕳 💖 🐁 ⏮️ Uvicorn 👨‍🏭. @@ -430,7 +430,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ### 1️⃣ 📐 ⚙ - 💗 👨‍🏭 📦 -🕐❔ 👷 ⏮️ **Kubernete** ⚖️ 🎏 📎 📦 🧾 ⚙️, ⚙️ 👫 🔗 🕸 🛠️ 🔜 ✔ 👁 **📐 ⚙** 👈 👂 🔛 👑 **⛴** 📶 📻 (📨) 🎲 **💗 📦** 🏃 👆 📱. +🕐❔ 👷 ⏮️ **Kubernetes** ⚖️ 🎏 📎 📦 🧾 ⚙️, ⚙️ 👫 🔗 🕸 🛠️ 🔜 ✔ 👁 **📐 ⚙** 👈 👂 🔛 👑 **⛴** 📶 📻 (📨) 🎲 **💗 📦** 🏃 👆 📱. 🔠 👫 📦 🏃‍♂ 👆 📱 🔜 🛎 ✔️ **1️⃣ 🛠️** (✅ Uvicorn 🛠️ 🏃 👆 FastAPI 🈸). 👫 🔜 🌐 **🌓 📦**, 🏃‍♂ 🎏 👜, ✋️ 🔠 ⏮️ 🚮 👍 🛠️, 💾, ♒️. 👈 🌌 👆 🔜 ✊ 📈 **🛠️** **🎏 🐚** 💽, ⚖️ **🎏 🎰**. @@ -489,7 +489,7 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] 🚥 👆 🏃 **👁 🛠️ 📍 📦** 👆 🔜 ✔️ 🌅 ⚖️ 🌘 👍-🔬, ⚖, & 📉 💸 💾 🍴 🔠 👈 📦 (🌅 🌘 1️⃣ 🚥 👫 🔁). -& ⤴️ 👆 💪 ⚒ 👈 🎏 💾 📉 & 📄 👆 📳 👆 📦 🧾 ⚙️ (🖼 **Kubernete**). 👈 🌌 ⚫️ 🔜 💪 **🔁 📦** **💪 🎰** ✊ 🔘 🏧 💸 💾 💪 👫, & 💸 💪 🎰 🌑. +& ⤴️ 👆 💪 ⚒ 👈 🎏 💾 📉 & 📄 👆 📳 👆 📦 🧾 ⚙️ (🖼 **Kubernetes**). 👈 🌌 ⚫️ 🔜 💪 **🔁 📦** **💪 🎰** ✊ 🔘 🏧 💸 💾 💪 👫, & 💸 💪 🎰 🌑. 🚥 👆 🈸 **🙅**, 👉 🔜 🎲 **🚫 ⚠**, & 👆 💪 🚫 💪 ✔ 🏋️ 💾 📉. ✋️ 🚥 👆 **⚙️ 📚 💾** (🖼 ⏮️ **🎰 🏫** 🏷), 👆 🔜 ✅ ❔ 🌅 💾 👆 😩 & 🔆 **🔢 📦** 👈 🏃 **🔠 🎰** (& 🎲 🚮 🌖 🎰 👆 🌑). @@ -497,14 +497,14 @@ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "80"] ## ⏮️ 🔁 ⏭ ▶️ & 📦 -🚥 👆 ⚙️ 📦 (✅ ☁, Kubernete), ⤴️ 📤 2️⃣ 👑 🎯 👆 💪 ⚙️. +🚥 👆 ⚙️ 📦 (✅ ☁, Kubernetes), ⤴️ 📤 2️⃣ 👑 🎯 👆 💪 ⚙️. ### 💗 📦 -🚥 👆 ✔️ **💗 📦**, 🎲 🔠 1️⃣ 🏃 **👁 🛠️** (🖼, **Kubernete** 🌑), ⤴️ 👆 🔜 🎲 💚 ✔️ **🎏 📦** 🔨 👷 **⏮️ 📶** 👁 📦, 🏃 👁 🛠️, **⏭** 🏃 🔁 👨‍🏭 📦. +🚥 👆 ✔️ **💗 📦**, 🎲 🔠 1️⃣ 🏃 **👁 🛠️** (🖼, **Kubernetes** 🌑), ⤴️ 👆 🔜 🎲 💚 ✔️ **🎏 📦** 🔨 👷 **⏮️ 📶** 👁 📦, 🏃 👁 🛠️, **⏭** 🏃 🔁 👨‍🏭 📦. !!! info - 🚥 👆 ⚙️ Kubernete, 👉 🔜 🎲 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">🕑 📦</a>. + 🚥 👆 ⚙️ Kubernetes, 👉 🔜 🎲 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">🕑 📦</a>. 🚥 👆 ⚙️ 💼 📤 🙅‍♂ ⚠ 🏃‍♂ 👈 ⏮️ 📶 **💗 🕰 🔗** (🖼 🚥 👆 🚫 🏃 💽 🛠️, ✋️ ✅ 🚥 💽 🔜), ⤴️ 👆 💪 🚮 👫 🔠 📦 ▶️️ ⏭ ▶️ 👑 🛠️. @@ -574,7 +574,7 @@ COPY ./app /app/app ### 🕐❔ ⚙️ -👆 🔜 🎲 **🚫** ⚙️ 👉 🛂 🧢 🖼 (⚖️ 🙆 🎏 🎏 1️⃣) 🚥 👆 ⚙️ **Kubernete** (⚖️ 🎏) & 👆 ⏪ ⚒ **🧬** 🌑 🎚, ⏮️ 💗 **📦**. 📚 💼, 👆 👍 📆 **🏗 🖼 ⚪️➡️ 🖌** 🔬 🔛: [🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). +👆 🔜 🎲 **🚫** ⚙️ 👉 🛂 🧢 🖼 (⚖️ 🙆 🎏 🎏 1️⃣) 🚥 👆 ⚙️ **Kubernetes** (⚖️ 🎏) & 👆 ⏪ ⚒ **🧬** 🌑 🎚, ⏮️ 💗 **📦**. 📚 💼, 👆 👍 📆 **🏗 🖼 ⚪️➡️ 🖌** 🔬 🔛: [🏗 ☁ 🖼 FastAPI](#build-a-docker-image-for-fastapi). 👉 🖼 🔜 ⚠ ✴️ 🎁 💼 🔬 🔛 [📦 ⏮️ 💗 🛠️ & 🎁 💼](#containers-with-multiple-processes-and-special-cases). 🖼, 🚥 👆 🈸 **🙅 🥃** 👈 ⚒ 🔢 🔢 🛠️ ⚓️ 🔛 💽 👷 👍, 👆 🚫 💚 😥 ⏮️ ❎ 🛠️ 🧬 🌑 🎚, & 👆 🚫 🏃 🌅 🌘 1️⃣ 📦 ⏮️ 👆 📱. ⚖️ 🚥 👆 🛠️ ⏮️ **☁ ✍**, 🏃 🔛 👁 💽, ♒️. @@ -585,7 +585,7 @@ COPY ./app /app/app 🖼: * ⏮️ **☁ ✍** 👁 💽 -* ⏮️ **Kubernete** 🌑 +* ⏮️ **Kubernetes** 🌑 * ⏮️ ☁ 🐝 📳 🌑 * ⏮️ ➕1️⃣ 🧰 💖 🖖 * ⏮️ ☁ 🐕‍🦺 👈 ✊ 👆 📦 🖼 & 🛠️ ⚫️ @@ -682,7 +682,7 @@ CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port" ## 🌃 -⚙️ 📦 ⚙️ (✅ ⏮️ **☁** & **Kubernete**) ⚫️ ▶️️ 📶 🎯 🍵 🌐 **🛠️ 🔧**: +⚙️ 📦 ⚙️ (✅ ⏮️ **☁** & **Kubernetes**) ⚫️ ▶️️ 📶 🎯 🍵 🌐 **🛠️ 🔧**: * 🇺🇸🔍 * 🏃‍♂ 🔛 🕴 diff --git a/docs/em/docs/deployment/server-workers.md b/docs/em/docs/deployment/server-workers.md index ca068d744..b7e58c4f4 100644 --- a/docs/em/docs/deployment/server-workers.md +++ b/docs/em/docs/deployment/server-workers.md @@ -18,9 +18,9 @@ 📥 👤 🔜 🎦 👆 ❔ ⚙️ <a href="https://gunicorn.org/" class="external-link" target="_blank">**🐁**</a> ⏮️ **Uvicorn 👨‍🏭 🛠️**. !!! info - 🚥 👆 ⚙️ 📦, 🖼 ⏮️ ☁ ⚖️ Kubernete, 👤 🔜 💬 👆 🌅 🔃 👈 ⏭ 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. + 🚥 👆 ⚙️ 📦, 🖼 ⏮️ ☁ ⚖️ Kubernetes, 👤 🔜 💬 👆 🌅 🔃 👈 ⏭ 📃: [FastAPI 📦 - ☁](./docker.md){.internal-link target=_blank}. - 🎯, 🕐❔ 🏃 🔛 **Kubernete** 👆 🔜 🎲 **🚫** 💚 ⚙️ 🐁 & ↩️ 🏃 **👁 Uvicorn 🛠️ 📍 📦**, ✋️ 👤 🔜 💬 👆 🔃 ⚫️ ⏪ 👈 📃. + 🎯, 🕐❔ 🏃 🔛 **Kubernetes** 👆 🔜 🎲 **🚫** 💚 ⚙️ 🐁 & ↩️ 🏃 **👁 Uvicorn 🛠️ 📍 📦**, ✋️ 👤 🔜 💬 👆 🔃 ⚫️ ⏪ 👈 📃. ## 🐁 ⏮️ Uvicorn 👨‍🏭 @@ -167,7 +167,7 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 👤 🔜 🎦 👆 **🛂 ☁ 🖼** 👈 🔌 **🐁 ⏮️ Uvicorn 👨‍🏭** & 🔢 📳 👈 💪 ⚠ 🙅 💼. -📤 👤 🔜 🎦 👆 ❔ **🏗 👆 👍 🖼 ⚪️➡️ 🖌** 🏃 👁 Uvicorn 🛠️ (🍵 🐁). ⚫️ 🙅 🛠️ & 🎲 ⚫️❔ 👆 🔜 💚 🕐❔ ⚙️ 📎 📦 🧾 ⚙️ 💖 **Kubernete**. +📤 👤 🔜 🎦 👆 ❔ **🏗 👆 👍 🖼 ⚪️➡️ 🖌** 🏃 👁 Uvicorn 🛠️ (🍵 🐁). ⚫️ 🙅 🛠️ & 🎲 ⚫️❔ 👆 🔜 💚 🕐❔ ⚙️ 📎 📦 🧾 ⚙️ 💖 **Kubernetes**. ## 🌃 @@ -175,4 +175,4 @@ $ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 👆 💪 ⚙️ 👉 🧰 & 💭 🚥 👆 ⚒ 🆙 **👆 👍 🛠️ ⚙️** ⏪ ✊ 💅 🎏 🛠️ 🔧 👆. -✅ 👅 ⏭ 📃 💡 🔃 **FastAPI** ⏮️ 📦 (✅ ☁ & Kubernete). 👆 🔜 👀 👈 👈 🧰 ✔️ 🙅 🌌 ❎ 🎏 **🛠️ 🔧** 👍. 👶 +✅ 👅 ⏭ 📃 💡 🔃 **FastAPI** ⏮️ 📦 (✅ ☁ & Kubernetes). 👆 🔜 👀 👈 👈 🧰 ✔️ 🙅 🌌 ❎ 🎏 **🛠️ 🔧** 👍. 👶 diff --git a/docs/em/docs/project-generation.md b/docs/em/docs/project-generation.md index 5fd667ad1..ae959e1d5 100644 --- a/docs/em/docs/project-generation.md +++ b/docs/em/docs/project-generation.md @@ -79,6 +79,6 @@ * **🌈** 🕜 🏷 🛠️. * **☁ 🧠 🔎** 📨 📁 🏗. * **🏭 🔜** 🐍 🕸 💽 ⚙️ Uvicorn & 🐁. -* **☁ 👩‍💻** Kubernete (🦲) 🆑/💿 🛠️ 🏗. +* **☁ 👩‍💻** Kubernetes (🦲) 🆑/💿 🛠️ 🏗. * **🤸‍♂** 💪 ⚒ 1️⃣ 🌈 🏗 🇪🇸 ⏮️ 🏗 🖥. * **💪 🏧** 🎏 🏷 🛠️ (Pytorch, 🇸🇲), 🚫 🌈. diff --git a/docs/en/docs/how-to/sql-databases-peewee.md b/docs/en/docs/how-to/sql-databases-peewee.md index bf2f2e714..b0ab7c633 100644 --- a/docs/en/docs/how-to/sql-databases-peewee.md +++ b/docs/en/docs/how-to/sql-databases-peewee.md @@ -363,7 +363,7 @@ It will have the database connection open at the beginning and will just wait so This will easily let you test that your app with Peewee and FastAPI is behaving correctly with all the stuff about threads. -If you want to check how Peewee would break your app if used without modification, go the the `sql_app/database.py` file and comment the line: +If you want to check how Peewee would break your app if used without modification, go the `sql_app/database.py` file and comment the line: ```Python # db._state = PeeweeConnectionState() diff --git a/docs_src/openapi_webhooks/tutorial001.py b/docs_src/openapi_webhooks/tutorial001.py index 5016f5b00..55822bb48 100644 --- a/docs_src/openapi_webhooks/tutorial001.py +++ b/docs_src/openapi_webhooks/tutorial001.py @@ -8,7 +8,7 @@ app = FastAPI() class Subscription(BaseModel): username: str - montly_fee: float + monthly_fee: float start_date: datetime diff --git a/fastapi/utils.py b/fastapi/utils.py index 267d64ce8..53b47a160 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -117,7 +117,7 @@ def create_cloned_field( if PYDANTIC_V2: return field # cloned_types caches already cloned types to support recursive models and improve - # performance by avoiding unecessary cloning + # performance by avoiding unnecessary cloning if cloned_types is None: cloned_types = _CLONED_TYPES_CACHE diff --git a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py index 9111fdb2f..dc67ec401 100644 --- a/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py +++ b/tests/test_tutorial/test_openapi_webhooks/test_tutorial001.py @@ -85,7 +85,7 @@ def test_openapi_schema(): "Subscription": { "properties": { "username": {"type": "string", "title": "Username"}, - "montly_fee": {"type": "number", "title": "Montly Fee"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, "start_date": { "type": "string", "format": "date-time", @@ -93,7 +93,7 @@ def test_openapi_schema(): }, }, "type": "object", - "required": ["username", "montly_fee", "start_date"], + "required": ["username", "monthly_fee", "start_date"], "title": "Subscription", }, "ValidationError": { diff --git a/tests/test_webhooks_security.py b/tests/test_webhooks_security.py index a1c7b18fb..21a694cb5 100644 --- a/tests/test_webhooks_security.py +++ b/tests/test_webhooks_security.py @@ -13,7 +13,7 @@ bearer_scheme = HTTPBearer() class Subscription(BaseModel): username: str - montly_fee: float + monthly_fee: float start_date: datetime @@ -93,7 +93,7 @@ def test_openapi_schema(): "Subscription": { "properties": { "username": {"type": "string", "title": "Username"}, - "montly_fee": {"type": "number", "title": "Montly Fee"}, + "monthly_fee": {"type": "number", "title": "Monthly Fee"}, "start_date": { "type": "string", "format": "date-time", @@ -101,7 +101,7 @@ def test_openapi_schema(): }, }, "type": "object", - "required": ["username", "montly_fee", "start_date"], + "required": ["username", "monthly_fee", "start_date"], "title": "Subscription", }, "ValidationError": { From eb017270fca06383e7d44f45a6850d58c315760f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 09:00:53 +0000 Subject: [PATCH 1398/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d75f9a08c..9c7d78cb8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). * ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). * 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). * ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). From f41eb5e0051c12d3508841a5d7c42b838fb63ad6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 09:03:34 +0000 Subject: [PATCH 1399/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9c7d78cb8..fd1aa90cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). * ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). * ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). * 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). From 89e03bad1644c7a0b63960c4299ca79ebb1ae31c Mon Sep 17 00:00:00 2001 From: Giulio Davide Carparelli <giulio.davide.97@gmail.com> Date: Fri, 20 Oct 2023 11:08:42 +0200 Subject: [PATCH 1400/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20example=20val?= =?UTF-8?q?idation=20error=20from=20Pydantic=20v1=20to=20match=20Pydantic?= =?UTF-8?q?=20v2=20in=20`docs/en/docs/tutorial/path-params.md`=20(#10043)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/path-params.md | 22 ++++++++++++---------- docs/en/docs/tutorial/query-params.md | 22 ++++++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/en/docs/tutorial/path-params.md b/docs/en/docs/tutorial/path-params.md index 6594a7a8b..847b56334 100644 --- a/docs/en/docs/tutorial/path-params.md +++ b/docs/en/docs/tutorial/path-params.md @@ -46,16 +46,18 @@ But if you go to the browser at <a href="http://127.0.0.1:8000/items/foo" class= ```JSON { - "detail": [ - { - "loc": [ - "path", - "item_id" - ], - "msg": "value is not a valid integer", - "type": "type_error.integer" - } - ] + "detail": [ + { + "type": "int_parsing", + "loc": [ + "path", + "item_id" + ], + "msg": "Input should be a valid integer, unable to parse string as an integer", + "input": "foo", + "url": "https://errors.pydantic.dev/2.1/v/int_parsing" + } + ] } ``` diff --git a/docs/en/docs/tutorial/query-params.md b/docs/en/docs/tutorial/query-params.md index 988ff4bf1..bc3b11948 100644 --- a/docs/en/docs/tutorial/query-params.md +++ b/docs/en/docs/tutorial/query-params.md @@ -173,16 +173,18 @@ http://127.0.0.1:8000/items/foo-item ```JSON { - "detail": [ - { - "loc": [ - "query", - "needy" - ], - "msg": "field required", - "type": "value_error.missing" - } - ] + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null, + "url": "https://errors.pydantic.dev/2.1/v/missing" + } + ] } ``` From 07a9b240e9a8acad3f9ab64fb921daf275aeeb85 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 09:13:35 +0000 Subject: [PATCH 1401/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fd1aa90cb..df004dd8c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). * ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). * ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). * ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). From 9b3e166b4343941790302dd8fe943cab50ffc4c0 Mon Sep 17 00:00:00 2001 From: worldworm <13227454+worldworm@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:19:31 +0200 Subject: [PATCH 1402/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20link=20to?= =?UTF-8?q?=20SPDX=20license=20identifier=20in=20`docs/en/docs/tutorial/me?= =?UTF-8?q?tadata.md`=20(#10433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/tutorial/metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md index e75b4a0b9..504204e98 100644 --- a/docs/en/docs/tutorial/metadata.md +++ b/docs/en/docs/tutorial/metadata.md @@ -14,7 +14,7 @@ You can set the following fields that are used in the OpenAPI specification and | `version` | `string` | The version of the API. This is the version of your own application, not of OpenAPI. For example `2.5.0`. | | `terms_of_service` | `str` | A URL to the Terms of Service for the API. If provided, this has to be a URL. | | `contact` | `dict` | The contact information for the exposed API. It can contain several fields. <details><summary><code>contact</code> fields</summary><table><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td>The identifying name of the contact person/organization.</td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>The URL pointing to the contact information. MUST be in the format of a URL.</td></tr><tr><td><code>email</code></td><td><code>str</code></td><td>The email address of the contact person/organization. MUST be in the format of an email address.</td></tr></tbody></table></details> | -| `license_info` | `dict` | The license information for the exposed API. It can contain several fields. <details><summary><code>license_info</code> fields</summary><table><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>REQUIRED</strong> (if a <code>license_info</code> is set). The license name used for the API.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>An <a href="https://spdx.dev/spdx-specification-21-web-version/#h.jxpfx0ykyb60" class="external-link" target="_blank">SPDX</a> license expression for the API. The <code>identifier</code> field is mutually exclusive of the <code>url</code> field. <small>Available since OpenAPI 3.1.0, FastAPI 0.99.0.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>A URL to the license used for the API. MUST be in the format of a URL.</td></tr></tbody></table></details> | +| `license_info` | `dict` | The license information for the exposed API. It can contain several fields. <details><summary><code>license_info</code> fields</summary><table><thead><tr><th>Parameter</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>name</code></td><td><code>str</code></td><td><strong>REQUIRED</strong> (if a <code>license_info</code> is set). The license name used for the API.</td></tr><tr><td><code>identifier</code></td><td><code>str</code></td><td>An <a href="https://spdx.org/licenses/" class="external-link" target="_blank">SPDX</a> license expression for the API. The <code>identifier</code> field is mutually exclusive of the <code>url</code> field. <small>Available since OpenAPI 3.1.0, FastAPI 0.99.0.</small></td></tr><tr><td><code>url</code></td><td><code>str</code></td><td>A URL to the license used for the API. MUST be in the format of a URL.</td></tr></tbody></table></details> | You can set them as follows: From ab65486e757400b84d0116367ba3db7dcac72fd3 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 20 Oct 2023 09:21:13 +0000 Subject: [PATCH 1403/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 df004dd8c..414afae12 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). * 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). * ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). * ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). From 9bfbacfe98c877ba661e0e40c8dcac7571de819a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 21 Oct 2023 11:10:18 +0400 Subject: [PATCH 1404/1881] =?UTF-8?q?=F0=9F=90=9B=20Fix=20overriding=20MKD?= =?UTF-8?q?ocs=20theme=20lang=20in=20hook=20(#10490)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/mkdocs_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index b12487b50..2b6a05642 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -24,7 +24,7 @@ def get_missing_translation_content(docs_dir: str) -> str: @lru_cache() def get_mkdocs_material_langs() -> List[str]: material_path = Path(material.__file__).parent - material_langs_path = material_path / "partials" / "languages" + material_langs_path = material_path / "templates" / "partials" / "languages" langs = [file.stem for file in material_langs_path.glob("*.html")] return langs From 808e3bb9d56ac85e28e6f5e982f06209ad059436 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 21 Oct 2023 07:11:00 +0000 Subject: [PATCH 1405/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 414afae12..163e708b8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). * 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). * ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). From e8bd645fa9ee0a8ed3ba4df9cd8c743332a5d2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 22 Oct 2023 11:35:13 +0400 Subject: [PATCH 1406/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20data=20struct?= =?UTF-8?q?ure=20and=20render=20for=20external-links=20(#10495)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update data structure and render for external-links * 📝 Update translations for external links --- docs/em/docs/external-links.md | 76 +++++---------------------------- docs/en/data/external_links.yml | 22 +++++----- docs/en/docs/external-links.md | 76 ++++----------------------------- docs/fr/docs/external-links.md | 67 ++++------------------------- docs/ja/docs/external-links.md | 67 ++++------------------------- docs/pt/docs/external-links.md | 67 ++++------------------------- docs/ru/docs/external-links.md | 67 ++++------------------------- 7 files changed, 66 insertions(+), 376 deletions(-) diff --git a/docs/em/docs/external-links.md b/docs/em/docs/external-links.md index 4440b1f12..5ba668bfa 100644 --- a/docs/em/docs/external-links.md +++ b/docs/em/docs/external-links.md @@ -11,77 +11,21 @@ ## 📄 -### 🇪🇸 +{% for section_name, section_content in external_links.items() %} -{% if external_links %} -{% for article in external_links.articles.english %} +## {{ section_name }} + +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### 🇯🇵 - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### 🇻🇳 - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### 🇷🇺 - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### 🇩🇪 - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### 🇹🇼 - -{% if external_links %} -{% for article in external_links.articles.taiwanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## 📻 - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## 💬 - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## 🏗 diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index a7f766d16..726e7eae7 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,5 @@ -articles: - english: +Articles: + English: - author: Adejumo Ridwan Suleiman author_link: https://www.linkedin.com/in/adejumoridwan/ link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b @@ -236,7 +236,7 @@ articles: author_link: https://medium.com/@krishnardt365 link: https://medium.com/@krishnardt365/fastapi-docker-and-postgres-91943e71be92 title: Fastapi, Docker(Docker compose) and Postgres - german: + German: - author: Marcel Sander (actidoo) author_link: https://www.actidoo.com link: https://www.actidoo.com/de/blog/python-fastapi-domain-driven-design @@ -249,7 +249,7 @@ articles: author_link: https://hellocoding.de/autor/felix-schuermeyer/ link: https://hellocoding.de/blog/coding-language/python/fastapi title: REST-API Programmieren mittels Python und dem FastAPI Modul - japanese: + Japanese: - author: '@bee2' author_link: https://qiita.com/bee2 link: https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9 @@ -298,7 +298,7 @@ articles: author_link: https://qiita.com/mtitg link: https://qiita.com/mtitg/items/47770e9a562dd150631d title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築 - russian: + Russian: - author: Troy Köhler author_link: https://www.linkedin.com/in/trkohler/ link: https://trkohler.com/fast-api-introduction-to-framework @@ -311,18 +311,18 @@ articles: author_link: https://habr.com/ru/users/57uff3r/ link: https://habr.com/ru/post/454440/ title: 'Мелкая питонячая радость #2: Starlette - Солидная примочка – FastAPI' - vietnamese: + Vietnamese: - author: Nguyễn Nhân author_link: https://fullstackstation.com/author/figonking/ link: https://fullstackstation.com/fastapi-trien-khai-bang-docker/ title: 'FASTAPI: TRIỂN KHAI BẰNG DOCKER' - taiwanese: + Taiwanese: - author: Leon author_link: http://editor.leonh.space/ link: https://editor.leonh.space/2022/tortoise/ title: 'Tortoise ORM / FastAPI 整合快速筆記' -podcasts: - english: +Podcasts: + English: - author: Podcast.`__init__` author_link: https://www.pythonpodcast.com/ link: https://www.pythonpodcast.com/fastapi-web-application-framework-episode-259/ @@ -331,8 +331,8 @@ podcasts: author_link: https://pythonbytes.fm/ link: https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855 title: FastAPI on PythonBytes -talks: - english: +Talks: + English: - author: Sebastián Ramírez (tiangolo) author_link: https://twitter.com/tiangolo link: https://www.youtube.com/watch?v=PnpTY1f4k2U diff --git a/docs/en/docs/external-links.md b/docs/en/docs/external-links.md index 0c91470bc..b89021ee2 100644 --- a/docs/en/docs/external-links.md +++ b/docs/en/docs/external-links.md @@ -9,79 +9,21 @@ Here's an incomplete list of some of them. !!! tip If you have an article, project, tool, or anything related to **FastAPI** that is not yet listed here, create a <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request adding it</a>. -## Articles +{% for section_name, section_content in external_links.items() %} -### English +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Japanese - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Vietnamese - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Russian - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### German - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### Taiwanese - -{% if external_links %} -{% for article in external_links.articles.taiwanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Talks - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## Projects diff --git a/docs/fr/docs/external-links.md b/docs/fr/docs/external-links.md index 002e6d2b2..37b8c5b13 100644 --- a/docs/fr/docs/external-links.md +++ b/docs/fr/docs/external-links.md @@ -9,70 +9,21 @@ Voici une liste incomplète de certains d'entre eux. !!! tip "Astuce" Si vous avez un article, projet, outil, ou quoi que ce soit lié à **FastAPI** qui n'est actuellement pas listé ici, créez une <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request l'ajoutant</a>. -## Articles +{% for section_name, section_content in external_links.items() %} -### Anglais +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Japonais - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Vietnamien - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Russe - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### Allemand - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Conférences - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> par <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## Projets diff --git a/docs/ja/docs/external-links.md b/docs/ja/docs/external-links.md index 6703f5fc2..aca5d5b34 100644 --- a/docs/ja/docs/external-links.md +++ b/docs/ja/docs/external-links.md @@ -9,70 +9,21 @@ !!! tip "豆知識" ここにまだ載っていない**FastAPI**に関連する記事、プロジェクト、ツールなどがある場合は、 <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">プルリクエストして下さい</a>。 -## 記事 +{% for section_name, section_content in external_links.items() %} -### 英語 +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### 日本語 - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### ベトナム語 - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### ロシア語 - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### ドイツ語 - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## ポッドキャスト - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## トーク - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## プロジェクト diff --git a/docs/pt/docs/external-links.md b/docs/pt/docs/external-links.md index 6ec6c3a27..77ec32351 100644 --- a/docs/pt/docs/external-links.md +++ b/docs/pt/docs/external-links.md @@ -9,70 +9,21 @@ Aqui tem uma lista, incompleta, de algumas delas. !!! tip "Dica" Se você tem um artigo, projeto, ferramenta ou qualquer coisa relacionada ao **FastAPI** que ainda não está listada aqui, crie um <a href="https://github.com/tiangolo/fastapi/edit/master/docs/external-links.md" class="external-link" target="_blank">_Pull Request_ adicionando ele</a>. -## Artigos +{% for section_name, section_content in external_links.items() %} -### Inglês +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Japonês - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Vietnamita - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### Russo - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### Alemão - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Podcasts - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Palestras - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## Projetos diff --git a/docs/ru/docs/external-links.md b/docs/ru/docs/external-links.md index 4daf65898..2448ef82e 100644 --- a/docs/ru/docs/external-links.md +++ b/docs/ru/docs/external-links.md @@ -9,70 +9,21 @@ !!! tip Если у вас есть статья, проект, инструмент или что-либо, связанное с **FastAPI**, что еще не перечислено здесь, создайте <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request</a>. -## Статьи +{% for section_name, section_content in external_links.items() %} -### На английском +## {{ section_name }} -{% if external_links %} -{% for article in external_links.articles.english %} +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### На японском - -{% if external_links %} -{% for article in external_links.articles.japanese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### На вьетнамском - -{% if external_links %} -{% for article in external_links.articles.vietnamese %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. {% endfor %} -{% endif %} - -### На русском - -{% if external_links %} -{% for article in external_links.articles.russian %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -### На немецком - -{% if external_links %} -{% for article in external_links.articles.german %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Подкасты - -{% if external_links %} -{% for article in external_links.podcasts.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} - -## Talks - -{% if external_links %} -{% for article in external_links.talks.english %} - -* <a href="{{ article.link }}" class="external-link" target="_blank">{{ article.title }}</a> by <a href="{{ article.author_link }}" class="external-link" target="_blank">{{ article.author }}</a>. -{% endfor %} -{% endif %} ## Проекты From f9b53ae77834eb803f8043493bd337b8a448ae5a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 22 Oct 2023 07:35:50 +0000 Subject: [PATCH 1407/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 163e708b8..60675c757 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). * 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). From e0a5edaaa3796bca703ab7f914a69fed98cffcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 22 Oct 2023 14:03:38 +0400 Subject: [PATCH 1408/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20`CITATION.cff`?= =?UTF-8?q?=20file=20for=20academic=20citations=20(#10496)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CITATION.cff | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..9028248b1 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,24 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: FastAPI +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Sebastián + family-names: Ramírez + email: tiangolo@gmail.com +identifiers: +repository-code: 'https://github.com/tiangolo/fastapi' +url: 'https://fastapi.tiangolo.com' +abstract: >- + FastAPI framework, high performance, easy to learn, fast to code, + ready for production +keywords: + - fastapi + - pydantic + - starlette +license: MIT From 4ef7a40eae98c0d346fbfa2472f9b52b1cbf9b8f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 22 Oct 2023 10:04:16 +0000 Subject: [PATCH 1409/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 60675c757..74b0b1b27 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). * 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). From 2e14c69c311d89dde86e8c033df87773a3a50121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 25 Oct 2023 00:26:06 +0400 Subject: [PATCH 1410/1881] =?UTF-8?q?=F0=9F=91=B7=20Adopt=20Ruff=20format?= =?UTF-8?q?=20(#10517)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 Update pre-commit, use ruff format * ⬆️ Upgrade dependencies, use Ruff for formatting * 🔧 Update Ruff config * 🔨 Update lint and format scripts, use Ruff * 🎨 Format internals with Ruff * 🎨 Format docs scripts * 🎨 Format tests * 🎨 Format extra commas in src for docs * 📝 Update docs mentioning `@lru_cache()`, use `@lru_cache` instead to keep consistency with the format * 🎨 Update src for docs, use plain `@lru_cache` * 🎨 Update src for docs format and docs references --- .pre-commit-config.yaml | 14 ++------------ docs/em/docs/advanced/settings.md | 12 ++++++------ .../tutorial/query-params-str-validations.md | 4 ++-- docs/en/docs/advanced/settings.md | 12 ++++++------ .../tutorial/query-params-str-validations.md | 4 ++-- .../tutorial/query-params-str-validations.md | 4 ++-- docs/zh/docs/advanced/settings.md | 12 ++++++------ docs_src/header_params/tutorial002_an.py | 2 +- docs_src/header_params/tutorial002_an_py39.py | 2 +- .../tutorial004.py | 2 +- .../tutorial004_an.py | 2 +- .../tutorial004_an_py310.py | 2 +- .../tutorial004_an_py310_regex.py | 2 +- .../tutorial004_an_py39.py | 2 +- .../tutorial004_py310.py | 5 +++-- .../tutorial008.py | 2 +- .../tutorial008_an.py | 2 +- .../tutorial008_an_py310.py | 2 +- .../tutorial008_an_py39.py | 2 +- .../tutorial008_py310.py | 5 ++--- .../tutorial010.py | 2 +- .../tutorial010_an.py | 2 +- .../tutorial010_an_py310.py | 2 +- .../tutorial010_an_py39.py | 2 +- .../tutorial010_py310.py | 5 ++--- docs_src/settings/app02/main.py | 2 +- docs_src/settings/app02_an/main.py | 2 +- docs_src/settings/app02_an_py39/main.py | 2 +- docs_src/settings/app03/main.py | 2 +- docs_src/settings/app03_an/main.py | 2 +- docs_src/settings/app03_an_py39/main.py | 2 +- fastapi/_compat.py | 6 +++--- fastapi/applications.py | 6 ++---- fastapi/security/http.py | 2 +- fastapi/security/oauth2.py | 4 +--- fastapi/utils.py | 3 ++- pyproject.toml | 18 ++++++++++++++++++ requirements-docs-tests.txt | 1 - requirements-docs.txt | 2 ++ requirements-tests.txt | 2 +- scripts/docs.py | 6 +++--- scripts/format.sh | 2 +- scripts/lint.sh | 2 +- scripts/mkdocs_hooks.py | 4 ++-- tests/test_openapi_examples.py | 2 +- tests/test_schema_extra_examples.py | 4 ++-- 46 files changed, 94 insertions(+), 89 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f7085f72..a7f2fb3f2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,23 +13,13 @@ repos: - --unsafe - id: end-of-file-fixer - id: trailing-whitespace -- repo: https://github.com/asottile/pyupgrade - rev: v3.7.0 - hooks: - - id: pyupgrade - args: - - --py3-plus - - --keep-runtime-typing - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.275 + rev: v0.1.2 hooks: - id: ruff args: - --fix -- repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black + - id: ruff-format ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate diff --git a/docs/em/docs/advanced/settings.md b/docs/em/docs/advanced/settings.md index bc50bf755..cc7a08bab 100644 --- a/docs/em/docs/advanced/settings.md +++ b/docs/em/docs/advanced/settings.md @@ -221,7 +221,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app ``` !!! tip - 👥 🔜 🔬 `@lru_cache()` 🍖. + 👥 🔜 🔬 `@lru_cache` 🍖. 🔜 👆 💪 🤔 `get_settings()` 😐 🔢. @@ -302,7 +302,7 @@ def get_settings(): 👥 🔜 ✍ 👈 🎚 🔠 📨, & 👥 🔜 👂 `.env` 📁 🔠 📨. 👶 👶 -✋️ 👥 ⚙️ `@lru_cache()` 👨‍🎨 🔛 🔝, `Settings` 🎚 🔜 ✍ 🕴 🕐, 🥇 🕰 ⚫️ 🤙. 👶 👶 +✋️ 👥 ⚙️ `@lru_cache` 👨‍🎨 🔛 🔝, `Settings` 🎚 🔜 ✍ 🕴 🕐, 🥇 🕰 ⚫️ 🤙. 👶 👶 ```Python hl_lines="1 10" {!../../../docs_src/settings/app03/main.py!} @@ -312,14 +312,14 @@ def get_settings(): #### `lru_cache` 📡 ℹ -`@lru_cache()` 🔀 🔢 ⚫️ 🎀 📨 🎏 💲 👈 📨 🥇 🕰, ↩️ 💻 ⚫️ 🔄, 🛠️ 📟 🔢 🔠 🕰. +`@lru_cache` 🔀 🔢 ⚫️ 🎀 📨 🎏 💲 👈 📨 🥇 🕰, ↩️ 💻 ⚫️ 🔄, 🛠️ 📟 🔢 🔠 🕰. , 🔢 🔛 ⚫️ 🔜 🛠️ 🕐 🔠 🌀 ❌. & ⤴️ 💲 📨 🔠 👈 🌀 ❌ 🔜 ⚙️ 🔄 & 🔄 🕐❔ 🔢 🤙 ⏮️ ⚫️❔ 🎏 🌀 ❌. 🖼, 🚥 👆 ✔️ 🔢: ```Python -@lru_cache() +@lru_cache def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` @@ -371,7 +371,7 @@ participant execute as Execute function 👈 🌌, ⚫️ 🎭 🌖 🚥 ⚫️ 🌐 🔢. ✋️ ⚫️ ⚙️ 🔗 🔢, ⤴️ 👥 💪 🔐 ⚫️ 💪 🔬. -`@lru_cache()` 🍕 `functools` ❔ 🍕 🐍 🐩 🗃, 👆 💪 ✍ 🌅 🔃 ⚫️ <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">🐍 🩺 `@lru_cache()`</a>. +`@lru_cache` 🍕 `functools` ❔ 🍕 🐍 🐩 🗃, 👆 💪 ✍ 🌅 🔃 ⚫️ <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">🐍 🩺 `@lru_cache`</a>. ## 🌃 @@ -379,4 +379,4 @@ participant execute as Execute function * ⚙️ 🔗 👆 💪 📉 🔬. * 👆 💪 ⚙️ `.env` 📁 ⏮️ ⚫️. -* ⚙️ `@lru_cache()` ➡️ 👆 ❎ 👂 🇨🇻 📁 🔄 & 🔄 🔠 📨, ⏪ 🤝 👆 🔐 ⚫️ ⏮️ 🔬. +* ⚙️ `@lru_cache` ➡️ 👆 ❎ 👂 🇨🇻 📁 🔄 & 🔄 🔠 📨, ⏪ 🤝 👆 🔐 ⚫️ ⏮️ 🔬. diff --git a/docs/em/docs/tutorial/query-params-str-validations.md b/docs/em/docs/tutorial/query-params-str-validations.md index d6b67bd51..f0e455abe 100644 --- a/docs/em/docs/tutorial/query-params-str-validations.md +++ b/docs/em/docs/tutorial/query-params-str-validations.md @@ -371,7 +371,7 @@ http://localhost:8000/items/ === "🐍 3️⃣.1️⃣0️⃣ & 🔛" - ```Python hl_lines="12" + ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` @@ -421,7 +421,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems === "🐍 3️⃣.1️⃣0️⃣ & 🔛" - ```Python hl_lines="17" + ```Python hl_lines="16" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index d39130777..32df90006 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -276,7 +276,7 @@ Now we create a dependency that returns a new `config.Settings()`. ``` !!! tip - We'll discuss the `@lru_cache()` in a bit. + We'll discuss the `@lru_cache` in a bit. For now you can assume `get_settings()` is a normal function. @@ -388,7 +388,7 @@ def get_settings(): we would create that object for each request, and we would be reading the `.env` file for each request. ⚠️ -But as we are using the `@lru_cache()` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ +But as we are using the `@lru_cache` decorator on top, the `Settings` object will be created only once, the first time it's called. ✔️ === "Python 3.9+" @@ -415,14 +415,14 @@ Then for any subsequent calls of `get_settings()` in the dependencies for the ne #### `lru_cache` Technical Details -`@lru_cache()` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time. +`@lru_cache` modifies the function it decorates to return the same value that was returned the first time, instead of computing it again, executing the code of the function every time. So, the function below it will be executed once for each combination of arguments. And then the values returned by each of those combinations of arguments will be used again and again whenever the function is called with exactly the same combination of arguments. For example, if you have a function: ```Python -@lru_cache() +@lru_cache def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` @@ -474,7 +474,7 @@ In the case of our dependency `get_settings()`, the function doesn't even take a That way, it behaves almost as if it was just a global variable. But as it uses a dependency function, then we can override it easily for testing. -`@lru_cache()` is part of `functools` which is part of Python's standard library, you can read more about it in the <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python docs for `@lru_cache()`</a>. +`@lru_cache` is part of `functools` which is part of Python's standard library, you can read more about it in the <a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python docs for `@lru_cache`</a>. ## Recap @@ -482,4 +482,4 @@ You can use Pydantic Settings to handle the settings or configurations for your * By using a dependency you can simplify testing. * You can use `.env` files with it. -* Using `@lru_cache()` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. +* Using `@lru_cache` lets you avoid reading the dotenv file again and again for each request, while allowing you to override it during testing. diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 0e777f6a0..91ae615ff 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -737,7 +737,7 @@ And a `description`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="12" + ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` @@ -835,7 +835,7 @@ Then pass the parameter `deprecated=True` to `Query`: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="17" + ```Python hl_lines="16" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` diff --git a/docs/ru/docs/tutorial/query-params-str-validations.md b/docs/ru/docs/tutorial/query-params-str-validations.md index 15be5dbf6..cc826b871 100644 --- a/docs/ru/docs/tutorial/query-params-str-validations.md +++ b/docs/ru/docs/tutorial/query-params-str-validations.md @@ -741,7 +741,7 @@ http://localhost:8000/items/ !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. - ```Python hl_lines="12" + ```Python hl_lines="11" {!> ../../../docs_src/query_params_str_validations/tutorial008_py310.py!} ``` @@ -839,7 +839,7 @@ http://127.0.0.1:8000/items/?item-query=foobaritems !!! tip "Подсказка" Рекомендуется использовать версию с `Annotated` если возможно. - ```Python hl_lines="17" + ```Python hl_lines="16" {!> ../../../docs_src/query_params_str_validations/tutorial010_py310.py!} ``` diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md index 7f718acef..93e48a610 100644 --- a/docs/zh/docs/advanced/settings.md +++ b/docs/zh/docs/advanced/settings.md @@ -239,7 +239,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app ``` !!! tip - 我们稍后会讨论 `@lru_cache()`。 + 我们稍后会讨论 `@lru_cache`。 目前,您可以将 `get_settings()` 视为普通函数。 @@ -337,7 +337,7 @@ def get_settings(): 我们将为每个请求创建该对象,并且将在每个请求中读取 `.env` 文件。 ⚠️ -但是,由于我们在顶部使用了 `@lru_cache()` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ +但是,由于我们在顶部使用了 `@lru_cache` 装饰器,因此只有在第一次调用它时,才会创建 `Settings` 对象一次。 ✔️ === "Python 3.9+" @@ -364,13 +364,13 @@ def get_settings(): #### `lru_cache` 技术细节 -`@lru_cache()` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 +`@lru_cache` 修改了它所装饰的函数,以返回第一次返回的相同值,而不是再次计算它,每次都执行函数的代码。 因此,下面的函数将对每个参数组合执行一次。然后,每个参数组合返回的值将在使用完全相同的参数组合调用函数时再次使用。 例如,如果您有一个函数: ```Python -@lru_cache() +@lru_cache def say_hi(name: str, salutation: str = "Ms."): return f"Hello {salutation} {name}" ``` @@ -422,7 +422,7 @@ participant execute as Execute function 这样,它的行为几乎就像是一个全局变量。但是由于它使用了依赖项函数,因此我们可以轻松地进行测试时的覆盖。 -`@lru_cache()` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文档中了解有关 `@lru_cache()` 的更多信息</a>。 +`@lru_cache` 是 `functools` 的一部分,它是 Python 标准库的一部分,您可以在<a href="https://docs.python.org/3/library/functools.html#functools.lru_cache" class="external-link" target="_blank">Python 文档中了解有关 `@lru_cache` 的更多信息</a>。 ## 小结 @@ -430,4 +430,4 @@ participant execute as Execute function * 通过使用依赖项,您可以简化测试。 * 您可以使用 `.env` 文件。 -* 使用 `@lru_cache()` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 +* 使用 `@lru_cache` 可以避免为每个请求重复读取 dotenv 文件,同时允许您在测试时进行覆盖。 diff --git a/docs_src/header_params/tutorial002_an.py b/docs_src/header_params/tutorial002_an.py index 65d972d46..82fe49ba2 100644 --- a/docs_src/header_params/tutorial002_an.py +++ b/docs_src/header_params/tutorial002_an.py @@ -10,6 +10,6 @@ app = FastAPI() async def read_items( strange_header: Annotated[ Union[str, None], Header(convert_underscores=False) - ] = None + ] = None, ): return {"strange_header": strange_header} diff --git a/docs_src/header_params/tutorial002_an_py39.py b/docs_src/header_params/tutorial002_an_py39.py index 7f6a99f9c..008e4b6e1 100644 --- a/docs_src/header_params/tutorial002_an_py39.py +++ b/docs_src/header_params/tutorial002_an_py39.py @@ -9,6 +9,6 @@ app = FastAPI() async def read_items( strange_header: Annotated[ Union[str, None], Header(convert_underscores=False) - ] = None + ] = None, ): return {"strange_header": strange_header} diff --git a/docs_src/query_params_str_validations/tutorial004.py b/docs_src/query_params_str_validations/tutorial004.py index 3639b6c38..64a647a16 100644 --- a/docs_src/query_params_str_validations/tutorial004.py +++ b/docs_src/query_params_str_validations/tutorial004.py @@ -9,7 +9,7 @@ app = FastAPI() async def read_items( q: Union[str, None] = Query( default=None, min_length=3, max_length=50, pattern="^fixedquery$" - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_an.py b/docs_src/query_params_str_validations/tutorial004_an.py index 24698c7b3..c75d45d63 100644 --- a/docs_src/query_params_str_validations/tutorial004_an.py +++ b/docs_src/query_params_str_validations/tutorial004_an.py @@ -10,7 +10,7 @@ app = FastAPI() async def read_items( q: Annotated[ Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310.py b/docs_src/query_params_str_validations/tutorial004_an_py310.py index b7b629ee8..20cf1988f 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py310.py @@ -9,7 +9,7 @@ app = FastAPI() async def read_items( q: Annotated[ str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$") - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py index 8fd375b3d..21e0d3eb8 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py @@ -9,7 +9,7 @@ app = FastAPI() async def read_items( q: Annotated[ str | None, Query(min_length=3, max_length=50, regex="^fixedquery$") - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_an_py39.py b/docs_src/query_params_str_validations/tutorial004_an_py39.py index 8e9a6fc32..de27097b3 100644 --- a/docs_src/query_params_str_validations/tutorial004_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial004_an_py39.py @@ -9,7 +9,7 @@ app = FastAPI() async def read_items( q: Annotated[ Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$") - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial004_py310.py b/docs_src/query_params_str_validations/tutorial004_py310.py index f80798bcb..7801e7500 100644 --- a/docs_src/query_params_str_validations/tutorial004_py310.py +++ b/docs_src/query_params_str_validations/tutorial004_py310.py @@ -5,8 +5,9 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$") + q: str | None = Query( + default=None, min_length=3, max_length=50, pattern="^fixedquery$" + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008.py b/docs_src/query_params_str_validations/tutorial008.py index d112a9ab8..e3e0b50aa 100644 --- a/docs_src/query_params_str_validations/tutorial008.py +++ b/docs_src/query_params_str_validations/tutorial008.py @@ -12,7 +12,7 @@ async def read_items( title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008_an.py b/docs_src/query_params_str_validations/tutorial008_an.py index 5699f1e88..01606a920 100644 --- a/docs_src/query_params_str_validations/tutorial008_an.py +++ b/docs_src/query_params_str_validations/tutorial008_an.py @@ -15,7 +15,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008_an_py310.py b/docs_src/query_params_str_validations/tutorial008_an_py310.py index 4aaadf8b4..44b3082b6 100644 --- a/docs_src/query_params_str_validations/tutorial008_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial008_an_py310.py @@ -14,7 +14,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008_an_py39.py b/docs_src/query_params_str_validations/tutorial008_an_py39.py index 1c3b36176..f3f2f2c0e 100644 --- a/docs_src/query_params_str_validations/tutorial008_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial008_an_py39.py @@ -14,7 +14,7 @@ async def read_items( description="Query string for the items to search in the database that have a good match", min_length=3, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial008_py310.py b/docs_src/query_params_str_validations/tutorial008_py310.py index 489f631d5..574385272 100644 --- a/docs_src/query_params_str_validations/tutorial008_py310.py +++ b/docs_src/query_params_str_validations/tutorial008_py310.py @@ -5,13 +5,12 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query( + q: str | None = Query( default=None, title="Query string", description="Query string for the items to search in the database that have a good match", min_length=3, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010.py b/docs_src/query_params_str_validations/tutorial010.py index 3314f8b6d..ff29176fe 100644 --- a/docs_src/query_params_str_validations/tutorial010.py +++ b/docs_src/query_params_str_validations/tutorial010.py @@ -16,7 +16,7 @@ async def read_items( max_length=50, pattern="^fixedquery$", deprecated=True, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010_an.py b/docs_src/query_params_str_validations/tutorial010_an.py index c5df00897..ed343230f 100644 --- a/docs_src/query_params_str_validations/tutorial010_an.py +++ b/docs_src/query_params_str_validations/tutorial010_an.py @@ -19,7 +19,7 @@ async def read_items( pattern="^fixedquery$", deprecated=True, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010_an_py310.py b/docs_src/query_params_str_validations/tutorial010_an_py310.py index a8e8c099b..775095bda 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py310.py @@ -18,7 +18,7 @@ async def read_items( pattern="^fixedquery$", deprecated=True, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010_an_py39.py b/docs_src/query_params_str_validations/tutorial010_an_py39.py index 955880dd6..b126c116f 100644 --- a/docs_src/query_params_str_validations/tutorial010_an_py39.py +++ b/docs_src/query_params_str_validations/tutorial010_an_py39.py @@ -18,7 +18,7 @@ async def read_items( pattern="^fixedquery$", deprecated=True, ), - ] = None + ] = None, ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/query_params_str_validations/tutorial010_py310.py b/docs_src/query_params_str_validations/tutorial010_py310.py index 9ea7b3c49..530e6cf5b 100644 --- a/docs_src/query_params_str_validations/tutorial010_py310.py +++ b/docs_src/query_params_str_validations/tutorial010_py310.py @@ -5,8 +5,7 @@ app = FastAPI() @app.get("/items/") async def read_items( - q: str - | None = Query( + q: str | None = Query( default=None, alias="item-query", title="Query string", @@ -15,7 +14,7 @@ async def read_items( max_length=50, pattern="^fixedquery$", deprecated=True, - ) + ), ): results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]} if q: diff --git a/docs_src/settings/app02/main.py b/docs_src/settings/app02/main.py index 163aa2614..941f82e6b 100644 --- a/docs_src/settings/app02/main.py +++ b/docs_src/settings/app02/main.py @@ -7,7 +7,7 @@ from .config import Settings app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return Settings() diff --git a/docs_src/settings/app02_an/main.py b/docs_src/settings/app02_an/main.py index cb679202d..3a578cc33 100644 --- a/docs_src/settings/app02_an/main.py +++ b/docs_src/settings/app02_an/main.py @@ -8,7 +8,7 @@ from .config import Settings app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return Settings() diff --git a/docs_src/settings/app02_an_py39/main.py b/docs_src/settings/app02_an_py39/main.py index 61be74fcb..6d5db12a8 100644 --- a/docs_src/settings/app02_an_py39/main.py +++ b/docs_src/settings/app02_an_py39/main.py @@ -8,7 +8,7 @@ from .config import Settings app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return Settings() diff --git a/docs_src/settings/app03/main.py b/docs_src/settings/app03/main.py index 69bc8c6e0..ea64a5709 100644 --- a/docs_src/settings/app03/main.py +++ b/docs_src/settings/app03/main.py @@ -7,7 +7,7 @@ from . import config app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return config.Settings() diff --git a/docs_src/settings/app03_an/main.py b/docs_src/settings/app03_an/main.py index c33b98f47..2f64b9cd1 100644 --- a/docs_src/settings/app03_an/main.py +++ b/docs_src/settings/app03_an/main.py @@ -8,7 +8,7 @@ from . import config app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return config.Settings() diff --git a/docs_src/settings/app03_an_py39/main.py b/docs_src/settings/app03_an_py39/main.py index b89c6b6cf..62f347639 100644 --- a/docs_src/settings/app03_an_py39/main.py +++ b/docs_src/settings/app03_an_py39/main.py @@ -8,7 +8,7 @@ from . import config app = FastAPI() -@lru_cache() +@lru_cache def get_settings(): return config.Settings() diff --git a/fastapi/_compat.py b/fastapi/_compat.py index a4b305d42..fc605d0ec 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -197,9 +197,9 @@ if PYDANTIC_V2: if "$ref" not in json_schema: # TODO remove when deprecating Pydantic v1 # Ref: https://github.com/pydantic/pydantic/blob/d61792cc42c80b13b23e3ffa74bc37ec7c77f7d1/pydantic/schema.py#L207 - json_schema[ - "title" - ] = field.field_info.title or field.alias.title().replace("_", " ") + json_schema["title"] = ( + field.field_info.title or field.alias.title().replace("_", " ") + ) return json_schema def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap: diff --git a/fastapi/applications.py b/fastapi/applications.py index 8ca374a54..3021d7593 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -896,9 +896,7 @@ class FastAPI(Starlette): [FastAPI docs for OpenAPI Webhooks](https://fastapi.tiangolo.com/advanced/openapi-webhooks/). """ ), - ] = ( - webhooks or routing.APIRouter() - ) + ] = webhooks or routing.APIRouter() self.root_path = root_path or openapi_prefix self.state: Annotated[ State, @@ -951,7 +949,7 @@ class FastAPI(Starlette): ) self.exception_handlers: Dict[ Any, Callable[[Request, Any], Union[Response, Awaitable[Response]]] - ] = ({} if exception_handlers is None else dict(exception_handlers)) + ] = {} if exception_handlers is None else dict(exception_handlers) self.exception_handlers.setdefault(HTTPException, http_exception_handler) self.exception_handlers.setdefault( RequestValidationError, request_validation_exception_handler diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 3627777d6..738455de3 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -210,7 +210,7 @@ class HTTPBasic(HTTPBase): try: data = b64decode(param).decode("ascii") except (ValueError, UnicodeDecodeError, binascii.Error): - raise invalid_user_credentials_exc + raise invalid_user_credentials_exc # noqa: B904 username, separator, password = data.partition(":") if not separator: raise invalid_user_credentials_exc diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index d427783ad..9281dfb64 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -626,9 +626,7 @@ class SecurityScopes: The list of all the scopes required by dependencies. """ ), - ] = ( - scopes or [] - ) + ] = scopes or [] self.scope_str: Annotated[ str, Doc( diff --git a/fastapi/utils.py b/fastapi/utils.py index 53b47a160..f8463dda2 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -152,7 +152,8 @@ def create_cloned_field( ] if field.key_field: # type: ignore[attr-defined] new_field.key_field = create_cloned_field( # type: ignore[attr-defined] - field.key_field, cloned_types=cloned_types # type: ignore[attr-defined] + field.key_field, # type: ignore[attr-defined] + cloned_types=cloned_types, ) new_field.validators = field.validators # type: ignore[attr-defined] new_field.pre_validators = field.pre_validators # type: ignore[attr-defined] diff --git a/pyproject.toml b/pyproject.toml index addde1d33..e67486ae3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,11 +130,13 @@ select = [ "I", # isort "C", # flake8-comprehensions "B", # flake8-bugbear + "UP", # pyupgrade ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults "C901", # too complex + "W191", # indentation contains tabs ] [tool.ruff.per-file-ignores] @@ -154,6 +156,22 @@ ignore = [ "docs_src/query_params_str_validations/tutorial012_an_py39.py" = ["B006"] "docs_src/query_params_str_validations/tutorial013_an.py" = ["B006"] "docs_src/query_params_str_validations/tutorial013_an_py39.py" = ["B006"] +"docs_src/security/tutorial004.py" = ["B904"] +"docs_src/security/tutorial004_an.py" = ["B904"] +"docs_src/security/tutorial004_an_py310.py" = ["B904"] +"docs_src/security/tutorial004_an_py39.py" = ["B904"] +"docs_src/security/tutorial004_py310.py" = ["B904"] +"docs_src/security/tutorial005.py" = ["B904"] +"docs_src/security/tutorial005_an.py" = ["B904"] +"docs_src/security/tutorial005_an_py310.py" = ["B904"] +"docs_src/security/tutorial005_an_py39.py" = ["B904"] +"docs_src/security/tutorial005_py310.py" = ["B904"] +"docs_src/security/tutorial005_py39.py" = ["B904"] + [tool.ruff.isort] known-third-party = ["fastapi", "pydantic", "starlette"] + +[tool.ruff.pyupgrade] +# Preserve types, even if a file imports `from __future__ import annotations`. +keep-runtime-typing = true diff --git a/requirements-docs-tests.txt b/requirements-docs-tests.txt index 1a4a57267..b82df4933 100644 --- a/requirements-docs-tests.txt +++ b/requirements-docs-tests.txt @@ -1,3 +1,2 @@ # For mkdocstrings and tests httpx >=0.23.0,<0.25.0 -black == 23.3.0 diff --git a/requirements-docs.txt b/requirements-docs.txt index 3e0df6483..69302f655 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -15,3 +15,5 @@ pillow==9.5.0 cairosvg==2.7.0 mkdocstrings[python]==0.23.0 griffe-typingdoc==0.2.2 +# For griffe, it formats with black +black==23.3.0 diff --git a/requirements-tests.txt b/requirements-tests.txt index de8d3f26c..e1a976c13 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -4,7 +4,7 @@ pydantic-settings >=2.0.0 pytest >=7.1.3,<8.0.0 coverage[toml] >= 6.5.0,< 8.0 mypy ==1.4.1 -ruff ==0.0.275 +ruff ==0.1.2 email_validator >=1.1.1,<3.0.0 dirty-equals ==0.6.0 # TODO: once removing databases from tutorial, upgrade SQLAlchemy diff --git a/scripts/docs.py b/scripts/docs.py index 0023c670c..73e1900ad 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -36,7 +36,7 @@ site_path = Path("site").absolute() build_site_path = Path("site_build").absolute() -@lru_cache() +@lru_cache def is_mkdocs_insiders() -> bool: version = metadata.version("mkdocs-material") return "insiders" in version @@ -104,7 +104,7 @@ def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): def build_lang( lang: str = typer.Argument( ..., callback=lang_callback, autocompletion=complete_existing_lang - ) + ), ) -> None: """ Build the docs for a language. @@ -251,7 +251,7 @@ def serve() -> None: def live( lang: str = typer.Argument( None, callback=lang_callback, autocompletion=complete_existing_lang - ) + ), ) -> None: """ Serve with livereload a docs site for a specific language. diff --git a/scripts/format.sh b/scripts/format.sh index 3fb3eb4f1..11f25f1ce 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -2,4 +2,4 @@ set -x ruff fastapi tests docs_src scripts --fix -black fastapi tests docs_src scripts +ruff format fastapi tests docs_src scripts diff --git a/scripts/lint.sh b/scripts/lint.sh index 4db5caa96..c0e24db9f 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,4 +5,4 @@ set -x mypy fastapi ruff fastapi tests docs_src scripts -black fastapi tests --check +ruff format fastapi tests --check diff --git a/scripts/mkdocs_hooks.py b/scripts/mkdocs_hooks.py index 2b6a05642..8335a13f6 100644 --- a/scripts/mkdocs_hooks.py +++ b/scripts/mkdocs_hooks.py @@ -14,14 +14,14 @@ non_traslated_sections = [ ] -@lru_cache() +@lru_cache def get_missing_translation_content(docs_dir: str) -> str: docs_dir_path = Path(docs_dir) missing_translation_path = docs_dir_path.parent.parent / "missing-translation.md" return missing_translation_path.read_text(encoding="utf-8") -@lru_cache() +@lru_cache def get_mkdocs_material_langs() -> List[str]: material_path = Path(material.__file__).parent material_langs_path = material_path / "templates" / "partials" / "languages" diff --git a/tests/test_openapi_examples.py b/tests/test_openapi_examples.py index 70664a8a4..6597e5058 100644 --- a/tests/test_openapi_examples.py +++ b/tests/test_openapi_examples.py @@ -28,7 +28,7 @@ def examples( "value": {"data": "Data in Body examples, example2"}, }, }, - ) + ), ): return item diff --git a/tests/test_schema_extra_examples.py b/tests/test_schema_extra_examples.py index a1505afe2..b313f47e9 100644 --- a/tests/test_schema_extra_examples.py +++ b/tests/test_schema_extra_examples.py @@ -40,7 +40,7 @@ def create_app(): {"data": "Data in Body examples, example1"}, {"data": "Data in Body examples, example2"}, ], - ) + ), ): return item @@ -54,7 +54,7 @@ def create_app(): {"data": "examples example_examples 1"}, {"data": "examples example_examples 2"}, ], - ) + ), ): return item From 223970e03c3ac43e3bec059f5b094e68029e9396 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 24 Oct 2023 20:26:43 +0000 Subject: [PATCH 1411/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 74b0b1b27..0acca3761 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). * 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). From f7e338dcd832b4fb9d3236f622fb6bf6e97ef40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 25 Oct 2023 12:25:03 +0400 Subject: [PATCH 1412/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors=20ba?= =?UTF-8?q?dges,=20Databento=20(#10519)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/sponsors_badge.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index d67e27c87..acbcc2205 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -14,6 +14,7 @@ logins: - nihpo - armand-sauzay - databento-bot + - databento - nanram22 - Flint-company - porter-dev From 2754d4e0fe2e45673d4ded8491a40f6d1bcd1d80 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 25 Oct 2023 08:25:52 +0000 Subject: [PATCH 1413/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0acca3761..d761fa20b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). * 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). * 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). From e45cbb7e5e804079a0eb57b3175a77b4d18a5fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 29 Oct 2023 13:12:11 +0400 Subject: [PATCH 1414/1881] =?UTF-8?q?=F0=9F=91=B7=20Install=20MkDocs=20Mat?= =?UTF-8?q?erial=20Insiders=20only=20when=20secrets=20are=20available,=20f?= =?UTF-8?q?or=20Dependabot=20(#10544)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 4100781c5..701c74697 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -50,7 +50,7 @@ jobs: run: pip install -r requirements-docs.txt # Install MkDocs Material Insiders here just to put it in the cache for the rest of the steps - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' + if: ( github.event_name != 'pull_request' || github.secret_source == 'Actions' ) && steps.cache.outputs.cache-hit != 'true' run: | pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git @@ -88,7 +88,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-docs.txt - name: Install Material for MkDocs Insiders - if: ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false ) && steps.cache.outputs.cache-hit != 'true' + if: ( github.event_name != 'pull_request' || github.secret_source != 'Actions' ) && steps.cache.outputs.cache-hit != 'true' run: | pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git From 072c701b0ed5c831f6c923d2af9d89eef4eacbf7 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 29 Oct 2023 09:12:56 +0000 Subject: [PATCH 1415/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d761fa20b..9db471b3f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#10544](https://github.com/tiangolo/fastapi/pull/10544) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). * 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). From e0c5beb5c8d22c90e9ae8c0d72728a60d451edd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:32:50 +0400 Subject: [PATCH 1416/1881] =?UTF-8?q?=E2=AC=86=20Bump=20mkdocs-material=20?= =?UTF-8?q?from=209.1.21=20to=209.4.7=20(#10545)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.21 to 9.4.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.21...9.4.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 69302f655..83dc94ee0 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,6 +1,6 @@ -e . -r requirements-docs-tests.txt -mkdocs-material==9.1.21 +mkdocs-material==9.4.7 mdx-include >=1.4.1,<2.0.0 mkdocs-markdownextradata-plugin >=0.1.7,<0.3.0 mkdocs-redirects>=1.2.1,<1.3.0 From 378e590757814099abdd49a7e42d1a393db2a206 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 29 Oct 2023 09:33:30 +0000 Subject: [PATCH 1418/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9db471b3f..036e8c45c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ⬆ Bump mkdocs-material from 9.1.21 to 9.4.7. PR [#10545](https://github.com/tiangolo/fastapi/pull/10545) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#10544](https://github.com/tiangolo/fastapi/pull/10544) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). * 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). From 38db1fe074ba48b8b60827d0aa4e523d888e5a07 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 29 Oct 2023 09:33:47 +0000 Subject: [PATCH 1419/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 036e8c45c..94c3451f8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ⬆ Update mkdocs-material requirement from <9.0.0,>=8.1.4 to >=8.1.4,<10.0.0. PR [#5862](https://github.com/tiangolo/fastapi/pull/5862) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.21 to 9.4.7. PR [#10545](https://github.com/tiangolo/fastapi/pull/10545) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#10544](https://github.com/tiangolo/fastapi/pull/10544) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). From 0b83491843ddeecea6708a58b25cd895f446e364 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:49:21 +0400 Subject: [PATCH 1420/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pillow=20from=209.?= =?UTF-8?q?5.0=20to=2010.1.0=20(#10446)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.1.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.1.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-docs.txt b/requirements-docs.txt index 83dc94ee0..28408a9f1 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -10,7 +10,7 @@ pyyaml >=5.3.1,<7.0.0 # For Material for MkDocs, Chinese search jieba==0.42.1 # For image processing by Material for MkDocs -pillow==9.5.0 +pillow==10.1.0 # For image processing by Material for MkDocs cairosvg==2.7.0 mkdocstrings[python]==0.23.0 From b84f9f6ecb4292fb51f22314ff7e60b3eadb97c0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 29 Oct 2023 09:49:57 +0000 Subject: [PATCH 1421/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 94c3451f8..ea2284cd5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ⬆ Bump pillow from 9.5.0 to 10.1.0. PR [#10446](https://github.com/tiangolo/fastapi/pull/10446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update mkdocs-material requirement from <9.0.0,>=8.1.4 to >=8.1.4,<10.0.0. PR [#5862](https://github.com/tiangolo/fastapi/pull/5862) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.21 to 9.4.7. PR [#10545](https://github.com/tiangolo/fastapi/pull/10545) by [@dependabot[bot]](https://github.com/apps/dependabot). * 👷 Install MkDocs Material Insiders only when secrets are available, for Dependabot. PR [#10544](https://github.com/tiangolo/fastapi/pull/10544) by [@tiangolo](https://github.com/tiangolo). From fbe6ba6df119b4f028f06cbcceb2ba4782899b1f Mon Sep 17 00:00:00 2001 From: Dmitry <nor808@yandex.ru> Date: Mon, 30 Oct 2023 10:01:00 +0300 Subject: [PATCH 1422/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/em/docs/index.md`,=20Python=203.8=20(#10521)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typo in index page 7️⃣❌ -> 8️⃣✅ --- docs/em/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/em/docs/index.md b/docs/em/docs/index.md index ea8a9d41c..c7df28160 100644 --- a/docs/em/docs/index.md +++ b/docs/em/docs/index.md @@ -27,7 +27,7 @@ --- -FastAPI 🏛, ⏩ (↕-🎭), 🕸 🛠️ 🏗 🛠️ ⏮️ 🐍 3️⃣.7️⃣ ➕ ⚓️ 🔛 🐩 🐍 🆎 🔑. +FastAPI 🏛, ⏩ (↕-🎭), 🕸 🛠️ 🏗 🛠️ ⏮️ 🐍 3️⃣.8️⃣ ➕ ⚓️ 🔛 🐩 🐍 🆎 🔑. 🔑 ⚒: From cbc8f186649419b962d978a4d604dee69ed70fd3 Mon Sep 17 00:00:00 2001 From: Hasnat Sajid <86589885+hasnatsajid@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:05:01 +0500 Subject: [PATCH 1423/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20links=20in?= =?UTF-8?q?=20`docs/em/docs/async.md`=20(#10507)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/async.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/em/docs/async.md b/docs/em/docs/async.md index 13b362b5d..ddcae1573 100644 --- a/docs/em/docs/async.md +++ b/docs/em/docs/async.md @@ -409,11 +409,11 @@ async def read_burgers(): ### 🔗 -🎏 ✔ [🔗](/tutorial/dependencies/index.md){.internal-link target=_blank}. 🚥 🔗 🐩 `def` 🔢 ↩️ `async def`, ⚫️ 🏃 🔢 🧵. +🎏 ✔ [🔗](./tutorial/dependencies/index.md){.internal-link target=_blank}. 🚥 🔗 🐩 `def` 🔢 ↩️ `async def`, ⚫️ 🏃 🔢 🧵. ### 🎧-🔗 -👆 💪 ✔️ 💗 🔗 & [🎧-🔗](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} 🚫 🔠 🎏 (🔢 🔢 🔑), 👫 💪 ✍ ⏮️ `async def` & ⏮️ 😐 `def`. ⚫️ 🔜 👷, & 🕐 ✍ ⏮️ 😐 `def` 🔜 🤙 🔛 🔢 🧵 (⚪️➡️ 🧵) ↩️ ➖ "⌛". +👆 💪 ✔️ 💗 🔗 & [🎧-🔗](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} 🚫 🔠 🎏 (🔢 🔢 🔑), 👫 💪 ✍ ⏮️ `async def` & ⏮️ 😐 `def`. ⚫️ 🔜 👷, & 🕐 ✍ ⏮️ 😐 `def` 🔜 🤙 🔛 🔢 🧵 (⚪️➡️ 🧵) ↩️ ➖ "⌛". ### 🎏 🚙 🔢 From 6b903ff1fbafd09f821a3c9f370317aaa1b26892 Mon Sep 17 00:00:00 2001 From: Hasnat Sajid <86589885+hasnatsajid@users.noreply.github.com> Date: Mon, 30 Oct 2023 12:07:15 +0500 Subject: [PATCH 1424/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20links?= =?UTF-8?q?=20in=20`docs/en/docs/async.md`=20and=20`docs/zh/docs/async.md`?= =?UTF-8?q?=20to=20make=20them=20relative=20(#10498)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/async.md | 4 ++-- docs/zh/docs/async.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/docs/async.md b/docs/en/docs/async.md index 3d4b1956a..2ead1f2db 100644 --- a/docs/en/docs/async.md +++ b/docs/en/docs/async.md @@ -409,11 +409,11 @@ Still, in both situations, chances are that **FastAPI** will [still be faster](/ ### Dependencies -The same applies for [dependencies](/tutorial/dependencies/index.md){.internal-link target=_blank}. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. +The same applies for [dependencies](./tutorial/dependencies/index.md){.internal-link target=_blank}. If a dependency is a standard `def` function instead of `async def`, it is run in the external threadpool. ### Sub-dependencies -You can have multiple dependencies and [sub-dependencies](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". +You can have multiple dependencies and [sub-dependencies](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} requiring each other (as parameters of the function definitions), some of them might be created with `async def` and some with normal `def`. It would still work, and the ones created with normal `def` would be called on an external thread (from the threadpool) instead of being "awaited". ### Other utility functions diff --git a/docs/zh/docs/async.md b/docs/zh/docs/async.md index 7cc76fc86..59eebd049 100644 --- a/docs/zh/docs/async.md +++ b/docs/zh/docs/async.md @@ -409,11 +409,11 @@ Starlette (和 **FastAPI**) 是基于 <a href="https://anyio.readthedocs.io/ ### 依赖 -这同样适用于[依赖](/tutorial/dependencies/index.md){.internal-link target=_blank}。如果一个依赖是标准的 `def` 函数而不是 `async def`,它将被运行在外部线程池中。 +这同样适用于[依赖](./tutorial/dependencies/index.md){.internal-link target=_blank}。如果一个依赖是标准的 `def` 函数而不是 `async def`,它将被运行在外部线程池中。 ### 子依赖 -你可以拥有多个相互依赖的依赖以及[子依赖](/tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作为函数的参数),它们中的一些可能是通过 `async def` 声明,也可能是通过 `def` 声明。它们仍然可以正常工作,这些通过 `def` 声明的函数将会在外部线程中调用(来自线程池),而不是"被等待"。 +你可以拥有多个相互依赖的依赖以及[子依赖](./tutorial/dependencies/sub-dependencies.md){.internal-link target=_blank} (作为函数的参数),它们中的一些可能是通过 `async def` 声明,也可能是通过 `def` 声明。它们仍然可以正常工作,这些通过 `def` 声明的函数将会在外部线程中调用(来自线程池),而不是"被等待"。 ### 其他函数 From 0066578bbedf9c41349f61b660ca47fc37990484 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 30 Oct 2023 07:42:04 +0000 Subject: [PATCH 1425/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ea2284cd5..38462ce6d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix typo in `docs/em/docs/index.md`, Python 3.8. PR [#10521](https://github.com/tiangolo/fastapi/pull/10521) by [@kerriop](https://github.com/kerriop). * ⬆ Bump pillow from 9.5.0 to 10.1.0. PR [#10446](https://github.com/tiangolo/fastapi/pull/10446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update mkdocs-material requirement from <9.0.0,>=8.1.4 to >=8.1.4,<10.0.0. PR [#5862](https://github.com/tiangolo/fastapi/pull/5862) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump mkdocs-material from 9.1.21 to 9.4.7. PR [#10545](https://github.com/tiangolo/fastapi/pull/10545) by [@dependabot[bot]](https://github.com/apps/dependabot). From 7702c5af361606fad8d031ab3e10c9b30ac2b781 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 30 Oct 2023 07:51:12 +0000 Subject: [PATCH 1426/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 38462ce6d..2b1f8dc38 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix typo in `docs/em/docs/index.md`, Python 3.8. PR [#10521](https://github.com/tiangolo/fastapi/pull/10521) by [@kerriop](https://github.com/kerriop). * ⬆ Bump pillow from 9.5.0 to 10.1.0. PR [#10446](https://github.com/tiangolo/fastapi/pull/10446) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Update mkdocs-material requirement from <9.0.0,>=8.1.4 to >=8.1.4,<10.0.0. PR [#5862](https://github.com/tiangolo/fastapi/pull/5862) by [@dependabot[bot]](https://github.com/apps/dependabot). From e7204ac7bf01ecc39cc37ef28fda3406a24a2ed8 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 30 Oct 2023 07:52:04 +0000 Subject: [PATCH 1427/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2b1f8dc38..a079bfd18 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Update links in `docs/en/docs/async.md` and `docs/zh/docs/async.md` to make them relative. PR [#10498](https://github.com/tiangolo/fastapi/pull/10498) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix typo in `docs/em/docs/index.md`, Python 3.8. PR [#10521](https://github.com/tiangolo/fastapi/pull/10521) by [@kerriop](https://github.com/kerriop). * ⬆ Bump pillow from 9.5.0 to 10.1.0. PR [#10446](https://github.com/tiangolo/fastapi/pull/10446) by [@dependabot[bot]](https://github.com/apps/dependabot). From 759378d67ff73da99a3f096aff42fc50ac900e61 Mon Sep 17 00:00:00 2001 From: Koke <31826970+White-Mask@users.noreply.github.com> Date: Mon, 30 Oct 2023 05:00:16 -0300 Subject: [PATCH 1428/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20Pydanti?= =?UTF-8?q?c=20links=20to=20dotenv=20support=20(#10511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/advanced/settings.md | 2 +- docs/en/docs/advanced/settings.md | 2 +- docs/zh/docs/advanced/settings.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/em/docs/advanced/settings.md b/docs/em/docs/advanced/settings.md index cc7a08bab..2ebe8ffcb 100644 --- a/docs/em/docs/advanced/settings.md +++ b/docs/em/docs/advanced/settings.md @@ -254,7 +254,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app ✋️ 🇨🇻 📁 🚫 🤙 ✔️ ✔️ 👈 ☑ 📁. -Pydantic ✔️ 🐕‍🦺 👂 ⚪️➡️ 👉 🆎 📁 ⚙️ 🔢 🗃. 👆 💪 ✍ 🌖 <a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic ⚒: 🇨🇻 (.🇨🇻) 🐕‍🦺</a>. +Pydantic ✔️ 🐕‍🦺 👂 ⚪️➡️ 👉 🆎 📁 ⚙️ 🔢 🗃. 👆 💪 ✍ 🌖 <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic ⚒: 🇨🇻 (.🇨🇻) 🐕‍🦺</a>. !!! tip 👉 👷, 👆 💪 `pip install python-dotenv`. diff --git a/docs/en/docs/advanced/settings.md b/docs/en/docs/advanced/settings.md index 32df90006..f6db8d2b1 100644 --- a/docs/en/docs/advanced/settings.md +++ b/docs/en/docs/advanced/settings.md @@ -326,7 +326,7 @@ This practice is common enough that it has a name, these environment variables a But a dotenv file doesn't really have to have that exact filename. -Pydantic has support for reading from these types of files using an external library. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>. +Pydantic has support for reading from these types of files using an external library. You can read more at <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic Settings: Dotenv (.env) support</a>. !!! tip For this to work, you need to `pip install python-dotenv`. diff --git a/docs/zh/docs/advanced/settings.md b/docs/zh/docs/advanced/settings.md index 93e48a610..76070fb7f 100644 --- a/docs/zh/docs/advanced/settings.md +++ b/docs/zh/docs/advanced/settings.md @@ -289,7 +289,7 @@ $ ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp"uvicorn main:app 但是,dotenv 文件实际上不一定要具有确切的文件名。 -Pydantic 支持使用外部库从这些类型的文件中读取。您可以在<a href="https://pydantic-docs.helpmanual.io/usage/settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic 设置: Dotenv (.env) 支持</a>中阅读更多相关信息。 +Pydantic 支持使用外部库从这些类型的文件中读取。您可以在<a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support" class="external-link" target="_blank">Pydantic 设置: Dotenv (.env) 支持</a>中阅读更多相关信息。 !!! tip 要使其工作,您需要执行 `pip install python-dotenv`。 From e4b21c6eab7cd58caf3c6c492ea1ce7945425dd1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 30 Oct 2023 08:08:48 +0000 Subject: [PATCH 1429/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a079bfd18..57fb5ac2f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* ✏️ Update Pydantic links to dotenv support. PR [#10511](https://github.com/tiangolo/fastapi/pull/10511) by [@White-Mask](https://github.com/White-Mask). * ✏️ Update links in `docs/en/docs/async.md` and `docs/zh/docs/async.md` to make them relative. PR [#10498](https://github.com/tiangolo/fastapi/pull/10498) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix typo in `docs/em/docs/index.md`, Python 3.8. PR [#10521](https://github.com/tiangolo/fastapi/pull/10521) by [@kerriop](https://github.com/kerriop). From 758a8f29e1c5b11a44499d23f003d61febb6e617 Mon Sep 17 00:00:00 2001 From: Alejandra Klachquin <aklachquin@gmail.com> Date: Mon, 30 Oct 2023 06:58:58 -0300 Subject: [PATCH 1430/1881] =?UTF-8?q?=F0=9F=93=8C=20Pin=20Swagger=20UI=20v?= =?UTF-8?q?ersion=20to=205.9.0=20temporarily=20to=20handle=20a=20bug=20cra?= =?UTF-8?q?shing=20it=20in=205.9.1=20(#10529)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/en/docs/how-to/custom-docs-ui-assets.md | 4 ++-- docs_src/custom_docs_ui/tutorial001.py | 4 ++-- fastapi/openapi/docs.py | 4 ++-- tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py | 6 ++++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/en/docs/how-to/custom-docs-ui-assets.md b/docs/en/docs/how-to/custom-docs-ui-assets.md index f26324869..9726be2c7 100644 --- a/docs/en/docs/how-to/custom-docs-ui-assets.md +++ b/docs/en/docs/how-to/custom-docs-ui-assets.md @@ -96,8 +96,8 @@ You can probably right-click each link and select an option similar to `Save lin **Swagger UI** uses the files: -* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a> -* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a> +* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js" class="external-link" target="_blank">`swagger-ui-bundle.js`</a> +* <a href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css" class="external-link" target="_blank">`swagger-ui.css`</a> And **ReDoc** uses the file: diff --git a/docs_src/custom_docs_ui/tutorial001.py b/docs_src/custom_docs_ui/tutorial001.py index f7ceb0c2f..4384433e3 100644 --- a/docs_src/custom_docs_ui/tutorial001.py +++ b/docs_src/custom_docs_ui/tutorial001.py @@ -14,8 +14,8 @@ async def custom_swagger_ui_html(): openapi_url=app.openapi_url, title=app.title + " - Swagger UI", oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, - swagger_js_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js", - swagger_css_url="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css", + swagger_js_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js", + swagger_css_url="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css", ) diff --git a/fastapi/openapi/docs.py b/fastapi/openapi/docs.py index 8cf0d17a1..69473d19c 100644 --- a/fastapi/openapi/docs.py +++ b/fastapi/openapi/docs.py @@ -53,7 +53,7 @@ def get_swagger_ui_html( It is normally set to a CDN URL. """ ), - ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js", + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui-bundle.js", swagger_css_url: Annotated[ str, Doc( @@ -63,7 +63,7 @@ def get_swagger_ui_html( It is normally set to a CDN URL. """ ), - ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css", + ] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.9.0/swagger-ui.css", swagger_favicon_url: Annotated[ str, Doc( diff --git a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py index aff070d74..34a18b12c 100644 --- a/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py +++ b/tests/test_tutorial/test_custom_docs_ui/test_tutorial001.py @@ -20,8 +20,10 @@ def client(): def test_swagger_ui_html(client: TestClient): response = client.get("/docs") assert response.status_code == 200, response.text - assert "https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js" in response.text - assert "https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" in response.text + assert ( + "https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js" in response.text + ) + assert "https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" in response.text def test_swagger_ui_oauth2_redirect_html(client: TestClient): From 0f1ddf5f69187f7f7666ffc7b58a0010d21e3289 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 30 Oct 2023 09:59:37 +0000 Subject: [PATCH 1431/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 57fb5ac2f..5565d362a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 📌 Pin Swagger UI version to 5.9.0 temporarily to handle a bug crashing it in 5.9.1. PR [#10529](https://github.com/tiangolo/fastapi/pull/10529) by [@alejandraklachquin](https://github.com/alejandraklachquin). * ✏️ Update Pydantic links to dotenv support. PR [#10511](https://github.com/tiangolo/fastapi/pull/10511) by [@White-Mask](https://github.com/White-Mask). * ✏️ Update links in `docs/en/docs/async.md` and `docs/zh/docs/async.md` to make them relative. PR [#10498](https://github.com/tiangolo/fastapi/pull/10498) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). From 6c53ddd084185c40f9ff960747ba1c9b5e2ce094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 30 Oct 2023 14:04:14 +0400 Subject: [PATCH 1432/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5565d362a..a52973f6f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,7 +7,25 @@ hide: ## Latest Changes +### Fixes + * 📌 Pin Swagger UI version to 5.9.0 temporarily to handle a bug crashing it in 5.9.1. PR [#10529](https://github.com/tiangolo/fastapi/pull/10529) by [@alejandraklachquin](https://github.com/alejandraklachquin). + * This is not really a bug in FastAPI but in Swagger UI, nevertheless pinning the version will work while a soulution is found on the [Swagger UI side](https://github.com/swagger-api/swagger-ui/issues/9337). + +### Docs + +* 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). +* ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). +* 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). +* ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). +* ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). +* ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). +* 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). +* ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). +* 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). + +### Internal + * ✏️ Update Pydantic links to dotenv support. PR [#10511](https://github.com/tiangolo/fastapi/pull/10511) by [@White-Mask](https://github.com/White-Mask). * ✏️ Update links in `docs/en/docs/async.md` and `docs/zh/docs/async.md` to make them relative. PR [#10498](https://github.com/tiangolo/fastapi/pull/10498) by [@hasnatsajid](https://github.com/hasnatsajid). * ✏️ Fix links in `docs/em/docs/async.md`. PR [#10507](https://github.com/tiangolo/fastapi/pull/10507) by [@hasnatsajid](https://github.com/hasnatsajid). @@ -19,17 +37,9 @@ hide: * 🔧 Update sponsors badges, Databento. PR [#10519](https://github.com/tiangolo/fastapi/pull/10519) by [@tiangolo](https://github.com/tiangolo). * 👷 Adopt Ruff format. PR [#10517](https://github.com/tiangolo/fastapi/pull/10517) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add `CITATION.cff` file for academic citations. PR [#10496](https://github.com/tiangolo/fastapi/pull/10496) by [@tiangolo](https://github.com/tiangolo). -* 📝 Update data structure and render for external-links. PR [#10495](https://github.com/tiangolo/fastapi/pull/10495) by [@tiangolo](https://github.com/tiangolo). * 🐛 Fix overriding MKDocs theme lang in hook. PR [#10490](https://github.com/tiangolo/fastapi/pull/10490) by [@tiangolo](https://github.com/tiangolo). -* ✏️ Fix link to SPDX license identifier in `docs/en/docs/tutorial/metadata.md`. PR [#10433](https://github.com/tiangolo/fastapi/pull/10433) by [@worldworm](https://github.com/worldworm). -* 📝 Update example validation error from Pydantic v1 to match Pydantic v2 in `docs/en/docs/tutorial/path-params.md`. PR [#10043](https://github.com/tiangolo/fastapi/pull/10043) by [@giuliowaitforitdavide](https://github.com/giuliowaitforitdavide). -* ✏️ Fix typos in emoji docs and in some source examples. PR [#10438](https://github.com/tiangolo/fastapi/pull/10438) by [@afuetterer](https://github.com/afuetterer). -* ✏️ Fix typo in `docs/en/docs/reference/dependencies.md`. PR [#10465](https://github.com/tiangolo/fastapi/pull/10465) by [@suravshresth](https://github.com/suravshresth). -* ✏️ Fix typos and rewordings in `docs/en/docs/tutorial/body-nested-models.md`. PR [#10468](https://github.com/tiangolo/fastapi/pull/10468) by [@yogabonito](https://github.com/yogabonito). -* 📝 Update docs, remove references to removed `pydantic.Required` in `docs/en/docs/tutorial/query-params-str-validations.md`. PR [#10469](https://github.com/tiangolo/fastapi/pull/10469) by [@yogabonito](https://github.com/yogabonito). -* ✏️ Fix typo in `docs/en/docs/reference/index.md`. PR [#10467](https://github.com/tiangolo/fastapi/pull/10467) by [@tarsil](https://github.com/tarsil). * 🔥 Drop/close Gitter chat. Questions should go to GitHub Discussions, free conversations to Discord.. PR [#10485](https://github.com/tiangolo/fastapi/pull/10485) by [@tiangolo](https://github.com/tiangolo). -* 🔥 Remove unnecessary duplicated docstrings. PR [#10484](https://github.com/tiangolo/fastapi/pull/10484) by [@tiangolo](https://github.com/tiangolo). + ## 0.104.0 ## Features From 7e5afe2cb9bf1fa30c04a4dd393ea397a46db29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 30 Oct 2023 14:04:54 +0400 Subject: [PATCH 1433/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?104.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 a52973f6f..3f05787fd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,9 @@ hide: ## Latest Changes + +## 0.104.1 + ### Fixes * 📌 Pin Swagger UI version to 5.9.0 temporarily to handle a bug crashing it in 5.9.1. PR [#10529](https://github.com/tiangolo/fastapi/pull/10529) by [@alejandraklachquin](https://github.com/alejandraklachquin). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 4fdb155c2..c81f09b27 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.104.0" +__version__ = "0.104.1" from starlette import status as status From 1c25e2d8dc99331290e99e17814ec4d83adce725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 30 Oct 2023 15:12:57 +0400 Subject: [PATCH 1434/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3f05787fd..fee5b1080 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,7 +13,7 @@ hide: ### Fixes * 📌 Pin Swagger UI version to 5.9.0 temporarily to handle a bug crashing it in 5.9.1. PR [#10529](https://github.com/tiangolo/fastapi/pull/10529) by [@alejandraklachquin](https://github.com/alejandraklachquin). - * This is not really a bug in FastAPI but in Swagger UI, nevertheless pinning the version will work while a soulution is found on the [Swagger UI side](https://github.com/swagger-api/swagger-ui/issues/9337). + * This is not really a bug in FastAPI but in Swagger UI, nevertheless pinning the version will work while a solution is found on the [Swagger UI side](https://github.com/swagger-api/swagger-ui/issues/9337). ### Docs From 4f89886b0030f7f0bb1bdb363b7c50ad96445a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 4 Nov 2023 05:52:42 +0400 Subject: [PATCH 1435/1881] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20latest-chang?= =?UTF-8?q?es=20GitHub=20Action=20(#10587)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index ffec5ee5e..b9b550d5e 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -34,9 +34,12 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.0.3 + - uses: docker://tiangolo/latest-changes:0.2.0 + # - uses: tiangolo/latest-changes@main with: token: ${{ secrets.GITHUB_TOKEN }} latest_changes_file: docs/en/docs/release-notes.md - latest_changes_header: '## Latest Changes\n\n' + latest_changes_header: '## Latest Changes' + end_regex: '^## ' debug_logs: true + label_header_prefix: '### ' From 46335068d2130839ca64a0484dd5b0ff2eeda818 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 4 Nov 2023 01:53:13 +0000 Subject: [PATCH 1436/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 fee5b1080..b0c13f5af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,7 @@ hide: ## Latest Changes +* 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). ## 0.104.1 From b04d07c9339b3ac3b1cf72724231982c318c2907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 4 Nov 2023 06:02:18 +0400 Subject: [PATCH 1437/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es,=20move=20and=20check=20latest-changes=20(#10588)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b0c13f5af..186d2117c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +### Internal + * 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). ## 0.104.1 From 480620372a662aa9025c47410fbc90a255b2fc94 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 4 Nov 2023 02:03:01 +0000 Subject: [PATCH 1438/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 186d2117c..b62656982 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 📝 Update release notes, move and check latest-changes. PR [#10588](https://github.com/tiangolo/fastapi/pull/10588) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). ## 0.104.1 From 781984b22651d04a33a260d981d59da53fd950f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 18 Nov 2023 14:38:01 +0100 Subject: [PATCH 1439/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Reflex=20(#10676)?= 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 + docs/en/docs/img/sponsors/reflex-banner.png | Bin 0 -> 8143 bytes docs/en/docs/img/sponsors/reflex.png | Bin 0 -> 11293 bytes docs/en/overrides/main.html | 6 ++++++ 6 files changed, 11 insertions(+) create mode 100644 docs/en/docs/img/sponsors/reflex-banner.png create mode 100644 docs/en/docs/img/sponsors/reflex.png diff --git a/README.md b/README.md index aeb29b587..06c0c4452 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ The key features are: <a href="https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Fern | SDKs and API docs"><img src="https://fastapi.tiangolo.com/img/sponsors/fern.svg"></a> <a href="https://www.porter.run" target="_blank" title="Deploy FastAPI on AWS with a few clicks"><img src="https://fastapi.tiangolo.com/img/sponsors/porter.png"></a> <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> +<a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> <a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a> <a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index dac47d2f0..f2e07cbd8 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -14,6 +14,9 @@ gold: - url: https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor title: Automate FastAPI documentation generation with Bump.sh img: https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg + - url: https://reflex.dev + title: Reflex + img: https://fastapi.tiangolo.com/img/sponsors/reflex.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index acbcc2205..43b69bf00 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -21,3 +21,4 @@ logins: - fern-api - ndimares - svixhq + - Alek99 diff --git a/docs/en/docs/img/sponsors/reflex-banner.png b/docs/en/docs/img/sponsors/reflex-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..3095c3a7b40906a8f6476c97370d03f95470e461 GIT binary patch literal 8143 zcmV;=A28sFP)<h;3K|Lk000e1NJLTq00C$K001Zm1^@s6)5>5C00009a7bBm000XU z000XU0RWnu7ytkPKxsomP;*c-LjVAHoMT{Me2|lzTg;-sz>u3)QWWIwq!1AqrNI7( zftf*sfs=ucK{heDxWG5qErbCGiWAF=lS_(n7*N2sYYdDa`m%0DVqS_|`yBVC^Ro|% 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/reflex.png b/docs/en/docs/img/sponsors/reflex.png new file mode 100644 index 0000000000000000000000000000000000000000..59c46a1104140e8a1778a0819b1d3743cee0176f GIT binary patch literal 11293 zcmV+&EaKCNP)<h;3K|Lk000e1NJLTq008g+003kN1^@s6xT3ws00009a7bBm000XU z000XU0RWnu7ytkPKxsomP;*c-LjVAHoMT{Me2|lzTg;-sz>u3)QWWIwq!1AqrNI7( zftf*sfs=ucK{heDxWG5qErbCGiWAF=lS_(n7*N2sYYdDa`m%0DVqS_|`yBVC^Ro|% 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 T*-t(000000NkvXXu0mjf_P{u$ literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 4c7f19fd4..ed08028ec 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -52,6 +52,12 @@ <img class="sponsor-image" src="/img/sponsors/bump-sh-banner.svg" /> </a> </div> + <div class="item"> + <a title="Reflex" style="display: block; position: relative;" href="https://reflex.dev" target="_blank"> + <span class="sponsor-badge">sponsor</span> + <img class="sponsor-image" src="/img/sponsors/reflex-banner.png" /> + </a> + </div> </div> </div> {% endblock %} From 81bab7761770264e5f6901d0a50fe5d79a5bffa1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 18 Nov 2023 13:38:23 +0000 Subject: [PATCH 1440/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b62656982..4b6a1967b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔧 Update sponsors, add Reflex. PR [#10676](https://github.com/tiangolo/fastapi/pull/10676) by [@tiangolo](https://github.com/tiangolo). * 📝 Update release notes, move and check latest-changes. PR [#10588](https://github.com/tiangolo/fastapi/pull/10588) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). From 71d51a99531da8489772bc295d88a0a9a452d45d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 18 Nov 2023 14:47:04 +0100 Subject: [PATCH 1441/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Codacy=20(#10677)?= 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 + docs/en/docs/img/sponsors/codacy.png | Bin 0 -> 30480 bytes 4 files changed, 5 insertions(+) create mode 100644 docs/en/docs/img/sponsors/codacy.png diff --git a/README.md b/README.md index 06c0c4452..655038c6f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ The key features are: <a href="https://databento.com/" target="_blank" title="Pay as you go for market data"><img src="https://fastapi.tiangolo.com/img/sponsors/databento.svg"></a> <a href="https://speakeasyapi.dev?utm_source=fastapi+repo&utm_medium=github+sponsorship" target="_blank" title="SDKs for your API | Speakeasy"><img src="https://fastapi.tiangolo.com/img/sponsors/speakeasy.png"></a> <a href="https://www.svix.com/" target="_blank" title="Svix - Webhooks as a service"><img src="https://fastapi.tiangolo.com/img/sponsors/svix.svg"></a> +<a href="https://www.codacy.com/?utm_source=github&utm_medium=sponsors&utm_id=pioneers" target="_blank" title="Take code reviews from hours to minutes"><img src="https://fastapi.tiangolo.com/img/sponsors/codacy.png"></a> <!-- /sponsors --> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index f2e07cbd8..4f98c01a0 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -42,6 +42,9 @@ silver: - url: https://www.svix.com/ title: Svix - Webhooks as a service img: https://fastapi.tiangolo.com/img/sponsors/svix.svg + - url: https://www.codacy.com/?utm_source=github&utm_medium=sponsors&utm_id=pioneers + title: Take code reviews from hours to minutes + img: https://fastapi.tiangolo.com/img/sponsors/codacy.png bronze: - url: https://www.exoflare.com/open-source/?utm_source=FastAPI&utm_campaign=open_source title: Biosecurity risk assessments made easy. diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 43b69bf00..4078454a8 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -22,3 +22,4 @@ logins: - ndimares - svixhq - Alek99 + - codacy diff --git a/docs/en/docs/img/sponsors/codacy.png b/docs/en/docs/img/sponsors/codacy.png new file mode 100644 index 0000000000000000000000000000000000000000..baa615c2a836413df2aec00ff582162c759a4aab GIT binary patch literal 30480 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 Z{9hD0a-~Z>ZF>L!002ovPDHLkV1i{;0#5({ literal 0 HcmV?d00001 From ac93277d3b506f4076aee1af6ae5a86406f545c6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 18 Nov 2023 13:47:35 +0000 Subject: [PATCH 1442/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4b6a1967b..34bbab135 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Internal +* 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Reflex. PR [#10676](https://github.com/tiangolo/fastapi/pull/10676) by [@tiangolo](https://github.com/tiangolo). * 📝 Update release notes, move and check latest-changes. PR [#10588](https://github.com/tiangolo/fastapi/pull/10588) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade latest-changes GitHub Action. PR [#10587](https://github.com/tiangolo/fastapi/pull/10587) by [@tiangolo](https://github.com/tiangolo). From 1560879a8417f71d038e25532577f78b55a69b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 28 Nov 2023 11:52:35 +0100 Subject: [PATCH 1443/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20Scalar=20(#10728)?= 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/docs/img/sponsors/scalar-banner.svg | 1 + docs/en/docs/img/sponsors/scalar.svg | 1 + docs/en/overrides/main.html | 6 ++++++ 5 files changed, 12 insertions(+) create mode 100644 docs/en/docs/img/sponsors/scalar-banner.svg create mode 100644 docs/en/docs/img/sponsors/scalar.svg diff --git a/README.md b/README.md index 655038c6f..b53076dcb 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The key features are: <a href="https://www.porter.run" target="_blank" title="Deploy FastAPI on AWS with a few clicks"><img src="https://fastapi.tiangolo.com/img/sponsors/porter.png"></a> <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> +<a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a> <a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 4f98c01a0..1a253f649 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -17,6 +17,9 @@ gold: - url: https://reflex.dev title: Reflex img: https://fastapi.tiangolo.com/img/sponsors/reflex.png + - url: https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge + title: "Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" + img: https://fastapi.tiangolo.com/img/sponsors/scalar.svg silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/scalar-banner.svg b/docs/en/docs/img/sponsors/scalar-banner.svg new file mode 100644 index 000000000..bab74e2d7 --- /dev/null +++ b/docs/en/docs/img/sponsors/scalar-banner.svg @@ -0,0 +1 @@ +<svg width="300px" height="40px" viewBox="0 0 300 40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><radialGradient cx="67.9042312%" cy="42.9553939%" fx="67.9042312%" fy="42.9553939%" r="59.2625266%" gradientTransform="matrix(.708 .502 -.411 .865 .37 -.28)" id="g"><stop stop-color="#FFF" offset="0%"/><stop stop-color="#AFAFAF" offset="100%"/></radialGradient><polygon id="c" points="8.40529079e-13 0.554129891 253.25984 8.42600936e-17 253.25984 21.7040109 228.693879 34.3251885 8.40529079e-13 38.5541299"/><rect id="a" x="0" y="0" width="300" height="40" rx="20"/><path d="M13.95 0a15.65 15.65 0 0 1 7.36 1.7l11.16.07.15-.07C34.82.6 37.23.05 39.84.05c1.92 0 3.7.27 5.37.8.84.28 1.63.62 2.38 1.02l10.75.07.5-1.51h13.78l.54 1.61 5.82.04V.48h12.91v10.25l10.68.28 2.8-8.47.67-2.06h13.79l.43 1.32 5.92.08V.53h14c2.31 0 4.41.44 6.26 1.33 1.93.94 3.47 2.32 4.56 4.11a11.74 11.74 0 0 1 1.62 6.21v.14l8.47.22c.32-1.77.88-3.39 1.7-4.84a14 14 0 0 1 5.62-5.58 15.9 15.9 0 0 1 7.63-1.86c1.92 0 3.7.27 5.37.8.59.2 1.15.42 1.7.68l11.6.03c2.14-1 4.49-1.5 7.02-1.5 2.54 0 4.91.5 7.07 1.54l7.18.02V.69h13.6l.5 1.18 7.12.02.5-1.2h13.6v31.45h-12.75v-2.29l-.07.19-.75 1.9h-8.18l-.74-1.91-.07-.18v2.3h-12.76v-1.23l-7.25-.02c-2.14 1.01-4.48 1.51-7 1.51-2.55 0-4.91-.51-7.06-1.54l-11.5-.02a15.95 15.95 0 0 1-7.12 1.57 16.1 16.1 0 0 1-7.2-1.6l-5.47-.02a6.27 6.27 0 0 1-4.32 1.69 6.36 6.36 0 0 1-3.88-1.28l-.16-.13.5.9h-14.37l-.65-1.23h-1.4v1.24h-12.92v-.06h-9.8L116 30.7l-6.13-.02-.37 1.25H78.98v-.05h-9.8l-.4-1.29h-6.1l-.39 1.3H48.46l.44-1.34h-1.76l-.82.4-.44.18c-1.79.72-3.8 1.07-6.04 1.07-2.68 0-5.15-.56-7.37-1.68l-10.73-.03c-.36.18-.72.35-1.1.5A18.05 18.05 0 0 1 14 32.1c-2.57 0-4.88-.39-6.9-1.2a11.02 11.02 0 0 1-5.21-4.15A12.28 12.28 0 0 1 0 19.87L0 16.85h2.48l-.13-.16a9.08 9.08 0 0 1-1.69-5.24v-.33A10 10 0 0 1 2.52 5.1 11.6 11.6 0 0 1 7.4 1.27 16.21 16.21 0 0 1 13.95 0Zm145.12 22.58a3.4 3.4 0 0 0-2.47 1 3.23 3.23 0 0 0-1.01 2.43c-.01.97.32 1.79 1 2.47.7.67 1.51 1 2.48 1 .6 0 1.18-.15 1.7-.46.52-.3.94-.73 1.27-1.25a3.28 3.28 0 0 0-.56-4.18c-.7-.68-1.5-1.01-2.41-1.01Z" id="e"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><g transform="translate(23 1)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M160.82 32.78a4.9 4.9 0 0 1-3.57-1.46 4.77 4.77 0 0 1-1.47-3.57 4.68 4.68 0 0 1 1.47-3.51 4.91 4.91 0 0 1 3.57-1.46 4.9 4.9 0 0 1 3.5 1.46 4.75 4.75 0 0 1 .8 6.05 5.4 5.4 0 0 1-1.84 1.82c-.75.45-1.57.67-2.46.67Z" fill="#2C328B" fill-rule="nonzero" mask="url(#d)"/><g mask="url(#d)"><g transform="rotate(-1.5 243.78 -22.04)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><use fill="#79C5BE" fill-rule="nonzero" xlink:href="#e"/><path d="M17.38 11.3a2.7 2.7 0 0 0-.93-1.94c-.55-.46-1.38-.7-2.5-.7-.71 0-1.3.1-1.75.26-.45.17-.78.4-1 .7-.21.28-.32.61-.33.99-.02.3.04.58.16.82.13.25.34.47.62.66.28.2.64.37 1.08.53.44.16.96.3 1.57.42l2.09.45c1.4.3 2.61.7 3.61 1.18 1 .49 1.83 1.06 2.46 1.72a6.3 6.3 0 0 1 1.41 2.22c.3.82.46 1.72.47 2.69 0 1.67-.43 3.09-1.26 4.25a7.67 7.67 0 0 1-3.54 2.64c-1.54.6-3.39.91-5.54.91a15.6 15.6 0 0 1-5.79-.98 8.02 8.02 0 0 1-3.82-3.03A9.34 9.34 0 0 1 3 19.85h6.56c.05.78.24 1.44.59 1.97s.84.93 1.47 1.2a5.7 5.7 0 0 0 2.27.41c.74 0 1.35-.09 1.85-.27.5-.18.88-.44 1.13-.76.26-.32.4-.69.4-1.1 0-.4-.13-.74-.38-1.03-.24-.3-.64-.56-1.2-.8-.55-.23-1.3-.45-2.25-.66l-2.53-.55a11.6 11.6 0 0 1-5.33-2.45 5.92 5.92 0 0 1-1.93-4.7 7 7 0 0 1 1.3-4.25A8.62 8.62 0 0 1 8.6 4.02 13.24 13.24 0 0 1 13.95 3c2.05 0 3.83.34 5.33 1.03a8.03 8.03 0 0 1 3.48 2.9 7.87 7.87 0 0 1 1.23 4.37h-6.61Zm34.05 1.65h-7.01a4.85 4.85 0 0 0-.4-1.58 3.75 3.75 0 0 0-2.2-2.02 5.26 5.26 0 0 0-1.78-.28c-1.16 0-2.14.28-2.94.84-.8.57-1.4 1.37-1.81 2.43a10.44 10.44 0 0 0-.62 3.79c0 1.56.21 2.86.63 3.9a4.9 4.9 0 0 0 1.82 2.37c.8.52 1.75.79 2.87.79.64 0 1.21-.08 1.72-.25a3.72 3.72 0 0 0 2.23-1.81c.23-.43.4-.93.48-1.47l7 .05a9.44 9.44 0 0 1-.9 3.28 10.65 10.65 0 0 1-5.77 5.36c-1.4.57-3.04.85-4.9.85-2.34 0-4.44-.5-6.29-1.5-1.85-1-3.31-2.48-4.39-4.43a14.63 14.63 0 0 1-1.6-7.14c0-2.84.54-5.22 1.63-7.17a11 11 0 0 1 4.43-4.41c1.85-1 3.93-1.5 6.21-1.5 1.61 0 3.09.22 4.44.66 1.35.44 2.54 1.08 3.55 1.92a9.58 9.58 0 0 1 2.47 3.1c.62 1.23 1 2.63 1.13 4.22Zm30.55 15.98V3.48h6.91v19.89h10.3v5.56H81.97Zm-21.92-.05h-7.45L61 3.43h9.44l8.4 25.45H71.4l-5.57-18.44h-.2l-5.57 18.44Zm-1.39-10.04H72.7V24H58.67v-5.17Zm48.6 10.1H99.8l8.4-25.46h9.45l8.4 25.45h-7.46l-5.57-18.44h-.2l-5.56 18.44Zm-1.4-10.05h14.02v5.17h-14.02V18.9ZM129.18 29V3.53h11c1.88 0 3.53.35 4.95 1.03a7.72 7.72 0 0 1 3.3 2.97 8.77 8.77 0 0 1 1.19 4.65c0 1.82-.4 3.36-1.22 4.61a7.53 7.53 0 0 1-3.39 2.84c-1.45.63-3.15.95-5.09.95h-6.56v-5.37h5.17c.81 0 1.5-.1 2.08-.3.58-.2 1.02-.53 1.33-.98.31-.44.47-1.03.47-1.75 0-.73-.16-1.32-.47-1.78-.3-.46-.75-.8-1.33-1.02a5.85 5.85 0 0 0-2.08-.33h-2.44v19.94h-6.9ZM144.1 17.3 150.46 29h-7.5l-6.22-11.69h7.36Zm43.53-4.14h-7.01a4.85 4.85 0 0 0-.4-1.58 3.75 3.75 0 0 0-2.2-2.02 5.26 5.26 0 0 0-1.78-.28c-1.16 0-2.14.28-2.94.84-.8.57-1.4 1.37-1.81 2.43a10.44 10.44 0 0 0-.62 3.79c0 1.55.21 2.86.63 3.9a4.9 4.9 0 0 0 1.82 2.37c.8.52 1.75.79 2.87.79.64 0 1.21-.08 1.72-.25a3.73 3.73 0 0 0 2.23-1.81c.24-.44.4-.93.48-1.47l7 .05a9.44 9.44 0 0 1-.9 3.28 10.65 10.65 0 0 1-5.77 5.35c-1.4.58-3.04.86-4.9.86-2.34 0-4.44-.5-6.29-1.5-1.85-1-3.31-2.48-4.39-4.43a14.63 14.63 0 0 1-1.6-7.14c0-2.84.54-5.23 1.63-7.17a11 11 0 0 1 4.43-4.41c1.86-1 3.93-1.5 6.21-1.5 1.61 0 3.09.22 4.44.66 1.35.44 2.54 1.08 3.56 1.92a9.58 9.58 0 0 1 2.46 3.1c.62 1.23 1 2.63 1.13 4.22Zm26.48 3.18c0 2.83-.55 5.22-1.66 7.16a11.04 11.04 0 0 1-4.46 4.41 13.1 13.1 0 0 1-6.26 1.5c-2.32 0-4.42-.5-6.28-1.5a11.1 11.1 0 0 1-4.45-4.43 14.32 14.32 0 0 1-1.65-7.14c0-2.84.55-5.23 1.65-7.17a11.01 11.01 0 0 1 4.45-4.41c1.86-1 3.96-1.5 6.28-1.5 2.3 0 4.39.5 6.26 1.5 1.88 1 3.36 2.47 4.46 4.41a14.33 14.33 0 0 1 1.66 7.17Zm-7.11 0c0-1.53-.2-2.81-.6-3.86-.4-1.05-1-1.85-1.78-2.39a4.95 4.95 0 0 0-2.9-.81c-1.14 0-2.1.27-2.88.81a4.96 4.96 0 0 0-1.78 2.39c-.4 1.05-.6 2.33-.6 3.86 0 1.52.2 2.8.6 3.86.4 1.04 1 1.84 1.78 2.38.78.55 1.74.82 2.89.82 1.14 0 2.1-.27 2.89-.82a4.96 4.96 0 0 0 1.78-2.38c.4-1.05.6-2.34.6-3.86Zm11.98-12.65h8.6l5.91 14.42h.3l5.92-14.42h8.6v25.45h-6.76V14.43h-.2l-5.67 14.51h-4.08l-5.66-14.61h-.2v14.81h-6.76V3.7Z" fill="#2C328B" fill-rule="nonzero" mask="url(#f)"/></g></g><path d="M252.59 19.06v2.64l-7.45 4.77-13.48 6.37-4.56.11c8.86-12.46 14.5-19.3 16.94-20.54 2.43-1.24 5.28.98 8.55 6.65Z" fill="url(#g)" mask="url(#d)"/></g></g></g></svg> diff --git a/docs/en/docs/img/sponsors/scalar.svg b/docs/en/docs/img/sponsors/scalar.svg new file mode 100644 index 000000000..174c57ee2 --- /dev/null +++ b/docs/en/docs/img/sponsors/scalar.svg @@ -0,0 +1 @@ +<svg width="240px" height="99px" viewBox="0 0 240 99" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" x="0" y="0" width="240" height="99" rx="6"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#0078D7" xlink:href="#a"/><rect fill="#D2D2C6" mask="url(#b)" width="74" height="100"/><image mask="url(#b)" x="0.0797953321" y="-0.423984273" width="73.7373737" height="100" xlink:href="data:image/png;base64,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"/><g mask="url(#b)" fill="#FFF" fill-rule="nonzero"><g transform="translate(83.6 10.6)"><path d="M4.2 11.4H.5V.5h3.7c1.1 0 2 .2 2.8.7.8.4 1.4 1 1.8 1.9.5.8.7 1.7.7 2.9 0 1.1-.3 2-.7 2.9-.4.8-1 1.4-1.8 1.9-.8.4-1.7.6-2.8.6ZM2.4 9.7h1.7A4 4 0 0 0 6 9.3c.5-.3.9-.7 1.1-1.2A5 5 0 0 0 7.5 6a5 5 0 0 0-.4-2.1c-.2-.6-.6-1-1.1-1.2-.5-.3-1.1-.5-1.9-.5H2.4v7.5ZM14.3 11.6a4 4 0 0 1-2.1-.5c-.6-.4-1-.9-1.4-1.5-.3-.6-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2.3-.7.8-1.2 1.4-1.5a4 4 0 0 1 2-.6 4 4 0 0 1 2.1.6c.6.3 1 .8 1.4 1.5.3.6.5 1.3.5 2.2 0 .8-.2 1.6-.5 2.2-.3.6-.8 1.1-1.4 1.5a4 4 0 0 1-2 .5Zm0-1.5c.4 0 .8-.2 1-.4.3-.2.6-.6.7-1 .2-.4.2-.8.2-1.3s0-1-.2-1.4c-.1-.4-.4-.7-.6-1-.3-.2-.7-.3-1.1-.3-.5 0-.8.1-1.1.3l-.7 1a4 4 0 0 0-.2 1.4c0 .5 0 1 .2 1.3.2.4.4.8.7 1 .3.2.6.4 1 .4ZM22.8 11.6a4 4 0 0 1-2-.5c-.7-.4-1.1-.9-1.4-1.5-.3-.7-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2 4 4 0 0 1 1.8.3c.5.3.9.6 1.2 1 .3.5.5 1 .5 1.6h-1.8c-.1-.4-.3-.7-.6-1-.2-.3-.6-.4-1-.4-.5 0-.8.1-1.1.3a2 2 0 0 0-.7 1c-.2.3-.3.8-.3 1.3 0 .6.1 1 .3 1.5.2.4.4.7.7.9.3.2.6.3 1 .3l.8-.1.6-.5.3-.8h1.8a3 3 0 0 1-1.7 2.6 4 4 0 0 1-1.8.4ZM32.6 8V3.3h1.9v8.1h-1.9V10l-1 1.1c-.4.3-1 .4-1.5.4-.6 0-1 0-1.4-.3-.5-.3-.8-.6-1-1-.2-.5-.4-1-.4-1.7V3.3h2v4.9c0 .5.1.9.4 1.2.3.3.7.5 1.1.5a1.8 1.8 0 0 0 1.5-.9c.2-.2.3-.6.3-1ZM36 11.4V3.3h1.7v1.3h.1c.2-.4.5-.8.9-1a2.4 2.4 0 0 1 2.8 0c.4.2.7.6.8 1h.1c.2-.4.5-.8 1-1 .4-.3.9-.5 1.5-.5.7 0 1.3.3 1.8.8.5.4.7 1.1.7 2v5.5h-2V6.2c0-.5 0-.8-.3-1-.3-.3-.6-.4-1-.4-.5 0-.8.1-1.1.4-.3.3-.4.7-.4 1.1v5.1h-1.9V6.2c0-.5-.1-.8-.4-1-.2-.3-.6-.4-1-.4-.2 0-.5 0-.7.2l-.6.6-.2.9v5H36ZM52.4 11.6c-.8 0-1.5-.2-2-.5-.7-.4-1.1-.8-1.5-1.5-.3-.6-.4-1.4-.4-2.2 0-.8.1-1.6.4-2.2a3.6 3.6 0 0 1 3.4-2c.5 0 1 0 1.5.2a3.3 3.3 0 0 1 2 2c.2.5.3 1.2.3 1.9v.6h-6.7V6.6h4.8c0-.4 0-.7-.2-1a1.8 1.8 0 0 0-1.6-1 1.9 1.9 0 0 0-1.8 1l-.2 1v1.2c0 .5 0 1 .2 1.2.2.4.4.6.8.8.3.2.6.3 1 .3l.8-.1.6-.4c.2-.1.3-.3.4-.6l1.8.2c-.1.5-.3 1-.7 1.3-.3.3-.7.6-1.2.8-.5.2-1 .3-1.7.3ZM59 6.6v4.8h-1.9V3.3H59v1.3c.3-.4.6-.8 1-1 .4-.3 1-.5 1.5-.5.6 0 1 .2 1.5.4l1 1c.2.5.3 1 .3 1.7v5.2h-2V6.5c0-.5 0-1-.3-1.3-.3-.3-.7-.4-1.2-.4-.3 0-.6 0-.9.2-.3.1-.5.4-.6.6-.2.3-.2.6-.2 1ZM69.8 3.3v1.4h-4.7V3.3h4.7Zm-3.5-2h1.9V9l.1.6.3.2.5.1a2 2 0 0 0 .5 0l.4 1.5a4.3 4.3 0 0 1-1.2.1c-.5 0-.9 0-1.3-.2a2 2 0 0 1-.9-.7c-.2-.4-.3-.8-.3-1.3v-8ZM73.1 10v.5c0 .5-.2 1-.3 1.4a18 18 0 0 1-.7 2.2h-1.3a264.2 264.2 0 0 1 .6-3.6V10h1.7ZM81.3 11.4h-3.7V.5h3.8c1 0 2 .2 2.8.7.8.4 1.4 1 1.8 1.9.4.8.6 1.7.6 2.9 0 1.1-.2 2-.6 2.9-.4.8-1 1.4-1.8 1.9-.8.4-1.8.6-2.9.6Zm-1.7-1.7h1.6a4 4 0 0 0 2-.4c.4-.3.8-.7 1-1.2a5 5 0 0 0 .5-2.1 5 5 0 0 0-.4-2.1c-.3-.6-.7-1-1.2-1.2-.5-.3-1-.5-1.8-.5h-1.7v7.5ZM87.8 11.4V3.3h2v8.1h-2Zm1-9.3c-.3 0-.6-.1-.8-.3a1 1 0 0 1-.3-.8c0-.2.1-.5.3-.7.3-.2.5-.3.8-.3.3 0 .6.1.8.3.2.2.3.5.3.7 0 .3 0 .6-.3.8-.2.2-.5.3-.8.3ZM97.7 5.4l-1.8.2c0-.2-.1-.3-.3-.5l-.4-.4-.8-.1c-.4 0-.7 0-1 .2-.3.2-.4.4-.4.7 0 .3 0 .5.3.6.1.2.4.3.8.4l1.4.3c.8.1 1.4.4 1.8.8.3.3.5.8.5 1.4 0 .5-.1 1-.4 1.3a3 3 0 0 1-1.2 1c-.6.2-1.2.3-1.9.3-1 0-1.8-.2-2.4-.6-.6-.5-1-1-1-1.8l1.8-.2c.1.4.3.7.6.9l1 .2c.5 0 .9 0 1.1-.2.3-.2.5-.5.5-.7 0-.3-.1-.5-.3-.6a2 2 0 0 0-.8-.4L93.4 8c-.8-.1-1.4-.4-1.8-.8a2 2 0 0 1-.5-1.5c0-.5.1-1 .4-1.3.3-.4.6-.6 1.1-.8.5-.2 1.1-.4 1.8-.4 1 0 1.7.3 2.2.7.6.4 1 1 1 1.6ZM102.4 11.6a4 4 0 0 1-2-.5c-.6-.4-1.1-.9-1.4-1.5-.3-.7-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2 4 4 0 0 1 1.8.3c.5.3 1 .6 1.2 1 .3.5.5 1 .5 1.6h-1.8c0-.4-.2-.7-.5-1-.3-.3-.7-.4-1.1-.4-.4 0-.8.1-1 .3a2 2 0 0 0-.8 1l-.2 1.3c0 .6 0 1 .2 1.5.2.4.4.7.7.9.3.2.7.3 1 .3l.8-.1.6-.5.3-.8h1.8a3 3 0 0 1-1.7 2.6 4 4 0 0 1-1.8.4ZM110.5 11.6a4 4 0 0 1-2-.5c-.6-.4-1-.9-1.4-1.5-.3-.6-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2.3-.7.8-1.2 1.4-1.5a4 4 0 0 1 2-.6 4 4 0 0 1 2.1.6c.6.3 1 .8 1.4 1.5.3.6.5 1.3.5 2.2 0 .8-.2 1.6-.5 2.2-.3.6-.8 1.1-1.4 1.5a4 4 0 0 1-2 .5Zm0-1.5c.5 0 .8-.2 1.1-.4.3-.2.5-.6.7-1l.2-1.3c0-.5 0-1-.2-1.4l-.7-1c-.3-.2-.6-.3-1-.3-.5 0-.9.1-1.2.3-.2.3-.5.6-.6 1a4 4 0 0 0-.2 1.4c0 .5 0 1 .2 1.3.1.4.4.8.6 1 .3.2.7.4 1.1.4Z"/><polygon points="122.552812 3.25461648 119.639104 11.4364347 117.508422 11.4364347 114.594715 3.25461648 116.650823 3.25461648 118.53115 9.33238636 118.616377 9.33238636 120.50203 3.25461648"/><path d="M126.7 11.6c-.9 0-1.6-.2-2.2-.5-.6-.4-1-.8-1.3-1.5-.4-.6-.5-1.4-.5-2.2 0-.8.1-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2c.5 0 1 0 1.4.2a3.3 3.3 0 0 1 2 2c.2.5.3 1.2.3 1.9v.6h-6.7V6.6h4.9c0-.4-.1-.7-.3-1a1.8 1.8 0 0 0-1.6-1 1.9 1.9 0 0 0-1.7 1l-.3 1v1.2c0 .5 0 1 .3 1.2.1.4.4.6.7.8.3.2.7.3 1 .3l.9-.1.5-.4c.2-.1.3-.3.4-.6l1.8.2c0 .5-.3 1-.6 1.3l-1.2.8c-.5.2-1.1.3-1.7.3ZM131.4 11.4V3.3h1.8v1.3h.1a2 2 0 0 1 2-1.5 5.5 5.5 0 0 1 .7 0V5l-.4-.1a4 4 0 0 0-.5 0 2 2 0 0 0-1 .2 1.7 1.7 0 0 0-.8 1.5v4.8h-2ZM2.7 30.6c-.5 0-1 0-1.4-.3-.4-.2-.7-.4-1-.8C.2 29 0 28.7 0 28c0-.4 0-.8.3-1.1.1-.3.4-.5.7-.7.2-.2.6-.3 1-.4l1.1-.2a37.4 37.4 0 0 0 1.8-.4c.2 0 .2-.2.2-.4 0-.4 0-.7-.3-1-.3-.2-.6-.3-1-.3-.5 0-.9.1-1.2.3-.3.2-.4.5-.5.8l-1.8-.3a2.9 2.9 0 0 1 1.9-2c.4-.2 1-.3 1.5-.3.4 0 .8 0 1.2.2.4 0 .8.2 1 .4.4.3.7.5.9.9.2.4.3.8.3 1.4v5.4H5.2v-1a2.3 2.3 0 0 1-1.3 1l-1.2.2Zm.5-1.4c.4 0 .8 0 1-.2l.7-.7c.2-.2.2-.5.2-.8v-1l-.3.2h-.5a14.8 14.8 0 0 1-1 .2l-.7.2-.5.4a1 1 0 0 0-.2.6c0 .4 0 .6.3.8.3.2.6.3 1 .3ZM10.4 25.6v4.8h-2v-8.1h1.9v1.3c.3-.4.6-.8 1-1 .4-.3 1-.5 1.5-.5.6 0 1 .2 1.5.4l1 1c.2.5.3 1 .3 1.7v5.2h-2v-4.9c0-.5 0-1-.3-1.3-.3-.3-.7-.4-1.2-.4-.3 0-.6 0-.9.2-.3.1-.5.4-.6.6-.2.3-.2.6-.2 1ZM20 30.6a3.1 3.1 0 0 1-2.9-2c-.3-.6-.4-1.3-.4-2.2 0-1 .1-1.7.4-2.3.3-.7.7-1.1 1.3-1.5.5-.3 1-.5 1.7-.5.4 0 .8.1 1.1.3.4.2.6.3.8.6l.4.6v-4h2v10.8h-1.9v-1.3h-.1l-.4.7a2.4 2.4 0 0 1-2 .8Zm.6-1.6c.4 0 .8-.1 1-.3l.7-1 .2-1.4c0-.5 0-1-.2-1.3a2 2 0 0 0-.6-1c-.3-.2-.7-.3-1.1-.3-.4 0-.8.1-1 .4a2 2 0 0 0-.7.9c-.2.4-.2.8-.2 1.3 0 .6 0 1 .2 1.4.1.4.3.7.6 1 .3.2.7.3 1.1.3Z"/><polygon points="28.6839458 21.1839489 28.6839458 19.5273438 37.387781 19.5273438 37.387781 21.1839489 34.015977 21.1839489 34.015977 30.4364347 32.0557498 30.4364347 32.0557498 21.1839489"/><path d="M40.8 30.6c-.8 0-1.5-.2-2-.5-.7-.4-1.1-.8-1.5-1.5-.3-.6-.4-1.4-.4-2.2 0-.8.1-1.6.5-2.2a3.6 3.6 0 0 1 3.3-2c.5 0 1 0 1.5.2a3.3 3.3 0 0 1 2 2c.2.5.3 1.2.3 1.9v.6h-6.7v-1.3h4.8c0-.4 0-.7-.2-1a1.8 1.8 0 0 0-1.6-1 1.9 1.9 0 0 0-1.8 1l-.2 1v1.2c0 .5 0 1 .2 1.2.2.4.5.6.8.8.3.2.6.3 1 .3l.8-.1c.3-.1.5-.2.6-.4.2-.1.3-.3.4-.6l1.8.2c-.1.5-.3 1-.6 1.3l-1.3.8c-.5.2-1 .3-1.7.3ZM52 24.4l-1.7.2c0-.2-.2-.3-.3-.5l-.5-.4-.8-.1c-.4 0-.7 0-1 .2-.2.2-.4.4-.4.7 0 .3.1.5.3.6.2.2.5.3.9.4l1.4.3c.8.1 1.3.4 1.7.8.4.3.6.8.6 1.4 0 .5-.2 1-.5 1.3a3 3 0 0 1-1.2 1c-.5.2-1.1.3-1.8.3-1 0-1.8-.2-2.4-.6-.6-.5-1-1-1.1-1.8L47 28c0 .4.2.7.5.9l1 .2c.5 0 1 0 1.2-.2.3-.2.4-.5.4-.7 0-.3 0-.5-.3-.6a2 2 0 0 0-.8-.4l-1.4-.3c-.8-.1-1.3-.4-1.7-.8a2 2 0 0 1-.6-1.5c0-.5.1-1 .4-1.3.3-.4.7-.6 1.2-.8.5-.2 1-.4 1.7-.4 1 0 1.7.3 2.3.7.5.4.9 1 1 1.6ZM57.3 22.3v1.4h-4.7v-1.4h4.7Zm-3.5-2h1.9V28l.1.6.3.2.5.1a2 2 0 0 0 .5 0l.4 1.5a4.3 4.3 0 0 1-1.2.1c-.5 0-.9 0-1.3-.2a2 2 0 0 1-.9-.7c-.2-.4-.3-.8-.3-1.3v-8ZM63 30.4H61l3.8-10.9h2.5l3.8 11h-2l-3-8.7-3 8.6Zm0-4.2h5.8v1.5h-5.7v-1.5ZM71.9 30.4V19.5h4c1 0 1.6.2 2.2.5.6.3 1 .7 1.3 1.3.3.5.4 1.1.4 1.8s-.1 1.4-.4 1.9c-.3.5-.7 1-1.3 1.3-.6.3-1.3.5-2.1.5h-2.7V25h2.4c.5 0 .9 0 1.2-.2l.7-.7.2-1c0-.4 0-.8-.2-1-.2-.4-.4-.6-.7-.8l-1.2-.2h-1.8v9.2h-2Z"/><polygon points="82.9651016 19.5273438 82.9651016 30.4364347 80.9888943 30.4364347 80.9888943 19.5273438"/><path d="m91 24.4-1.8.2c0-.2-.2-.3-.3-.5l-.5-.4-.7-.1c-.4 0-.8 0-1 .2-.3.2-.4.4-.4.7 0 .3 0 .5.2.6.2.2.5.3 1 .4l1.3.3c.8.1 1.4.4 1.7.8.4.3.6.8.6 1.4 0 .5-.1 1-.4 1.3a3 3 0 0 1-1.3 1c-.5.2-1.1.3-1.8.3-1 0-1.8-.2-2.4-.6-.6-.5-1-1-1.1-1.8L86 28c0 .4.2.7.5.9l1.1.2c.5 0 .8 0 1.1-.2.3-.2.4-.5.4-.7 0-.3 0-.5-.2-.6a2 2 0 0 0-.9-.4l-1.4-.3c-.8-.1-1.3-.4-1.7-.8a2 2 0 0 1-.6-1.5c0-.5.2-1 .4-1.3.3-.4.7-.6 1.2-.8.5-.2 1-.4 1.7-.4 1 0 1.8.3 2.3.7.6.4.9 1 1 1.6Z"/><polygon points="96.9873827 30.4364347 94.675593 22.2546165 96.6411469 22.2546165 98.0793572 28.0074574 98.153931 28.0074574 99.6241015 22.2546165 101.568349 22.2546165 103.038519 27.9754972 103.11842 27.9754972 104.535323 22.2546165 106.506204 22.2546165 104.189087 30.4364347 102.18092 30.4364347 100.646829 24.9073153 100.534968 24.9073153 99.000877 30.4364347"/><path d="M107.3 30.4v-8.1h2v8.1h-2Zm1-9.3c-.3 0-.6-.1-.8-.3a1 1 0 0 1-.4-.8l.4-.7c.2-.2.5-.3.8-.3.3 0 .5.1.8.3.2.2.3.5.3.7 0 .3-.1.6-.3.8-.3.2-.5.3-.8.3ZM114.7 22.3v1.4H110v-1.4h4.7Zm-3.5-2h2v8.3l.4.2.4.1a2 2 0 0 0 .6 0l.3 1.5a4.3 4.3 0 0 1-1.2.1c-.4 0-.9 0-1.3-.2a2 2 0 0 1-.9-.7c-.2-.4-.3-.8-.3-1.3v-8ZM117.9 25.6v4.8h-2V19.5h2v4.1c.2-.4.5-.8 1-1 .3-.3.8-.5 1.5-.5.5 0 1 .2 1.5.4.4.2.7.6 1 1 .2.5.3 1 .3 1.7v5.2h-2v-4.9c0-.5 0-1-.4-1.3-.2-.3-.6-.4-1.2-.4-.3 0-.6 0-.9.2l-.6.6a2 2 0 0 0-.2 1ZM6.4 41.5c0-.4-.3-.8-.6-1-.4-.3-.9-.4-1.5-.4l-1 .1-.7.5a1.2 1.2 0 0 0 0 1.4l.4.4.6.3.6.2 1 .2 1.2.4 1 .6c.4.3.6.6.8 1 .2.3.2.7.2 1.2a3 3 0 0 1-1.9 2.8c-.6.3-1.3.4-2.2.4-.8 0-1.6-.1-2.2-.4-.6-.3-1.1-.6-1.5-1.1-.3-.5-.5-1.2-.5-1.9h2a1.7 1.7 0 0 0 1 1.5c.4.2.8.2 1.2.2.4 0 .8 0 1.1-.2.4-.1.6-.3.8-.5.2-.3.3-.5.3-.8 0-.3-.1-.6-.3-.7-.1-.2-.4-.4-.7-.5a7 7 0 0 0-1-.4l-1.2-.3a5 5 0 0 1-2.2-1c-.5-.5-.7-1.1-.7-1.9 0-.6.1-1.2.5-1.7.3-.5.8-.9 1.4-1.1a5 5 0 0 1 2-.4c.8 0 1.5.1 2.1.4.6.2 1 .6 1.4 1 .3.6.5 1.1.5 1.7H6.4ZM13.1 49.6a4 4 0 0 1-2-.5c-.7-.4-1.1-.9-1.4-1.5-.3-.7-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2 4 4 0 0 1 1.8.3c.5.3 1 .6 1.2 1 .3.5.5 1 .5 1.6h-1.8c0-.4-.3-.7-.5-1-.3-.3-.7-.4-1.2-.4-.4 0-.7.1-1 .3a2 2 0 0 0-.7 1c-.2.3-.2.8-.2 1.3 0 .6 0 1 .2 1.5.2.4.4.7.7.9.3.2.6.3 1 .3l.8-.1.6-.5.3-.8h1.8a3 3 0 0 1-1.7 2.6 4 4 0 0 1-1.8.4ZM20 49.6c-.5 0-1 0-1.4-.3-.4-.2-.7-.4-1-.8-.2-.4-.3-.8-.3-1.4 0-.4 0-.8.2-1.1l.7-.7 1-.4 1.2-.2a37.4 37.4 0 0 0 1.8-.4l.2-.4c0-.4-.1-.7-.4-1-.2-.2-.5-.3-1-.3s-.8.1-1.1.3c-.3.2-.5.5-.6.8l-1.8-.3a2.9 2.9 0 0 1 2-2c.4-.2 1-.3 1.5-.3.4 0 .8 0 1.2.2.4 0 .7.2 1 .4.4.3.6.5.8.9.2.4.3.8.3 1.4v5.4h-1.8v-1a2.3 2.3 0 0 1-1.4 1l-1 .2Zm.5-1.4c.4 0 .7 0 1-.2l.7-.7.2-.8v-1l-.3.2h-.5a14.8 14.8 0 0 1-1 .2l-.7.2-.6.4a1 1 0 0 0-.2.6c0 .4.2.6.4.8.3.2.6.3 1 .3Z"/><polygon points="27.6426646 38.5273438 27.6426646 49.4364347 25.7143976 49.4364347 25.7143976 38.5273438"/><path d="M31.4 49.6c-.5 0-1 0-1.4-.3-.4-.2-.7-.4-1-.8-.2-.4-.3-.8-.3-1.4 0-.4 0-.8.2-1.1l.7-.7 1-.4 1.2-.2a37.4 37.4 0 0 0 1.8-.4l.2-.4c0-.4-.1-.7-.3-1-.3-.2-.6-.3-1-.3-.5 0-1 .1-1.2.3-.3.2-.5.5-.6.8l-1.8-.3a2.9 2.9 0 0 1 2-2c.4-.2 1-.3 1.5-.3.4 0 .8 0 1.2.2.4 0 .7.2 1 .4.4.3.6.5.8.9.2.4.3.8.3 1.4v5.4H34v-1a2.3 2.3 0 0 1-1.4 1l-1 .2Zm.5-1.4c.4 0 .7 0 1-.2l.7-.7.2-.8v-1l-.3.2H33a14.8 14.8 0 0 1-1 .2l-.7.2-.6.4a1 1 0 0 0-.2.6c0 .4.2.6.4.8.3.2.6.3 1 .3ZM37.1 49.4v-8.1H39v1.3a2 2 0 0 1 2-1.5 5.5 5.5 0 0 1 .8 0V43l-.4-.1a4 4 0 0 0-.5 0 2 2 0 0 0-1 .2 1.7 1.7 0 0 0-.9 1.5v4.8h-1.9ZM42.8 49.6c-.4 0-.6-.2-.9-.4-.2-.2-.3-.5-.3-.8 0-.3.1-.6.3-.8.3-.3.5-.4.9-.4.3 0 .5.1.8.4a1.1 1.1 0 0 1 .2 1.4l-.5.4-.5.2ZM51.8 49.6a4 4 0 0 1-2.1-.5c-.6-.4-1-.9-1.4-1.5-.3-.7-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2 4 4 0 0 1 1.8.3c.5.3 1 .6 1.2 1 .3.5.5 1 .6 1.6h-1.9c0-.4-.2-.7-.5-1-.3-.3-.7-.4-1.1-.4-.4 0-.8.1-1 .3a2 2 0 0 0-.8 1l-.2 1.3c0 .6 0 1 .2 1.5.2.4.4.7.7.9.3.2.7.3 1 .3l.8-.1c.3-.1.4-.3.6-.5.2-.2.3-.5.3-.8h1.9c0 .6-.3 1.1-.6 1.6a3 3 0 0 1-1.2 1 4 4 0 0 1-1.7.4ZM59.9 49.6a4 4 0 0 1-2.1-.5c-.6-.4-1-.9-1.4-1.5-.3-.6-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2.3-.7.8-1.2 1.4-1.5a4 4 0 0 1 2-.6 4 4 0 0 1 2.1.6c.6.3 1 .8 1.4 1.5.3.6.5 1.3.5 2.2 0 .8-.2 1.6-.5 2.2-.3.6-.8 1.1-1.4 1.5a4 4 0 0 1-2 .5Zm0-1.5c.4 0 .8-.2 1-.4.3-.2.6-.6.7-1l.2-1.3c0-.5 0-1-.2-1.4-.1-.4-.4-.7-.6-1-.3-.2-.7-.3-1.1-.3-.5 0-.8.1-1.1.3-.3.3-.6.6-.7 1a4 4 0 0 0-.2 1.4c0 .5 0 1 .2 1.3.1.4.4.8.7 1 .3.2.6.4 1 .4ZM64.8 49.4v-8.1h1.9v1.3c.3-.4.5-.8 1-1 .3-.3.8-.5 1.3-.5.6 0 1 .2 1.4.5.4.2.7.6.8 1h.1c.2-.4.5-.8 1-1 .4-.3.9-.5 1.5-.5.7 0 1.4.3 1.8.8.5.4.7 1.1.7 2v5.5h-1.9v-5.2c0-.5-.1-.8-.4-1-.3-.3-.6-.4-1-.4s-.8.1-1 .4c-.3.3-.5.7-.5 1.1v5.1h-1.9v-5.2c0-.5 0-.8-.3-1-.3-.3-.6-.4-1-.4-.3 0-.6 0-.8.2-.2.1-.4.3-.5.6-.2.2-.2.5-.2.9v5h-2Z"/></g></g><g mask="url(#b)" fill="#FFF" fill-rule="nonzero"><g transform="translate(83.8 76.8)"><polygon points="4.59161932 0.303622159 4.59161932 11.2127131 2.61541193 11.2127131 2.61541193 2.2265625 2.55149148 2.2265625 0 3.85653409 0 2.04545455 2.71129261 0.303622159"/><path d="M10.8 11.4c-.8 0-1.6-.2-2.2-.6-.6-.5-1.1-1.1-1.5-2-.3-.8-.5-1.8-.5-3s.2-2.2.5-3c.4-1 .9-1.5 1.5-2C9.2.4 10 .2 10.8.2c1 0 1.7.2 2.3.6.6.5 1.1 1 1.4 2 .4.8.5 1.8.5 3s-.1 2.2-.5 3c-.3.9-.8 1.5-1.4 2-.6.4-1.4.6-2.3.6Zm0-1.6c.7 0 1.3-.4 1.7-1a6 6 0 0 0 .5-3c0-.9 0-1.6-.2-2.2a3 3 0 0 0-.8-1.3c-.3-.3-.7-.5-1.2-.5-.6 0-1.2.3-1.6 1a6 6 0 0 0-.6 3c0 .8.1 1.6.3 2.2.2.6.4 1 .8 1.3.3.3.7.5 1.1.5ZM20.3 11.4c-.9 0-1.7-.2-2.3-.6-.6-.5-1-1.1-1.4-2-.4-.8-.5-1.8-.5-3s.1-2.2.5-3c.3-1 .8-1.5 1.4-2 .7-.4 1.4-.6 2.3-.6.9 0 1.6.2 2.2.6.7.5 1.1 1 1.5 2 .3.8.5 1.8.5 3s-.2 2.2-.5 3c-.4.9-.8 1.5-1.5 2-.6.4-1.3.6-2.2.6Zm0-1.6c.7 0 1.2-.4 1.6-1a6 6 0 0 0 .6-3c0-.9-.1-1.6-.3-2.2a3 3 0 0 0-.7-1.3c-.4-.3-.8-.5-1.2-.5-.7 0-1.2.3-1.6 1a6 6 0 0 0-.6 3c0 .8 0 1.6.2 2.2.2.6.5 1 .8 1.3.3.3.7.5 1.2.5ZM31.5 9.2v-.6l.3-1.2a2.1 2.1 0 0 1 2-1.2c.5 0 1 .2 1.3.4l.8.8c.2.4.2.8.2 1.2v.6c0 .4 0 .8-.2 1.1-.2.4-.5.7-.8.9-.4.2-.8.3-1.3.3s-.9 0-1.2-.3c-.4-.2-.6-.5-.8-.9-.2-.3-.3-.7-.3-1.1Zm1.4-.6v.6c0 .2 0 .5.2.7.1.3.4.4.7.4.4 0 .6-.1.7-.4.2-.2.2-.4.2-.7v-.6c0-.3 0-.6-.2-.8 0-.2-.3-.3-.7-.3-.3 0-.5 0-.7.3l-.2.8Zm-7-5.7v-.6c0-.4.2-.8.3-1.1.2-.4.5-.7.8-.9.4-.2.8-.3 1.3-.3s1 .1 1.3.3c.3.2.6.5.7.9.2.3.3.7.3 1.1V3c0 .5-.1.8-.3 1.2a2 2 0 0 1-.8.8c-.3.3-.7.4-1.2.4s-1-.1-1.3-.4a2 2 0 0 1-.8-.8L26 2.9Zm1.5-.6V3c0 .3 0 .6.2.8.1.2.3.3.7.3.3 0 .6 0 .7-.3l.2-.8v-.6c0-.2 0-.5-.2-.7-.1-.3-.4-.4-.7-.4-.4 0-.6.1-.7.4-.2.2-.2.5-.2.7Zm-1 9L34 .2h1.3l-7.5 11h-1.3ZM50.8 5.8c0 1.1-.2 2.1-.7 3a4.7 4.7 0 0 1-4.3 2.6 4.7 4.7 0 0 1-4.4-2.6c-.4-.9-.6-1.9-.6-3 0-1.2.2-2.2.6-3A4.7 4.7 0 0 1 45.8.1c1 0 1.8.2 2.6.6.7.5 1.3 1.1 1.7 2 .5.8.7 1.8.7 3Zm-2 0a5 5 0 0 0-.4-2.1 3 3 0 0 0-1-1.3c-.5-.3-1-.5-1.6-.5a3 3 0 0 0-2.6 1.7 5 5 0 0 0-.4 2.2c0 .8 0 1.5.3 2 .3.6.7 1 1.1 1.3.5.3 1 .5 1.6.5a3 3 0 0 0 2.6-1.7 5 5 0 0 0 .4-2.1ZM52 14.3V3h2v1.4l.5-.7a2.3 2.3 0 0 1 1.9-.8 3.1 3.1 0 0 1 2.9 2c.3.6.4 1.3.4 2.2 0 1-.1 1.7-.4 2.3-.3.7-.7 1.1-1.2 1.5-.5.3-1 .5-1.7.5-.5 0-.9-.1-1.2-.3l-.7-.5-.5-.7v4.4h-2ZM54 7l.1 1.4.7 1c.3.2.6.3 1 .3.5 0 .8-.1 1.1-.4.3-.2.5-.5.7-1l.2-1.3c0-.5 0-1-.2-1.3a2 2 0 0 0-.7-1c-.3-.2-.6-.3-1-.3-.5 0-.8.1-1.1.3a2 2 0 0 0-.7 1L54 7ZM64.5 11.4c-.9 0-1.6-.2-2.2-.5-.6-.4-1-.9-1.3-1.5-.4-.6-.5-1.4-.5-2.2 0-.9.1-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2c.5 0 1 0 1.4.2a3.3 3.3 0 0 1 2 2c.2.5.3 1.1.3 1.9v.5h-6.7V6.3h4.8c0-.3 0-.6-.2-1a1.8 1.8 0 0 0-1.6-.9 1.9 1.9 0 0 0-1.8 1l-.2 1v1.2c0 .5 0 .9.3 1.2.1.4.4.6.7.8.3.2.7.3 1 .3l.8-.1c.3-.1.5-.2.6-.4l.4-.6L68 9c-.1.5-.3 1-.6 1.3L66 11c-.4.2-1 .3-1.6.3ZM71 6.4v4.8h-1.8V3H71v1.4h.1c.2-.4.5-.8.9-1 .4-.3 1-.5 1.5-.5.6 0 1.1.1 1.5.4.4.2.8.6 1 1 .2.5.3 1 .3 1.7v5.2h-1.9V6.3c0-.5-.1-1-.4-1.3-.3-.3-.7-.4-1.2-.4-.3 0-.6 0-.9.2l-.6.6-.2 1ZM87 3.3c-.1-.5-.3-.8-.7-1-.4-.3-.8-.5-1.4-.5-.4 0-.8 0-1.1.2l-.7.5a1.2 1.2 0 0 0 0 1.3c0 .2.2.3.4.5l.6.3.6.2 1 .2 1.3.4 1 .6.7 1c.2.3.3.7.3 1.1a3 3 0 0 1-2 2.9c-.5.2-1.3.4-2.2.4-.8 0-1.6-.2-2.2-.4-.6-.3-1-.7-1.4-1.2-.4-.5-.6-1-.6-1.8h2a1.7 1.7 0 0 0 1.1 1.5l1.1.2c.4 0 .8 0 1.1-.2.4-.1.6-.3.8-.5L87 8c0-.3 0-.5-.3-.7-.1-.2-.3-.3-.6-.4a7 7 0 0 0-1-.4l-1.3-.3a5 5 0 0 1-2.1-1c-.6-.5-.8-1.1-.8-2 0-.6.2-1.1.5-1.6.4-.5.8-.9 1.4-1.1a5 5 0 0 1 2-.4c.9 0 1.5 0 2.1.4.6.2 1 .6 1.4 1 .3.5.5 1 .5 1.7H87ZM93.7 11.4a4 4 0 0 1-2.1-.6c-.6-.3-1-.8-1.4-1.4-.3-.7-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.3.3-.6.8-1.1 1.4-1.4a4 4 0 0 1 2-.6 4 4 0 0 1 2.1.6c.6.3 1 .8 1.4 1.4.3.7.5 1.4.5 2.3 0 .8-.2 1.5-.5 2.2-.3.6-.8 1-1.4 1.4a4 4 0 0 1-2 .6Zm0-1.6c.4 0 .8 0 1-.3.3-.3.6-.6.7-1l.2-1.4c0-.5 0-1-.2-1.3-.1-.4-.4-.7-.7-1-.2-.2-.6-.3-1-.3-.5 0-.9 0-1.1.3-.3.3-.6.6-.7 1a4 4 0 0 0-.2 1.3c0 .5 0 1 .2 1.4.1.4.4.7.7 1 .2.2.6.3 1 .3ZM103.9 7.8V3h1.9v8.2h-1.9V9.8c-.3.4-.6.8-1 1-.4.4-1 .5-1.5.5-.6 0-1-.1-1.5-.3l-1-1c-.1-.5-.3-1.1-.3-1.8V3h2v5c0 .5.1.9.4 1.2.3.3.7.4 1.1.4a1.8 1.8 0 0 0 1.5-.8c.2-.3.3-.6.3-1ZM107.2 11.2V3h1.9v1.4a2 2 0 0 1 2-1.5 5.5 5.5 0 0 1 .8 0v1.8h-.4a4 4 0 0 0-.5 0 2 2 0 0 0-1 .2 1.7 1.7 0 0 0-.9 1.5v4.8h-1.9ZM115.8 11.4a4 4 0 0 1-2-.6c-.7-.3-1.1-.8-1.4-1.5a4.9 4.9 0 0 1 0-4.4 3.6 3.6 0 0 1 3.4-2 4 4 0 0 1 1.8.4c.5.2.9.6 1.2 1 .3.5.5 1 .5 1.6h-1.8c-.1-.4-.3-.8-.6-1-.2-.3-.6-.4-1-.4-.5 0-.8 0-1.1.3a2 2 0 0 0-.7.9c-.2.4-.2.9-.2 1.4 0 .6 0 1 .2 1.5.2.4.4.7.7.9.3.2.6.3 1 .3.3 0 .6 0 .8-.2.2 0 .4-.2.6-.4l.3-.8h1.8a3 3 0 0 1-1.7 2.6 4 4 0 0 1-1.8.4ZM124 11.4c-.9 0-1.6-.2-2.2-.5-.5-.4-1-.9-1.3-1.5-.3-.6-.5-1.4-.5-2.2 0-.9.2-1.6.5-2.2a3.6 3.6 0 0 1 3.4-2c.5 0 1 0 1.4.2a3.3 3.3 0 0 1 2 2c.2.5.3 1.1.3 1.9v.5H121V6.3h4.9l-.3-1a1.8 1.8 0 0 0-1.6-.9 1.9 1.9 0 0 0-1.7 1l-.3 1v1.2c0 .5.1.9.3 1.2.1.4.4.6.7.8a2.3 2.3 0 0 0 1.9.1c.2 0 .4-.1.5-.3l.4-.6 1.8.2c0 .5-.3 1-.6 1.3-.3.3-.7.6-1.2.8-.5.2-1 .3-1.7.3Z"/></g></g></g></svg> diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index ed08028ec..9aa452e36 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -58,6 +58,12 @@ <img class="sponsor-image" src="/img/sponsors/reflex-banner.png" /> </a> </div> + <div class="item"> + <a title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" style="display: block; position: relative;" href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=top-banner" target="_blank"> + <span class="sponsor-badge">sponsor</span> + <img class="sponsor-image" src="/img/sponsors/scalar-banner.svg" /> + </a> + </div> </div> </div> {% endblock %} From e9ce31e96bd61f09decc1ba8d1908fb5d3315043 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 28 Nov 2023 10:52:59 +0000 Subject: [PATCH 1444/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 34bbab135..9a6c969ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +* 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). From 13cef7a21a8fb941f91c981e93b138d1b44aabd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 28 Nov 2023 13:10:12 +0100 Subject: [PATCH 1445/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Fern=20(#10729)?= 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/docs/advanced/generate-clients.md | 5 ++--- docs/en/docs/alternatives.md | 2 -- docs/en/overrides/main.html | 6 ------ 5 files changed, 2 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index b53076dcb..c8d17889d 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ The key features are: <a href="https://cryptapi.io/" target="_blank" title="CryptAPI: Your easy to use, secure and privacy oriented payment gateway."><img src="https://fastapi.tiangolo.com/img/sponsors/cryptapi.svg"></a> <a href="https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" target="_blank" title="Build, run and scale your apps on a modern, reliable, and secure PaaS."><img src="https://fastapi.tiangolo.com/img/sponsors/platform-sh.png"></a> -<a href="https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Fern | SDKs and API docs"><img src="https://fastapi.tiangolo.com/img/sponsors/fern.svg"></a> <a href="https://www.porter.run" target="_blank" title="Deploy FastAPI on AWS with a few clicks"><img src="https://fastapi.tiangolo.com/img/sponsors/porter.png"></a> <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 1a253f649..113772964 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -5,9 +5,6 @@ gold: - url: https://platform.sh/try-it-now/?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023 title: "Build, run and scale your apps on a modern, reliable, and secure PaaS." img: https://fastapi.tiangolo.com/img/sponsors/platform-sh.png - - url: https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=main-badge - title: Fern | SDKs and API docs - img: https://fastapi.tiangolo.com/img/sponsors/fern.svg - url: https://www.porter.run title: Deploy FastAPI on AWS with a few clicks img: https://fastapi.tiangolo.com/img/sponsors/porter.png diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index 07a8f039f..fb9aa643e 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -20,10 +20,9 @@ Some of them also ✨ [**sponsor FastAPI**](../help-fastapi.md#sponsor-the-autho And it shows their true commitment to FastAPI and its **community** (you), as they not only want to provide you a **good service** but also want to make sure you have a **good and healthy framework**, FastAPI. 🙇 -You might want to try their services and follow their guides: +For example, you might want to try <a href="https://speakeasyapi.dev/?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a>. -* <a href="https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=docs-generate-clients" class="external-link" target="_blank">Fern</a> -* <a href="https://speakeasyapi.dev/?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a> +There are also several other companies offering similar services that you can search and find online. 🤓 ## Generate a TypeScript Frontend Client diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index a777ddb98..0f074ccf3 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -119,8 +119,6 @@ That's why when talking about version 2.0 it's common to say "Swagger", and for These two were chosen for being fairly popular and stable, but doing a quick search, you could find dozens of additional alternative user interfaces for OpenAPI (that you can use with **FastAPI**). - For example, you could try <a href="https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=docs-alternatives" class="external-link" target="_blank">Fern</a> which is also a FastAPI sponsor. 😎🎉 - ### Flask REST frameworks There are several Flask REST frameworks, but after investing the time and work into investigating them, I found that many are discontinued or abandoned, with several standing issues that made them unfit. diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 9aa452e36..c4aea9a8e 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -34,12 +34,6 @@ <img class="sponsor-image" src="/img/sponsors/platform-sh-banner.png" /> </a> </div> - <div class="item"> - <a title="Fern | SDKs and API docs" style="display: block; position: relative;" href="https://www.buildwithfern.com/?utm_source=tiangolo&utm_medium=website&utm_campaign=top-banner" target="_blank"> - <span class="sponsor-badge">sponsor</span> - <img class="sponsor-image" src="/img/sponsors/fern-banner.svg" /> - </a> - </div> <div class="item"> <a title="Deploy FastAPI on AWS with a few clicks" style="display: block; position: relative;" href="https://www.porter.run" target="_blank"> <span class="sponsor-badge">sponsor</span> From 6fb951bae2cc1177cf4b03e37b751f81f690402e Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 28 Nov 2023 12:10:38 +0000 Subject: [PATCH 1446/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9a6c969ab..153229bc7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,7 @@ hide: ### Internal +* 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Reflex. PR [#10676](https://github.com/tiangolo/fastapi/pull/10676) by [@tiangolo](https://github.com/tiangolo). * 📝 Update release notes, move and check latest-changes. PR [#10588](https://github.com/tiangolo/fastapi/pull/10588) by [@tiangolo](https://github.com/tiangolo). From 99a2ec981b5dc8803d801a667cd8a56b56950730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Thu, 30 Nov 2023 21:48:01 +0100 Subject: [PATCH 1447/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20default=20sugg?= =?UTF-8?q?ested=20configs=20for=20generating=20clients=20(#10736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/generate-clients.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index fb9aa643e..e8d771f71 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -87,7 +87,7 @@ It could look like this: "description": "", "main": "index.js", "scripts": { - "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios" + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" }, "author": "", "license": "", @@ -106,7 +106,7 @@ After having that NPM `generate-client` script there, you can run it with: $ npm run generate-client frontend-app@1.0.0 generate-client /home/user/code/frontend-app -> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes ``` </div> @@ -246,7 +246,7 @@ Now as the end result is in a file `openapi.json`, you would modify the `package "description": "", "main": "index.js", "scripts": { - "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios" + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" }, "author": "", "license": "", From 8cf2fa0fe4f8701411db779176cc4cfac68d4216 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 30 Nov 2023 20:48:22 +0000 Subject: [PATCH 1448/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 153229bc7..f31534457 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,10 @@ hide: * 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). +### Docs + +* 📝 Tweak default suggested configs for generating clients. PR [#10736](https://github.com/tiangolo/fastapi/pull/10736) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). From ca03379b655165e203460c23a2b036a04d41bd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 4 Dec 2023 12:10:54 +0100 Subject: [PATCH 1449/1881] =?UTF-8?q?=F0=9F=91=B7=20Update=20build=20docs,?= =?UTF-8?q?=20verify=20README=20on=20CI=20(#10750)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 701c74697..51c069d9e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -28,6 +28,8 @@ jobs: - docs/** - docs_src/** - requirements-docs.txt + - .github/workflows/build-docs.yml + - .github/workflows/deploy-docs.yml langs: needs: - changes @@ -55,6 +57,8 @@ jobs: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git + - name: Verify README + run: python ./scripts/docs.py verify-readme - name: Export Language Codes id: show-langs run: | From 01e570c56da2e1e662cc4ebdbce18ec21320a7e6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 4 Dec 2023 11:11:17 +0000 Subject: [PATCH 1450/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f31534457..669f19de7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 👷 Update build docs, verify README on CI. PR [#10750](https://github.com/tiangolo/fastapi/pull/10750) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Reflex. PR [#10676](https://github.com/tiangolo/fastapi/pull/10676) by [@tiangolo](https://github.com/tiangolo). From 33493ce694d2f7f1900f6e3128268b10620405f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 6 Dec 2023 12:33:48 +0100 Subject: [PATCH 1451/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20a?= =?UTF-8?q?dd=20PropelAuth=20(#10760)?= 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/docs/img/sponsors/propelauth-banner.png | Bin 0 -> 12806 bytes docs/en/docs/img/sponsors/propelauth.png | Bin 0 -> 28140 bytes docs/en/overrides/main.html | 6 ++++++ 5 files changed, 10 insertions(+) create mode 100644 docs/en/docs/img/sponsors/propelauth-banner.png create mode 100644 docs/en/docs/img/sponsors/propelauth.png diff --git a/README.md b/README.md index c8d17889d..2df5cba0b 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ The key features are: <a href="https://bump.sh/fastapi?utm_source=fastapi&utm_medium=referral&utm_campaign=sponsor" target="_blank" title="Automate FastAPI documentation generation with Bump.sh"><img src="https://fastapi.tiangolo.com/img/sponsors/bump-sh.svg"></a> <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> +<a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> <a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a> <a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 113772964..121a3b761 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -17,6 +17,9 @@ gold: - url: https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge title: "Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files" img: https://fastapi.tiangolo.com/img/sponsors/scalar.svg + - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge + title: Auth, user management and more for your B2B product + img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png silver: - url: https://www.deta.sh/?ref=fastapi title: The launchpad for all your (team's) ideas diff --git a/docs/en/docs/img/sponsors/propelauth-banner.png b/docs/en/docs/img/sponsors/propelauth-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..7a1bb2580395a3b4168073114fa076a3fd759cd8 GIT binary patch literal 12806 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 QZ#W?2q?9D9#0`V~2UC@28~^|S literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/propelauth.png b/docs/en/docs/img/sponsors/propelauth.png new file mode 100644 index 0000000000000000000000000000000000000000..8234d631f6b672d206726279a49dbea30088aee8 GIT binary patch literal 28140 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 UF3yy*BLDyZ07*qoM6N<$f*2~LU;qFB literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index c4aea9a8e..476b43676 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -58,6 +58,12 @@ <img class="sponsor-image" src="/img/sponsors/scalar-banner.svg" /> </a> </div> + <div class="item"> + <a title="Auth, user management and more for your B2B product" style="display: block; position: relative;" href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=topbanner" target="_blank"> + <span class="sponsor-badge">sponsor</span> + <img class="sponsor-image" src="/img/sponsors/propelauth-banner.png" /> + </a> + </div> </div> </div> {% endblock %} From 73dcc40f09e3587b10d3a93ee225c2f5d3fc83cb Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 6 Dec 2023 11:34:10 +0000 Subject: [PATCH 1452/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 669f19de7..dcff8a69d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,7 @@ hide: ### Internal +* 🔧 Update sponsors, add PropelAuth. PR [#10760](https://github.com/tiangolo/fastapi/pull/10760) by [@tiangolo](https://github.com/tiangolo). * 👷 Update build docs, verify README on CI. PR [#10750](https://github.com/tiangolo/fastapi/pull/10750) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add Codacy. PR [#10677](https://github.com/tiangolo/fastapi/pull/10677) by [@tiangolo](https://github.com/tiangolo). From 6f5aa81c076d22e38afbe7d602db6730e28bc3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 12 Dec 2023 00:22:47 +0000 Subject: [PATCH 1453/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20mul?= =?UTF-8?q?tiple=20Annotated=20annotations,=20e.g.=20`Annotated[str,=20Fie?= =?UTF-8?q?ld(),=20Query()]`=20(#10773)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 4 +-- fastapi/_compat.py | 7 ++++- fastapi/dependencies/utils.py | 53 +++++++++++++++++++--------------- tests/test_ambiguous_params.py | 27 +++++++++++------ tests/test_annotated.py | 2 +- 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59754525d..7ebb80efd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v06 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v07 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt @@ -62,7 +62,7 @@ jobs: id: cache with: path: ${{ env.pythonLocation }} - key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v06 + key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt', 'requirements-docs-tests.txt') }}-test-v07 - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: pip install -r requirements-tests.txt diff --git a/fastapi/_compat.py b/fastapi/_compat.py index fc605d0ec..35d4a8723 100644 --- a/fastapi/_compat.py +++ b/fastapi/_compat.py @@ -249,7 +249,12 @@ if PYDANTIC_V2: return is_bytes_sequence_annotation(field.type_) def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo: - return type(field_info).from_annotation(annotation) + cls = type(field_info) + merged_field_info = cls.from_annotation(annotation) + new_field_info = copy(field_info) + new_field_info.metadata = merged_field_info.metadata + new_field_info.annotation = merged_field_info.annotation + return new_field_info def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]: origin_type = ( diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 96e07a45c..4e88410a5 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -325,10 +325,11 @@ def analyze_param( field_info = None depends = None type_annotation: Any = Any - if ( - annotation is not inspect.Signature.empty - and get_origin(annotation) is Annotated - ): + use_annotation: Any = Any + if annotation is not inspect.Signature.empty: + use_annotation = annotation + type_annotation = annotation + if get_origin(use_annotation) is Annotated: annotated_args = get_args(annotation) type_annotation = annotated_args[0] fastapi_annotations = [ @@ -336,14 +337,21 @@ def analyze_param( for arg in annotated_args[1:] if isinstance(arg, (FieldInfo, params.Depends)) ] - assert ( - len(fastapi_annotations) <= 1 - ), f"Cannot specify multiple `Annotated` FastAPI arguments for {param_name!r}" - fastapi_annotation = next(iter(fastapi_annotations), None) + fastapi_specific_annotations = [ + arg + for arg in fastapi_annotations + if isinstance(arg, (params.Param, params.Body, params.Depends)) + ] + if fastapi_specific_annotations: + fastapi_annotation: Union[ + FieldInfo, params.Depends, None + ] = fastapi_specific_annotations[-1] + else: + fastapi_annotation = None if isinstance(fastapi_annotation, FieldInfo): # Copy `field_info` because we mutate `field_info.default` below. field_info = copy_field_info( - field_info=fastapi_annotation, annotation=annotation + field_info=fastapi_annotation, annotation=use_annotation ) assert field_info.default is Undefined or field_info.default is Required, ( f"`{field_info.__class__.__name__}` default value cannot be set in" @@ -356,8 +364,6 @@ def analyze_param( field_info.default = Required elif isinstance(fastapi_annotation, params.Depends): depends = fastapi_annotation - elif annotation is not inspect.Signature.empty: - type_annotation = annotation if isinstance(value, params.Depends): assert depends is None, ( @@ -402,15 +408,15 @@ def analyze_param( # We might check here that `default_value is Required`, but the fact is that the same # parameter might sometimes be a path parameter and sometimes not. See # `tests/test_infer_param_optionality.py` for an example. - field_info = params.Path(annotation=type_annotation) + field_info = params.Path(annotation=use_annotation) elif is_uploadfile_or_nonable_uploadfile_annotation( type_annotation ) or is_uploadfile_sequence_annotation(type_annotation): - field_info = params.File(annotation=type_annotation, default=default_value) + field_info = params.File(annotation=use_annotation, default=default_value) elif not field_annotation_is_scalar(annotation=type_annotation): - field_info = params.Body(annotation=type_annotation, default=default_value) + field_info = params.Body(annotation=use_annotation, default=default_value) else: - field_info = params.Query(annotation=type_annotation, default=default_value) + field_info = params.Query(annotation=use_annotation, default=default_value) field = None if field_info is not None: @@ -424,8 +430,8 @@ def analyze_param( and getattr(field_info, "in_", None) is None ): field_info.in_ = params.ParamTypes.query - use_annotation = get_annotation_from_field_info( - type_annotation, + use_annotation_from_field_info = get_annotation_from_field_info( + use_annotation, field_info, param_name, ) @@ -436,7 +442,7 @@ def analyze_param( field_info.alias = alias field = create_response_field( name=param_name, - type_=use_annotation, + type_=use_annotation_from_field_info, default=field_info.default, alias=alias, required=field_info.default in (Required, Undefined), @@ -466,16 +472,17 @@ def is_body_param(*, param_field: ModelField, is_path_param: bool) -> bool: def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None: - field_info = cast(params.Param, field.field_info) - if field_info.in_ == params.ParamTypes.path: + field_info = field.field_info + field_info_in = getattr(field_info, "in_", None) + if field_info_in == params.ParamTypes.path: dependant.path_params.append(field) - elif field_info.in_ == params.ParamTypes.query: + elif field_info_in == params.ParamTypes.query: dependant.query_params.append(field) - elif field_info.in_ == params.ParamTypes.header: + elif field_info_in == params.ParamTypes.header: dependant.header_params.append(field) else: assert ( - field_info.in_ == params.ParamTypes.cookie + field_info_in == params.ParamTypes.cookie ), f"non-body parameters must be in path, query, header or cookie: {field.name}" dependant.cookie_params.append(field) diff --git a/tests/test_ambiguous_params.py b/tests/test_ambiguous_params.py index 42bcc27a1..8a31442eb 100644 --- a/tests/test_ambiguous_params.py +++ b/tests/test_ambiguous_params.py @@ -1,6 +1,8 @@ import pytest from fastapi import Depends, FastAPI, Path from fastapi.param_functions import Query +from fastapi.testclient import TestClient +from fastapi.utils import PYDANTIC_V2 from typing_extensions import Annotated app = FastAPI() @@ -28,18 +30,13 @@ def test_no_annotated_defaults(): pass # pragma: nocover -def test_no_multiple_annotations(): +def test_multiple_annotations(): async def dep(): pass # pragma: nocover - with pytest.raises( - AssertionError, - match="Cannot specify multiple `Annotated` FastAPI arguments for 'foo'", - ): - - @app.get("/") - async def get(foo: Annotated[int, Query(min_length=1), Query()]): - pass # pragma: nocover + @app.get("/multi-query") + async def get(foo: Annotated[int, Query(gt=2), Query(lt=10)]): + return foo with pytest.raises( AssertionError, @@ -64,3 +61,15 @@ def test_no_multiple_annotations(): @app.get("/") async def get3(foo: Annotated[int, Query(min_length=1)] = Depends(dep)): pass # pragma: nocover + + client = TestClient(app) + response = client.get("/multi-query", params={"foo": "5"}) + assert response.status_code == 200 + assert response.json() == 5 + + response = client.get("/multi-query", params={"foo": "123"}) + assert response.status_code == 422 + + if PYDANTIC_V2: + response = client.get("/multi-query", params={"foo": "1"}) + assert response.status_code == 422 diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 541f84bca..2222be978 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -57,7 +57,7 @@ foo_is_short = { { "ctx": {"min_length": 1}, "loc": ["query", "foo"], - "msg": "String should have at least 1 characters", + "msg": "String should have at least 1 character", "type": "string_too_short", "input": "", "url": match_pydantic_error_url("string_too_short"), From ba99214417f4229277aced266f69511d5ce50f6d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 12 Dec 2023 00:23:15 +0000 Subject: [PATCH 1454/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index dcff8a69d..915f70696 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,10 @@ hide: * 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). +### Features + +* ✨ Add support for multiple Annotated annotations, e.g. `Annotated[str, Field(), Query()]`. PR [#10773](https://github.com/tiangolo/fastapi/pull/10773) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Tweak default suggested configs for generating clients. PR [#10736](https://github.com/tiangolo/fastapi/pull/10736) by [@tiangolo](https://github.com/tiangolo). From b98c65cb36b5f6ee159f4b2a99fbc380247b8820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 12 Dec 2023 00:29:03 +0000 Subject: [PATCH 1455/1881] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20None?= =?UTF-8?q?Type=20(#10774)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastapi/types.py b/fastapi/types.py index 7adf565a7..3205654c7 100644 --- a/fastapi/types.py +++ b/fastapi/types.py @@ -6,6 +6,5 @@ from pydantic import BaseModel DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]) UnionType = getattr(types, "UnionType", Union) -NoneType = getattr(types, "UnionType", None) ModelNameMap = Dict[Union[Type[BaseModel], Type[Enum]], str] IncEx = Union[Set[int], Set[str], Dict[int, Any], Dict[str, Any]] From fc51d7e3c7908d3e588ddc57cb942129f8c55d4c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 12 Dec 2023 00:29:29 +0000 Subject: [PATCH 1456/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 915f70696..a3517f51d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,10 @@ hide: * ✨ Add support for multiple Annotated annotations, e.g. `Annotated[str, Field(), Query()]`. PR [#10773](https://github.com/tiangolo/fastapi/pull/10773) by [@tiangolo](https://github.com/tiangolo). +### Refactors + +* 🔥 Remove unused NoneType. PR [#10774](https://github.com/tiangolo/fastapi/pull/10774) by [@tiangolo](https://github.com/tiangolo). + ### Docs * 📝 Tweak default suggested configs for generating clients. PR [#10736](https://github.com/tiangolo/fastapi/pull/10736) by [@tiangolo](https://github.com/tiangolo). From d8185efb6effd36c162bc7ab8e1e2c597fb3b7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 12 Dec 2023 00:32:48 +0000 Subject: [PATCH 1457/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?105.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 5 +++-- fastapi/__init__.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a3517f51d..2d60a6aa9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -5,9 +5,9 @@ hide: # Release Notes -## Latest Changes +## 0.105.0 -* 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). +## Latest Changes ### Features @@ -23,6 +23,7 @@ hide: ### Internal +* 🔧 Update sponsors, add Scalar. PR [#10728](https://github.com/tiangolo/fastapi/pull/10728) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, add PropelAuth. PR [#10760](https://github.com/tiangolo/fastapi/pull/10760) by [@tiangolo](https://github.com/tiangolo). * 👷 Update build docs, verify README on CI. PR [#10750](https://github.com/tiangolo/fastapi/pull/10750) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Fern. PR [#10729](https://github.com/tiangolo/fastapi/pull/10729) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index c81f09b27..dd16ea34d 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.104.1" +__version__ = "0.105.0" from starlette import status as status From 36c26677682c4245183912e00fc057c05cc6cf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 12 Dec 2023 00:34:36 +0000 Subject: [PATCH 1458/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2d60a6aa9..835df984c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -5,10 +5,10 @@ hide: # Release Notes -## 0.105.0 - ## Latest Changes +## 0.105.0 + ### Features * ✨ Add support for multiple Annotated annotations, e.g. `Annotated[str, Field(), Query()]`. PR [#10773](https://github.com/tiangolo/fastapi/pull/10773) by [@tiangolo](https://github.com/tiangolo). From dc2fdd56af0fc9f98179486151650c522530a7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 20 Dec 2023 18:05:37 +0100 Subject: [PATCH 1459/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#10567)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions <github-actions@github.com> --- docs/en/data/github_sponsors.yml | 97 ++++++++++---------- docs/en/data/people.yml | 148 +++++++++++++++++-------------- 2 files changed, 127 insertions(+), 118 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index b9d74ea7b..43b4b8c6b 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,5 +1,8 @@ sponsors: -- - login: bump-sh +- - login: codacy + avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 + url: https://github.com/codacy + - login: bump-sh avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 url: https://github.com/bump-sh - login: cryptapi @@ -11,9 +14,6 @@ sponsors: - login: fern-api avatarUrl: https://avatars.githubusercontent.com/u/102944815?v=4 url: https://github.com/fern-api - - login: nanram22 - avatarUrl: https://avatars.githubusercontent.com/u/116367316?v=4 - url: https://github.com/nanram22 - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -32,12 +32,12 @@ sponsors: - login: deepset-ai avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 url: https://github.com/deepset-ai + - login: databento + avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 + url: https://github.com/databento - login: svix avatarUrl: https://avatars.githubusercontent.com/u/80175132?v=4 url: https://github.com/svix - - login: databento-bot - avatarUrl: https://avatars.githubusercontent.com/u/98378480?u=494f679996e39427f7ddb1a7de8441b7c96fb670&v=4 - url: https://github.com/databento-bot - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes @@ -65,10 +65,7 @@ sponsors: - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie -- - login: moellenbeck - avatarUrl: https://avatars.githubusercontent.com/u/169372?v=4 - url: https://github.com/moellenbeck - - login: birkjernstrom +- - login: birkjernstrom avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 url: https://github.com/birkjernstrom - login: yasyf @@ -83,15 +80,15 @@ sponsors: - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries + - login: deployplex + avatarUrl: https://avatars.githubusercontent.com/u/57115726?v=4 + url: https://github.com/deployplex - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io -- - login: NateXVI - avatarUrl: https://avatars.githubusercontent.com/u/48195620?u=4bc8751ae50cb087c40c1fe811764aa070b9eea6&v=4 - url: https://github.com/NateXVI - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex @@ -125,12 +122,12 @@ sponsors: - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith - - login: mrkmcknz - avatarUrl: https://avatars.githubusercontent.com/u/1089376?u=2b9b8a8c25c33a4f6c220095638bd821cdfd13a3&v=4 - url: https://github.com/mrkmcknz - login: mickaelandrieu avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 url: https://github.com/mickaelandrieu + - login: knallgelb + avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 + url: https://github.com/knallgelb - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 @@ -174,7 +171,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 url: https://github.com/iwpnd - login: FernandoCelmer - avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=ab6108a843a2fb9df0934f482375d2907609f3ff&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=d29fff3fd862fda4ca752079f13f32e84c762ea4&v=4 url: https://github.com/FernandoCelmer - login: simw avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 @@ -188,6 +185,9 @@ sponsors: - login: wdwinslow avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 url: https://github.com/wdwinslow + - login: drcat101 + avatarUrl: https://avatars.githubusercontent.com/u/11951946?u=e714b957185b8cf3d301cced7fc3ad2842122c6a&v=4 + url: https://github.com/drcat101 - login: jsoques avatarUrl: https://avatars.githubusercontent.com/u/12414216?u=620921d94196546cc8b9eae2cc4cbc3f95bab42f&v=4 url: https://github.com/jsoques @@ -212,9 +212,6 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: rahulsalgare - avatarUrl: https://avatars.githubusercontent.com/u/21974430?u=ade6f182b94554ab8491d7421de5e78f711dcaf8&v=4 - url: https://github.com/rahulsalgare - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy @@ -269,9 +266,12 @@ sponsors: - login: pyt3h avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 url: https://github.com/pyt3h -- - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota + - login: apitally + avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 + url: https://github.com/apitally +- - login: getsentry + avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 + url: https://github.com/getsentry - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy @@ -299,6 +299,9 @@ sponsors: - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy + - login: natehouk + avatarUrl: https://avatars.githubusercontent.com/u/805439?u=d8e4be629dc5d7efae7146157e41ee0bd129d9bc&v=4 + url: https://github.com/natehouk - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke @@ -323,9 +326,9 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: nikeee - avatarUrl: https://avatars.githubusercontent.com/u/4068864?u=bbe73151f2b409c120160d032dc9aa6875ef0c4b&v=4 - url: https://github.com/nikeee + - login: erhan + avatarUrl: https://avatars.githubusercontent.com/u/3872888?u=cd9a20fcd33c5598d9d7797a78dedfc9148592f6&v=4 + url: https://github.com/erhan - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa @@ -366,7 +369,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/8574425?u=aad2a9674273c9275fe414d99269b7418d144089&v=4 url: https://github.com/albertkun - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=866a1311e4bd3ec5ae84185c4fcc99f397c883d7&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=05cb2d7c797a02f666805ad4639d9582f31d432c&v=4 url: https://github.com/xncbf - login: DMantis avatarUrl: https://avatars.githubusercontent.com/u/9536869?v=4 @@ -398,9 +401,6 @@ sponsors: - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia - - login: timzaz - avatarUrl: https://avatars.githubusercontent.com/u/19709244?u=264d7db95c28156363760229c30ee1116efd4eeb&v=4 - url: https://github.com/timzaz - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu @@ -419,15 +419,15 @@ sponsors: - login: joerambo avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 url: https://github.com/joerambo - - login: msniezynski - avatarUrl: https://avatars.githubusercontent.com/u/27588547?u=0e3be5ac57dcfdf124f470bcdf74b5bf79af1b6c&v=4 - url: https://github.com/msniezynski - login: rlnchow avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 url: https://github.com/rlnchow - login: mertguvencli avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 url: https://github.com/mertguvencli + - login: White-Mask + avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 + url: https://github.com/White-Mask - login: HosamAlmoghraby avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 url: https://github.com/HosamAlmoghraby @@ -435,14 +435,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=1a104991a2ea90bfe304bc0b9ef191c7e4891a0e&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=ea88e4bd668c984cff1bca3e71ab2deb37fafdc4&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon - - login: miraedbswo - avatarUrl: https://avatars.githubusercontent.com/u/36796047?u=9e7a5b3e558edc61d35d0f9dfac37541bae7f56d&v=4 - url: https://github.com/miraedbswo - login: DSMilestone6538 avatarUrl: https://avatars.githubusercontent.com/u/37230924?u=f299dce910366471523155e0cb213356d34aadc1&v=4 url: https://github.com/DSMilestone6538 @@ -458,9 +455,6 @@ sponsors: - login: ArtyomVancyan avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 url: https://github.com/ArtyomVancyan - - login: josehenriqueroveda - avatarUrl: https://avatars.githubusercontent.com/u/46685746?u=2e672057a7dbe1dba47e57c378fc0cac336022eb&v=4 - url: https://github.com/josehenriqueroveda - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby @@ -473,12 +467,18 @@ sponsors: - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun + - login: fernandosmither + avatarUrl: https://avatars.githubusercontent.com/u/66154723?u=a76a037b5d674938a75d2cff862fb6dfd63ec214&v=4 + url: https://github.com/fernandosmither - login: romabozhanovgithub avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 url: https://github.com/romabozhanovgithub - login: mbukeRepo avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=d2eb23e2b222a3b316c4183b05a3236b32819dc2&v=4 url: https://github.com/mbukeRepo + - login: PelicanQ + avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 + url: https://github.com/PelicanQ - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea @@ -491,18 +491,15 @@ sponsors: - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu - - login: samnimoh - avatarUrl: https://avatars.githubusercontent.com/u/33413170?u=147bc516be6cb647b28d7e3b3fea3a018a331145&v=4 - url: https://github.com/samnimoh + - login: msniezynski + avatarUrl: https://avatars.githubusercontent.com/u/27588547?u=0e3be5ac57dcfdf124f470bcdf74b5bf79af1b6c&v=4 + url: https://github.com/msniezynski - login: danburonline avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 url: https://github.com/danburonline - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: shywn-mrk - avatarUrl: https://avatars.githubusercontent.com/u/51455763?u=389e2608e4056fe5e1f23e9ad56a9415277504d3&v=4 - url: https://github.com/shywn-mrk - - login: almeida-matheus - avatarUrl: https://avatars.githubusercontent.com/u/66216198?u=54335eaa0ced626be5c1ff52fead1ebc032286ec&v=4 - url: https://github.com/almeida-matheus + - login: IvanReyesO7 + avatarUrl: https://avatars.githubusercontent.com/u/74359151?u=4b2c368f71e1411b462a8c2290c920ad35dc1af8&v=4 + url: https://github.com/IvanReyesO7 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index db06cbdaf..2e84f1128 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1868 - prs: 496 + answers: 1870 + prs: 508 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 501 + count: 512 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -26,21 +26,21 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT - login: jgould22 - count: 168 + count: 186 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: euri10 count: 153 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 +- login: iudeen + count: 126 + avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 + url: https://github.com/iudeen - login: phy25 count: 126 avatarUrl: https://avatars.githubusercontent.com/u/331403?v=4 url: https://github.com/phy25 -- login: iudeen - count: 122 - avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 - url: https://github.com/iudeen - login: raphaelauv count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 @@ -61,14 +61,14 @@ experts: count: 49 avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen +- login: yinziyan1206 + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 + url: https://github.com/yinziyan1206 - login: acidjunk count: 45 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 url: https://github.com/acidjunk -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 @@ -77,18 +77,18 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb -- login: yinziyan1206 - count: 44 - avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 - url: https://github.com/yinziyan1206 -- login: odiseo0 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 - url: https://github.com/odiseo0 +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 url: https://github.com/frankie567 +- login: odiseo0 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 + url: https://github.com/odiseo0 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 @@ -101,14 +101,14 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary +- login: n8sty + count: 36 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 url: https://github.com/krishnardt -- login: n8sty - count: 32 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: panla count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 @@ -137,18 +137,22 @@ experts: count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf -- login: JavierSanchezCastro - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro -- login: nsidnev - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: ebottos94 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 +- login: nsidnev + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 + url: https://github.com/nsidnev +- login: JavierSanchezCastro + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: chrisK824 count: 19 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 @@ -161,10 +165,10 @@ experts: count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: ebottos94 +- login: nymous count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -181,39 +185,47 @@ experts: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/16540232?u=05d2beb8e034d584d0a374b99d8826327bd7f614&v=4 url: https://github.com/caeser1996 -- login: nymous - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: jonatasoli - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 - url: https://github.com/jonatasoli - login: dstlny count: 16 avatarUrl: https://avatars.githubusercontent.com/u/41964673?u=9f2174f9d61c15c6e3a4c9e3aeee66f711ce311f&v=4 url: https://github.com/dstlny +- login: jonatasoli + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 + url: https://github.com/jonatasoli - login: abhint count: 15 avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 url: https://github.com/abhint last_month_active: +- login: jgould22 + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: Kludex - count: 8 + count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: White-Mask + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 + url: https://github.com/White-Mask - login: n8sty - count: 7 + count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: chrisK824 +- login: hasansezertasan count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 - url: https://github.com/chrisK824 -- login: danielfcollier + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=e389634d494d503cca867f76c2d00cacc273a46e&v=4 + url: https://github.com/hasansezertasan +- login: pythonweb2 count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/38995330?u=5799be795fc310f75f3a5fe9242307d59b194520&v=4 - url: https://github.com/danielfcollier + avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 + url: https://github.com/pythonweb2 +- login: ebottos94 + count: 3 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 top_contributors: - login: waynerv count: 25 @@ -349,17 +361,17 @@ top_contributors: url: https://github.com/rostik1410 top_reviewers: - login: Kludex - count: 139 + count: 145 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex +- login: BilalAlpaslan + count: 82 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: yezz123 count: 80 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 -- login: BilalAlpaslan - count: 79 - avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 - url: https://github.com/BilalAlpaslan - login: iudeen count: 54 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 @@ -376,14 +388,14 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 +- login: Xewus + count: 47 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd -- login: Xewus - count: 44 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: cikay count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 @@ -412,14 +424,14 @@ top_reviewers: count: 26 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: Ryandaydev + count: 25 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=ba0eea19429e7cf77cf2ab8ad2f3d3af202bc1cf&v=4 + url: https://github.com/Ryandaydev - login: LorhanSohaky count: 24 avatarUrl: https://avatars.githubusercontent.com/u/16273730?u=095b66f243a2cd6a0aadba9a095009f8aaf18393&v=4 url: https://github.com/LorhanSohaky -- login: Ryandaydev - count: 24 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=809f3d1074d04bbc28012a7f17f06ea56f5bd71a&v=4 - url: https://github.com/Ryandaydev - login: dmontagu count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 @@ -476,14 +488,14 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 +- login: peidrao + count: 14 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 + url: https://github.com/peidrao - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 url: https://github.com/sh0nk -- login: peidrao - count: 13 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 - url: https://github.com/peidrao - login: wdh99 count: 13 avatarUrl: https://avatars.githubusercontent.com/u/108172295?u=8a8fb95d5afe3e0fa33257b2aecae88d436249eb&v=4 From e7756ae7dcffa0b73767d354ca158fd6b7bc87ed Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 20 Dec 2023 17:06:01 +0000 Subject: [PATCH 1460/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 835df984c..b8b6f9ae9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +* 👥 Update FastAPI People. PR [#10567](https://github.com/tiangolo/fastapi/pull/10567) by [@tiangolo](https://github.com/tiangolo). + ## 0.105.0 ### Features From a4aa79e0b4cacc6b428d415d04d234a8c77af9d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 25 Dec 2023 18:57:35 +0100 Subject: [PATCH 1461/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20rai?= =?UTF-8?q?sing=20exceptions=20(including=20`HTTPException`)=20in=20depend?= =?UTF-8?q?encies=20with=20`yield`=20in=20the=20exit=20code,=20do=20not=20?= =?UTF-8?q?support=20them=20in=20background=20tasks=20(#10831)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor dependency AsyncExitStack logic, exit dependencies after creating the response, before sending it * ✅ Update tests for dependencies exit, check they are finished before the response is sent * 🔥 Remove ExitAsyncStackMiddleware as it's no longer needed * 📝 Update docs for dependencies with yield * 📝 Update release notes * 📝 Add source examples for new dependencies with yield raising * ✅ Add tests for new dependencies raising after yield * 📝 Update release notes --- docs/en/docs/release-notes.md | 102 ++++++++ .../dependencies/dependencies-with-yield.md | 107 +++++--- docs_src/dependencies/tutorial008b.py | 30 +++ docs_src/dependencies/tutorial008b_an.py | 31 +++ docs_src/dependencies/tutorial008b_an_py39.py | 32 +++ fastapi/applications.py | 52 ---- fastapi/concurrency.py | 1 - fastapi/dependencies/utils.py | 9 +- fastapi/middleware/asyncexitstack.py | 25 -- fastapi/routing.py | 231 ++++++++++-------- pyproject.toml | 9 + tests/test_dependency_contextmanager.py | 20 +- .../test_dependencies/test_tutorial008b.py | 23 ++ .../test_dependencies/test_tutorial008b_an.py | 23 ++ .../test_tutorial008b_an_py39.py | 23 ++ 15 files changed, 498 insertions(+), 220 deletions(-) create mode 100644 docs_src/dependencies/tutorial008b.py create mode 100644 docs_src/dependencies/tutorial008b_an.py create mode 100644 docs_src/dependencies/tutorial008b_an_py39.py delete mode 100644 fastapi/middleware/asyncexitstack.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008b.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008b_an.py create mode 100644 tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b8b6f9ae9..12bc12d26 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,108 @@ hide: ## Latest Changes +### Dependencies with `yield`, `HTTPException` and Background Tasks + +Dependencies with `yield` now can raise `HTTPException` and other exceptions after `yield`. 🎉 + +Read the new docs here: [Dependencies with `yield` and `HTTPException`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-httpexception). + +```Python +from fastapi import Depends, FastAPI, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item +``` + +--- + +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. + +This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. + +Nevertheless, as this would mean waiting for the response to travel through the network while unnecessarily holding a resource in a dependency with yield (for example a database connection), this was changed in FastAPI 0.106.0. + +Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection). + +If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`. + +For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function. + +The sequence of execution before FastAPI 0.106.0 was like this diagram: + +Time flows from top to bottom. And each column is one of the parts interacting or executing code. + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +The new execution flow can be found in the docs: [Execution of dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#execution-of-dependencies-with-yield). + +### Internal + * 👥 Update FastAPI People. PR [#10567](https://github.com/tiangolo/fastapi/pull/10567) by [@tiangolo](https://github.com/tiangolo). ## 0.105.0 diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index fe18f1f1d..4ead4682c 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -1,8 +1,8 @@ # Dependencies with yield -FastAPI supports dependencies that do some <abbr title='sometimes also called "exit", "cleanup", "teardown", "close", "context managers", ...'>extra steps after finishing</abbr>. +FastAPI supports dependencies that do some <abbr title='sometimes also called "exit code", "cleanup code", "teardown code", "closing code", "context manager exit code", etc.'>extra steps after finishing</abbr>. -To do this, use `yield` instead of `return`, and write the extra steps after. +To do this, use `yield` instead of `return`, and write the extra steps (code) after. !!! tip Make sure to use `yield` one single time. @@ -21,7 +21,7 @@ To do this, use `yield` instead of `return`, and write the extra steps after. For example, you could use this to create a database session and close it after finishing. -Only the code prior to and including the `yield` statement is executed before sending a response: +Only the code prior to and including the `yield` statement is executed before creating a response: ```Python hl_lines="2-4" {!../../../docs_src/dependencies/tutorial007.py!} @@ -40,7 +40,7 @@ The code following the `yield` statement is executed after the response has been ``` !!! tip - You can use `async` or normal functions. + You can use `async` or regular functions. **FastAPI** will do the right thing with each, the same as with normal dependencies. @@ -114,7 +114,7 @@ And, in turn, `dependency_b` needs the value from `dependency_a` (here named `de {!> ../../../docs_src/dependencies/tutorial008.py!} ``` -The same way, you could have dependencies with `yield` and `return` mixed. +The same way, you could have some dependencies with `yield` and some other dependencies with `return`, and have some of those depend on some of the others. And you could have a single dependency that requires several other dependencies with `yield`, etc. @@ -131,24 +131,38 @@ You can have any combinations of dependencies that you want. You saw that you can use dependencies with `yield` and have `try` blocks that catch exceptions. -It might be tempting to raise an `HTTPException` or similar in the exit code, after the `yield`. But **it won't work**. - -The exit code in dependencies with `yield` is executed *after* the response is sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} will have already run. There's nothing catching exceptions thrown by your dependencies in the exit code (after the `yield`). - -So, if you raise an `HTTPException` after the `yield`, the default (or any custom) exception handler that catches `HTTPException`s and returns an HTTP 400 response won't be there to catch that exception anymore. - -This is what allows anything set in the dependency (e.g. a DB session) to, for example, be used by background tasks. - -Background tasks are run *after* the response has been sent. So there's no way to raise an `HTTPException` because there's not even a way to change the response that is *already sent*. - -But if a background task creates a DB error, at least you can rollback or cleanly close the session in the dependency with `yield`, and maybe log the error or report it to a remote tracking system. - -If you have some code that you know could raise an exception, do the most normal/"Pythonic" thing and add a `try` block in that section of the code. - -If you have custom exceptions that you would like to handle *before* returning the response and possibly modifying the response, maybe even raising an `HTTPException`, create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. +The same way, you could raise an `HTTPException` or similar in the exit code, after the `yield`. !!! tip - You can still raise exceptions including `HTTPException` *before* the `yield`. But not after. + + This is a somewhat advanced technique, and in most of the cases you won't really need it, as you can raise exceptions (including `HTTPException`) from inside of the rest of your application code, for example, in the *path operation function*. + + But it's there for you if you need it. 🤓 + +=== "Python 3.9+" + + ```Python hl_lines="18-22 31" + {!> ../../../docs_src/dependencies/tutorial008b_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-21 30" + {!> ../../../docs_src/dependencies/tutorial008b_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Prefer to use the `Annotated` version if possible. + + ```Python hl_lines="16-20 29" + {!> ../../../docs_src/dependencies/tutorial008b.py!} + ``` + +An alternative you could use to catch exceptions (and possibly also raise another `HTTPException`) is ot create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + +## Execution of dependencies with `yield` The sequence of execution is more or less like this diagram. Time flows from top to bottom. And each column is one of the parts interacting or executing code. @@ -161,34 +175,30 @@ participant dep as Dep with yield participant operation as Path Operation participant tasks as Background tasks - Note over client,tasks: Can raise exception for dependency, handled after response is sent - Note over client,operation: Can raise HTTPException and can change the response + Note over client,operation: Can raise exceptions, including HTTPException client ->> dep: Start request Note over dep: Run code up to yield - opt raise - dep -->> handler: Raise HTTPException + opt raise Exception + dep -->> handler: Raise Exception handler -->> client: HTTP error response - dep -->> dep: Raise other exception end dep ->> operation: Run dependency, e.g. DB session opt raise - operation -->> dep: Raise HTTPException - dep -->> handler: Auto forward exception + operation -->> dep: Raise Exception (e.g. HTTPException) + opt handle + dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception + dep -->> handler: Auto forward exception + end handler -->> client: HTTP error response - operation -->> dep: Raise other exception - dep -->> handler: Auto forward exception end + operation ->> client: Return response to client Note over client,operation: Response is already sent, can't change it anymore opt Tasks operation -->> tasks: Send background tasks end opt Raise other exception - tasks -->> dep: Raise other exception - end - Note over dep: After yield - opt Handle other exception - dep -->> dep: Handle exception, can't change response. E.g. close DB session. + tasks -->> tasks: Handle exceptions in the background task code end ``` @@ -198,10 +208,33 @@ participant tasks as Background tasks After one of those responses is sent, no other response can be sent. !!! tip - This diagram shows `HTTPException`, but you could also raise any other exception for which you create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. + This diagram shows `HTTPException`, but you could also raise any other exception that you catch in a dependency with `yield` or with a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`, and then **again** to the exception handlers. If there's no exception handler for that exception, it will then be handled by the default internal `ServerErrorMiddleware`, returning a 500 HTTP status code, to let the client know that there was an error in the server. +## Dependencies with `yield`, `HTTPException` and Background Tasks + +!!! warning + You most probably don't need these technical details, you can skip this section and continue below. + + These details are useful mainly if you were using a version of FastAPI prior to 0.106.0 and used resources from dependencies with `yield` in background tasks. + +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. + +This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. + +Nevertheless, as this would mean waiting for the response to travel through the network while unnecessarily holding a resource in a dependency with yield (for example a database connection), this was changed in FastAPI 0.106.0. + +!!! tip + + Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection). + + So, this way you will probably have cleaner code. + +If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`. + +For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function. + ## Context Managers ### What are "Context Managers" @@ -220,7 +253,7 @@ Underneath, the `open("./somefile.txt")` creates an object that is a called a "C When the `with` block finishes, it makes sure to close the file, even if there were exceptions. -When you create a dependency with `yield`, **FastAPI** will internally convert it to a context manager, and combine it with some other related tools. +When you create a dependency with `yield`, **FastAPI** will internally create a context manager for it, and combine it with some other related tools. ### Using context managers in dependencies with `yield` diff --git a/docs_src/dependencies/tutorial008b.py b/docs_src/dependencies/tutorial008b.py new file mode 100644 index 000000000..4a1a70dcf --- /dev/null +++ b/docs_src/dependencies/tutorial008b.py @@ -0,0 +1,30 @@ +from fastapi import Depends, FastAPI, HTTPException + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: str = Depends(get_username)): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/docs_src/dependencies/tutorial008b_an.py b/docs_src/dependencies/tutorial008b_an.py new file mode 100644 index 000000000..3a0f1399a --- /dev/null +++ b/docs_src/dependencies/tutorial008b_an.py @@ -0,0 +1,31 @@ +from fastapi import Depends, FastAPI, HTTPException +from typing_extensions import Annotated + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/docs_src/dependencies/tutorial008b_an_py39.py b/docs_src/dependencies/tutorial008b_an_py39.py new file mode 100644 index 000000000..30c9cdc69 --- /dev/null +++ b/docs_src/dependencies/tutorial008b_an_py39.py @@ -0,0 +1,32 @@ +from typing import Annotated + +from fastapi import Depends, FastAPI, HTTPException + +app = FastAPI() + + +data = { + "plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"}, + "portal-gun": {"description": "Gun to create portals", "owner": "Rick"}, +} + + +class OwnerError(Exception): + pass + + +def get_username(): + try: + yield "Rick" + except OwnerError as e: + raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + + +@app.get("/items/{item_id}") +def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): + if item_id not in data: + raise HTTPException(status_code=404, detail="Item not found") + item = data[item_id] + if item["owner"] != username: + raise OwnerError(username) + return item diff --git a/fastapi/applications.py b/fastapi/applications.py index 3021d7593..597c60a56 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -22,7 +22,6 @@ from fastapi.exception_handlers import ( ) from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError from fastapi.logger import logger -from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( get_redoc_html, get_swagger_ui_html, @@ -37,8 +36,6 @@ from starlette.datastructures import State from starlette.exceptions import HTTPException from starlette.middleware import Middleware from starlette.middleware.base import BaseHTTPMiddleware -from starlette.middleware.errors import ServerErrorMiddleware -from starlette.middleware.exceptions import ExceptionMiddleware from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse, Response from starlette.routing import BaseRoute @@ -966,55 +963,6 @@ class FastAPI(Starlette): self.middleware_stack: Union[ASGIApp, None] = None self.setup() - def build_middleware_stack(self) -> ASGIApp: - # Duplicate/override from Starlette to add AsyncExitStackMiddleware - # inside of ExceptionMiddleware, inside of custom user middlewares - debug = self.debug - error_handler = None - exception_handlers = {} - - for key, value in self.exception_handlers.items(): - if key in (500, Exception): - error_handler = value - else: - exception_handlers[key] = value - - middleware = ( - [Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)] - + self.user_middleware - + [ - Middleware( - ExceptionMiddleware, handlers=exception_handlers, debug=debug - ), - # Add FastAPI-specific AsyncExitStackMiddleware for dependencies with - # contextvars. - # This needs to happen after user middlewares because those create a - # new contextvars context copy by using a new AnyIO task group. - # The initial part of dependencies with 'yield' is executed in the - # FastAPI code, inside all the middlewares. However, the teardown part - # (after 'yield') is executed in the AsyncExitStack in this middleware. - # If the AsyncExitStack lived outside of the custom middlewares and - # contextvars were set in a dependency with 'yield' in that internal - # contextvars context, the values would not be available in the - # outer context of the AsyncExitStack. - # By placing the middleware and the AsyncExitStack here, inside all - # user middlewares, the code before and after 'yield' in dependencies - # with 'yield' is executed in the same contextvars context. Thus, all values - # set in contextvars before 'yield' are still available after 'yield,' as - # expected. - # Additionally, by having this AsyncExitStack here, after the - # ExceptionMiddleware, dependencies can now catch handled exceptions, - # e.g. HTTPException, to customize the teardown code (e.g. DB session - # rollback). - Middleware(AsyncExitStackMiddleware), - ] - ) - - app = self.router - for cls, options in reversed(middleware): - app = cls(app=app, **options) - return app - def openapi(self) -> Dict[str, Any]: """ Generate the OpenAPI schema of the application. This is called by FastAPI diff --git a/fastapi/concurrency.py b/fastapi/concurrency.py index 754061c86..894bd3ed1 100644 --- a/fastapi/concurrency.py +++ b/fastapi/concurrency.py @@ -1,4 +1,3 @@ -from contextlib import AsyncExitStack as AsyncExitStack # noqa from contextlib import asynccontextmanager as asynccontextmanager from typing import AsyncGenerator, ContextManager, TypeVar diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4e88410a5..b73473484 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,5 +1,5 @@ import inspect -from contextlib import contextmanager +from contextlib import AsyncExitStack, contextmanager from copy import deepcopy from typing import ( Any, @@ -46,7 +46,6 @@ from fastapi._compat import ( ) from fastapi.background import BackgroundTasks from fastapi.concurrency import ( - AsyncExitStack, asynccontextmanager, contextmanager_in_threadpool, ) @@ -529,6 +528,7 @@ async def solve_dependencies( response: Optional[Response] = None, dependency_overrides_provider: Optional[Any] = None, dependency_cache: Optional[Dict[Tuple[Callable[..., Any], Tuple[str]], Any]] = None, + async_exit_stack: AsyncExitStack, ) -> Tuple[ Dict[str, Any], List[Any], @@ -575,6 +575,7 @@ async def solve_dependencies( response=response, dependency_overrides_provider=dependency_overrides_provider, dependency_cache=dependency_cache, + async_exit_stack=async_exit_stack, ) ( sub_values, @@ -590,10 +591,8 @@ async def solve_dependencies( if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache: solved = dependency_cache[sub_dependant.cache_key] elif is_gen_callable(call) or is_async_gen_callable(call): - stack = request.scope.get("fastapi_astack") - assert isinstance(stack, AsyncExitStack) solved = await solve_generator( - call=call, stack=stack, sub_values=sub_values + call=call, stack=async_exit_stack, sub_values=sub_values ) elif is_coroutine_callable(call): solved = await call(**sub_values) diff --git a/fastapi/middleware/asyncexitstack.py b/fastapi/middleware/asyncexitstack.py deleted file mode 100644 index 30a0ae626..000000000 --- a/fastapi/middleware/asyncexitstack.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Optional - -from fastapi.concurrency import AsyncExitStack -from starlette.types import ASGIApp, Receive, Scope, Send - - -class AsyncExitStackMiddleware: - def __init__(self, app: ASGIApp, context_name: str = "fastapi_astack") -> None: - self.app = app - self.context_name = context_name - - async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - dependency_exception: Optional[Exception] = None - async with AsyncExitStack() as stack: - scope[self.context_name] = stack - try: - await self.app(scope, receive, send) - except Exception as e: - dependency_exception = e - raise e - if dependency_exception: - # This exception was possibly handled by the dependency but it should - # still bubble up so that the ServerErrorMiddleware can return a 500 - # or the ExceptionMiddleware can catch and handle any other exceptions - raise dependency_exception diff --git a/fastapi/routing.py b/fastapi/routing.py index 54d53bbbf..589ecca2a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -216,95 +216,124 @@ def get_request_handler( actual_response_class = response_class async def app(request: Request) -> Response: - try: - body: Any = None - if body_field: - if is_body_form: - body = await request.form() - stack = request.scope.get("fastapi_astack") - assert isinstance(stack, AsyncExitStack) - stack.push_async_callback(body.close) + exception_to_reraise: Optional[Exception] = None + response: Union[Response, None] = None + async with AsyncExitStack() as async_exit_stack: + # TODO: remove this scope later, after a few releases + # This scope fastapi_astack is no longer used by FastAPI, kept for + # compatibility, just in case + request.scope["fastapi_astack"] = async_exit_stack + try: + body: Any = None + if body_field: + if is_body_form: + body = await request.form() + async_exit_stack.push_async_callback(body.close) + else: + body_bytes = await request.body() + if body_bytes: + json_body: Any = Undefined + content_type_value = request.headers.get("content-type") + if not content_type_value: + json_body = await request.json() + else: + message = email.message.Message() + message["content-type"] = content_type_value + if message.get_content_maintype() == "application": + subtype = message.get_content_subtype() + if subtype == "json" or subtype.endswith("+json"): + json_body = await request.json() + if json_body != Undefined: + body = json_body + else: + body = body_bytes + except json.JSONDecodeError as e: + validation_error = RequestValidationError( + [ + { + "type": "json_invalid", + "loc": ("body", e.pos), + "msg": "JSON decode error", + "input": {}, + "ctx": {"error": e.msg}, + } + ], + body=e.doc, + ) + exception_to_reraise = validation_error + raise validation_error from e + except HTTPException as e: + exception_to_reraise = e + raise + except Exception as e: + http_error = HTTPException( + status_code=400, detail="There was an error parsing the body" + ) + exception_to_reraise = http_error + raise http_error from e + try: + solved_result = await solve_dependencies( + request=request, + dependant=dependant, + body=body, + dependency_overrides_provider=dependency_overrides_provider, + async_exit_stack=async_exit_stack, + ) + values, errors, background_tasks, sub_response, _ = solved_result + except Exception as e: + exception_to_reraise = e + raise e + if errors: + validation_error = RequestValidationError( + _normalize_errors(errors), body=body + ) + exception_to_reraise = validation_error + raise validation_error + else: + try: + raw_response = await run_endpoint_function( + dependant=dependant, values=values, is_coroutine=is_coroutine + ) + except Exception as e: + exception_to_reraise = e + raise e + if isinstance(raw_response, Response): + if raw_response.background is None: + raw_response.background = background_tasks + response = raw_response else: - body_bytes = await request.body() - if body_bytes: - json_body: Any = Undefined - content_type_value = request.headers.get("content-type") - if not content_type_value: - json_body = await request.json() - else: - message = email.message.Message() - message["content-type"] = content_type_value - if message.get_content_maintype() == "application": - subtype = message.get_content_subtype() - if subtype == "json" or subtype.endswith("+json"): - json_body = await request.json() - if json_body != Undefined: - body = json_body - else: - body = body_bytes - except json.JSONDecodeError as e: - raise RequestValidationError( - [ - { - "type": "json_invalid", - "loc": ("body", e.pos), - "msg": "JSON decode error", - "input": {}, - "ctx": {"error": e.msg}, - } - ], - body=e.doc, - ) from e - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=400, detail="There was an error parsing the body" - ) from e - solved_result = await solve_dependencies( - request=request, - dependant=dependant, - body=body, - dependency_overrides_provider=dependency_overrides_provider, - ) - values, errors, background_tasks, sub_response, _ = solved_result - if errors: - raise RequestValidationError(_normalize_errors(errors), body=body) - else: - raw_response = await run_endpoint_function( - dependant=dependant, values=values, is_coroutine=is_coroutine - ) - - if isinstance(raw_response, Response): - if raw_response.background is None: - raw_response.background = background_tasks - return raw_response - response_args: Dict[str, Any] = {"background": background_tasks} - # If status_code was set, use it, otherwise use the default from the - # response class, in the case of redirect it's 307 - current_status_code = ( - status_code if status_code else sub_response.status_code - ) - if current_status_code is not None: - response_args["status_code"] = current_status_code - if sub_response.status_code: - response_args["status_code"] = sub_response.status_code - content = await serialize_response( - field=response_field, - response_content=raw_response, - include=response_model_include, - exclude=response_model_exclude, - by_alias=response_model_by_alias, - exclude_unset=response_model_exclude_unset, - exclude_defaults=response_model_exclude_defaults, - exclude_none=response_model_exclude_none, - is_coroutine=is_coroutine, - ) - response = actual_response_class(content, **response_args) - if not is_body_allowed_for_status_code(response.status_code): - response.body = b"" - response.headers.raw.extend(sub_response.headers.raw) - return response + response_args: Dict[str, Any] = {"background": background_tasks} + # If status_code was set, use it, otherwise use the default from the + # response class, in the case of redirect it's 307 + current_status_code = ( + status_code if status_code else sub_response.status_code + ) + if current_status_code is not None: + response_args["status_code"] = current_status_code + if sub_response.status_code: + response_args["status_code"] = sub_response.status_code + content = await serialize_response( + field=response_field, + response_content=raw_response, + include=response_model_include, + exclude=response_model_exclude, + by_alias=response_model_by_alias, + exclude_unset=response_model_exclude_unset, + exclude_defaults=response_model_exclude_defaults, + exclude_none=response_model_exclude_none, + is_coroutine=is_coroutine, + ) + response = actual_response_class(content, **response_args) + if not is_body_allowed_for_status_code(response.status_code): + response.body = b"" + response.headers.raw.extend(sub_response.headers.raw) + # This exception was possibly handled by the dependency but it should + # still bubble up so that the ServerErrorMiddleware can return a 500 + # or the ExceptionMiddleware can catch and handle any other exceptions + if exception_to_reraise: + raise exception_to_reraise + assert response is not None, "An error occurred while generating the request" + return response return app @@ -313,16 +342,22 @@ def get_websocket_app( dependant: Dependant, dependency_overrides_provider: Optional[Any] = None ) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]: async def app(websocket: WebSocket) -> None: - solved_result = await solve_dependencies( - request=websocket, - dependant=dependant, - dependency_overrides_provider=dependency_overrides_provider, - ) - values, errors, _, _2, _3 = solved_result - if errors: - raise WebSocketRequestValidationError(_normalize_errors(errors)) - assert dependant.call is not None, "dependant.call must be a function" - await dependant.call(**values) + async with AsyncExitStack() as async_exit_stack: + # TODO: remove this scope later, after a few releases + # This scope fastapi_astack is no longer used by FastAPI, kept for + # compatibility, just in case + websocket.scope["fastapi_astack"] = async_exit_stack + solved_result = await solve_dependencies( + request=websocket, + dependant=dependant, + dependency_overrides_provider=dependency_overrides_provider, + async_exit_stack=async_exit_stack, + ) + values, errors, _, _2, _3 = solved_result + if errors: + raise WebSocketRequestValidationError(_normalize_errors(errors)) + assert dependant.call is not None, "dependant.call must be a function" + await dependant.call(**values) return app diff --git a/pyproject.toml b/pyproject.toml index e67486ae3..fa072e595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,12 @@ module = "fastapi.tests.*" ignore_missing_imports = true check_untyped_defs = true +[[tool.mypy.overrides]] +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + [tool.pytest.ini_options] addopts = [ "--strict-config", @@ -167,6 +173,9 @@ ignore = [ "docs_src/security/tutorial005_an_py39.py" = ["B904"] "docs_src/security/tutorial005_py310.py" = ["B904"] "docs_src/security/tutorial005_py39.py" = ["B904"] +"docs_src/dependencies/tutorial008b.py" = ["B904"] +"docs_src/dependencies/tutorial008b_an.py" = ["B904"] +"docs_src/dependencies/tutorial008b_an_py39.py" = ["B904"] [tool.ruff.isort] diff --git a/tests/test_dependency_contextmanager.py b/tests/test_dependency_contextmanager.py index 03ef56c4d..b07f9aa5b 100644 --- a/tests/test_dependency_contextmanager.py +++ b/tests/test_dependency_contextmanager.py @@ -1,7 +1,9 @@ +import json from typing import Dict import pytest from fastapi import BackgroundTasks, Depends, FastAPI +from fastapi.responses import StreamingResponse from fastapi.testclient import TestClient app = FastAPI() @@ -200,6 +202,13 @@ async def get_sync_context_b_bg( return state +@app.middleware("http") +async def middleware(request, call_next): + response: StreamingResponse = await call_next(request) + response.headers["x-state"] = json.dumps(state.copy()) + return response + + client = TestClient(app) @@ -274,9 +283,13 @@ def test_background_tasks(): assert data["context_b"] == "started b" assert data["context_a"] == "started a" assert data["bg"] == "not set" + middleware_state = json.loads(response.headers["x-state"]) + assert middleware_state["context_b"] == "finished b with a: started a" + assert middleware_state["context_a"] == "finished a" + assert middleware_state["bg"] == "not set" assert state["context_b"] == "finished b with a: started a" assert state["context_a"] == "finished a" - assert state["bg"] == "bg set - b: started b - a: started a" + assert state["bg"] == "bg set - b: finished b with a: started a - a: finished a" def test_sync_raise_raises(): @@ -382,4 +395,7 @@ def test_sync_background_tasks(): assert data["sync_bg"] == "not set" assert state["context_b"] == "finished b with a: started a" assert state["context_a"] == "finished a" - assert state["sync_bg"] == "sync_bg set - b: started b - a: started a" + assert ( + state["sync_bg"] + == "sync_bg set - b: finished b with a: started a - a: finished a" + ) diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b.py b/tests/test_tutorial/test_dependencies/test_tutorial008b.py new file mode 100644 index 000000000..ed4f4aaca --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Onwer error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py new file mode 100644 index 000000000..aa76ad6af --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b_an import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Onwer error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py new file mode 100644 index 000000000..aa76ad6af --- /dev/null +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py @@ -0,0 +1,23 @@ +from fastapi.testclient import TestClient + +from docs_src.dependencies.tutorial008b_an import app + +client = TestClient(app) + + +def test_get_no_item(): + response = client.get("/items/foo") + assert response.status_code == 404, response.text + assert response.json() == {"detail": "Item not found"} + + +def test_owner_error(): + response = client.get("/items/plumbus") + assert response.status_code == 400, response.text + assert response.json() == {"detail": "Onwer error: Rick"} + + +def test_get_item(): + response = client.get("/items/portal-gun") + assert response.status_code == 200, response.text + assert response.json() == {"description": "Gun to create portals", "owner": "Rick"} From 678bed2fc9cbf38ba7a6ba9782e5be1a33d852c5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 25 Dec 2023 17:57:54 +0000 Subject: [PATCH 1462/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 12bc12d26..bc6fd9d11 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -107,6 +107,10 @@ participant tasks as Background tasks The new execution flow can be found in the docs: [Execution of dependencies with `yield`](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#execution-of-dependencies-with-yield). +### Features + +* ✨ Add support for raising exceptions (including `HTTPException`) in dependencies with `yield` in the exit code, do not support them in background tasks. PR [#10831](https://github.com/tiangolo/fastapi/pull/10831) by [@tiangolo](https://github.com/tiangolo). + ### Internal * 👥 Update FastAPI People. PR [#10567](https://github.com/tiangolo/fastapi/pull/10567) by [@tiangolo](https://github.com/tiangolo). From bcd5a424cdca5973a2f4927de83c8eb2ca658011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 25 Dec 2023 19:00:47 +0100 Subject: [PATCH 1463/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bc6fd9d11..a2424a383 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,12 @@ hide: ## Latest Changes +### Breaking Changes + +Using resources from dependencies with `yield` in background tasks is no longer supported. + +This change is what supports the new features, read below. 🤓 + ### Dependencies with `yield`, `HTTPException` and Background Tasks Dependencies with `yield` now can raise `HTTPException` and other exceptions after `yield`. 🎉 From 91510db62012b817627833e896ddb579056c5922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 25 Dec 2023 19:01:26 +0100 Subject: [PATCH 1464/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?106.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index a2424a383..fc38cb475 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.106.0 + ### Breaking Changes Using resources from dependencies with `yield` in background tasks is no longer supported. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index dd16ea34d..4348bd98e 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.105.0" +__version__ = "0.106.0" from starlette import status as status From 5826c4f31f8b6e0d8a289d4c5a6c5a7f8ccb263a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 25 Dec 2023 19:06:04 +0100 Subject: [PATCH 1465/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20release=20note?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fc38cb475..0c118e7c9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -57,7 +57,7 @@ def get_item(item_id: str, username: Annotated[str, Depends(get_username)]): --- -Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run. +Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers) would have already run. This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished. From 8b5843ebcd1ccc9582ef34b9dba0a9162140396b Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 26 Dec 2023 12:13:50 -0500 Subject: [PATCH 1466/1881] =?UTF-8?q?=F0=9F=93=9D=20Restructure=20Docs=20s?= =?UTF-8?q?ection=20in=20Contributing=20page=20(#10844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 Restructure Docs section in Contributing page --- docs/en/docs/contributing.md | 58 ++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index cfdb607d7..35bc1c501 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -150,32 +150,7 @@ For it to sort them correctly, you need to have FastAPI installed locally in you First, make sure you set up your environment as described above, that will install all the requirements. -The documentation uses <a href="https://www.mkdocs.org/" class="external-link" target="_blank">MkDocs</a>. - -And there are extra tools/scripts in place to handle translations in `./scripts/docs.py`. - -!!! tip - You don't need to see the code in `./scripts/docs.py`, you just use it in the command line. - -All the documentation is in Markdown format in the directory `./docs/en/`. - -Many of the tutorials have blocks of code. - -In most of the cases, these blocks of code are actual complete applications that can be run as is. - -In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. - -And those Python files are included/injected in the documentation when generating the site. - -### Docs for tests - -Most of the tests actually run against the example source files in the documentation. - -This helps making sure that: - -* The documentation is up to date. -* The documentation examples can be run as is. -* Most of the features are covered by the documentation, ensured by test coverage. +### Docs live During local development, there is a script that builds the site and checks for any changes, live-reloading: @@ -229,6 +204,37 @@ Completion will take effect once you restart the terminal. </div> +### Docs Structure + +The documentation uses <a href="https://www.mkdocs.org/" class="external-link" target="_blank">MkDocs</a>. + +And there are extra tools/scripts in place to handle translations in `./scripts/docs.py`. + +!!! tip + You don't need to see the code in `./scripts/docs.py`, you just use it in the command line. + +All the documentation is in Markdown format in the directory `./docs/en/`. + +Many of the tutorials have blocks of code. + +In most of the cases, these blocks of code are actual complete applications that can be run as is. + +In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. + +And those Python files are included/injected in the documentation when generating the site. + +### Docs for tests + +Most of the tests actually run against the example source files in the documentation. + +This helps making sure that: + +* The documentation is up to date. +* The documentation examples can be run as is. +* Most of the features are covered by the documentation, ensured by test coverage. + + + ### Apps and docs at the same time If you run the examples with, e.g.: From 4de60e153a73ee51e6be3378b64fc16bbd251d8a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 26 Dec 2023 17:14:13 +0000 Subject: [PATCH 1467/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0c118e7c9..b5fbdfa45 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Restructure Docs section in Contributing page. PR [#10844](https://github.com/tiangolo/fastapi/pull/10844) by [@alejsdev](https://github.com/alejsdev). + ## 0.106.0 ### Breaking Changes From 505ae06c0bb2ba745587731ca88e4165b64003a6 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 26 Dec 2023 12:23:20 -0500 Subject: [PATCH 1468/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20docs:=20Node.js?= =?UTF-8?q?=20script=20alternative=20to=20update=20OpenAPI=20for=20generat?= =?UTF-8?q?ed=20clients=20(#10845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/generate-clients.md | 14 ++++++++--- docs_src/generate_clients/tutorial004.js | 29 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 docs_src/generate_clients/tutorial004.js diff --git a/docs/en/docs/advanced/generate-clients.md b/docs/en/docs/advanced/generate-clients.md index e8d771f71..3a810baee 100644 --- a/docs/en/docs/advanced/generate-clients.md +++ b/docs/en/docs/advanced/generate-clients.md @@ -229,9 +229,17 @@ But for the generated client we could **modify** the OpenAPI operation IDs right We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this: -```Python -{!../../../docs_src/generate_clients/tutorial004.py!} -``` +=== "Python" + + ```Python + {!> ../../../docs_src/generate_clients/tutorial004.py!} + ``` + +=== "Node.js" + + ```Python + {!> ../../../docs_src/generate_clients/tutorial004.js!} + ``` With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names. diff --git a/docs_src/generate_clients/tutorial004.js b/docs_src/generate_clients/tutorial004.js new file mode 100644 index 000000000..18dc38267 --- /dev/null +++ b/docs_src/generate_clients/tutorial004.js @@ -0,0 +1,29 @@ +import * as fs from "fs"; + +const filePath = "./openapi.json"; + +fs.readFile(filePath, (err, data) => { + const openapiContent = JSON.parse(data); + if (err) throw err; + + const paths = openapiContent.paths; + + Object.keys(paths).forEach((pathKey) => { + const pathData = paths[pathKey]; + Object.keys(pathData).forEach((method) => { + const operation = pathData[method]; + if (operation.tags && operation.tags.length > 0) { + const tag = operation.tags[0]; + const operationId = operation.operationId; + const toRemove = `${tag}-`; + if (operationId.startsWith(toRemove)) { + const newOperationId = operationId.substring(toRemove.length); + operation.operationId = newOperationId; + } + } + }); + }); + fs.writeFile(filePath, JSON.stringify(openapiContent, null, 2), (err) => { + if (err) throw err; + }); +}); From a751032c0929cdc4a891666240f0dc4849cf58a9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 26 Dec 2023 17:23:45 +0000 Subject: [PATCH 1469/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b5fbdfa45..f683b1802 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add docs: Node.js script alternative to update OpenAPI for generated clients. PR [#10845](https://github.com/tiangolo/fastapi/pull/10845) by [@alejsdev](https://github.com/alejsdev). * 📝 Restructure Docs section in Contributing page. PR [#10844](https://github.com/tiangolo/fastapi/pull/10844) by [@alejsdev](https://github.com/alejsdev). ## 0.106.0 From d633953f13d70706daa3bbc03f6b3ab66648e02f Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 26 Dec 2023 20:03:07 +0100 Subject: [PATCH 1470/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20to=200.28.0=20(#9636)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa072e595..499d0e5dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.27.0,<0.28.0", + "starlette>=0.28.0,<0.29.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", # TODO: remove this pin after upgrading Starlette 0.31.1 From 9090bf4084ac3bd9527a938a5dc814b139740a6b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 26 Dec 2023 19:03:31 +0000 Subject: [PATCH 1471/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f683b1802..d165ba52f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Upgrade Starlette to 0.28.0. PR [#9636](https://github.com/tiangolo/fastapi/pull/9636) by [@adriangb](https://github.com/adriangb). + ### Docs * 📝 Add docs: Node.js script alternative to update OpenAPI for generated clients. PR [#10845](https://github.com/tiangolo/fastapi/pull/10845) by [@alejsdev](https://github.com/alejsdev). From f933fd6ff8db06ff77be8406ad56b6b07f13a532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 26 Dec 2023 20:04:08 +0100 Subject: [PATCH 1472/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?107.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d165ba52f..f7b6207c2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.107.0 + ### Upgrades * ⬆️ Upgrade Starlette to 0.28.0. PR [#9636](https://github.com/tiangolo/fastapi/pull/9636) by [@adriangb](https://github.com/adriangb). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 4348bd98e..8a22b74b6 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.106.0" +__version__ = "0.107.0" from starlette import status as status From c55f90df32101006ad3ddd8060d30f24c8f44eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 26 Dec 2023 21:12:34 +0100 Subject: [PATCH 1473/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20to=20`>=3D0.29.0,<0.33.0`,=20update=20docs=20and=20usage?= =?UTF-8?q?=20of=20templates=20with=20new=20Starlette=20arguments=20(#1084?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 Update docs for compatibility with Starlette 0.29.0 and new template arguments * ⬆️ Upgrade Starlette to >=0.29.0,<0.33.0 * 📌 Remove AnyIO pin --- docs/em/docs/advanced/templates.md | 2 +- docs/en/docs/advanced/templates.md | 10 ++++++---- docs_src/templates/tutorial001.py | 4 +++- pyproject.toml | 4 +--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/em/docs/advanced/templates.md b/docs/em/docs/advanced/templates.md index 1fb57725a..0a73a4f47 100644 --- a/docs/em/docs/advanced/templates.md +++ b/docs/em/docs/advanced/templates.md @@ -27,7 +27,7 @@ $ pip install jinja2 * 📣 `Request` 🔢 *➡ 🛠️* 👈 🔜 📨 📄. * ⚙️ `templates` 👆 ✍ ✍ & 📨 `TemplateResponse`, 🚶‍♀️ `request` 1️⃣ 🔑-💲 👫 Jinja2️⃣ "🔑". -```Python hl_lines="4 11 15-16" +```Python hl_lines="4 11 15-18" {!../../../docs_src/templates/tutorial001.py!} ``` diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index 38618aeeb..583abda7f 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -25,14 +25,16 @@ $ pip install jinja2 * Import `Jinja2Templates`. * Create a `templates` object that you can re-use later. * Declare a `Request` parameter in the *path operation* that will return a template. -* Use the `templates` you created to render and return a `TemplateResponse`, passing the `request` as one of the key-value pairs in the Jinja2 "context". +* Use the `templates` you created to render and return a `TemplateResponse`, pass the name of the template, the request object, and a "context" dictionary with key-value pairs to be used inside of the Jinja2 template. -```Python hl_lines="4 11 15-16" +```Python hl_lines="4 11 15-18" {!../../../docs_src/templates/tutorial001.py!} ``` !!! note - Notice that you have to pass the `request` as part of the key-value pairs in the context for Jinja2. So, you also have to declare it in your *path operation*. + Before FastAPI 0.108.0, Starlette 0.29.0, the `name` was the first parameter. + + Also, before that, in previous versions, the `request` object was passed as part of the key-value pairs in the context for Jinja2. !!! tip By declaring `response_class=HTMLResponse` the docs UI will be able to know that the response will be HTML. @@ -58,7 +60,7 @@ It will show the `id` taken from the "context" `dict` you passed: ## Templates and static files -And you can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted. +You can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted. ```jinja hl_lines="4" {!../../../docs_src/templates/templates/item.html!} diff --git a/docs_src/templates/tutorial001.py b/docs_src/templates/tutorial001.py index 245e7110b..81ccc8d4d 100644 --- a/docs_src/templates/tutorial001.py +++ b/docs_src/templates/tutorial001.py @@ -13,4 +13,6 @@ templates = Jinja2Templates(directory="templates") @app.get("/items/{id}", response_class=HTMLResponse) async def read_item(request: Request, id: str): - return templates.TemplateResponse("item.html", {"request": request, "id": id}) + return templates.TemplateResponse( + request=request, name="item.html", context={"id": id} + ) diff --git a/pyproject.toml b/pyproject.toml index 499d0e5dc..38728d99e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,11 +40,9 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.28.0,<0.29.0", + "starlette>=0.29.0,<0.33.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", - # TODO: remove this pin after upgrading Starlette 0.31.1 - "anyio>=3.7.1,<4.0.0", ] dynamic = ["version"] From 43e2223804d79a4c7309660a411487f0fa47c1c7 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 26 Dec 2023 20:12:59 +0000 Subject: [PATCH 1474/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f7b6207c2..f82577e0c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Upgrade Starlette to `>=0.29.0,<0.33.0`, update docs and usage of templates with new Starlette arguments. PR [#10846](https://github.com/tiangolo/fastapi/pull/10846) by [@tiangolo](https://github.com/tiangolo). + ## 0.107.0 ### Upgrades From fe0249a23ebb294be183b3e2cab82addbd68c42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 26 Dec 2023 21:17:18 +0100 Subject: [PATCH 1475/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?108.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index f82577e0c..3c7936ea9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.108.0 + ### Upgrades * ⬆️ Upgrade Starlette to `>=0.29.0,<0.33.0`, update docs and usage of templates with new Starlette arguments. PR [#10846](https://github.com/tiangolo/fastapi/pull/10846) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 8a22b74b6..02ac83b5e 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.107.0" +__version__ = "0.108.0" from starlette import status as status From dd790c34ff6f92dba68bef0f46a4a18ba7015f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 26 Dec 2023 21:37:34 +0100 Subject: [PATCH 1476/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20dependencies=20with=20yield=20source=20examples=20(#10847)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/dependencies/tutorial008b.py | 2 +- docs_src/dependencies/tutorial008b_an.py | 2 +- docs_src/dependencies/tutorial008b_an_py39.py | 2 +- tests/test_tutorial/test_dependencies/test_tutorial008b.py | 2 +- tests/test_tutorial/test_dependencies/test_tutorial008b_an.py | 2 +- .../test_dependencies/test_tutorial008b_an_py39.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs_src/dependencies/tutorial008b.py b/docs_src/dependencies/tutorial008b.py index 4a1a70dcf..163e96600 100644 --- a/docs_src/dependencies/tutorial008b.py +++ b/docs_src/dependencies/tutorial008b.py @@ -17,7 +17,7 @@ def get_username(): try: yield "Rick" except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + raise HTTPException(status_code=400, detail=f"Owner error: {e}") @app.get("/items/{item_id}") diff --git a/docs_src/dependencies/tutorial008b_an.py b/docs_src/dependencies/tutorial008b_an.py index 3a0f1399a..84d8f12c1 100644 --- a/docs_src/dependencies/tutorial008b_an.py +++ b/docs_src/dependencies/tutorial008b_an.py @@ -18,7 +18,7 @@ def get_username(): try: yield "Rick" except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + raise HTTPException(status_code=400, detail=f"Owner error: {e}") @app.get("/items/{item_id}") diff --git a/docs_src/dependencies/tutorial008b_an_py39.py b/docs_src/dependencies/tutorial008b_an_py39.py index 30c9cdc69..3b8434c81 100644 --- a/docs_src/dependencies/tutorial008b_an_py39.py +++ b/docs_src/dependencies/tutorial008b_an_py39.py @@ -19,7 +19,7 @@ def get_username(): try: yield "Rick" except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + raise HTTPException(status_code=400, detail=f"Owner error: {e}") @app.get("/items/{item_id}") diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b.py b/tests/test_tutorial/test_dependencies/test_tutorial008b.py index ed4f4aaca..86acba9e4 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b.py @@ -14,7 +14,7 @@ def test_get_no_item(): def test_owner_error(): response = client.get("/items/plumbus") assert response.status_code == 400, response.text - assert response.json() == {"detail": "Onwer error: Rick"} + assert response.json() == {"detail": "Owner error: Rick"} def test_get_item(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py index aa76ad6af..7f51fc52a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an.py @@ -14,7 +14,7 @@ def test_get_no_item(): def test_owner_error(): response = client.get("/items/plumbus") assert response.status_code == 400, response.text - assert response.json() == {"detail": "Onwer error: Rick"} + assert response.json() == {"detail": "Owner error: Rick"} def test_get_item(): diff --git a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py index aa76ad6af..7f51fc52a 100644 --- a/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py +++ b/tests/test_tutorial/test_dependencies/test_tutorial008b_an_py39.py @@ -14,7 +14,7 @@ def test_get_no_item(): def test_owner_error(): response = client.get("/items/plumbus") assert response.status_code == 400, response.text - assert response.json() == {"detail": "Onwer error: Rick"} + assert response.json() == {"detail": "Owner error: Rick"} def test_get_item(): From 84d400b9164f0a1727322de154855023b5eee73c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 26 Dec 2023 20:37:55 +0000 Subject: [PATCH 1477/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3c7936ea9..d17e62414 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). + ## 0.108.0 ### Upgrades From 040ad986d48bb9a400de804f7f25abb856d85e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 26 Dec 2023 21:47:18 +0100 Subject: [PATCH 1478/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d17e62414..bb96bce66 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -63,7 +63,7 @@ def get_username(): try: yield "Rick" except OwnerError as e: - raise HTTPException(status_code=400, detail=f"Onwer error: {e}") + raise HTTPException(status_code=400, detail=f"Owner error: {e}") @app.get("/items/{item_id}") From 1780c21e7ad8f4db048c443cdf9b03edfd34a1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 8 Jan 2024 22:49:53 +0400 Subject: [PATCH 1479/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20GitHub?= =?UTF-8?q?=20Action=20label-approved=20(#10905)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/label-approved.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 2113c468a..1138e6043 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -13,6 +13,6 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: docker://tiangolo/label-approved:0.0.2 + - uses: docker://tiangolo/label-approved:0.0.3 with: token: ${{ secrets.FASTAPI_LABEL_APPROVED }} From 04016d3bf93927f0358a8ceaa7c6cf6669b709bd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 8 Jan 2024 18:50:12 +0000 Subject: [PATCH 1480/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bb96bce66..4cae0c5a3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). +### Internal + +* ⬆️ Upgrade GitHub Action label-approved. PR [#10905](https://github.com/tiangolo/fastapi/pull/10905) by [@tiangolo](https://github.com/tiangolo). + ## 0.108.0 ### Upgrades From 3c7685273f799ad1d931be0923343320bbb1ca33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 9 Jan 2024 18:21:10 +0400 Subject: [PATCH 1481/1881] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20GitHub=20Act?= =?UTF-8?q?ion=20label-approved=20(#10913)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/label-approved.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 1138e6043..62daf2608 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -3,6 +3,7 @@ name: Label Approved on: schedule: - cron: "0 12 * * *" + workflow_dispatch: jobs: label-approved: @@ -13,6 +14,6 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: docker://tiangolo/label-approved:0.0.3 + - uses: docker://tiangolo/label-approved:0.0.4 with: token: ${{ secrets.FASTAPI_LABEL_APPROVED }} From d5498274f924e7f3091928ff4962ae9992542c34 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:21:30 +0000 Subject: [PATCH 1482/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4cae0c5a3..b33edba84 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Internal +* 👷 Upgrade GitHub Action label-approved. PR [#10913](https://github.com/tiangolo/fastapi/pull/10913) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade GitHub Action label-approved. PR [#10905](https://github.com/tiangolo/fastapi/pull/10905) by [@tiangolo](https://github.com/tiangolo). ## 0.108.0 From e9ffa20c8e916336bd37c4f1477e0ec04445e0ce Mon Sep 17 00:00:00 2001 From: Andrey Otto <andrey.otto@saritasa.com> Date: Tue, 9 Jan 2024 21:28:58 +0700 Subject: [PATCH 1483/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20code=20exampl?= =?UTF-8?q?es=20in=20docs=20for=20body,=20replace=20name=20`create=5Fitem`?= =?UTF-8?q?=20with=20`update=5Fitem`=20when=20appropriate=20(#5913)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/body/tutorial003.py | 2 +- docs_src/body/tutorial003_py310.py | 2 +- docs_src/body/tutorial004.py | 2 +- docs_src/body/tutorial004_py310.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs_src/body/tutorial003.py b/docs_src/body/tutorial003.py index 89a6b833c..2f33cc038 100644 --- a/docs_src/body/tutorial003.py +++ b/docs_src/body/tutorial003.py @@ -15,5 +15,5 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item): +async def update_item(item_id: int, item: Item): return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial003_py310.py b/docs_src/body/tutorial003_py310.py index a936f28fd..440b210e6 100644 --- a/docs_src/body/tutorial003_py310.py +++ b/docs_src/body/tutorial003_py310.py @@ -13,5 +13,5 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item): +async def update_item(item_id: int, item: Item): return {"item_id": item_id, **item.dict()} diff --git a/docs_src/body/tutorial004.py b/docs_src/body/tutorial004.py index e2df0df2b..0671e0a27 100644 --- a/docs_src/body/tutorial004.py +++ b/docs_src/body/tutorial004.py @@ -15,7 +15,7 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item, q: Union[str, None] = None): +async def update_item(item_id: int, item: Item, q: Union[str, None] = None): result = {"item_id": item_id, **item.dict()} if q: result.update({"q": q}) diff --git a/docs_src/body/tutorial004_py310.py b/docs_src/body/tutorial004_py310.py index 60cfd9610..b352b70ab 100644 --- a/docs_src/body/tutorial004_py310.py +++ b/docs_src/body/tutorial004_py310.py @@ -13,7 +13,7 @@ app = FastAPI() @app.put("/items/{item_id}") -async def create_item(item_id: int, item: Item, q: str | None = None): +async def update_item(item_id: int, item: Item, q: str | None = None): result = {"item_id": item_id, **item.dict()} if q: result.update({"q": q}) From 136fe2b70fd631ed6c62216496255f27e9e12664 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:30:16 +0000 Subject: [PATCH 1484/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b33edba84..18f5c50b6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). * ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). ### Internal From 57d4d938412c9ac0daf8f32eacb2adc8023375d4 Mon Sep 17 00:00:00 2001 From: Keshav Malik <33570148+theinfosecguy@users.noreply.github.com> Date: Tue, 9 Jan 2024 20:02:46 +0530 Subject: [PATCH 1485/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20blog=20for=20Fas?= =?UTF-8?q?tAPI=20&=20Supabase=20(#6018)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 726e7eae7..35c9b6718 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Keshav Malik + author_link: https://theinfosecguy.xyz/ + link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide + title: Building a CRUD API with FastAPI and Supabase - author: Adejumo Ridwan Suleiman author_link: https://www.linkedin.com/in/adejumoridwan/ link: https://medium.com/python-in-plain-english/build-an-sms-spam-classifier-serverless-database-with-faunadb-and-fastapi-23dbb275bc5b From 4491ea688201db4f32d294b8a73862b9df367d99 Mon Sep 17 00:00:00 2001 From: Moustapha Sall <58856327+s-mustafa@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:35:33 -0500 Subject: [PATCH 1486/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20example=20sou?= =?UTF-8?q?rce=20files=20for=20SQL=20databases=20with=20SQLAlchemy=20(#950?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> --- docs_src/sql_databases/sql_app/models.py | 4 ++-- docs_src/sql_databases/sql_app_py310/models.py | 4 ++-- docs_src/sql_databases/sql_app_py39/models.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs_src/sql_databases/sql_app/models.py b/docs_src/sql_databases/sql_app/models.py index 62d8ab4aa..09ae2a807 100644 --- a/docs_src/sql_databases/sql_app/models.py +++ b/docs_src/sql_databases/sql_app/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/docs_src/sql_databases/sql_app_py310/models.py b/docs_src/sql_databases/sql_app_py310/models.py index 62d8ab4aa..09ae2a807 100644 --- a/docs_src/sql_databases/sql_app_py310/models.py +++ b/docs_src/sql_databases/sql_app_py310/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) diff --git a/docs_src/sql_databases/sql_app_py39/models.py b/docs_src/sql_databases/sql_app_py39/models.py index 62d8ab4aa..09ae2a807 100644 --- a/docs_src/sql_databases/sql_app_py39/models.py +++ b/docs_src/sql_databases/sql_app_py39/models.py @@ -7,7 +7,7 @@ from .database import Base class User(Base): __tablename__ = "users" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) email = Column(String, unique=True, index=True) hashed_password = Column(String) is_active = Column(Boolean, default=True) @@ -18,7 +18,7 @@ class User(Base): class Item(Base): __tablename__ = "items" - id = Column(Integer, primary_key=True, index=True) + id = Column(Integer, primary_key=True) title = Column(String, index=True) description = Column(String, index=True) owner_id = Column(Integer, ForeignKey("users.id")) From ed628ddb92505234b026d8ce2e2e8d07178137ba Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:37:20 +0000 Subject: [PATCH 1487/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 18f5c50b6..d44600ab8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update example source files for SQL databases with SQLAlchemy. PR [#9508](https://github.com/tiangolo/fastapi/pull/9508) by [@s-mustafa](https://github.com/s-mustafa). * 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). * ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). From 897cde9fe2843655130bafb191764459456d0761 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:37:53 +0000 Subject: [PATCH 1488/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d44600ab8..6f96b0a3d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add blog for FastAPI & Supabase. PR [#6018](https://github.com/tiangolo/fastapi/pull/6018) by [@theinfosecguy](https://github.com/theinfosecguy). * 📝 Update example source files for SQL databases with SQLAlchemy. PR [#9508](https://github.com/tiangolo/fastapi/pull/9508) by [@s-mustafa](https://github.com/s-mustafa). * 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). * ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). From a1ea70804401625d50811e32840b08d94426805d Mon Sep 17 00:00:00 2001 From: Tristan Marion <trismarion@gmail.com> Date: Tue, 9 Jan 2024 15:44:08 +0100 Subject: [PATCH 1489/1881] =?UTF-8?q?=F0=9F=93=9D=20Replace=20HTTP=20code?= =?UTF-8?q?=20returned=20in=20case=20of=20existing=20user=20error=20in=20d?= =?UTF-8?q?ocs=20for=20testing=20(#4482)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs_src/app_testing/app_b/main.py | 2 +- docs_src/app_testing/app_b/test_main.py | 2 +- docs_src/app_testing/app_b_py310/main.py | 2 +- docs_src/app_testing/app_b_py310/test_main.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs_src/app_testing/app_b/main.py b/docs_src/app_testing/app_b/main.py index 11558b8e8..45a103378 100644 --- a/docs_src/app_testing/app_b/main.py +++ b/docs_src/app_testing/app_b/main.py @@ -33,6 +33,6 @@ async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: - raise HTTPException(status_code=400, detail="Item already exists") + raise HTTPException(status_code=409, detail="Item already exists") fake_db[item.id] = item return item diff --git a/docs_src/app_testing/app_b/test_main.py b/docs_src/app_testing/app_b/test_main.py index d186b8ecb..4e2b98e23 100644 --- a/docs_src/app_testing/app_b/test_main.py +++ b/docs_src/app_testing/app_b/test_main.py @@ -61,5 +61,5 @@ def test_create_existing_item(): "description": "There goes my stealer", }, ) - assert response.status_code == 400 + assert response.status_code == 409 assert response.json() == {"detail": "Item already exists"} diff --git a/docs_src/app_testing/app_b_py310/main.py b/docs_src/app_testing/app_b_py310/main.py index b4c72de5c..eccedcc7c 100644 --- a/docs_src/app_testing/app_b_py310/main.py +++ b/docs_src/app_testing/app_b_py310/main.py @@ -31,6 +31,6 @@ async def create_item(item: Item, x_token: str = Header()): if x_token != fake_secret_token: raise HTTPException(status_code=400, detail="Invalid X-Token header") if item.id in fake_db: - raise HTTPException(status_code=400, detail="Item already exists") + raise HTTPException(status_code=409, detail="Item already exists") fake_db[item.id] = item return item diff --git a/docs_src/app_testing/app_b_py310/test_main.py b/docs_src/app_testing/app_b_py310/test_main.py index d186b8ecb..4e2b98e23 100644 --- a/docs_src/app_testing/app_b_py310/test_main.py +++ b/docs_src/app_testing/app_b_py310/test_main.py @@ -61,5 +61,5 @@ def test_create_existing_item(): "description": "There goes my stealer", }, ) - assert response.status_code == 400 + assert response.status_code == 409 assert response.json() == {"detail": "Item already exists"} From fe694766ae5d4f56d017cfc28ff358a3a6193dcf Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:45:35 +0000 Subject: [PATCH 1490/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6f96b0a3d..8dfb1a9ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Replace HTTP code returned in case of existing user error in docs for testing. PR [#4482](https://github.com/tiangolo/fastapi/pull/4482) by [@TristanMarion](https://github.com/TristanMarion). * 📝 Add blog for FastAPI & Supabase. PR [#6018](https://github.com/tiangolo/fastapi/pull/6018) by [@theinfosecguy](https://github.com/theinfosecguy). * 📝 Update example source files for SQL databases with SQLAlchemy. PR [#9508](https://github.com/tiangolo/fastapi/pull/9508) by [@s-mustafa](https://github.com/s-mustafa). * 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). From 78ff6e3efd8899918957f6176585be3610c8c730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 9 Jan 2024 18:57:33 +0400 Subject: [PATCH 1491/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20GitHub?= =?UTF-8?q?=20Action=20latest-changes=20(#10915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/latest-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/latest-changes.yml b/.github/workflows/latest-changes.yml index b9b550d5e..27e062d09 100644 --- a/.github/workflows/latest-changes.yml +++ b/.github/workflows/latest-changes.yml @@ -34,7 +34,7 @@ jobs: if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }} with: limit-access-to-actor: true - - uses: docker://tiangolo/latest-changes:0.2.0 + - uses: docker://tiangolo/latest-changes:0.3.0 # - uses: tiangolo/latest-changes@main with: token: ${{ secrets.GITHUB_TOKEN }} From 7fbb7963d31489fc157bf2de3e437d9084042080 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 14:57:58 +0000 Subject: [PATCH 1492/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8dfb1a9ea..a33c1bc79 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* ⬆️ Upgrade GitHub Action latest-changes. PR [#10915](https://github.com/tiangolo/fastapi/pull/10915) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade GitHub Action label-approved. PR [#10913](https://github.com/tiangolo/fastapi/pull/10913) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade GitHub Action label-approved. PR [#10905](https://github.com/tiangolo/fastapi/pull/10905) by [@tiangolo](https://github.com/tiangolo). From 423cdd24ccd38957e50be5016c6feb88a5bb3ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 9 Jan 2024 19:02:53 +0400 Subject: [PATCH 1493/1881] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20custom=20Git?= =?UTF-8?q?Hub=20Action=20comment-docs-preview-in-pr=20(#10916)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/actions/comment-docs-preview-in-pr/Dockerfile | 6 ++++-- .github/actions/comment-docs-preview-in-pr/app/main.py | 3 ++- .github/actions/comment-docs-preview-in-pr/requirements.txt | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .github/actions/comment-docs-preview-in-pr/requirements.txt diff --git a/.github/actions/comment-docs-preview-in-pr/Dockerfile b/.github/actions/comment-docs-preview-in-pr/Dockerfile index 14b0d0269..42627fe19 100644 --- a/.github/actions/comment-docs-preview-in-pr/Dockerfile +++ b/.github/actions/comment-docs-preview-in-pr/Dockerfile @@ -1,6 +1,8 @@ -FROM python:3.9 +FROM python:3.10 -RUN pip install httpx "pydantic==1.5.1" pygithub +COPY ./requirements.txt /app/requirements.txt + +RUN pip install -r /app/requirements.txt COPY ./app /app diff --git a/.github/actions/comment-docs-preview-in-pr/app/main.py b/.github/actions/comment-docs-preview-in-pr/app/main.py index 68914fdb9..8cc119fe0 100644 --- a/.github/actions/comment-docs-preview-in-pr/app/main.py +++ b/.github/actions/comment-docs-preview-in-pr/app/main.py @@ -6,7 +6,8 @@ from typing import Union import httpx from github import Github from github.PullRequest import PullRequest -from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError +from pydantic import BaseModel, SecretStr, ValidationError +from pydantic_settings import BaseSettings github_api = "https://api.github.com" diff --git a/.github/actions/comment-docs-preview-in-pr/requirements.txt b/.github/actions/comment-docs-preview-in-pr/requirements.txt new file mode 100644 index 000000000..74a3631f4 --- /dev/null +++ b/.github/actions/comment-docs-preview-in-pr/requirements.txt @@ -0,0 +1,4 @@ +PyGithub +pydantic>=2.5.3,<3.0.0 +pydantic-settings>=2.1.0,<3.0.0 +httpx From 635d1a2d6dcd7c26af9d8b7db19dd8c1f25a777a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:04:35 +0000 Subject: [PATCH 1494/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a33c1bc79..ad11537eb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Internal +* 👷 Upgrade custom GitHub Action comment-docs-preview-in-pr. PR [#10916](https://github.com/tiangolo/fastapi/pull/10916) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade GitHub Action latest-changes. PR [#10915](https://github.com/tiangolo/fastapi/pull/10915) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade GitHub Action label-approved. PR [#10913](https://github.com/tiangolo/fastapi/pull/10913) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade GitHub Action label-approved. PR [#10905](https://github.com/tiangolo/fastapi/pull/10905) by [@tiangolo](https://github.com/tiangolo). From 7111d69f285469a1fb2f9389e8dc2de41a5bd113 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Tue, 9 Jan 2024 10:08:24 -0500 Subject: [PATCH 1495/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/resources/index.md`=20(#10909)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/resources/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/es/docs/resources/index.md diff --git a/docs/es/docs/resources/index.md b/docs/es/docs/resources/index.md new file mode 100644 index 000000000..92898d319 --- /dev/null +++ b/docs/es/docs/resources/index.md @@ -0,0 +1,3 @@ +# Recursos + +Recursos adicionales, enlaces externos, artículos y más. ✈️ From e10bdb82cc9199b47c7804bfda842990723c4a03 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Tue, 9 Jan 2024 10:09:12 -0500 Subject: [PATCH 1496/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/about/index.md`=20(#10908)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/es/docs/about/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/es/docs/about/index.md diff --git a/docs/es/docs/about/index.md b/docs/es/docs/about/index.md new file mode 100644 index 000000000..e83400a8d --- /dev/null +++ b/docs/es/docs/about/index.md @@ -0,0 +1,3 @@ +# Acerca de + +Acerca de FastAPI, su diseño, inspiración y más. 🤓 From d1299103236eb88b37553b94996bcf74c8fe4485 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Tue, 9 Jan 2024 10:09:47 -0500 Subject: [PATCH 1497/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/help/index.md`=20(#10907)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/help/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/es/docs/help/index.md diff --git a/docs/es/docs/help/index.md b/docs/es/docs/help/index.md new file mode 100644 index 000000000..f6ce35e9c --- /dev/null +++ b/docs/es/docs/help/index.md @@ -0,0 +1,3 @@ +# Ayuda + +Ayuda y recibe ayuda, contribuye, involúcrate. 🤝 From 631601787b6bc8835edc0967653bfa6378686473 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:11:10 +0000 Subject: [PATCH 1498/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ad11537eb..872464de8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,6 +15,10 @@ hide: * 📝 Update code examples in docs for body, replace name `create_item` with `update_item` when appropriate. PR [#5913](https://github.com/tiangolo/fastapi/pull/5913) by [@OttoAndrey](https://github.com/OttoAndrey). * ✏️ Fix typo in dependencies with yield source examples. PR [#10847](https://github.com/tiangolo/fastapi/pull/10847) by [@tiangolo](https://github.com/tiangolo). +### Translations + +* 🌐 Add Spanish translation for `docs/es/docs/resources/index.md`. PR [#10909](https://github.com/tiangolo/fastapi/pull/10909) by [@pablocm83](https://github.com/pablocm83). + ### Internal * 👷 Upgrade custom GitHub Action comment-docs-preview-in-pr. PR [#10916](https://github.com/tiangolo/fastapi/pull/10916) by [@tiangolo](https://github.com/tiangolo). From ca10d3927b7f16acf109a848769b27beb40251c2 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:11:39 +0000 Subject: [PATCH 1499/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 872464de8..8a80aa295 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Spanish translation for `docs/es/docs/about/index.md`. PR [#10908](https://github.com/tiangolo/fastapi/pull/10908) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/resources/index.md`. PR [#10909](https://github.com/tiangolo/fastapi/pull/10909) by [@pablocm83](https://github.com/pablocm83). ### Internal From 5b63406aa5dcc0260a45c7bb0c86d607f3ff532c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:12:19 +0000 Subject: [PATCH 1500/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8a80aa295..4c380ed3f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Spanish translation for `docs/es/docs/help/index.md`. PR [#10907](https://github.com/tiangolo/fastapi/pull/10907) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/about/index.md`. PR [#10908](https://github.com/tiangolo/fastapi/pull/10908) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/resources/index.md`. PR [#10909](https://github.com/tiangolo/fastapi/pull/10909) by [@pablocm83](https://github.com/pablocm83). From 2090e9a3e258cc1517018ebdd6283171f334247c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:14:23 +0300 Subject: [PATCH 1501/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/newsletter.md`=20(#10550)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/newsletter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/tr/docs/newsletter.md diff --git a/docs/tr/docs/newsletter.md b/docs/tr/docs/newsletter.md new file mode 100644 index 000000000..22ca1b1e2 --- /dev/null +++ b/docs/tr/docs/newsletter.md @@ -0,0 +1,5 @@ +# FastAPI ve Arkadaşları Bülteni + +<iframe data-w-type="embedded" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://xr4n4.mjt.lu/wgt/xr4n4/hj5/form?c=40a44fa4" width="100%" style="height: 0;"></iframe> + +<script type="text/javascript" src="https://app.mailjet.com/pas-nc-embedded-v1.js"></script> From eecc7a81136eaa6617b9491cfc3c10930c90e30c Mon Sep 17 00:00:00 2001 From: David Takacs <44911031+takacs@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:16:04 -0500 Subject: [PATCH 1502/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Hungarian=20tran?= =?UTF-8?q?slation=20for=20`/docs/hu/docs/index.md`=20(#10812)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Peter Panko <prike18@gmail.com> --- docs/hu/docs/index.md | 469 ++++++++++++++++++++++++++++++++++++++++++ docs/hu/mkdocs.yml | 1 + 2 files changed, 470 insertions(+) create mode 100644 docs/hu/docs/index.md create mode 100644 docs/hu/mkdocs.yml diff --git a/docs/hu/docs/index.md b/docs/hu/docs/index.md new file mode 100644 index 000000000..29c3c05ac --- /dev/null +++ b/docs/hu/docs/index.md @@ -0,0 +1,469 @@ +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>FastAPI keretrendszer, nagy teljesítmény, könnyen tanulható, gyorsan kódolható, productionre kész</em> +</p> +<p align="center"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> +</a> +<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/fastapi" target="_blank"> + <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/fastapi.svg" alt="Coverage"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Supported Python versions"> +</a> +</p> + +--- + +**Dokumentáció**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**Forrás kód**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- +A FastAPI egy modern, gyors (nagy teljesítményű), webes keretrendszer API-ok építéséhez Python 3.8+-al, a Python szabványos típusjelöléseire építve. + + +Kulcs funkciók: + +* **Gyors**: Nagyon nagy teljesítmény, a **NodeJS**-el és a **Go**-val egyenrangú (a Starlettenek és a Pydantic-nek köszönhetően). [Az egyik leggyorsabb Python keretrendszer](#performance). +* **Gyorsan kódolható**: A funkciók fejlesztési sebességét 200-300 százalékkal megnöveli. * +* **Kevesebb hiba**: Körülbelül 40%-al csökkenti az emberi (fejlesztői) hibák számát. * +* **Intuitív**: Kiváló szerkesztő támogatás. <abbr title="más néven auto-complete, autocompletion, IntelliSense">Kiegészítés</abbr> mindenhol. Kevesebb hibakereséssel töltött idő. +* **Egyszerű**: Egyszerű tanulásra és használatra tervezve. Kevesebb dokumentáció olvasással töltött idő. +* **Rövid**: Kód duplikáció minimalizálása. Több funkció minden paraméter deklarálásával. Kevesebb hiba. +* **Robosztus**: Production ready kód. Automatikus interaktív dokumentáció val. +* **Szabvány alapú**: Az API-ok nyílt szabványaira alapuló (és azokkal teljesen kompatibilis): <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (korábban Swagger néven ismert) és a <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. + +<small>* Egy production alkalmazásokat építő belső fejlesztői csapat tesztjein alapuló becslés. </small> + +## Szponzorok + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">További szponzorok</a> + +## Vélemények + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +"_If anyone is looking to build a production Python API, I would highly recommend **FastAPI**. It is **beautifully designed**, **simple to use** and **highly scalable**, it has become a **key component** in our API first development strategy and is driving many automations and services such as our Virtual TAC Engineer._" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**, a CLI-ok FastAPI-ja + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +Ha egy olyan CLI alkalmazást fejlesztesz amit a parancssorban kell használni webes API helyett, tekintsd meg: <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. + +**Typer** a FastAPI kistestvére. A **CLI-k FastAPI-ja**. ⌨️ 🚀 + +## Követelmények + +Python 3.8+ + +A FastAPI óriások vállán áll: + +* <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> a webes részekhez. +* <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> az adat részekhez. + +## Telepítés + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +A production-höz egy ASGI szerverre is szükség lesz, mint például az <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> vagy a <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>. + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +</div> + +## Példa + +### Hozd létre + +* Hozz létre a `main.py` fájlt a következő tartalommal: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>Vagy használd az <code>async def</code>-et...</summary> + +Ha a kódod `async` / `await`-et, használ `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Megjegyzés**: + +Ha nem tudod, tekintsd meg a _"Sietsz?"_ szekciót <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` és `await`-ről dokumentációba</a>. + +</details> + +### Futtasd le + +Indítsd el a szervert a következő paranccsal: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary>A parancsról <code>uvicorn main:app --reload</code>...</summary> + +A `uvicorn main:app` parancs a következőre utal: + +* `main`: fájl `main.py` (a Python "modul"). +* `app`: a `main.py`-ban a `app = FastAPI()` sorral létrehozott objektum. +* `--reload`: kód változtatás esetén újra indítja a szervert. Csak fejlesztés közben használandó. + +</details> + +### Ellenőrizd + +Nyisd meg a böngésződ a következő címen: <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. + +A következő JSON választ fogod látni: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Máris létrehoztál egy API-t ami: + +* HTTP kéréseket fogad a `/` és `/items/{item_id}` _útvonalakon_. +* Mindkét _útvonal_ a `GET` <em>műveletet</em> használja (másik elnevezés: HTTP _metódus_). +* A `/items/{item_id}` _útvonalnak_ van egy _path paramétere_, az `item_id`, aminek `int` típusúnak kell lennie. +* A `/items/{item_id}` _útvonalnak_ még van egy opcionális, `str` típusú _query paramétere_ is, a `q`. + +### Interaktív API dokumentáció + +Most nyisd meg a <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> címet. + +Az automatikus interaktív API dokumentációt fogod látni (amit a <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>-al hozunk létre): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternatív API dokumentáció + +És most menj el a <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> címre. + +Az alternatív automatikus dokumentációt fogod látni. (lásd <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Példa frissítése + +Módosítsuk a `main.py` fájlt, hogy `PUT` kérések esetén tudjon body-t fogadni. + +Deklaráld a body-t standard Python típusokkal, a Pydantic-nak köszönhetően. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +A szerver automatikusan újraindul (mert hozzáadtuk a --reload paramétert a fenti `uvicorn` parancshoz). + +### Interaktív API dokumentáció frissítése + +Most menj el a <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> címre. + +* Az interaktív API dokumentáció automatikusan frissült így már benne van az új body. + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Kattints rá a "Try it out" gombra, ennek segítségével kitöltheted a paramétereket és közvetlen használhatod az API-t: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Ezután kattints az "Execute" gompra, a felhasználói felület kommunikálni fog az API-oddal. Elküldi a paramétereket és a visszakapott választ megmutatja a képernyődön. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternatív API dokumentáció frissítés + +Most menj el a <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> címre. + +* Az alternatív dokumentáció szintúgy tükrözni fogja az új kérési paraméter és body-t. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Összefoglalás + +Összegzésül, deklarálod **egyszer** a paraméterek, body, stb típusát funkciós paraméterekként. + +Ezt standard modern Python típusokkal csinálod. + +Nem kell új szintaxist, vagy specifikus könyvtár mert metódósait, stb. megtanulnod. + +Csak standard **Python 3.8+**. + +Például egy `int`-nek: + +```Python +item_id: int +``` + +Egy komplexebb `Item` modellnek: + +```Python +item: Item +``` + +... És csupán egy deklarációval megkapod a: + +* Szerkesztő támogatást, beleértve: + * Szövegkiegészítés. + * Típus ellenőrzés. +* Adatok validációja: + * Automatikus és érthető hibák amikor az adatok hibásak. + * Validáció mélyen ágyazott objektumok esetén is. +* Bemeneti adatok<abbr title="also known as: serialization, parsing, marshalling"> átváltása</abbr> : a hálózatról érkező Python adatokká és típusokká. Adatok olvasása következő forrásokból: + * JSON. + * Cím paraméterek. + * Query paraméterek. + * Cookie-k. + * Header-ök. + * Formok. + * Fájlok. +* Kimeneti adatok <abbr title=" más néven: serialization, parsing, marshalling">átváltása</abbr>: Python adatok is típusokról hálózati adatokká: + * válts át Python típusokat (`str`, `int`, `float`, `bool`, `list`, etc). + * `datetime` csak objektumokat. + * `UUID` objektumokat. + * Adatbázis modelleket. + * ...És sok mást. +* Automatikus interaktív dokumentáció, beleértve két alternatív dokumentációt is: + * Swagger UI. + * ReDoc. + +--- + +Visszatérve az előző kód példához. A **FastAPI**: + +* Validálja hogy van egy `item_id` mező a `GET` és `PUT` kérésekben. +* Validálja hogy az `item_id` `int` típusú a `GET` és `PUT` kérésekben. + * Ha nem akkor látni fogunk egy tiszta hibát ezzel kapcsolatban. +* ellenőrzi hogyha van egy opcionális query paraméter `q` névvel (azaz `http://127.0.0.1:8000/items/foo?q=somequery`) `GET` kérések esetén. + * Mivel a `q` paraméter `= None`-al van deklarálva, ezért opcionális. + * `None` nélkül ez a mező kötelező lenne (mint például a body `PUT` kérések esetén). +* a `/items/{item_id}` címre érkező `PUT` kérések esetén, a JSON-t a következőképpen olvassa be: + * Ellenőrzi hogy létezik a kötelező `name` nevű attribútum és `string`. + * Ellenőrzi hogy létezik a kötelező `price` nevű attribútum és `float`. + * Ellenőrzi hogy létezik a `is_offer` nevű opcionális paraméter, ami ha létezik akkor `bool` + * Ez ágyazott JSON objektumokkal is működik +* JSONről való automatikus konvertálás. +* dokumentáljuk mindent OpenAPI-al amit használható: + * Interaktív dokumentációs rendszerekkel. + * Automatikus kliens kód generáló a rendszerekkel, több nyelven. +* Hozzá tartozik kettő interaktív dokumentációs web felület. + +--- + +Eddig csak a felszínt kapargattuk, de a lényeg hogy most már könnyebben érthető hogyan működik. + +Próbáld kicserélni a következő sorban: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...ezt: + +```Python + ... "item_name": item.name ... +``` + +...erre: + +```Python + ... "item_price": item.price ... +``` + +... És figyeld meg hogy a szerkesztő automatikusan tudni fogja a típusokat és kiegészíti azokat: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Teljesebb példákért és funkciókért tekintsd meg a <a href="https://fastapi.tiangolo.com/tutorial/">Tutorial - User Guide</a> -t. + +**Spoiler veszély**: a Tutorial - User Guidehoz tartozik: + +* **Paraméterek** deklarációja különböző helyekről: **header-ök**, **cookie-k**, **form mezők** és **fájlok**. +* Hogyan állíts be **validációs feltételeket** mint a `maximum_length` vagy a `regex`. +* Nagyon hatékony és erős **<abbr title="also known as components, resources, providers, services, injectables">Függőség Injekció</abbr>** rendszerek. +* Biztonság és autentikáció beleértve, **OAuth2**, **JWT tokens** és **HTTP Basic** támogatást. +* Több haladó (de ugyanannyira könnyű) technika **mélyen ágyazott JSON modellek deklarációjára** (Pydantic-nek köszönhetően). +* **GraphQL** integráció <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a>-vel és más könyvtárakkal. +* több extra funkció (Starlette-nek köszönhetően) pl.: + * **WebSockets** + * rendkívül könnyű tesztek HTTPX és `pytest` alapokra építve + * **CORS** + * **Cookie Sessions** + * ...és több. + +## Teljesítmény + +A független TechEmpower benchmarkok szerint az Uvicorn alatt futó **FastAPI** alkalmazások az <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">egyik leggyorsabb Python keretrendszerek közé tartoznak</a>, éppen lemaradva a Starlette és az Uvicorn (melyeket a FastAPI belsőleg használ) mögött.(*) + +Ezeknek a további megértéséhez: <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. + +## Opcionális követelmények + +Pydantic által használt: + +* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - e-mail validációkra. +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - Beállítások követésére. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Extra típusok Pydantic-hoz. + +Starlette által használt: + +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Követelmény ha a `TestClient`-et akarod használni. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Követelmény ha az alap template konfigurációt akarod használni. +* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Követelmény ha <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>-ot akarsz támogatni, `request.form()`-al. +* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Követelmény `SessionMiddleware` támogatáshoz. +* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Követelmény a Starlette `SchemaGenerator`-ának támogatásához (valószínűleg erre nincs szükség FastAPI használása esetén). +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Követelmény ha `UJSONResponse`-t akarsz használni. + +FastAPI / Starlette által használt + +* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - Szerverekhez amíg betöltik és szolgáltatják az applikációdat. +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - Követelmény ha `ORJSONResponse`-t akarsz használni. + +Ezeket mind telepítheted a `pip install "fastapi[all]"` paranccsal. + +## Licensz +Ez a projekt az MIT license, licensz alatt fut diff --git a/docs/hu/mkdocs.yml b/docs/hu/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/hu/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From f9cbaa5f39f2cb199948f5dfa6d15c48e465e7d1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:18:47 +0000 Subject: [PATCH 1503/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4c380ed3f..3a1a1b794 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/newsletter.md`. PR [#10550](https://github.com/tiangolo/fastapi/pull/10550) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/help/index.md`. PR [#10907](https://github.com/tiangolo/fastapi/pull/10907) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/about/index.md`. PR [#10908](https://github.com/tiangolo/fastapi/pull/10908) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/resources/index.md`. PR [#10909](https://github.com/tiangolo/fastapi/pull/10909) by [@pablocm83](https://github.com/pablocm83). From ce9aba258e47e5128afb0947b75527c05b17d2f7 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:22:55 +0000 Subject: [PATCH 1504/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3a1a1b794..734ab2d60 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Hungarian translation for `/docs/hu/docs/index.md`. PR [#10812](https://github.com/tiangolo/fastapi/pull/10812) by [@takacs](https://github.com/takacs). * 🌐 Add Turkish translation for `docs/tr/docs/newsletter.md`. PR [#10550](https://github.com/tiangolo/fastapi/pull/10550) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/help/index.md`. PR [#10907](https://github.com/tiangolo/fastapi/pull/10907) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/about/index.md`. PR [#10908](https://github.com/tiangolo/fastapi/pull/10908) by [@pablocm83](https://github.com/pablocm83). From 3af6766e26607cfd1007cbf33f7440f3c879fe79 Mon Sep 17 00:00:00 2001 From: ArtemKhymenko <artem.khymenko@gmail.com> Date: Tue, 9 Jan 2024 17:25:48 +0200 Subject: [PATCH 1505/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/tutorial/body-fields.md`=20(#1067?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maksym Zavalniuk <mezgoodle@gmail.com> Co-authored-by: Rostyslav <rostik1410@users.noreply.github.com> --- docs/uk/docs/tutorial/body-fields.md | 116 +++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/uk/docs/tutorial/body-fields.md diff --git a/docs/uk/docs/tutorial/body-fields.md b/docs/uk/docs/tutorial/body-fields.md new file mode 100644 index 000000000..eee993cbe --- /dev/null +++ b/docs/uk/docs/tutorial/body-fields.md @@ -0,0 +1,116 @@ +# Тіло - Поля + +Так само як ви можете визначати додаткову валідацію та метадані у параметрах *функції обробки шляху* за допомогою `Query`, `Path` та `Body`, ви можете визначати валідацію та метадані всередині моделей Pydantic за допомогою `Field` від Pydantic. + +## Імпорт `Field` + +Спочатку вам потрібно імпортувати це: + +=== "Python 3.10+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо. + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +!!! warning + Зверніть увагу, що `Field` імпортується прямо з `pydantic`, а не з `fastapi`, як всі інші (`Query`, `Path`, `Body` тощо). + +## Оголошення атрибутів моделі + +Ви можете використовувати `Field` з атрибутами моделі: + +=== "Python 3.10+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо.. + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + Варто користуватись `Annotated` версією, якщо це можливо.. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +`Field` працює так само, як `Query`, `Path` і `Body`, у нього такі самі параметри тощо. + +!!! note "Технічні деталі" + Насправді, `Query`, `Path` та інші, що ви побачите далі, створюють об'єкти підкласів загального класу `Param`, котрий сам є підкласом класу `FieldInfo` з Pydantic. + + І `Field` від Pydantic також повертає екземпляр `FieldInfo`. + + `Body` також безпосередньо повертає об'єкти підкласу `FieldInfo`. І є інші підкласи, які ви побачите пізніше, що є підкласами класу Body. + + Пам'ятайте, що коли ви імпортуєте 'Query', 'Path' та інше з 'fastapi', вони фактично є функціями, які повертають спеціальні класи. + +!!! tip + Зверніть увагу, що кожен атрибут моделі із типом, значенням за замовчуванням та `Field` має ту саму структуру, що й параметр *функції обробки шляху*, з `Field` замість `Path`, `Query` і `Body`. + +## Додавання додаткової інформації + +Ви можете визначити додаткову інформацію у `Field`, `Query`, `Body` тощо. І вона буде включена у згенеровану JSON схему. + +Ви дізнаєтеся більше про додавання додаткової інформації пізніше у документації, коли вивчатимете визначення прикладів. + +!!! warning + Додаткові ключі, передані в `Field`, також будуть присутні у згенерованій схемі OpenAPI для вашого додатка. + Оскільки ці ключі не обов'язково можуть бути частиною специфікації OpenAPI, деякі інструменти OpenAPI, наприклад, [OpenAPI валідатор](https://validator.swagger.io/), можуть не працювати з вашою згенерованою схемою. + +## Підсумок + +Ви можете використовувати `Field` з Pydantic для визначення додаткових перевірок та метаданих для атрибутів моделі. + +Ви також можете використовувати додаткові іменовані аргументи для передачі додаткових метаданих JSON схеми. From 4fa251beb5546ea1b72b1c7bf3d3ed35324928a4 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Tue, 9 Jan 2024 10:26:26 -0500 Subject: [PATCH 1506/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/learn/index.md`=20(#10885)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/es/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/es/docs/learn/index.md diff --git a/docs/es/docs/learn/index.md b/docs/es/docs/learn/index.md new file mode 100644 index 000000000..b8d26cf34 --- /dev/null +++ b/docs/es/docs/learn/index.md @@ -0,0 +1,5 @@ +# Aprender + +Aquí están las secciones introductorias y los tutoriales para aprender **FastAPI**. + +Podrías considerar esto como un **libro**, un **curso**, la forma **oficial** y recomendada de aprender FastAPI. 😎 From 23ad8275975bc3474d5f7ed448db4b6c3a83f432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:28:47 +0300 Subject: [PATCH 1507/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/external-links.md`=20(#10549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/external-links.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/tr/docs/external-links.md diff --git a/docs/tr/docs/external-links.md b/docs/tr/docs/external-links.md new file mode 100644 index 000000000..78eaf1729 --- /dev/null +++ b/docs/tr/docs/external-links.md @@ -0,0 +1,33 @@ +# Harici Bağlantılar ve Makaleler + +**FastAPI** sürekli büyüyen harika bir topluluğa sahiptir. + +**FastAPI** ile alakalı birçok yazı, makale, araç ve proje bulunmaktadır. + +Bunlardan bazılarının tamamlanmamış bir listesi aşağıda bulunmaktadır. + +!!! tip "İpucu" + Eğer **FastAPI** ile alakalı henüz burada listelenmemiş bir makale, proje, araç veya başka bir şeyiniz varsa, bunu eklediğiniz bir <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request</a> oluşturabilirsiniz. + +{% for section_name, section_content in external_links.items() %} + +## {{ section_name }} + +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. + +{% endfor %} +{% endfor %} +{% endfor %} + +## Projeler + +`fastapi` konulu en son GitHub projeleri: + +<div class="github-topic-projects"> +</div> From 0a3dc7d107aebee308896c6e1d49a71067db5660 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:28:54 +0000 Subject: [PATCH 1508/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 734ab2d60..db3a4d4b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body-fields.md`. PR [#10670](https://github.com/tiangolo/fastapi/pull/10670) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Add Hungarian translation for `/docs/hu/docs/index.md`. PR [#10812](https://github.com/tiangolo/fastapi/pull/10812) by [@takacs](https://github.com/takacs). * 🌐 Add Turkish translation for `docs/tr/docs/newsletter.md`. PR [#10550](https://github.com/tiangolo/fastapi/pull/10550) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/help/index.md`. PR [#10907](https://github.com/tiangolo/fastapi/pull/10907) by [@pablocm83](https://github.com/pablocm83). From 0f4b6294bf17741cb2399fb2584c4da924168b95 Mon Sep 17 00:00:00 2001 From: Royc30ne <pinyilyu@gmail.com> Date: Tue, 9 Jan 2024 23:29:37 +0800 Subject: [PATCH 1509/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20SQLAlchemy=20?= =?UTF-8?q?instruction=20in=20Chinese=20translation=20`docs/zh/docs/tutori?= =?UTF-8?q?al/sql-databases.md`=20(#9712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/zh/docs/tutorial/sql-databases.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index 8b09dc677..a936eb27b 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -78,9 +78,23 @@ ORM 具有在代码和数据库表(“*关系型”)中的**对象**之间 现在让我们看看每个文件/模块的作用。 +## 安装 SQLAlchemy + +先下载`SQLAlchemy`所需要的依赖: + +<div class="termy"> + +```console +$ pip install sqlalchemy + +---> 100% +``` + +</div> + ## 创建 SQLAlchemy 部件 -让我们涉及到文件`sql_app/database.py`。 +让我们转到文件`sql_app/database.py`。 ### 导入 SQLAlchemy 部件 From d6b4c6c65c8ee8922940b7def38bf60ed1dfeb53 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:31:14 +0000 Subject: [PATCH 1510/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 db3a4d4b2..c3e1606cb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Spanish translation for `docs/es/docs/learn/index.md`. PR [#10885](https://github.com/tiangolo/fastapi/pull/10885) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body-fields.md`. PR [#10670](https://github.com/tiangolo/fastapi/pull/10670) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Add Hungarian translation for `/docs/hu/docs/index.md`. PR [#10812](https://github.com/tiangolo/fastapi/pull/10812) by [@takacs](https://github.com/takacs). * 🌐 Add Turkish translation for `docs/tr/docs/newsletter.md`. PR [#10550](https://github.com/tiangolo/fastapi/pull/10550) by [@hasansezertasan](https://github.com/hasansezertasan). From 3256c3ff073fa24bcb1a1a089a78869f9b7de77e Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 9 Jan 2024 18:31:45 +0300 Subject: [PATCH 1511/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/learn/index.md`=20(#10539)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/ru/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/ru/docs/learn/index.md diff --git a/docs/ru/docs/learn/index.md b/docs/ru/docs/learn/index.md new file mode 100644 index 000000000..b2e4cabc7 --- /dev/null +++ b/docs/ru/docs/learn/index.md @@ -0,0 +1,5 @@ +# Обучение + +Здесь представлены вводные разделы и учебные пособия для изучения **FastAPI**. + +Вы можете считать это **книгой**, **курсом**, **официальным** и рекомендуемым способом изучения FastAPI. 😎 From 01b106c29089208507611e48d98409912f18ad80 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:31:54 +0000 Subject: [PATCH 1512/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c3e1606cb..f410e96e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/external-links.md`. PR [#10549](https://github.com/tiangolo/fastapi/pull/10549) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/learn/index.md`. PR [#10885](https://github.com/tiangolo/fastapi/pull/10885) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body-fields.md`. PR [#10670](https://github.com/tiangolo/fastapi/pull/10670) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). * 🌐 Add Hungarian translation for `/docs/hu/docs/index.md`. PR [#10812](https://github.com/tiangolo/fastapi/pull/10812) by [@takacs](https://github.com/takacs). From cb53749798f5ccbc4016cb37827cf46f4c75f0ce Mon Sep 17 00:00:00 2001 From: KAZAMA-DREAM <73453137+KAZAMA-DREAM@users.noreply.github.com> Date: Tue, 9 Jan 2024 23:33:25 +0800 Subject: [PATCH 1513/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/learn/index.md`=20(#10479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: KAZAMA <wyy1778789301@163.com> --- docs/zh/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/zh/docs/learn/index.md diff --git a/docs/zh/docs/learn/index.md b/docs/zh/docs/learn/index.md new file mode 100644 index 000000000..38696f6fe --- /dev/null +++ b/docs/zh/docs/learn/index.md @@ -0,0 +1,5 @@ +# 学习 + +以下是学习 **FastAPI** 的介绍部分和教程。 + +您可以认为这是一本 **书**,一门 **课程**,是 **官方** 且推荐的学习FastAPI的方法。😎 From b4ad143e3753b19628e6f7f339184f95219b223c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:33:53 +0000 Subject: [PATCH 1514/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f410e96e9..188e09a8f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Update SQLAlchemy instruction in Chinese translation `docs/zh/docs/tutorial/sql-databases.md`. PR [#9712](https://github.com/tiangolo/fastapi/pull/9712) by [@Royc30ne](https://github.com/Royc30ne). * 🌐 Add Turkish translation for `docs/tr/docs/external-links.md`. PR [#10549](https://github.com/tiangolo/fastapi/pull/10549) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/learn/index.md`. PR [#10885](https://github.com/tiangolo/fastapi/pull/10885) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Ukrainian translation for `docs/uk/docs/tutorial/body-fields.md`. PR [#10670](https://github.com/tiangolo/fastapi/pull/10670) by [@ArtemKhymenko](https://github.com/ArtemKhymenko). From 1021152f0a35a78954b36c7e6b534c4eeb9ff45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:35:44 +0300 Subject: [PATCH 1515/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Turkish=20tra?= =?UTF-8?q?nslation=20for=20`docs/tr/docs/index.md`=20(#10444)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/index.md | 283 +++++++++++++++++++++--------------------- 1 file changed, 142 insertions(+), 141 deletions(-) diff --git a/docs/tr/docs/index.md b/docs/tr/docs/index.md index e61f5b82c..ac8830880 100644 --- a/docs/tr/docs/index.md +++ b/docs/tr/docs/index.md @@ -2,45 +2,47 @@ <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> </p> <p align="center"> - <em>FastAPI framework, yüksek performanslı, öğrenmesi kolay, geliştirmesi hızlı, kullanıma sunulmaya hazır.</em> + <em>FastAPI framework, yüksek performanslı, öğrenmesi oldukça kolay, kodlaması hızlı, kullanıma hazır</em> </p> <p align="center"> -<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest" target="_blank"> - <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg" alt="Test"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> </a> -<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank"> - <img src="https://img.shields.io/codecov/c/github/tiangolo/fastapi?color=%2334D058" alt="Coverage"> +<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/fastapi" target="_blank"> + <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/fastapi.svg" alt="Coverage"> </a> <a href="https://pypi.org/project/fastapi" target="_blank"> <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> </a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Supported Python versions"> +</a> </p> --- -**dokümantasyon**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> +**Dokümantasyon**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> -**Kaynak kodu**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> +**Kaynak Kod**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> --- -FastAPI, Python 3.8+'nın standart type hintlerine dayanan modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'ü. +FastAPI, Python <abbr title="Python 3.8 ve üzeri">3.8+</abbr>'nin standart <abbr title="Tip Belirteçleri: Type Hints">tip belirteçleri</abbr>ne dayalı, modern ve hızlı (yüksek performanslı) API'lar oluşturmak için kullanılabilecek web framework'tür. -Ana özellikleri: +Temel özellikleri şunlardır: -* **Hızlı**: çok yüksek performanslı, **NodeJS** ve **Go** ile eşdeğer seviyede performans sağlıyor, (Starlette ve Pydantic sayesinde.) [Python'un en hızlı frameworklerinden bir tanesi.](#performans). -* **Kodlaması hızlı**: Yeni özellikler geliştirmek neredeyse %200 - %300 daha hızlı. * -* **Daha az bug**: Geliştirici (insan) kaynaklı hatalar neredeyse %40 azaltıldı. * -* **Sezgileri güçlü**: Editor (otomatik-tamamlama) desteği harika. <abbr title="Otomatik tamamlama-IntelliSense">Otomatik tamamlama</abbr> her yerde. Debuglamak ile daha az zaman harcayacaksınız. -* **Kolay**: Öğrenmesi ve kullanması kolay olacak şekilde. Doküman okumak için harcayacağınız süre azaltıldı. -* **Kısa**: Kod tekrarını minimuma indirdik. Fonksiyon parametrelerinin tiplerini belirtmede farklı yollar sunarak karşılaşacağınız bug'ları azalttık. -* **Güçlü**: Otomatik dokümantasyon ile beraber, kullanıma hazır kod yaz. +* **Hızlı**: Çok yüksek performanslı, **NodeJS** ve **Go** ile eşit düzeyde (Starlette ve Pydantic sayesinde). [En hızlı Python framework'lerinden bir tanesidir](#performans). +* **Kodlaması Hızlı**: Geliştirme hızını yaklaşık %200 ile %300 aralığında arttırır. * +* **Daha az hata**: İnsan (geliştirici) kaynaklı hataları yaklaşık %40 azaltır. * +* **Sezgisel**: Muhteşem bir editör desteği. Her yerde <abbr title="Otomatik Tamamlama: auto-complete, autocompletion, IntelliSense">otomatik tamamlama</abbr>. Hata ayıklama ile daha az zaman harcayacaksınız. +* **Kolay**: Öğrenmesi ve kullanması kolay olacak şekilde tasarlandı. Doküman okuma ile daha az zaman harcayacaksınız. +* **Kısa**: Kod tekrarı minimize edildi. Her parametre tanımlamasında birden fazla özellik ve daha az hatayla karşılaşacaksınız. +* **Güçlü**: Otomatik ve etkileşimli dokümantasyon ile birlikte, kullanıma hazır kod elde edebilirsiniz. +* **Standard öncelikli**: API'lar için açık standartlara dayalı (ve tamamen uyumlu); <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (eski adıyla Swagger) ve <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. -* **Standartlar belirli**: Tamamiyle API'ların açık standartlara bağlı ve (tam uyumlululuk içerisinde); <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (eski adıyla Swagger) ve <a href="http://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. +<small>* ilgili kanılar, dahili geliştirme ekibinin geliştirdikleri ürünlere yaptıkları testlere dayanmaktadır.</small> -<small>* Bahsi geçen rakamsal ifadeler tamamiyle, geliştirme takımının kendi sundukları ürünü geliştirirken yaptıkları testlere dayanmakta.</small> - -## Sponsors +## Sponsorlar <!-- sponsors --> @@ -55,63 +57,61 @@ Ana özellikleri: <!-- /sponsors --> -<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Other sponsors</a> +<a href="https://fastapi.tiangolo.com/tr/fastapi-people/#sponsors" class="external-link" target="_blank">Diğer Sponsorlar</a> ## Görüşler - -"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum [...] Aslına bakarsanız **Microsoft'taki Machine Learning servislerimizin** hepsinde kullanmayı düşünüyorum. FastAPI ile geliştirdiğimiz servislerin bazıları çoktan **Windows**'un ana ürünlerine ve **Office** ürünlerine entegre edilmeye başlandı bile._" +"_[...] Bugünlerde **FastAPI**'ı çok fazla kullanıyorum. [...] Aslında bunu ekibimin **Microsoft'taki Machine Learning servislerinin** tamamında kullanmayı planlıyorum. Bunlardan bazıları **Windows**'un ana ürünlerine ve **Office** ürünlerine entegre ediliyor._" <div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> --- - -"_**FastAPI**'ı **tahminlerimiz**'i sorgulanabilir hale getirmek için **REST** mimarisı ile beraber server üzerinde kullanmaya başladık._" - +"_**FastAPI**'ı **tahminlerimiz**'i sorgulanabilir hale getirecek bir **REST** sunucu oluşturmak için benimsedik/kullanmaya başladık._" <div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> --- - -"_**Netflix** **kriz yönetiminde** orkestrasyon yapabilmek için geliştirdiği yeni framework'ü **Dispatch**'in, açık kaynak versiyonunu paylaşmaktan gurur duyuyor. [**FastAPI** ile yapıldı.]_" +"_**Netflix**, **kriz yönetiminde** orkestrasyon yapabilmek için geliştirdiği yeni framework'ü **Dispatch**'in, açık kaynak sürümünü paylaşmaktan gurur duyuyor. [**FastAPI** ile yapıldı.]_" <div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> --- - "_**FastAPI** için ayın üzerindeymişcesine heyecanlıyım. Çok eğlenceli!_" - <div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> --- -"_Dürüst olmak gerekirse, geliştirdiğin şey bir çok açıdan çok sağlam ve parlak gözüküyor. Açıkcası benim **Hug**'ı tasarlarken yapmaya çalıştığım şey buydu - bunu birisinin başardığını görmek gerçekten çok ilham verici._" +"_Dürüst olmak gerekirse, inşa ettiğiniz şey gerçekten sağlam ve profesyonel görünüyor. Birçok açıdan **Hug**'ın olmasını istediğim şey tam da bu - böyle bir şeyi inşa eden birini görmek gerçekten ilham verici._" -<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a>'ın Yaratıcısı</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="http://www.hug.rest/" target="_blank">Hug</a>'ın Yaratıcısı</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> --- -"_Eğer REST API geliştirmek için **modern bir framework** öğrenme arayışında isen, **FastAPI**'a bir göz at [...] Hızlı, kullanımı ve öğrenmesi kolay. [...]_" - -"_Biz **API** servislerimizi **FastAPI**'a geçirdik [...] Sizin de beğeneceğinizi düşünüyoruz. [...]_" - +"_Eğer REST API geliştirmek için **modern bir framework** öğrenme arayışında isen, **FastAPI**'a bir göz at [...] Hızlı, kullanımı ve öğrenmesi kolay. [...]_" +"_**API** servislerimizi **FastAPI**'a taşıdık [...] Sizin de beğeneceğinizi düşünüyoruz. [...]_" <div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> kurucuları - <a href="https://spacy.io" target="_blank">spaCy</a> yaratıcıları</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> --- -## **Typer**, komut satırı uygulamalarının FastAPI'ı +"_Python ile kullanıma hazır bir API oluşturmak isteyen herhangi biri için, **FastAPI**'ı şiddetle tavsiye ederim. **Harika tasarlanmış**, **kullanımı kolay** ve **yüksek ölçeklenebilir**, API odaklı geliştirme stratejimizin **ana bileşeni** haline geldi ve Virtual TAC Engineer gibi birçok otomasyon ve servisi yönetiyor._" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## Komut Satırı Uygulamalarının FastAPI'ı: **Typer** <a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> -Eğer API yerine <abbr title="Command Line Interface">komut satırı uygulaması</abbr> geliştiriyor isen <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>'a bir göz at. +Eğer API yerine, terminalde kullanılmak üzere bir <abbr title="Komut Satırı: Command Line Interface">komut satırı uygulaması</abbr> geliştiriyorsanız <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>'a göz atabilirsiniz. -**Typer** kısaca FastAPI'ın küçük kız kardeşi. Komut satırı uygulamalarının **FastAPI'ı** olması hedeflendi. ⌨️ 🚀 +**Typer** kısaca FastAPI'ın küçük kardeşi. Ve hedefi komut satırı uygulamalarının **FastAPI'ı** olmak. ⌨️ 🚀 ## Gereksinimler @@ -122,7 +122,7 @@ FastAPI iki devin omuzları üstünde duruyor: * Web tarafı için <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a>. * Data tarafı için <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>. -## Yükleme +## Kurulum <div class="termy"> @@ -134,7 +134,7 @@ $ pip install fastapi </div> -Uygulamanı kullanılabilir hale getirmek için <a href="http://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> ya da <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a> gibi bir ASGI serverına ihtiyacın olacak. +Uygulamamızı kullanılabilir hale getirmek için <a href="http://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> ya da <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a> gibi bir ASGI sunucusuna ihtiyacımız olacak. <div class="termy"> @@ -148,9 +148,9 @@ $ pip install "uvicorn[standard]" ## Örnek -### Şimdi dene +### Kodu Oluşturalım -* `main.py` adında bir dosya oluştur : +* `main.py` adında bir dosya oluşturup içine şu kodu yapıştıralım: ```Python from typing import Union @@ -173,9 +173,9 @@ def read_item(item_id: int, q: Union[str, None] = None): <details markdown="1"> <summary>Ya da <code>async def</code>...</summary> -Eğer kodunda `async` / `await` var ise, `async def` kullan: +Eğer kodunuzda `async` / `await` varsa, `async def` kullanalım: -```Python hl_lines="9 14" +```Python hl_lines="9 14" from typing import Union from fastapi import FastAPI @@ -195,13 +195,13 @@ async def read_item(item_id: int, q: Union[str, None] = None): **Not**: -Eğer ne olduğunu bilmiyor isen _"Acelen mi var?"_ kısmını oku <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` ve `await`</a>. +Eğer bu konu hakkında bilginiz yoksa <a href="https://fastapi.tiangolo.com/tr/async/#in-a-hurry" target="_blank">`async` ve `await`</a> dokümantasyonundaki _"Aceleniz mi var?"_ kısmını kontrol edebilirsiniz. </details> -### Çalıştır +### Kodu Çalıştıralım -Serverı aşağıdaki komut ile çalıştır: +Sunucuyu aşağıdaki komutla çalıştıralım: <div class="termy"> @@ -218,56 +218,56 @@ INFO: Application startup complete. </div> <details markdown="1"> -<summary>Çalıştırdığımız <code>uvicorn main:app --reload</code> hakkında...</summary> +<summary><code>uvicorn main:app --reload</code> komutuyla ilgili...</summary> -`uvicorn main:app` şunları ifade ediyor: +`uvicorn main:app` komutunu şu şekilde açıklayabiliriz: * `main`: dosya olan `main.py` (yani Python "modülü"). -* `app`: ise `main.py` dosyasının içerisinde oluşturduğumuz `app = FastAPI()` 'a denk geliyor. -* `--reload`: ise kodda herhangi bir değişiklik yaptığımızda serverın yapılan değişiklerileri algılayıp, değişiklikleri siz herhangi bir şey yapmadan uygulamasını sağlıyor. +* `app`: ise `main.py` dosyasının içerisinde `app = FastAPI()` satırında oluşturduğumuz `FastAPI` nesnesi. +* `--reload`: kod değişikliklerinin ardından sunucuyu otomatik olarak yeniden başlatır. Bu parameteyi sadece geliştirme aşamasında kullanmalıyız. </details> -### Dokümantasyonu kontrol et +### Şimdi de Kontrol Edelim -Browserını aç ve şu linke git <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. +Tarayıcımızda şu bağlantıyı açalım <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. -Bir JSON yanıtı göreceksin: +Aşağıdaki gibi bir JSON yanıtıyla karşılaşacağız: ```JSON {"item_id": 5, "q": "somequery"} ``` -Az önce oluşturduğun API: +Az önce oluşturduğumuz API: -* `/` ve `/items/{item_id}` adreslerine HTTP talebi alabilir hale geldi. -* İki _adresde_ `GET` <em>operasyonlarını</em> (HTTP _metodları_ olarakta bilinen) yapabilir hale geldi. -* `/items/{item_id}` _adresi_ ayrıca bir `item_id` _adres parametresine_ sahip ve bu bir `int` olmak zorunda. -* `/items/{item_id}` _adresi_ opsiyonel bir `str` _sorgu paramtersine_ sahip bu da `q`. +* `/` ve `/items/{item_id}` <abbr title="Adres / Yol: Path ">_yollarına_</abbr> HTTP isteği alabilir. +* İki _yolda_ `GET` <em>operasyonlarını</em> (HTTP _metodları_ olarak da bilinen) kabul ediyor. +* `/items/{item_id}` _yolu_ `item_id` adında bir _yol parametresine_ sahip ve bu parametre `int` değer almak zorundadır. +* `/items/{item_id}` _yolu_ `q` adında bir _yol parametresine_ sahip ve bu parametre opsiyonel olmakla birlikte, `str` değer almak zorundadır. -### İnteraktif API dokümantasyonu +### Etkileşimli API Dokümantasyonu -Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> adresine git. +Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısını açalım. -Senin için otomatik oluşturulmuş(<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanan) interaktif bir API dokümanı göreceksin: +<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tarafından sağlanan otomatik etkileşimli bir API dokümantasyonu göreceğiz: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) -### Alternatif API dokümantasyonu +### Alternatif API Dokümantasyonu -Şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresine git. +Şimdi <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> bağlantısını açalım. -Senin için alternatif olarak (<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanan) bir API dokümantasyonu daha göreceksin: +<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tarafından sağlanan otomatik dokümantasyonu göreceğiz: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) -## Örnek bir değişiklik +## Örneği Güncelleyelim -Şimdi `main.py` dosyasını değiştirelim ve body ile `PUT` talebi alabilir hale getirelim. +Şimdi `main.py` dosyasını, `PUT` isteğiyle birlikte bir gövde alacak şekilde değiştirelim. -Şimdi Pydantic sayesinde, Python'un standart tiplerini kullanarak bir body tanımlayacağız. +<abbr title="Gövde: Body">Gövde</abbr>yi Pydantic sayesinde standart python tiplerini kullanarak tanımlayalım. -```Python hl_lines="4 9 10 11 12 25 26 27" +```Python hl_lines="4 9-12 25-27" from typing import Union from fastapi import FastAPI @@ -297,41 +297,41 @@ def update_item(item_id: int, item: Item): return {"item_name": item.name, "item_id": item_id} ``` -Server otomatik olarak yeniden başlamalı (çünkü yukarıda `uvicorn`'u çalıştırırken `--reload` parametresini kullandık.). +Sunucu otomatik olarak yeniden başlamış olmalı (çünkü yukarıda `uvicorn` komutuyla birlikte `--reload` parametresini kullandık). -### İnteraktif API dokümantasyonu'nda değiştirme yapmak +### Etkileşimli API Dokümantasyonundaki Değişimi Görelim -Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısına tekrar git. +Şimdi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> bağlantısına tekrar gidelim. -* İnteraktif API dokümantasyonu, yeni body ile beraber çoktan yenilenmiş olması lazım: +* Etkileşimli API dokümantasyonu, yeni gövdede dahil olmak üzere otomatik olarak güncellenmiş olacak: ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* "Try it out"a tıkla, bu senin API parametleri üzerinde deneme yapabilmene izin veriyor: +* "Try it out" butonuna tıklayalım, bu işlem API parametleri üzerinde değişiklik yapmamıza ve doğrudan API ile etkileşime geçmemize imkan sağlayacak: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Şimdi "Execute" butonuna tıkla, kullanıcı arayüzü otomatik olarak API'ın ile bağlantı kurarak ona bu parametreleri gönderecek ve sonucu karşına getirecek. +* Şimdi "Execute" butonuna tıklayalım, kullanıcı arayüzü API'ımız ile bağlantı kurup parametreleri gönderecek ve sonucu ekranımıza getirecek: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) -### Alternatif API dokümantasyonunda değiştirmek +### Alternatif API Dokümantasyonundaki Değişimi Görelim -Şimdi ise <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> adresine git. +Şimdi ise <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> bağlantısına tekrar gidelim. -* Alternatif dokümantasyonda koddaki değişimler ile beraber kendini yeni query ve body ile güncelledi. +* Alternatif dokümantasyonda yaptığımız değişiklikler ile birlikte yeni sorgu parametresi ve gövde bilgisi ile güncelemiş olacak: ![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) ### Özet -Özetleyecek olursak, URL, sorgu veya request body'deki parametrelerini fonksiyon parametresi olarak kullanıyorsun. Bu parametrelerin veri tiplerini bir kere belirtmen yeterli. +Özetlemek gerekirse, parametrelerin, gövdenin, vb. veri tiplerini fonksiyon parametreleri olarak **bir kere** tanımlıyoruz. -Type-hinting işlemini Python dilindeki standart veri tipleri ile yapabilirsin +Bu işlemi standart modern Python tipleriyle yapıyoruz. -Yeni bir syntax'e alışmana gerek yok, metodlar ve classlar zaten spesifik kütüphanelere ait. +Yeni bir sözdizimi yapısını, bir kütüphane özel metod veya sınıfları öğrenmeye gerek yoktur. -Sadece standart **Python 3.8+**. +Hepsi sadece **Python 3.8+** standartlarına dayalıdır. Örnek olarak, `int` tanımlamak için: @@ -339,64 +339,64 @@ Sadece standart **Python 3.8+**. item_id: int ``` -ya da daha kompleks `Item` tipi: +ya da daha kompleks herhangi bir python modelini tanımlayabiliriz, örneğin `Item` modeli için: ```Python item: Item ``` -...sadece kısa bir parametre tipi belirtmekle beraber, sahip olacakların: +...ve sadece kısa bir parametre tipi belirterek elde ettiklerimiz: -* Editör desteği dahil olmak üzere: +* Editör desteğiyle birlikte: * Otomatik tamamlama. - * Tip sorguları. -* Datanın tipe uyumunun sorgulanması: - * Eğer data geçersiz ise, otomatik olarak hataları ayıklar. - * Çok derin JSON objelerinde bile veri tipi sorgusu yapar. -* Gelen verinin <abbr title="parsing, serializing, marshalling olarakta biliniyor">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirebiliyor. + * Tip kontrolü. +* Veri Doğrulama: + * Veri geçerli değilse, otomatik olarak açıklayıcı hatalar gösterir. + * Çok <abbr title="Derin / İç içe: Nested">derin</abbr> JSON nesnelerinde bile doğrulama yapar. +* Gelen verinin <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirir: * JSON. - * Path parametreleri. - * Query parametreleri. - * Cookies. + * Yol parametreleri. + * Sorgu parametreleri. + * Çerezler. * Headers. - * Forms. - * Files. -* Giden verinin <abbr title="also known as: serialization, parsing, marshalling">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirebiliyor (JSON olarak): - * Python tiplerinin (`str`, `int`, `float`, `bool`, `list`, vs) çevirisi. - * `datetime` objesi. - * `UUID` objesi. + * Formlar. + * Dosyalar. +* Giden verinin <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümünü</abbr> aşağıdaki veri tiplerini kullanarak gerçekleştirir (JSON olarak): + * Python tiplerinin (`str`, `int`, `float`, `bool`, `list`, vb) dönüşümü. + * `datetime` nesnesi. + * `UUID` nesnesi. * Veritabanı modelleri. - * ve daha fazlası... -* 2 alternatif kullanıcı arayüzü dahil olmak üzere, otomatik interaktif API dokümanu: + * ve çok daha fazlası... +* 2 alternatif kullanıcı arayüzü dahil olmak üzere, otomatik etkileşimli API dokümantasyonu sağlar: * Swagger UI. * ReDoc. --- -Az önceki kod örneğine geri dönelim, **FastAPI**'ın yapacaklarına bir bakış atalım: +Az önceki örneğe geri dönelim, **FastAPI**'ın yapacaklarına bir bakış atalım: -* `item_id`'nin `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. -* `item_id`'nin tipinin `int` olduğunu `GET` ve `PUT` talepleri içinde olup olmadığının doğruluğunu kontol edecek. - * Eğer `GET` ve `PUT` içinde yok ise ve `int` değil ise, sebebini belirten bir hata mesajı gösterecek -* Opsiyonel bir `q` parametresinin `GET` talebi için (`http://127.0.0.1:8000/items/foo?q=somequery` içinde) olup olmadığını kontrol edecek +* `item_id`'nin `GET` ve `PUT` istekleri için, yolda olup olmadığının kontol edecek. +* `item_id`'nin `GET` ve `PUT` istekleri için, tipinin `int` olduğunu doğrulayacak. + * Eğer değilse, sebebini belirten bir hata mesajı gösterecek. +* Opsiyonel bir `q` parametresinin `GET` isteği içinde (`http://127.0.0.1:8000/items/foo?q=somequery` gibi) olup olmadığını kontrol edecek * `q` parametresini `= None` ile oluşturduğumuz için, opsiyonel bir parametre olacak. - * Eğer `None` olmasa zorunlu bir parametre olacak idi (bu yüzden body'de `PUT` parametresi var). -* `PUT` talebi için `/items/{item_id}`'nin body'sini, JSON olarak okuyor: - * `name` adında bir parametetre olup olmadığını ve var ise onun `str` olup olmadığını kontol ediyor. - * `price` adında bir parametetre olup olmadığını ve var ise onun `float` olup olmadığını kontol ediyor. - * `is_offer` adında bir parametetre olup olmadığını ve var ise onun `bool` olup olmadığını kontol ediyor. - * Bunların hepsini en derin JSON modellerinde bile yapacaktır. -* Bütün veri tiplerini otomatik olarak JSON'a çeviriyor veya tam tersi. -* Her şeyi dokümanlayıp, çeşitli yerlerde: - * İnteraktif dokümantasyon sistemleri. - * Otomatik alıcı kodu üretim sistemlerinde ve çeşitli dillerde. -* İki ayrı web arayüzüyle direkt olarak interaktif bir dokümantasyon sunuyor. + * Eğer `None` olmasa zorunlu bir parametre olacaktı (`PUT` metodunun gövdesinde olduğu gibi). +* `PUT` isteği için `/items/{item_id}`'nin gövdesini, JSON olarak doğrulayıp okuyacak: + * `name` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `str` olup olmadığını kontol edecek. + * `price` adında zorunlu bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. + * `is_offer` adında opsiyonel bir parametre olup olmadığını ve varsa tipinin `float` olup olmadığını kontol edecek. + * Bunların hepsi en derin JSON nesnelerinde bile çalışacak. +* Verilerin JSON'a ve JSON'ın python nesnesine dönüşümü otomatik olarak yapılacak. +* Her şeyi OpenAPI ile uyumlu bir şekilde otomatik olarak dokümanlayacak ve bunlarda aşağıdaki gibi kullanılabilecek: + * Etkileşimli dokümantasyon sistemleri. + * Bir çok programlama dili için otomatik istemci kodu üretim sistemleri. +* İki ayrı etkileşimli dokümantasyon arayüzünü doğrudan sağlayacak. --- -Henüz yüzeysel bir bakış attık, fakat sen çoktan çalışma mantığını anladın. +Daha yeni başladık ama çalışma mantığını çoktan anlamış oldunuz. -Şimdi aşağıdaki satırı değiştirmeyi dene: +Şimdi aşağıdaki satırı değiştirmeyi deneyin: ```Python return {"item_name": item.name, "item_id": item_id} @@ -414,22 +414,22 @@ Henüz yüzeysel bir bakış attık, fakat sen çoktan çalışma mantığını ... "item_price": item.price ... ``` -...şimdi editör desteğinin nasıl veri tiplerini bildiğini ve otomatik tamamladığını gör: +...ve editörünün veri tiplerini bildiğini ve otomatik tamamladığını göreceksiniz: ![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) -Daha fazla örnek ve özellik için <a href="https://fastapi.tiangolo.com/tutorial/">Tutorial - User Guide</a> sayfasını git. +Daha fazal özellik içeren, daha eksiksiz bir örnek için <a href="https://fastapi.tiangolo.com/tr/tutorial/">Öğretici - Kullanıcı Rehberi</a> sayfasını ziyaret edebilirsin. -**Spoiler**: Öğretici - Kullanıcı rehberi şunları içeriyor: +**Spoiler**: Öğretici - Kullanıcı rehberi şunları içerir: -* **Parameterlerini** nasıl **headers**, **cookies**, **form fields** ve **files** olarak deklare edebileceğini. -* `maximum_length` ya da `regex` gibi şeylerle nasıl **doğrulama** yapabileceğini. -* Çok güçlü ve kullanımı kolay **<abbr title="also known as components, resources, providers, services, injectables">Zorunluluk Entegrasyonu</abbr>** oluşturmayı. -* Güvenlik ve kimlik doğrulama, **JWT tokenleri**'yle beraber **OAuth2** desteği, ve **HTTP Basic** doğrulaması. -* İleri seviye fakat ona göre oldukça basit olan **derince oluşturulmuş JSON modelleri** (Pydantic sayesinde). +* **Parameterlerin**, **headers**, **çerezler**, **form alanları** ve **dosyalar** olarak tanımlanması. +* `maximum_length` ya da `regex` gibi **doğrulama kısıtlamalarının** nasıl yapılabileceği. +* Çok güçlü ve kullanımı kolay **<abbr title="Bağımlılık Enjeksiyonu: components, resources, providers, services, injectables olarak da biliniyor.">Bağımlılık Enjeksiyonu</abbr>** sistemi oluşturmayı. +* Güvenlik ve kimlik doğrulama, **JWT tokenleri** ile **OAuth2** desteği, ve **HTTP Basic** doğrulaması. +* İleri seviye fakat bir o kadarda basit olan **çok derin JSON modelleri** (Pydantic sayesinde). +* **GraphQL** entegrasyonu: <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> ve diğer kütüphaneleri kullanarak. * Diğer ekstra özellikler (Starlette sayesinde): - * **WebSockets** - * **GraphQL** + * **WebSocketler** * HTTPX ve `pytest` sayesinde aşırı kolay testler. * **CORS** * **Cookie Sessions** @@ -437,33 +437,34 @@ Daha fazla örnek ve özellik için <a href="https://fastapi.tiangolo.com/tutori ## Performans -Bağımsız TechEmpower kıyaslamaları gösteriyor ki, Uvicorn'la beraber çalışan **FastAPI** uygulamaları <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">Python'un en hızlı frameworklerinden birisi </a>, sadece Starlette ve Uvicorn'dan daha yavaş ki FastAPI bunların üzerine kurulu. +Bağımsız TechEmpower kıyaslamaları gösteriyor ki, Uvicorn ile çalıştırılan **FastAPI** uygulamaları <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">en hızlı Python framework'lerinden birisi</a>, sadece Starlette ve Uvicorn'dan yavaş, ki FastAPI bunların üzerine kurulu bir kütüphanedir. -Daha fazla bilgi için, bu bölüme bir göz at <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. +Daha fazla bilgi için, bu bölüme bir göz at <a href="https://fastapi.tiangolo.com/tr/benchmarks/" class="internal-link" target="_blank">Kıyaslamalar</a>. -## Opsiyonel gereksinimler +## Opsiyonel Gereksinimler Pydantic tarafında kullanılan: * <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - email doğrulaması için. +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - ayar yönetimi için. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Pydantic ile birlikte kullanılabilecek ek tipler için. Starlette tarafında kullanılan: -* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Eğer `TestClient` kullanmak istiyorsan gerekli. -* <a href="http://jinja.pocoo.org" target="_blank"><code>jinja2</code></a> - Eğer kendine ait template konfigürasyonu oluşturmak istiyorsan gerekli -* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Form kullanmak istiyorsan gerekli <abbr title="HTTP bağlantısından gelen stringi Python objesine çevirmek için">("dönüşümü")</abbr>. +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Eğer `TestClient` yapısını kullanacaksanız gereklidir. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Eğer varsayılan template konfigürasyonunu kullanacaksanız gereklidir. +* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Eğer `request.form()` ile form <abbr title="HTTP isteği ile gelen string veriyi Python nesnesine çevirme.">dönüşümü</abbr> desteğini kullanacaksanız gereklidir. * <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - `SessionMiddleware` desteği için gerekli. * <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - `SchemaGenerator` desteği için gerekli (Muhtemelen FastAPI kullanırken ihtiyacınız olmaz). -* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - `GraphQLApp` desteği için gerekli. -* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse` kullanmak istiyorsan gerekli. +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse` kullanacaksanız gerekli. Hem FastAPI hem de Starlette tarafından kullanılan: -* <a href="http://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - oluşturduğumuz uygulamayı bir web sunucusuna servis etmek için gerekli -* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse` kullanmak istiyor isen gerekli. +* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - oluşturduğumuz uygulamayı servis edecek web sunucusu görevini üstlenir. +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse` kullanacaksanız gereklidir. Bunların hepsini `pip install fastapi[all]` ile yükleyebilirsin. ## Lisans -Bu proje, MIT lisansı şartlarına göre lisanslanmıştır. +Bu proje, MIT lisansı şartları altında lisanslanmıştır. From c471c9311371d12b3dd2ed80a5d078e8991a4c19 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:36:04 +0000 Subject: [PATCH 1516/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 188e09a8f..8706abf0f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/learn/index.md`. PR [#10539](https://github.com/tiangolo/fastapi/pull/10539) by [@AlertRED](https://github.com/AlertRED). * 🌐 Update SQLAlchemy instruction in Chinese translation `docs/zh/docs/tutorial/sql-databases.md`. PR [#9712](https://github.com/tiangolo/fastapi/pull/9712) by [@Royc30ne](https://github.com/Royc30ne). * 🌐 Add Turkish translation for `docs/tr/docs/external-links.md`. PR [#10549](https://github.com/tiangolo/fastapi/pull/10549) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Spanish translation for `docs/es/docs/learn/index.md`. PR [#10885](https://github.com/tiangolo/fastapi/pull/10885) by [@pablocm83](https://github.com/pablocm83). From 031000fc6e242fb4582fe2753f6a62eea2890ef3 Mon Sep 17 00:00:00 2001 From: fhabers21 <58401847+fhabers21@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:36:32 +0100 Subject: [PATCH 1517/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/first-steps.md`=20(#9530)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: Georg Wicke-Arndt <g.wicke-arndt@outlook.com> --- docs/de/docs/tutorial/first-steps.md | 333 +++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 docs/de/docs/tutorial/first-steps.md diff --git a/docs/de/docs/tutorial/first-steps.md b/docs/de/docs/tutorial/first-steps.md new file mode 100644 index 000000000..5997f138f --- /dev/null +++ b/docs/de/docs/tutorial/first-steps.md @@ -0,0 +1,333 @@ +# Erste Schritte + +Die einfachste FastAPI-Datei könnte wie folgt aussehen: + +```Python +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Kopieren Sie dies in eine Datei `main.py`. + +Starten Sie den Live-Server: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +<span style="color: green;">INFO</span>: Started reloader process [28720] +<span style="color: green;">INFO</span>: Started server process [28722] +<span style="color: green;">INFO</span>: Waiting for application startup. +<span style="color: green;">INFO</span>: Application startup complete. +``` + +</div> + +!!! note "Hinweis" + Der Befehl `uvicorn main:app` bezieht sich auf: + + * `main`: die Datei `main.py` (das sogenannte Python-„Modul“). + * `app`: das Objekt, welches in der Datei `main.py` mit der Zeile `app = FastAPI()` erzeugt wurde. + * `--reload`: lässt den Server nach Codeänderungen neu starten. Verwenden Sie das nur während der Entwicklung. + +In der Konsolenausgabe sollte es eine Zeile geben, die ungefähr so aussieht: + +```hl_lines="4" +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +Diese Zeile zeigt die URL, unter der Ihre Anwendung auf Ihrem lokalen Computer bereitgestellt wird. + +### Testen Sie es + +Öffnen Sie Ihren Browser unter <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000.</a> + +Sie werden folgende JSON-Antwort sehen: + +```JSON +{"message": "Hello World"} +``` + +### Interaktive API-Dokumentation + +Gehen Sie als Nächstes auf <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs </a>. + +Sie werden die automatisch erzeugte, interaktive API-Dokumentation sehen (bereitgestellt durch <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternative API-Dokumentation + +Gehen Sie nun auf <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. + +Dort sehen Sie die alternative, automatische Dokumentation (bereitgestellt durch <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +### OpenAPI + +**FastAPI** generiert ein „Schema“ mit all Ihren APIs unter Verwendung des **OpenAPI**-Standards zur Definition von APIs. + +#### „Schema“ + +Ein „Schema“ ist eine Definition oder Beschreibung von etwas. Nicht der eigentliche Code, der es implementiert, sondern lediglich eine abstrakte Beschreibung. + +#### API-„Schema“ + +In diesem Fall ist <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> eine Spezifikation, die vorschreibt, wie ein Schema für Ihre API zu definieren ist. + +Diese Schemadefinition enthält Ihre API-Pfade, die möglichen Parameter, welche diese entgegennehmen, usw. + +#### Daten-„Schema“ + +Der Begriff „Schema“ kann sich auch auf die Form von Daten beziehen, wie z.B. einen JSON-Inhalt. + +In diesem Fall sind die JSON-Attribute und deren Datentypen, usw. gemeint. + +#### OpenAPI und JSON Schema + +OpenAPI definiert ein API-Schema für Ihre API. Dieses Schema enthält Definitionen (oder „Schemas“) der Daten, die von Ihrer API unter Verwendung von **JSON Schema**, dem Standard für JSON-Datenschemata, gesendet und empfangen werden. + +#### Überprüfen Sie die `openapi.json` + +Falls Sie wissen möchten, wie das rohe OpenAPI-Schema aussieht: FastAPI generiert automatisch ein JSON (Schema) mit den Beschreibungen Ihrer gesamten API. + +Sie können es direkt einsehen unter: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. + +Es wird ein JSON angezeigt, welches ungefähr so aussieht: + +```JSON +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.0" + }, + "paths": { + "/items/": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + + + +... +``` + +#### Wofür OpenAPI gedacht ist + +Das OpenAPI-Schema ist die Grundlage für die beiden enthaltenen interaktiven Dokumentationssysteme. + +Es gibt dutzende Alternativen, die alle auf OpenAPI basieren. Sie können jede dieser Alternativen problemlos zu Ihrer mit **FastAPI** erstellten Anwendung hinzufügen. + +Ebenfalls können Sie es verwenden, um automatisch Code für Clients zu generieren, die mit Ihrer API kommunizieren. Zum Beispiel für Frontend-, Mobile- oder IoT-Anwendungen. + +## Rückblick, Schritt für Schritt + +### Schritt 1: Importieren von `FastAPI` + +```Python hl_lines="1" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +`FastAPI` ist eine Python-Klasse, die die gesamte Funktionalität für Ihre API bereitstellt. + +!!! note "Technische Details" + `FastAPI` ist eine Klasse, die direkt von `Starlette` erbt. + + Sie können alle <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a>-Funktionalitäten auch mit `FastAPI` nutzen. + +### Schritt 2: Erzeugen einer `FastAPI`-„Instanz“ + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +In diesem Beispiel ist die Variable `app` eine „Instanz“ der Klasse `FastAPI`. + +Dies wird der Hauptinteraktionspunkt für die Erstellung all Ihrer APIs sein. + +Die Variable `app` ist dieselbe, auf die sich der Befehl `uvicorn` bezieht: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +Wenn Sie Ihre Anwendung wie folgt erstellen: + +```Python hl_lines="3" +{!../../../docs_src/first_steps/tutorial002.py!} +``` + +Und in eine Datei `main.py` einfügen, dann würden Sie `uvicorn` wie folgt aufrufen: + +<div class="termy"> + +```console +$ uvicorn main:my_awesome_api --reload + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +### Schritt 3: Erstellen einer *Pfadoperation* + +#### Pfad + +„Pfad“ bezieht sich hier auf den letzten Teil der URL, beginnend mit dem ersten `/`. + +In einer URL wie: + +``` +https://example.com/items/foo +``` + +... wäre der Pfad folglich: + +``` +/items/foo +``` + +!!! info + Ein „Pfad“ wird häufig auch als „Endpunkt“ oder „Route“ bezeichnet. + +Bei der Erstellung einer API ist der „Pfad“ die wichtigste Möglichkeit zur Trennung von „Anliegen“ und „Ressourcen“. + +#### Operation + +„Operation“ bezieht sich hier auf eine der HTTP-„Methoden“. + +Eine von diesen: + +* `POST` +* `GET` +* `PUT` +* `DELETE` + +... und die etwas Exotischeren: + +* `OPTIONS` +* `HEAD` +* `PATCH` +* `TRACE` + +Im HTTP-Protokoll können Sie mit jedem Pfad über eine (oder mehrere) dieser „Methoden“ kommunizieren. + +--- + +Bei der Erstellung von APIs verwenden Sie normalerweise diese spezifischen HTTP-Methoden, um eine bestimmte Aktion durchzuführen. + +Normalerweise verwenden Sie: + +* `POST`: um Daten zu erzeugen (create). +* `GET`: um Daten zu lesen (read). +* `PUT`: um Daten zu aktualisieren (update). +* `DELETE`: um Daten zu löschen (delete). + +In OpenAPI wird folglich jede dieser HTTP-Methoden als „Operation“ bezeichnet. + +Wir werden sie auch „**Operationen**“ nennen. + +#### Definieren eines *Pfadoperation-Dekorators* + +```Python hl_lines="6" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Das `@app.get("/")` sagt **FastAPI**, dass die Funktion direkt darunter für die Bearbeitung von Anfragen zuständig ist, die an: + + * den Pfad `/` + * unter der Verwendung der <abbr title="eine HTTP GET Methode"><code>get</code>-Operation</abbr> gehen + +!!! info "`@decorator` Information" + Diese `@something`-Syntax wird in Python „Dekorator“ genannt. + + Sie platzieren ihn über einer Funktion. Wie ein hübscher, dekorativer Hut (daher kommt wohl der Begriff). + + Ein „Dekorator“ nimmt die darunter stehende Funktion und macht etwas damit. + + In unserem Fall teilt dieser Dekorator **FastAPI** mit, dass die folgende Funktion mit dem **Pfad** `/` und der **Operation** `get` zusammenhängt. + + Dies ist der „**Pfadoperation-Dekorator**“. + +Sie können auch die anderen Operationen verwenden: + +* `@app.post()` +* `@app.put()` +* `@app.delete()` + +Oder die exotischeren: + +* `@app.options()` +* `@app.head()` +* `@app.patch()` +* `@app.trace()` + +!!! tip "Tipp" + Es steht Ihnen frei, jede Operation (HTTP-Methode) so zu verwenden, wie Sie es möchten. + + **FastAPI** erzwingt keine bestimmte Bedeutung. + + Die hier aufgeführten Informationen dienen als Leitfaden und sind nicht verbindlich. + + Wenn Sie beispielsweise GraphQL verwenden, führen Sie normalerweise alle Aktionen nur mit „POST“-Operationen durch. + +### Schritt 4: Definieren der **Pfadoperation-Funktion** + +Das ist unsere „**Pfadoperation-Funktion**“: + +* **Pfad**: ist `/`. +* **Operation**: ist `get`. +* **Funktion**: ist die Funktion direkt unter dem „Dekorator“ (unter `@app.get("/")`). + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Dies ist eine Python-Funktion. + +Sie wird von **FastAPI** immer dann aufgerufen, wenn sie eine Anfrage an die URL "`/`" mittels einer `GET`-Operation erhält. + +In diesem Fall handelt es sich um eine `async`-Funktion. + +--- + +Sie könnten sie auch als normale Funktion anstelle von `async def` definieren: + +```Python hl_lines="7" +{!../../../docs_src/first_steps/tutorial003.py!} +``` + +!!! note "Hinweis" + Wenn Sie den Unterschied nicht kennen, lesen Sie [Async: *„In Eile?“*](../async.md#in-eile){.internal-link target=_blank}. + +### Schritt 5: den Inhalt zurückgeben + +```Python hl_lines="8" +{!../../../docs_src/first_steps/tutorial001.py!} +``` + +Sie können ein `dict`, eine `list`, einzelne Werte wie `str`, `int`, usw. zurückgeben. + +Sie können auch Pydantic-Modelle zurückgeben (dazu später mehr). + +Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert werden (einschließlich ORMs usw.). Versuchen Sie, Ihre Lieblingsobjekte zu verwenden. Es ist sehr wahrscheinlich, dass sie bereits unterstützt werden. + +## Zusammenfassung + +* Importieren Sie `FastAPI`. +* Erstellen Sie eine `app` Instanz. +* Schreiben Sie einen **Pfadoperation-Dekorator** (wie z.B. `@app.get("/")`). +* Schreiben Sie eine **Pfadoperation-Funktion** (wie z.B. oben `def root(): ...`). +* Starten Sie den Entwicklungsserver (z.B. `uvicorn main:app --reload`). From 271b4f31440d29af4264c68c0376c44b790cc0a5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:37:13 +0000 Subject: [PATCH 1518/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8706abf0f..e200823d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/learn/index.md`. PR [#10479](https://github.com/tiangolo/fastapi/pull/10479) by [@KAZAMA-DREAM](https://github.com/KAZAMA-DREAM). * 🌐 Add Russian translation for `docs/ru/docs/learn/index.md`. PR [#10539](https://github.com/tiangolo/fastapi/pull/10539) by [@AlertRED](https://github.com/AlertRED). * 🌐 Update SQLAlchemy instruction in Chinese translation `docs/zh/docs/tutorial/sql-databases.md`. PR [#9712](https://github.com/tiangolo/fastapi/pull/9712) by [@Royc30ne](https://github.com/Royc30ne). * 🌐 Add Turkish translation for `docs/tr/docs/external-links.md`. PR [#10549](https://github.com/tiangolo/fastapi/pull/10549) by [@hasansezertasan](https://github.com/hasansezertasan). From 152171e455cbd7f93214e014c441da39a26ae7ed Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Tue, 9 Jan 2024 23:37:29 +0800 Subject: [PATCH 1519/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/index.md`=20(#10275)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: 吴定焕 <108172295+wdh99@users.noreply.github.com> --- docs/zh/docs/deployment/index.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/zh/docs/deployment/index.md diff --git a/docs/zh/docs/deployment/index.md b/docs/zh/docs/deployment/index.md new file mode 100644 index 000000000..1ec0c5c5b --- /dev/null +++ b/docs/zh/docs/deployment/index.md @@ -0,0 +1,21 @@ +# 部署 + +部署 **FastAPI** 应用程序相对容易。 + +## 部署是什么意思 + +**部署**应用程序意味着执行必要的步骤以使其**可供用户使用**。 + +对于**Web API**来说,通常涉及将上传到**云服务器**中,搭配一个性能和稳定性都不错的**服务器程序**,以便你的**用户**可以高效地**访问**你的应用程序,而不会出现中断或其他问题。 + +这与**开发**阶段形成鲜明对比,在**开发**阶段,你不断更改代码、破坏代码、修复代码, 来回停止和重启服务器等。 + +## 部署策略 + +根据你的使用场景和使用的工具,有多种方法可以实现此目的。 + +你可以使用一些工具自行**部署服务器**,你也可以使用能为你完成部分工作的**云服务**,或其他可能的选项。 + +我将向你展示在部署 **FastAPI** 应用程序时你可能应该记住的一些主要概念(尽管其中大部分适用于任何其他类型的 Web 应用程序)。 + +在接下来的部分中,你将看到更多需要记住的细节以及一些技巧。 ✨ From 5eab5dbed659d4c97bb468bf29e99be57e122d05 Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Tue, 9 Jan 2024 23:38:25 +0800 Subject: [PATCH 1520/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/https.md`=20(#10277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Big Yellow Dog <dognasus@outlook.com> Co-authored-by: Lion <121552599+socket-socket@users.noreply.github.com> --- docs/zh/docs/deployment/https.md | 192 +++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/zh/docs/deployment/https.md diff --git a/docs/zh/docs/deployment/https.md b/docs/zh/docs/deployment/https.md new file mode 100644 index 000000000..cf01a4585 --- /dev/null +++ b/docs/zh/docs/deployment/https.md @@ -0,0 +1,192 @@ +# 关于 HTTPS + +人们很容易认为 HTTPS 仅仅是“启用”或“未启用”的东西。 + +但实际情况比这复杂得多。 + +!!!提示 + 如果你很赶时间或不在乎,请继续阅读下一部分,下一部分会提供一个step-by-step的教程,告诉你怎么使用不同技术来把一切都配置好。 + +要从用户的视角**了解 HTTPS 的基础知识**,请查看 <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>。 + +现在,从**开发人员的视角**,在了解 HTTPS 时需要记住以下几点: + +* 要使用 HTTPS,**服务器**需要拥有由**第三方**生成的**"证书(certificate)"**。 + * 这些证书实际上是从第三方**获取**的,而不是“生成”的。 +* 证书有**生命周期**。 + * 它们会**过期**。 + * 然后它们需要**更新**,**再次从第三方获取**。 +* 连接的加密发生在 **TCP 层**。 + * 这是 HTTP 协议**下面的一层**。 + * 因此,**证书和加密**处理是在 **HTTP之前**完成的。 +* **TCP 不知道域名**。 仅仅知道 IP 地址。 + * 有关所请求的 **特定域名** 的信息位于 **HTTP 数据**中。 +* **HTTPS 证书**“证明”**某个域名**,但协议和加密发生在 TCP 层,在知道正在处理哪个域名**之前**。 +* **默认情况下**,这意味着你**每个 IP 地址只能拥有一个 HTTPS 证书**。 + * 无论你的服务器有多大,或者服务器上的每个应用程序有多小。 + * 不过,对此有一个**解决方案**。 +* **TLS** 协议(在 HTTP 之下的TCP 层处理加密的协议)有一个**扩展**,称为 **<a href="https://en.wikipedia.org/wiki/Server_Name_Indication" class="external-link" target="_blank"><abbr title="服务器名称指示">SNI</abbr></a>**。 + * SNI 扩展允许一台服务器(具有 **单个 IP 地址**)拥有 **多个 HTTPS 证书** 并提供 **多个 HTTPS 域名/应用程序**。 + * 为此,服务器上会有**单独**的一个组件(程序)侦听**公共 IP 地址**,这个组件必须拥有服务器中的**所有 HTTPS 证书**。 +* **获得安全连接后**,通信协议**仍然是HTTP**。 + * 内容是 **加密过的**,即使它们是通过 **HTTP 协议** 发送的。 + +通常的做法是在服务器上运行**一个程序/HTTP 服务器**并**管理所有 HTTPS 部分**:接收**加密的 HTTPS 请求**, 将 **解密的 HTTP 请求** 发送到在同一服务器中运行的实际 HTTP 应用程序(在本例中为 **FastAPI** 应用程序),从应用程序中获取 **HTTP 响应**, 使用适当的 **HTTPS 证书**对其进行加密并使用 **HTTPS** 将其发送回客户端。 此服务器通常被称为 **<a href="https://en.wikipedia.org/wiki/TLS_termination_proxy" class="external-link" target="_blank">TLS 终止代理(TLS Termination Proxy)</a>**。 + +你可以用作 TLS 终止代理的一些选项包括: + +* Traefik(也可以处理证书更新) +* Caddy(也可以处理证书更新) +* Nginx +* HAProxy + +## Let's Encrypt + +在 Let's Encrypt 之前,这些 **HTTPS 证书** 由受信任的第三方出售。 + +过去,获得这些证书的过程非常繁琐,需要大量的文书工作,而且证书非常昂贵。 + +但随后 **<a href="https://letsencrypt.org/" class="external-link" target="_blank">Let's Encrypt</a>** 创建了。 + +它是 Linux 基金会的一个项目。 它以自动方式免费提供 **HTTPS 证书**。 这些证书可以使用所有符合标准的安全加密,并且有效期很短(大约 3 个月),因此**安全性实际上更好**,因为它们的生命周期缩短了。 + +域可以被安全地验证并自动生成证书。 这还允许自动更新这些证书。 + +我们的想法是自动获取和更新这些证书,以便你可以永远免费拥有**安全的 HTTPS**。 + +## 面向开发人员的 HTTPS + +这里有一个 HTTPS API 看起来是什么样的示例,我们会分步说明,并且主要关注对开发人员重要的部分。 + + +### 域名 + +第一步我们要先**获取**一些**域名(Domain Name)**。 然后可以在 DNS 服务器(可能是你的同一家云服务商提供的)中配置它。 + +你可能拥有一个云服务器(虚拟机)或类似的东西,并且它会有一个<abbr title="That isn't Change">固定</abbr> **公共IP地址**。 + +在 DNS 服务器中,你可以配置一条记录(“A 记录”)以将 **你的域名** 指向你服务器的公共 **IP 地址**。 + +这个操作一般只需要在最开始执行一次。 + +!!! tip + 域名这部分发生在 HTTPS 之前,由于这一切都依赖于域名和 IP 地址,所以先在这里提一下。 + +### DNS + +现在让我们关注真正的 HTTPS 部分。 + +首先,浏览器将通过 **DNS 服务器** 查询**域名的IP** 是什么,在本例中为 `someapp.example.com`。 + +DNS 服务器会告诉浏览器使用某个特定的 **IP 地址**。 这将是你在 DNS 服务器中为你的服务器配置的公共 IP 地址。 + +<img src="/img/deployment/https/https01.svg"> + +### TLS 握手开始 + +然后,浏览器将在**端口 443**(HTTPS 端口)上与该 IP 地址进行通信。 + +通信的第一部分只是建立客户端和服务器之间的连接并决定它们将使用的加密密钥等。 + +<img src="/img/deployment/https/https02.svg"> + +客户端和服务器之间建立 TLS 连接的过程称为 **TLS 握手**。 + +### 带有 SNI 扩展的 TLS + +**服务器中只有一个进程**可以侦听特定 **IP 地址**的特定 **端口**。 可能有其他进程在同一 IP 地址的其他端口上侦听,但每个 IP 地址和端口组合只有一个进程。 + +TLS (HTTPS) 默认使用端口`443`。 这就是我们需要的端口。 + +由于只有一个进程可以监听此端口,因此监听端口的进程将是 **TLS 终止代理**。 + +TLS 终止代理可以访问一个或多个 **TLS 证书**(HTTPS 证书)。 + +使用上面讨论的 **SNI 扩展**,TLS 终止代理将检查应该用于此连接的可用 TLS (HTTPS) 证书,并使用与客户端期望的域名相匹配的证书。 + +在这种情况下,它将使用`someapp.example.com`的证书。 + +<img src="/img/deployment/https/https03.svg"> + +客户端已经**信任**生成该 TLS 证书的实体(在本例中为 Let's Encrypt,但我们稍后会看到),因此它可以**验证**该证书是否有效。 + +然后,通过使用证书,客户端和 TLS 终止代理 **决定如何加密** **TCP 通信** 的其余部分。 这就完成了 **TLS 握手** 部分。 + +此后,客户端和服务器就拥有了**加密的 TCP 连接**,这就是 TLS 提供的功能。 然后他们可以使用该连接来启动实际的 **HTTP 通信**。 + +这就是 **HTTPS**,它只是 **安全 TLS 连接** 内的普通 **HTTP**,而不是纯粹的(未加密的)TCP 连接。 + +!!! tip + 请注意,通信加密发生在 **TCP 层**,而不是 HTTP 层。 + +### HTTPS 请求 + +现在客户端和服务器(特别是浏览器和 TLS 终止代理)具有 **加密的 TCP 连接**,它们可以开始 **HTTP 通信**。 + +接下来,客户端发送一个 **HTTPS 请求**。 这其实只是一个通过 TLS 加密连接的 HTTP 请求。 + +<img src="/img/deployment/https/https04.svg"> + +### 解密请求 + +TLS 终止代理将使用协商好的加密算法**解密请求**,并将**(解密的)HTTP 请求**传输到运行应用程序的进程(例如运行 FastAPI 应用的 Uvicorn 进程)。 + +<img src="/img/deployment/https/https05.svg"> + +### HTTP 响应 + +应用程序将处理请求并向 TLS 终止代理发送**(未加密)HTTP 响应**。 + +<img src="/img/deployment/https/https06.svg"> + +### HTTPS 响应 + +然后,TLS 终止代理将使用之前协商的加密算法(以`someapp.example.com`的证书开头)对响应进行加密,并将其发送回浏览器。 + +接下来,浏览器将验证响应是否有效和是否使用了正确的加密密钥等。然后它会**解密响应**并处理它。 + +<img src="/img/deployment/https/https07.svg"> + +客户端(浏览器)将知道响应来自正确的服务器,因为它使用了他们之前使用 **HTTPS 证书** 协商出的加密算法。 + +### 多个应用程序 + +在同一台(或多台)服务器中,可能存在**多个应用程序**,例如其他 API 程序或数据库。 + +只有一个进程可以处理特定的 IP 和端口(在我们的示例中为 TLS 终止代理),但其他应用程序/进程也可以在服务器上运行,只要它们不尝试使用相同的 **公共 IP 和端口的组合**。 + +<img src="/img/deployment/https/https08.svg"> + +这样,TLS 终止代理就可以为多个应用程序处理**多个域名**的 HTTPS 和证书,然后在每种情况下将请求传输到正确的应用程序。 + +### 证书更新 + +在未来的某个时候,每个证书都会**过期**(大约在获得证书后 3 个月)。 + +然后,会有另一个程序(在某些情况下是另一个程序,在某些情况下可能是同一个 TLS 终止代理)与 Let's Encrypt 通信并更新证书。 + +<img src="/img/deployment/https/https.svg"> + +**TLS 证书** **与域名相关联**,而不是与 IP 地址相关联。 + +因此,要更新证书,更新程序需要向权威机构(Let's Encrypt)**证明**它确实**“拥有”并控制该域名**。 + +有多种方法可以做到这一点。 一些流行的方式是: + +* **修改一些DNS记录**。 + * 为此,续订程序需要支持 DNS 提供商的 API,因此,要看你使用的 DNS 提供商是否提供这一功能。 +* **在与域名关联的公共 IP 地址上作为服务器运行**(至少在证书获取过程中)。 + * 正如我们上面所说,只有一个进程可以监听特定的 IP 和端口。 + * 这就是当同一个 TLS 终止代理还负责证书续订过程时它非常有用的原因之一。 + * 否则,你可能需要暂时停止 TLS 终止代理,启动续订程序以获取证书,然后使用 TLS 终止代理配置它们,然后重新启动 TLS 终止代理。 这并不理想,因为你的应用程序在 TLS 终止代理关闭期间将不可用。 + +通过拥有一个**单独的系统来使用 TLS 终止代理来处理 HTTPS**, 而不是直接将 TLS 证书与应用程序服务器一起使用 (例如 Uvicorn),你可以在 +更新证书的过程中同时保持提供服务。 + +## 回顾 + +拥有**HTTPS** 非常重要,并且在大多数情况下相当**关键**。 作为开发人员,你围绕 HTTPS 所做的大部分努力就是**理解这些概念**以及它们的工作原理。 + +一旦你了解了**面向开发人员的 HTTPS** 的基础知识,你就可以轻松组合和配置不同的工具,以帮助你以简单的方式管理一切。 + +在接下来的一些章节中,我将向你展示几个为 **FastAPI** 应用程序设置 **HTTPS** 的具体示例。 🔒 From da9bd0ee4cf2b1eb63154d9ff45106a92e94b89b Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Tue, 9 Jan 2024 23:39:41 +0800 Subject: [PATCH 1521/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/manually.md`=20(#10279)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Big Yellow Dog <dognasus@outlook.com> --- docs/zh/docs/deployment/manually.md | 148 ++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/zh/docs/deployment/manually.md diff --git a/docs/zh/docs/deployment/manually.md b/docs/zh/docs/deployment/manually.md new file mode 100644 index 000000000..15588043f --- /dev/null +++ b/docs/zh/docs/deployment/manually.md @@ -0,0 +1,148 @@ +# 手动运行服务器 - Uvicorn + +在远程服务器计算机上运行 **FastAPI** 应用程序所需的主要东西是 ASGI 服务器程序,例如 **Uvicorn**。 + +有 3 个主要可选方案: + +* <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a>:高性能 ASGI 服务器。 +* <a href="https://pgjones.gitlab.io/hypercorn/" class="external-link" target="_blank">Hypercorn</a>:与 HTTP/2 和 Trio 等兼容的 ASGI 服务器。 +* <a href="https://github.com/django/daphne" class="external-link" target="_blank">Daphne</a>:为 Django Channels 构建的 ASGI 服务器。 + +## 服务器主机和服务器程序 + +关于名称,有一个小细节需要记住。 💡 + +“**服务器**”一词通常用于指远程/云计算机(物理机或虚拟机)以及在该计算机上运行的程序(例如 Uvicorn)。 + +请记住,当您一般读到“服务器”这个名词时,它可能指的是这两者之一。 + +当提到远程主机时,通常将其称为**服务器**,但也称为**机器**(machine)、**VM**(虚拟机)、**节点**。 这些都是指某种类型的远程计算机,通常运行 Linux,您可以在其中运行程序。 + + +## 安装服务器程序 + +您可以使用以下命令安装 ASGI 兼容服务器: + +=== "Uvicorn" + + * <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a>,一个快如闪电 ASGI 服务器,基于 uvloop 和 httptools 构建。 + + <div class="termy"> + + ```console + $ pip install "uvicorn[standard]" + + ---> 100% + ``` + + </div> + + !!! tip + 通过添加`standard`,Uvicorn 将安装并使用一些推荐的额外依赖项。 + + 其中包括`uvloop`,它是`asyncio`的高性能替代品,它提供了巨大的并发性能提升。 + +=== "Hypercorn" + + * <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>,一个也与 HTTP/2 兼容的 ASGI 服务器。 + + <div class="termy"> + + ```console + $ pip install hypercorn + + ---> 100% + ``` + + </div> + + ...或任何其他 ASGI 服务器。 + + +## 运行服务器程序 + +您可以按照之前教程中的相同方式运行应用程序,但不使用`--reload`选项,例如: + +=== "Uvicorn" + + <div class="termy"> + + ```console + $ uvicorn main:app --host 0.0.0.0 --port 80 + + <span style="color: green;">INFO</span>: Uvicorn running on http://0.0.0.0:80 (Press CTRL+C to quit) + ``` + + </div> + + +=== "Hypercorn" + + <div class="termy"> + + ```console + $ hypercorn main:app --bind 0.0.0.0:80 + + Running on 0.0.0.0:8080 over http (CTRL + C to quit) + ``` + + </div> + +!!! warning + 如果您正在使用`--reload`选项,请记住删除它。 + + `--reload` 选项消耗更多资源,并且更不稳定。 + + 它在**开发**期间有很大帮助,但您**不应该**在**生产环境**中使用它。 + +## Hypercorn with Trio + +Starlette 和 **FastAPI** 基于 <a href="https://anyio.readthedocs.io/en/stable/" class="external-link" target="_blank">AnyIO</a>, 所以它们才能同时与 Python 的标准库 <a href="https://docs.python.org/3/library/asyncio-task.html" class="external-link" target="_blank">asyncio</a> 和<a href="https://trio.readthedocs.io/en/stable/" class="external-link" target="_blank">Trio</a> 兼容。 + +尽管如此,Uvicorn 目前仅与 asyncio 兼容,并且通常使用 <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a >, 它是`asyncio`的高性能替代品。 + +但如果你想直接使用**Trio**,那么你可以使用**Hypercorn**,因为它支持它。 ✨ + +### 安装具有 Trio 的 Hypercorn + +首先,您需要安装具有 Trio 支持的 Hypercorn: + +<div class="termy"> + +```console +$ pip install "hypercorn[trio]" +---> 100% +``` + +</div> + +### Run with Trio + +然后你可以传递值`trio`给命令行选项`--worker-class`: + +<div class="termy"> + +```console +$ hypercorn main:app --worker-class trio +``` + +</div> + +这将通过您的应用程序启动 Hypercorn,并使用 Trio 作为后端。 + +现在您可以在应用程序内部使用 Trio。 或者更好的是,您可以使用 AnyIO,使您的代码与 Trio 和 asyncio 兼容。 🎉 + +## 部署概念 + +这些示例运行服务器程序(例如 Uvicorn),启动**单个进程**,在所有 IP(`0.0.0.0`)上监听预定义端口(例如`80`)。 + +这是基本思路。 但您可能需要处理一些其他事情,例如: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* Replication(运行的进程数) +* 内存 +* 开始前的步骤 + +在接下来的章节中,我将向您详细介绍每个概念、如何思考它们,以及一些具体示例以及处理它们的策略。 🚀 From 623ee4460b92617919b974ff97b0927cd289de32 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:41:08 +0000 Subject: [PATCH 1522/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e200823d0..0f475a67d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Update Turkish translation for `docs/tr/docs/index.md`. PR [#10444](https://github.com/tiangolo/fastapi/pull/10444) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Chinese translation for `docs/zh/docs/learn/index.md`. PR [#10479](https://github.com/tiangolo/fastapi/pull/10479) by [@KAZAMA-DREAM](https://github.com/KAZAMA-DREAM). * 🌐 Add Russian translation for `docs/ru/docs/learn/index.md`. PR [#10539](https://github.com/tiangolo/fastapi/pull/10539) by [@AlertRED](https://github.com/AlertRED). * 🌐 Update SQLAlchemy instruction in Chinese translation `docs/zh/docs/tutorial/sql-databases.md`. PR [#9712](https://github.com/tiangolo/fastapi/pull/9712) by [@Royc30ne](https://github.com/Royc30ne). From d29709fee8565ae5403795c2ce2d5c1e7b5f1c2a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:43:37 +0000 Subject: [PATCH 1523/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0f475a67d..43e09a59a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/first-steps.md`. PR [#9530](https://github.com/tiangolo/fastapi/pull/9530) by [@fhabers21](https://github.com/fhabers21). * 🌐 Update Turkish translation for `docs/tr/docs/index.md`. PR [#10444](https://github.com/tiangolo/fastapi/pull/10444) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Chinese translation for `docs/zh/docs/learn/index.md`. PR [#10479](https://github.com/tiangolo/fastapi/pull/10479) by [@KAZAMA-DREAM](https://github.com/KAZAMA-DREAM). * 🌐 Add Russian translation for `docs/ru/docs/learn/index.md`. PR [#10539](https://github.com/tiangolo/fastapi/pull/10539) by [@AlertRED](https://github.com/AlertRED). From 4023510e4c7a3c668e6b7c97934679a3378b8ebd Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Tue, 9 Jan 2024 23:44:17 +0800 Subject: [PATCH 1524/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/cloud.md`=20(#10291)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Lion <121552599+socket-socket@users.noreply.github.com> --- docs/zh/docs/deployment/cloud.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/zh/docs/deployment/cloud.md diff --git a/docs/zh/docs/deployment/cloud.md b/docs/zh/docs/deployment/cloud.md new file mode 100644 index 000000000..398f61372 --- /dev/null +++ b/docs/zh/docs/deployment/cloud.md @@ -0,0 +1,17 @@ +# 在云上部署 FastAPI + +您几乎可以使用**任何云服务商**来部署 FastAPI 应用程序。 + +在大多数情况下,主要的云服务商都有部署 FastAPI 的指南。 + +## 云服务商 - 赞助商 + +一些云服务商 ✨ [**赞助 FastAPI**](../help-fastapi.md#sponsor-the-author){.internal-link target=_blank} ✨,这确保了FastAPI 及其**生态系统**持续健康地**发展**。 + +这表明了他们对 FastAPI 及其**社区**(您)的真正承诺,因为他们不仅想为您提供**良好的服务**,而且还想确保您拥有一个**良好且健康的框架**:FastAPI。 🙇 + +您可能想尝试他们的服务并阅读他们的指南: + +* <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank" >Platform.sh</a> +* <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> +* <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> From 179c8a07633f65481f161e891b99d9611d930d94 Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Tue, 9 Jan 2024 23:46:41 +0800 Subject: [PATCH 1525/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/server-workers.md`=20(#1?= =?UTF-8?q?0292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Big Yellow Dog <dognasus@outlook.com> --- docs/zh/docs/deployment/server-workers.md | 184 ++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 docs/zh/docs/deployment/server-workers.md diff --git a/docs/zh/docs/deployment/server-workers.md b/docs/zh/docs/deployment/server-workers.md new file mode 100644 index 000000000..ee3de9b5d --- /dev/null +++ b/docs/zh/docs/deployment/server-workers.md @@ -0,0 +1,184 @@ +# Server Workers - Gunicorn with Uvicorn + +让我们回顾一下之前的部署概念: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* **复制(运行的进程数)** +* 内存 +* 启动前的先前步骤 + +到目前为止,通过文档中的所有教程,您可能已经在**单个进程**上运行了像 Uvicorn 这样的**服务器程序**。 + +部署应用程序时,您可能希望进行一些**进程复制**,以利用**多核**并能够处理更多请求。 + +正如您在上一章有关[部署概念](./concepts.md){.internal-link target=_blank}中看到的,您可以使用多种策略。 + +在这里我将向您展示如何将 <a href="https://gunicorn.org/" class="external-link" target="_blank">**Gunicorn**</a> 与 **Uvicorn worker 进程** 一起使用。 + +!!! info + 如果您正在使用容器,例如 Docker 或 Kubernetes,我将在下一章中告诉您更多相关信息:[容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank}。 + + 特别是,当在 **Kubernetes** 上运行时,您可能**不想**使用 Gunicorn,而是运行 **每个容器一个 Uvicorn 进程**,但我将在本章后面告诉您这一点。 + + + +## Gunicorn with Uvicorn Workers + +**Gunicorn**主要是一个使用**WSGI标准**的应用服务器。 这意味着 Gunicorn 可以为 Flask 和 Django 等应用程序提供服务。 Gunicorn 本身与 **FastAPI** 不兼容,因为 FastAPI 使用最新的 **<a href="https://asgi.readthedocs.io/en/latest/" class="external-link" target=" _blank">ASGI 标准</a>**。 + +但 Gunicorn 支持充当 **进程管理器** 并允许用户告诉它要使用哪个特定的 **worker类**。 然后 Gunicorn 将使用该类启动一个或多个 **worker进程**。 + +**Uvicorn** 有一个 Gunicorn 兼容的worker类。 + +使用这种组合,Gunicorn 将充当 **进程管理器**,监听 **端口** 和 **IP**。 它会将通信**传输**到运行**Uvicorn类**的worker进程。 + +然后与Gunicorn兼容的**Uvicorn worker**类将负责将Gunicorn发送的数据转换为ASGI标准以供FastAPI使用。 + +## 安装 Gunicorn 和 Uvicorn + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +</div> + +这将安装带有`standard`扩展包(以获得高性能)的 Uvicorn 和 Gunicorn。 + +## Run Gunicorn with Uvicorn Workers + +接下来你可以通过以下命令运行Gunicorn: + +<div class="termy"> + +```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. +``` + +</div> + + +让我们看看每个选项的含义: + +* `main:app`:这与 Uvicorn 使用的语法相同,`main` 表示名为"`main`"的 Python 模块,因此是文件 `main.py`。 `app` 是 **FastAPI** 应用程序的变量名称。 + * 你可以想象 `main:app` 相当于一个 Python `import` 语句,例如: + + ```Python + from main import app + ``` + + * 因此,`main:app` 中的冒号相当于 `from main import app` 中的 Python `import` 部分。 + +* `--workers`:要使用的worker进程数量,每个进程将运行一个 Uvicorn worker进程,在本例中为 4 个worker进程。 + +* `--worker-class`:在worker进程中使用的与 Gunicorn 兼容的工作类。 + * 这里我们传递了 Gunicorn 可以导入和使用的类: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`:这告诉 Gunicorn 要监听的 IP 和端口,使用冒号 (`:`) 分隔 IP 和端口。 + * 如果您直接运行 Uvicorn,则可以使用`--host 0.0.0.0`和`--port 80`,而不是`--bind 0.0.0.0:80`(Gunicorn 选项)。 + + +在输出中,您可以看到它显示了每个进程的 **PID**(进程 ID)(它只是一个数字)。 + +你可以看到: + +* Gunicorn **进程管理器** 以 PID `19499` 开头(在您的情况下,它将是一个不同的数字)。 +* 然后它开始`Listening at: http://0.0.0.0:80`。 +* 然后它检测到它必须使用 `uvicorn.workers.UvicornWorker` 处的worker类。 +* 然后它启动**4个worker**,每个都有自己的PID:`19511`、`19513`、`19514`和`19515`。 + +Gunicorn 还将负责管理**死进程**和**重新启动**新进程(如果需要保持worker数量)。 因此,这在一定程度上有助于上面列表中**重启**的概念。 + +尽管如此,您可能还希望有一些外部的东西,以确保在必要时**重新启动 Gunicorn**,并且**在启动时运行它**等。 + +## Uvicorn with Workers + +Uvicorn 也有一个选项可以启动和运行多个 **worker进程**。 + +然而,到目前为止,Uvicorn 处理worker进程的能力比 Gunicorn 更有限。 因此,如果您想拥有这个级别(Python 级别)的进程管理器,那么最好尝试使用 Gunicorn 作为进程管理器。 + +无论如何,您都可以像这样运行它: + +<div class="termy"> + +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +<font color="#A6E22E">INFO</font>: Uvicorn running on <b>http://0.0.0.0:8080</b> (Press CTRL+C to quit) +<font color="#A6E22E">INFO</font>: Started parent process [<font color="#A1EFE4"><b>27365</b></font>] +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27368</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27369</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27370</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27367</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +``` + +</div> + +这里唯一的新选项是 `--workers` 告诉 Uvicorn 启动 4 个工作进程。 + +您还可以看到它显示了每个进程的 **PID**,父进程(这是 **进程管理器**)的 PID 为`27365`,每个工作进程的 PID 为:`27368`、`27369`, `27370`和`27367`。 + +## 部署概念 + +在这里,您了解了如何使用 **Gunicorn**(或 Uvicorn)管理 **Uvicorn 工作进程**来**并行**应用程序的执行,利用 CPU 中的 **多核**,并 能够满足**更多请求**。 + +从上面的部署概念列表来看,使用worker主要有助于**复制**部分,并对**重新启动**有一点帮助,但您仍然需要照顾其他部分: + +* **安全 - HTTPS** +* **启动时运行** +* ***重新启动*** +* 复制(运行的进程数) +* **内存** +* **启动之前的先前步骤** + +## 容器和 Docker + +在关于 [容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank} 的下一章中,我将介绍一些可用于处理其他 **部署概念** 的策略。 + +我还将向您展示 **官方 Docker 镜像**,其中包括 **Gunicorn 和 Uvicorn worker** 以及一些对简单情况有用的默认配置。 + +在那里,我还将向您展示如何 **从头开始构建自己的镜像** 以运行单个 Uvicorn 进程(没有 Gunicorn)。 这是一个简单的过程,并且可能是您在使用像 **Kubernetes** 这样的分布式容器管理系统时想要做的事情。 + +## 回顾 + +您可以使用**Gunicorn**(或Uvicorn)作为Uvicorn工作进程的进程管理器,以利用**多核CPU**,**并行运行多个进程**。 + +如果您要设置**自己的部署系统**,同时自己处理其他部署概念,则可以使用这些工具和想法。 + +请查看下一章,了解带有容器(例如 Docker 和 Kubernetes)的 **FastAPI**。 您将看到这些工具也有简单的方法来解决其他**部署概念**。 ✨ From fe620a6c127cf5134d520abe91914e205fcac605 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:46:50 +0000 Subject: [PATCH 1526/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 43e09a59a..b292a818e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/index.md`. PR [#10275](https://github.com/tiangolo/fastapi/pull/10275) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add German translation for `docs/de/docs/tutorial/first-steps.md`. PR [#9530](https://github.com/tiangolo/fastapi/pull/9530) by [@fhabers21](https://github.com/fhabers21). * 🌐 Update Turkish translation for `docs/tr/docs/index.md`. PR [#10444](https://github.com/tiangolo/fastapi/pull/10444) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Chinese translation for `docs/zh/docs/learn/index.md`. PR [#10479](https://github.com/tiangolo/fastapi/pull/10479) by [@KAZAMA-DREAM](https://github.com/KAZAMA-DREAM). From f27e818edb0fdd24680235b59298ba60b0c30fe0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:47:49 +0000 Subject: [PATCH 1527/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b292a818e..b71ee8f13 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/https.md`. PR [#10277](https://github.com/tiangolo/fastapi/pull/10277) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/index.md`. PR [#10275](https://github.com/tiangolo/fastapi/pull/10275) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add German translation for `docs/de/docs/tutorial/first-steps.md`. PR [#9530](https://github.com/tiangolo/fastapi/pull/9530) by [@fhabers21](https://github.com/fhabers21). * 🌐 Update Turkish translation for `docs/tr/docs/index.md`. PR [#10444](https://github.com/tiangolo/fastapi/pull/10444) by [@hasansezertasan](https://github.com/hasansezertasan). From c5bbcb8c9c323f23e0d97ce3d0dfc00772362b15 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 15:49:16 +0000 Subject: [PATCH 1528/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b71ee8f13..e0b82fbcb 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/manually.md`. PR [#10279](https://github.com/tiangolo/fastapi/pull/10279) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/https.md`. PR [#10277](https://github.com/tiangolo/fastapi/pull/10277) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/index.md`. PR [#10275](https://github.com/tiangolo/fastapi/pull/10275) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add German translation for `docs/de/docs/tutorial/first-steps.md`. PR [#9530](https://github.com/tiangolo/fastapi/pull/9530) by [@fhabers21](https://github.com/fhabers21). From 933668b42e025f00b6d1de7b1e1ebf18f4f1f2ae Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 9 Jan 2024 18:51:54 +0300 Subject: [PATCH 1529/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/request-files.md`=20(#1033?= =?UTF-8?q?2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> Co-authored-by: oubush <oubush@users.noreply.github.com> --- docs/ru/docs/tutorial/request-files.md | 314 +++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 docs/ru/docs/tutorial/request-files.md diff --git a/docs/ru/docs/tutorial/request-files.md b/docs/ru/docs/tutorial/request-files.md new file mode 100644 index 000000000..00f8c8377 --- /dev/null +++ b/docs/ru/docs/tutorial/request-files.md @@ -0,0 +1,314 @@ +# Загрузка файлов + +Используя класс `File`, мы можем позволить клиентам загружать файлы. + +!!! info "Дополнительная информация" + Чтобы получать загруженные файлы, сначала установите <a href="https://andrew-d.github.io/python-multipart/" class="external-link" target="_blank">`python-multipart`</a>. + + Например: `pip install python-multipart`. + + Это связано с тем, что загружаемые файлы передаются как данные формы. + +## Импорт `File` + +Импортируйте `File` и `UploadFile` из модуля `fastapi`: + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +## Определите параметры `File` + +Создайте параметры `File` так же, как вы это делаете для `Body` или `Form`: + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +!!! info "Дополнительная информация" + `File` - это класс, который наследуется непосредственно от `Form`. + + Но помните, что когда вы импортируете `Query`, `Path`, `File` и другие из `fastapi`, на самом деле это функции, которые возвращают специальные классы. + +!!! tip "Подсказка" + Для объявления тела файла необходимо использовать `File`, поскольку в противном случае параметры будут интерпретироваться как параметры запроса или параметры тела (JSON). + +Файлы будут загружены как данные формы. + +Если вы объявите тип параметра у *функции операции пути* как `bytes`, то **FastAPI** прочитает файл за вас, и вы получите его содержимое в виде `bytes`. + +Следует иметь в виду, что все содержимое будет храниться в памяти. Это хорошо подходит для небольших файлов. + +Однако возможны случаи, когда использование `UploadFile` может оказаться полезным. + +## Загрузка файла с помощью `UploadFile` + +Определите параметр файла с типом `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/request_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/request_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="12" + {!> ../../../docs_src/request_files/tutorial001.py!} + ``` + +Использование `UploadFile` имеет ряд преимуществ перед `bytes`: + +* Использовать `File()` в значении параметра по умолчанию не обязательно. +* При этом используется "буферный" файл: + * Файл, хранящийся в памяти до максимального предела размера, после преодоления которого он будет храниться на диске. +* Это означает, что он будет хорошо работать с большими файлами, такими как изображения, видео, большие бинарные файлы и т.д., не потребляя при этом всю память. +* Из загруженного файла можно получить метаданные. +* Он реализует <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> `async` интерфейс. +* Он предоставляет реальный объект Python <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> который вы можете передать непосредственно другим библиотекам, которые ожидают файл в качестве объекта. + +### `UploadFile` + +`UploadFile` имеет следующие атрибуты: + +* `filename`: Строка `str` с исходным именем файла, который был загружен (например, `myimage.jpg`). +* `content_type`: Строка `str` с типом содержимого (MIME type / media type) (например, `image/jpeg`). +* `file`: <a href="https://docs.python.org/3/library/tempfile.html#tempfile.SpooledTemporaryFile" class="external-link" target="_blank">`SpooledTemporaryFile`</a> (a <a href="https://docs.python.org/3/glossary.html#term-file-like-object" class="external-link" target="_blank">file-like</a> объект). Это фактический файл Python, который можно передавать непосредственно другим функциям или библиотекам, ожидающим файл в качестве объекта. + +`UploadFile` имеет следующие методы `async`. Все они вызывают соответствующие файловые методы (используя внутренний SpooledTemporaryFile). + +* `write(data)`: Записать данные `data` (`str` или `bytes`) в файл. +* `read(size)`: Прочитать количество `size` (`int`) байт/символов из файла. +* `seek(offset)`: Перейти к байту на позиции `offset` (`int`) в файле. + * Наример, `await myfile.seek(0)` перейдет к началу файла. + * Это особенно удобно, если вы один раз выполнили команду `await myfile.read()`, а затем вам нужно прочитать содержимое файла еще раз. +* `close()`: Закрыть файл. + +Поскольку все эти методы являются `async` методами, вам следует использовать "await" вместе с ними. + +Например, внутри `async` *функции операции пути* можно получить содержимое с помощью: + +```Python +contents = await myfile.read() +``` + +Если вы находитесь внутри обычной `def` *функции операции пути*, можно получить прямой доступ к файлу `UploadFile.file`, например: + +```Python +contents = myfile.file.read() +``` + +!!! note "Технические детали `async`" + При использовании методов `async` **FastAPI** запускает файловые методы в пуле потоков и ожидает их. + +!!! note "Технические детали Starlette" + **FastAPI** наследует `UploadFile` непосредственно из **Starlette**, но добавляет некоторые детали для совместимости с **Pydantic** и другими частями FastAPI. + +## Про данные формы ("Form Data") + +Способ, которым HTML-формы (`<form></form>`) отправляют данные на сервер, обычно использует "специальную" кодировку для этих данных, отличную от JSON. + +**FastAPI** позаботится о том, чтобы считать эти данные из нужного места, а не из JSON. + +!!! note "Технические детали" + Данные из форм обычно кодируются с использованием "media type" `application/x-www-form-urlencoded` когда он не включает файлы. + + Но когда форма включает файлы, она кодируется как multipart/form-data. Если вы используете `File`, **FastAPI** будет знать, что ему нужно получить файлы из нужной части тела. + + Если вы хотите узнать больше об этих кодировках и полях форм, перейдите по ссылке <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> web docs for <code>POST</code></a>. + +!!! warning "Внимание" + В операции *функции операции пути* можно объявить несколько параметров `File` и `Form`, но нельзя также объявлять поля `Body`, которые предполагается получить в виде JSON, поскольку тело запроса будет закодировано с помощью `multipart/form-data`, а не `application/json`. + + Это не является ограничением **FastAPI**, это часть протокола HTTP. + +## Необязательная загрузка файлов + +Вы можете сделать загрузку файла необязательной, используя стандартные аннотации типов и установив значение по умолчанию `None`: + +=== "Python 3.10+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10 18" + {!> ../../../docs_src/request_files/tutorial001_02_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7 15" + {!> ../../../docs_src/request_files/tutorial001_02_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9 17" + {!> ../../../docs_src/request_files/tutorial001_02.py!} + ``` + +## `UploadFile` с дополнительными метаданными + +Вы также можете использовать `File()` вместе с `UploadFile`, например, для установки дополнительных метаданных: + +=== "Python 3.9+" + + ```Python hl_lines="9 15" + {!> ../../../docs_src/request_files/tutorial001_03_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="8 14" + {!> ../../../docs_src/request_files/tutorial001_03_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="7 13" + {!> ../../../docs_src/request_files/tutorial001_03.py!} + ``` + +## Загрузка нескольких файлов + +Можно одновременно загружать несколько файлов. + +Они будут связаны с одним и тем же "полем формы", отправляемым с помощью данных формы. + +Для этого необходимо объявить список `bytes` или `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="11 16" + {!> ../../../docs_src/request_files/tutorial002_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8 13" + {!> ../../../docs_src/request_files/tutorial002_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="10 15" + {!> ../../../docs_src/request_files/tutorial002.py!} + ``` + +Вы получите, как и было объявлено, список `list` из `bytes` или `UploadFile`. + +!!! note "Technical Details" + Можно также использовать `from starlette.responses import HTMLResponse`. + + **FastAPI** предоставляет тот же `starlette.responses`, что и `fastapi.responses`, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette. + +### Загрузка нескольких файлов с дополнительными метаданными + +Так же, как и раньше, вы можете использовать `File()` для задания дополнительных параметров, даже для `UploadFile`: + +=== "Python 3.9+" + + ```Python hl_lines="11 18-20" + {!> ../../../docs_src/request_files/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12 19-21" + {!> ../../../docs_src/request_files/tutorial003_an.py!} + ``` + +=== "Python 3.9+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="9 16" + {!> ../../../docs_src/request_files/tutorial003_py39.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="11 18" + {!> ../../../docs_src/request_files/tutorial003.py!} + ``` + +## Резюме + +Используйте `File`, `bytes` и `UploadFile` для работы с файлами, которые будут загружаться и передаваться в виде данных формы. From a64b2fed91fdda259e2363b9bd7ba83d3a8fee98 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 9 Jan 2024 18:52:07 +0300 Subject: [PATCH 1530/1881] =?UTF-8?q?=F0=9F=8C=90=20Fix=20typos=20in=20Rus?= =?UTF-8?q?sian=20translations=20for=20`docs/ru/docs/tutorial/background-t?= =?UTF-8?q?asks.md`,=20`docs/ru/docs/tutorial/body-nested-models.md`,=20`d?= =?UTF-8?q?ocs/ru/docs/tutorial/debugging.md`,=20`docs/ru/docs/tutorial/te?= =?UTF-8?q?sting.md`=20(#10311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/background-tasks.md | 2 +- docs/ru/docs/tutorial/body-nested-models.md | 2 +- docs/ru/docs/tutorial/debugging.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ru/docs/tutorial/background-tasks.md b/docs/ru/docs/tutorial/background-tasks.md index 7a3cf6d83..73ba860bc 100644 --- a/docs/ru/docs/tutorial/background-tasks.md +++ b/docs/ru/docs/tutorial/background-tasks.md @@ -71,7 +71,7 @@ В этом примере сообщения будут записаны в `log.txt` *после* того, как ответ сервера был отправлен. -Если бы в запросе была очередь `q`, она бы первой записалась в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). +Если бы в запрос был передан query-параметр `q`, он бы первыми записался в `log.txt` фоновой задачей (потому что вызывается в зависимости `get_query`). После другая фоновая задача, которая была сгенерирована в функции, запишет сообщение из параметра `email`. diff --git a/docs/ru/docs/tutorial/body-nested-models.md b/docs/ru/docs/tutorial/body-nested-models.md index a6d123d30..bbf9b7685 100644 --- a/docs/ru/docs/tutorial/body-nested-models.md +++ b/docs/ru/docs/tutorial/body-nested-models.md @@ -85,7 +85,7 @@ my_list: List[str] И в Python есть специальный тип данных для множеств уникальных элементов - `set`. -Тогда мы может обьявить поле `tags` как множество строк: +Тогда мы можем обьявить поле `tags` как множество строк: === "Python 3.10+" diff --git a/docs/ru/docs/tutorial/debugging.md b/docs/ru/docs/tutorial/debugging.md index 755d98cf2..38709e56d 100644 --- a/docs/ru/docs/tutorial/debugging.md +++ b/docs/ru/docs/tutorial/debugging.md @@ -22,7 +22,7 @@ $ python myapp.py </div> -но не вызывался, когда другой файл импортирует это, например:: +но не вызывался, когда другой файл импортирует это, например: ```Python from myapp import app From 9f7902925a74419f7c3e439e31554c25fd9f3109 Mon Sep 17 00:00:00 2001 From: _Shuibei <33409883+ShuibeiC@users.noreply.github.com> Date: Tue, 9 Jan 2024 23:53:39 +0800 Subject: [PATCH 1531/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/additional-responses.md`?= =?UTF-8?q?=20(#10325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: unknown <lemonc2021@foxmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/zh/docs/advanced/additional-responses.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/zh/docs/advanced/additional-responses.md diff --git a/docs/zh/docs/advanced/additional-responses.md b/docs/zh/docs/advanced/additional-responses.md new file mode 100644 index 000000000..2a1e1ed89 --- /dev/null +++ b/docs/zh/docs/advanced/additional-responses.md @@ -0,0 +1,219 @@ +# OPENAPI 中的其他响应 + +您可以声明附加响应,包括附加状态代码、媒体类型、描述等。 + +这些额外的响应将包含在OpenAPI模式中,因此它们也将出现在API文档中。 + +但是对于那些额外的响应,你必须确保你直接返回一个像 `JSONResponse` 一样的 `Response` ,并包含你的状态代码和内容。 + +## `model`附加响应 +您可以向路径操作装饰器传递参数 `responses` 。 + +它接收一个 `dict`,键是每个响应的状态代码(如`200`),值是包含每个响应信息的其他 `dict`。 + +每个响应字典都可以有一个关键模型,其中包含一个 `Pydantic` 模型,就像 `response_model` 一样。 + +**FastAPI**将采用该模型,生成其`JSON Schema`并将其包含在`OpenAPI`中的正确位置。 + +例如,要声明另一个具有状态码 `404` 和`Pydantic`模型 `Message` 的响应,可以写: +```Python hl_lines="18 22" +{!../../../docs_src/additional_responses/tutorial001.py!} +``` + + +!!! Note + 请记住,您必须直接返回 `JSONResponse` 。 + +!!! Info + `model` 密钥不是OpenAPI的一部分。 + **FastAPI**将从那里获取`Pydantic`模型,生成` JSON Schema` ,并将其放在正确的位置。 + - 正确的位置是: + - 在键 `content` 中,其具有另一个`JSON`对象( `dict` )作为值,该`JSON`对象包含: + - 媒体类型的密钥,例如 `application/json` ,它包含另一个`JSON`对象作为值,该对象包含: + - 一个键` schema` ,它的值是来自模型的`JSON Schema`,正确的位置在这里。 + - **FastAPI**在这里添加了对OpenAPI中另一个地方的全局JSON模式的引用,而不是直接包含它。这样,其他应用程序和客户端可以直接使用这些JSON模式,提供更好的代码生成工具等。 + + +**在OpenAPI中为该路径操作生成的响应将是:** + +```json hl_lines="3-12" +{ + "responses": { + "404": { + "description": "Additional Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Message" + } + } + } + }, + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Item" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } +} + +``` +**模式被引用到OpenAPI模式中的另一个位置:** +```json hl_lines="4-16" +{ + "components": { + "schemas": { + "Message": { + "title": "Message", + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "title": "Message", + "type": "string" + } + } + }, + "Item": { + "title": "Item", + "required": [ + "id", + "value" + ], + "type": "object", + "properties": { + "id": { + "title": "Id", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + } + }, + "ValidationError": { + "title": "ValidationError", + "required": [ + "loc", + "msg", + "type" + ], + "type": "object", + "properties": { + "loc": { + "title": "Location", + "type": "array", + "items": { + "type": "string" + } + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + } + }, + "HTTPValidationError": { + "title": "HTTPValidationError", + "type": "object", + "properties": { + "detail": { + "title": "Detail", + "type": "array", + "items": { + "$ref": "#/components/schemas/ValidationError" + } + } + } + } + } + } +} + +``` +## 主响应的其他媒体类型 + +您可以使用相同的 `responses` 参数为相同的主响应添加不同的媒体类型。 + +例如,您可以添加一个额外的媒体类型` image/png` ,声明您的路径操作可以返回JSON对象(媒体类型 `application/json` )或PNG图像: + +```Python hl_lines="19-24 28" +{!../../../docs_src/additional_responses/tutorial002.py!} +``` + +!!! Note + - 请注意,您必须直接使用 `FileResponse` 返回图像。 + +!!! Info + - 除非在 `responses` 参数中明确指定不同的媒体类型,否则**FastAPI**将假定响应与主响应类具有相同的媒体类型(默认为` application/json` )。 + - 但是如果您指定了一个自定义响应类,并将 `None `作为其媒体类型,**FastAPI**将使用 `application/json` 作为具有关联模型的任何其他响应。 + +## 组合信息 +您还可以联合接收来自多个位置的响应信息,包括 `response_model `、 `status_code` 和 `responses `参数。 + +您可以使用默认的状态码 `200` (或者您需要的自定义状态码)声明一个 `response_model `,然后直接在OpenAPI模式中在 `responses` 中声明相同响应的其他信息。 + +**FastAPI**将保留来自 `responses` 的附加信息,并将其与模型中的JSON Schema结合起来。 + +例如,您可以使用状态码 `404` 声明响应,该响应使用`Pydantic`模型并具有自定义的` description` 。 + +以及一个状态码为 `200` 的响应,它使用您的 `response_model` ,但包含自定义的 `example` : + +```Python hl_lines="20-31" +{!../../../docs_src/additional_responses/tutorial003.py!} +``` + +所有这些都将被合并并包含在您的OpenAPI中,并在API文档中显示: + +## 联合预定义响应和自定义响应 + +您可能希望有一些应用于许多路径操作的预定义响应,但是你想将不同的路径和自定义的相应组合在一块。 +对于这些情况,你可以使用Python的技术,将 `dict` 与 `**dict_to_unpack` 解包: +```Python +old_dict = { + "old key": "old value", + "second old key": "second old value", +} +new_dict = {**old_dict, "new key": "new value"} +``` + +这里, new_dict 将包含来自 old_dict 的所有键值对加上新的键值对: +```python +{ + "old key": "old value", + "second old key": "second old value", + "new key": "new value", +} +``` +您可以使用该技术在路径操作中重用一些预定义的响应,并将它们与其他自定义响应相结合。 +**例如:** +```Python hl_lines="13-17 26" +{!../../../docs_src/additional_responses/tutorial004.py!} +``` +## 有关OpenAPI响应的更多信息 + +要了解您可以在响应中包含哪些内容,您可以查看OpenAPI规范中的以下部分: + + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responsesObject),它包括 Response Object 。 + + [OpenAPI响应对象](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#responseObject),您可以直接在 `responses` 参数中的每个响应中包含任何内容。包括 `description` 、 `headers` 、 `content` (其中是声明不同的媒体类型和JSON Schemas)和 `links` 。 From 11a5993c8cf037611b87b8064c7c7d5e3adc9fb5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:01:13 +0000 Subject: [PATCH 1532/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e0b82fbcb..31c637fcc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/cloud.md`. PR [#10291](https://github.com/tiangolo/fastapi/pull/10291) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/manually.md`. PR [#10279](https://github.com/tiangolo/fastapi/pull/10279) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/https.md`. PR [#10277](https://github.com/tiangolo/fastapi/pull/10277) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/index.md`. PR [#10275](https://github.com/tiangolo/fastapi/pull/10275) by [@xzmeng](https://github.com/xzmeng). From 8f70f8c43b4ce046958fd846e82f5610ed4bf91a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:03:03 +0000 Subject: [PATCH 1533/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 31c637fcc..c6e539ea4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/server-workers.md`. PR [#10292](https://github.com/tiangolo/fastapi/pull/10292) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/cloud.md`. PR [#10291](https://github.com/tiangolo/fastapi/pull/10291) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/manually.md`. PR [#10279](https://github.com/tiangolo/fastapi/pull/10279) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/https.md`. PR [#10277](https://github.com/tiangolo/fastapi/pull/10277) by [@xzmeng](https://github.com/xzmeng). From d305a67a8101cab32dec71469cb6fd5923097802 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:07:49 +0000 Subject: [PATCH 1534/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c6e539ea4..69894b2a9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-files.md`. PR [#10332](https://github.com/tiangolo/fastapi/pull/10332) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/server-workers.md`. PR [#10292](https://github.com/tiangolo/fastapi/pull/10292) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/cloud.md`. PR [#10291](https://github.com/tiangolo/fastapi/pull/10291) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/manually.md`. PR [#10279](https://github.com/tiangolo/fastapi/pull/10279) by [@xzmeng](https://github.com/xzmeng). From 9ddc71e317938176098722f9f80e335cfc0d0ba1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:08:31 +0000 Subject: [PATCH 1535/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 69894b2a9..ea1652195 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Fix typos in Russian translations for `docs/ru/docs/tutorial/background-tasks.md`, `docs/ru/docs/tutorial/body-nested-models.md`, `docs/ru/docs/tutorial/debugging.md`, `docs/ru/docs/tutorial/testing.md`. PR [#10311](https://github.com/tiangolo/fastapi/pull/10311) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-files.md`. PR [#10332](https://github.com/tiangolo/fastapi/pull/10332) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/server-workers.md`. PR [#10292](https://github.com/tiangolo/fastapi/pull/10292) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/cloud.md`. PR [#10291](https://github.com/tiangolo/fastapi/pull/10291) by [@xzmeng](https://github.com/xzmeng). From cee422f073ddcc37bea3d8f9c0a4bf2d902fe4e5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:09:04 +0000 Subject: [PATCH 1536/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ea1652195..83cae1dda 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -17,6 +17,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/additional-responses.md`. PR [#10325](https://github.com/tiangolo/fastapi/pull/10325) by [@ShuibeiC](https://github.com/ShuibeiC). * 🌐 Fix typos in Russian translations for `docs/ru/docs/tutorial/background-tasks.md`, `docs/ru/docs/tutorial/body-nested-models.md`, `docs/ru/docs/tutorial/debugging.md`, `docs/ru/docs/tutorial/testing.md`. PR [#10311](https://github.com/tiangolo/fastapi/pull/10311) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-files.md`. PR [#10332](https://github.com/tiangolo/fastapi/pull/10332) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/server-workers.md`. PR [#10292](https://github.com/tiangolo/fastapi/pull/10292) by [@xzmeng](https://github.com/xzmeng). From 60e1259ca4c69feb8698f65ffd9744ae92db0721 Mon Sep 17 00:00:00 2001 From: Sepehr Shirkhanlu <sepehr.shirkhanlou@yahoo.com> Date: Tue, 9 Jan 2024 19:40:37 +0330 Subject: [PATCH 1537/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`fastapi/routing.py`=20(#10520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix: https://github.com/tiangolo/fastapi/discussions/10493 --- fastapi/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 589ecca2a..acebabfca 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -4328,7 +4328,7 @@ class APIRouter(routing.Router): app = FastAPI() router = APIRouter() - @router.put("/items/{item_id}") + @router.trace("/items/{item_id}") def trace_item(item_id: str): return None From 7d8241acb95bb5094363d9e8abc243d3119ffaf0 Mon Sep 17 00:00:00 2001 From: Amir Khorasani <amirilf@protonmail.com> Date: Tue, 9 Jan 2024 19:44:01 +0330 Subject: [PATCH 1538/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20transl?= =?UTF-8?q?ation=20for=20`docs/fa/docs/features.md`=20(#5887)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: Amin Alaee <mohammadamin.alaee@gmail.com> --- docs/fa/docs/features.md | 206 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 docs/fa/docs/features.md diff --git a/docs/fa/docs/features.md b/docs/fa/docs/features.md new file mode 100644 index 000000000..3040ce3dd --- /dev/null +++ b/docs/fa/docs/features.md @@ -0,0 +1,206 @@ +# ویژگی ها + +## ویژگی های FastAPI + +**FastAPI** موارد زیر را به شما ارائه میدهد: + +### برپایه استاندارد های باز + +* <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> برای ساخت API, شامل مشخص سازی <abbr title="که علاوه بر path, به عنوان endpoint و route نیز شناخته میشود">path</abbr> <abbr title="که به عنوان متودهای HTTP یعنی POST,GET,PUT,DELETE و ... شناخته میشوند">operation</abbr> ها, <abbr title="parameters">پارامترها</abbr>, body request ها, امنیت و غیره. +* مستندسازی خودکار data model با <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (همانطور که OpenAPI خود نیز مبتنی بر JSON Schema است). +* طراحی شده بر اساس استاندارد هایی که پس از یک مطالعه دقیق بدست آمده اند بجای طرحی ناپخته و بدون فکر. +* همچنین به شما اجازه میدهد تا از تولید خودکار client code در بسیاری از زبان ها استفاده کنید. + +### مستندات خودکار + +مستندات API تعاملی و ایجاد رابط کاربری وب. از آنجایی که این فریم ورک برپایه OpenAPI میباشد، آپشن های متعددی وجود دارد که ۲ مورد بصورت پیش فرض گنجانده شده اند. + +* <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>، با <abbr title="interactive exploration">کاوش تعاملی</abbr>، API خود را مستقیما از طریق مرورگر صدازده و تست کنید. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* مستندات API جایگزین با <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### فقط پایتون مدرن + +همه اینها برپایه type declaration های **پایتون ۳.۶** استاندارد (به لطف Pydantic) میباشند. سینتکس جدیدی درکار نیست. تنها پایتون مدرن استاندارد. + +اگر به یک یادآوری ۲ دقیقه ای در مورد نحوه استفاده از تایپ های پایتون دارید (حتی اگر از FastAPI استفاده نمیکنید) این آموزش کوتاه را بررسی کنید: [Python Types](python-types.md){.internal-link target=\_blank}. + +شما پایتون استاندارد را با استفاده از تایپ ها مینویسید: + +```Python +from datetime import date + +from pydantic import BaseModel + +# Declare a variable as a str +# and get editor support inside the function +def main(user_id: str): + return user_id + + +# A Pydantic model +class User(BaseModel): + id: int + name: str + joined: date +``` + +که سپس میتوان به این شکل از آن استفاده کرد: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! info + `**second_user_data` یعنی: + + کلید ها و مقادیر دیکشنری `second_user_data` را مستقیما به عنوان ارگومان های key-value بفرست، که معادل است با : `User(id=4, name="Mary", joined="2018-11-30")` + +### پشتیبانی ویرایشگر + +تمام فریم ورک به گونه ای طراحی شده که استفاده از آن آسان و شهودی باشد، تمام تصمیمات حتی قبل از شروع توسعه بر روی چندین ویرایشگر آزمایش شده اند، تا از بهترین تجربه توسعه اطمینان حاصل شود. + +در آخرین نظرسنجی توسعه دهندگان پایتون کاملا مشخص بود که <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">بیشترین ویژگی مورد استفاده از "<abbr title="autocompletion">تکمیل خودکار</abbr>" است</a>. + +تمام فریم ورک **FastAPI** برپایه ای برای براورده کردن این نیاز نیز ایجاد گشته است. تکمیل خودکار در همه جا کار میکند. + +شما به ندرت نیاز به بازگشت به مستندات را خواهید داشت. + +ببینید که چگونه ویرایشگر شما ممکن است به شما کمک کند: + +* در <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* در <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +شما پیشنهاد های تکمیل خودکاری را خواهید گرفت که حتی ممکن است قبلا آن را غیرممکن تصور میکردید. به عنوان مثال کلید `price` در داخل بدنه JSON (که میتوانست تودرتو نیز باشد) که از یک درخواست آمده است. + +دیگر خبری از تایپ کلید اشتباهی، برگشتن به مستندات یا پایین بالا رفتن برای فهمیدن اینکه شما از `username` یا `user_name` استفاده کرده اید نیست. + +### مختصر + +FastAPI **پیش فرض** های معقولی برای همه چیز دارد، با قابلیت تنظیمات اختیاری در همه جا. تمام پارامترها را میتوانید برای انجام انچه نیاز دارید و برای تعریف API مورد نیاز خود به خوبی تنظیم کنید. + +اما به طور پیش فرض، همه چیز **کار میکند**. + +### اعتبارسنجی + +* اعتبارسنجی برای بیشتر (یا همه؟) **data type** های پایتون، شامل: + + * JSON objects (`dict`) + * آرایه های (‍‍‍‍`list`) JSON با قابلیت مشخص سازی تایپ ایتم های درون لیست. + * فیلد های رشته (`str`)، به همراه مشخص سازی حداقل و حداکثر طول رشته. + * اعداد (‍‍`int`,`float`) با حداقل و حداکثر مقدار و غیره. + +* اعتبارسنجی برای تایپ های عجیب تر، مثل: + * URL. + * Email. + * UUID. + * و غیره. + +تمام اعتبارسنجی ها توسط کتابخانه اثبات شده و قدرتمند **Pydantic** انجام میشود. + +### <abbr title="Security and authentication">امنیت و احراز هویت</abbr> + +امنیت و احرازهویت بدون هیچگونه ارتباط و مصالحه ای با پایگاه های داده یا مدل های داده ایجاد شده اند. + +تمام طرح های امنیتی در OpenAPI تعریف شده اند، از جمله: + +* . +* **OAuth2** (همچنین با **JWT tokens**). آموزش را در [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=\_blank} مشاهده کنید. +* کلید های API: + * <abbr title="سرصفحه ها">Headers</abbr> + * <abbr title="پارامترهای پرسمان">Query parameters</abbr> + * <abbr title="کوکی ها">Cookies</abbr>، و غیره. + +به علاوه تمام ویژگی های امنیتی از **Statlette** (شامل **<abbr title="کوکی های جلسه">session cookies</abbr>**) + +همه اینها به عنوان ابزارها و اجزای قابل استفاده ای ساخته شده اند که به راحتی با سیستم های شما، مخازن داده، پایگاه های داده رابطه ای و NoSQL و غیره ادغام میشوند. + +### <abbr title="تزریق وابستگی">Dependency Injection</abbr> + +FastAPI شامل یک سیستم <abbr title='همچنین به عنوان "components", "resources", "services" و "providers" شناخته میشود'><strong>Dependency Injection</strong></abbr> بسیار آسان اما بسیار قدرتمند است. + +* حتی وابستگی ها نیز میتوانند وابستگی هایی داشته باشند و یک سلسله مراتب یا **"گرافی" از وابستگی ها** ایجاد کنند. + +* همه چیز توسط فریم ورک **به طور خودکار اداره میشود** + +* همه وابستگی ها میتوانند به داده های request ها نیاز داشته باشند و مستندات خودکار و محدودیت های <abbr title="عملیات مسیر">path operation</abbr> را **افزایش** دهند. + +* با قابلیت **اعتبارسنجی خودکار** حتی برای path operation parameter های تعریف شده در وابستگی ها. + +* پشتیبانی از سیستم های پیچیده احرازهویت کاربر، **اتصالات پایگاه داده** و غیره. + +* بدون هیچ ارتباطی با دیتابیس ها، فرانت اند و غیره. اما ادغام آسان و راحت با همه آنها. + +### پلاگین های نامحدود + +یا به عبارت دیگر، هیچ نیازی به آنها نیست، کد موردنیاز خود را وارد و استفاده کنید. + +هر یکپارچه سازی به گونه ای طراحی شده است که استفاده از آن بسیار ساده باشد (با وابستگی ها) که میتوانید با استفاده از همان ساختار و روشی که برای _path operation_ های خود استفاده کرده اید تنها در ۲ خط کد "پلاگین" برنامه خودتان را ایجاد کنید. + +### تست شده + +* 100% <abbr title="مقدار کدی که به طور خودکار تست شده است">پوشش تست</abbr>. + +* 100% کد بر اساس <abbr title="حاشیه نویسی تایپ های پایتون (Python type annotations)، با استفاده از آن ویرایشگر و ابزارهای خارجی شما می توانند پشتیبانی بهتری از شما ارائه دهند">type annotate ها</abbr>. + +* استفاده شده در اپلیکیشن های تولید + +## ویژگی های Starlette + +**FastAPI** کاملا (و براساس) با <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a> سازگار است. بنابراین، هرکد اضافی Starlette که دارید، نیز کار خواهد کرد. + +‍‍`FastAPI` در واقع یک زیرکلاس از `Starlette` است. بنابراین اگر از قبل Starlette را میشناسید یا با آن کار کرده اید، بیشتر قابلیت ها به همین روش کار خواهد کرد. + +با **FastAPI** شما تمام ویژگی های **Starlette** را خواهید داشت (زیرا FastAPI یک نسخه و نمونه به تمام معنا از Starlette است): + +* عملکرد به طورجدی چشمگیر. <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">این یکی از سریعترین فریم ورک های موجود در پایتون است که همتراز با **نود جی اس** و **گو**</a> است. +* پشتیبانی از **WebSocket**. +* <abbr title="In-process background tasks">تسک های درجریان در پس زمینه</abbr>. +* <abbr title="Startup and shutdown events">رویداد های راه اندازی و متوفق شدن<abbr>. +* تست کلاینت ساخته شده به روی HTTPX. +* **CORS**, GZip, فایل های استاتیک, <abbr title="Streaming responses">پاسخ های جریانی</abbr>. +* پشتیبانی از **نشست ها و کوکی ها**. +* 100% پوشش با تست. +* 100% کد براساس type annotate ها. + +## ویژگی های Pydantic + +**FastAPI** کاملا (و براساس) با <a href="https://pydantic-docs.helpmanual.io" class="external-link" target="_blank"><strong>Pydantic</strong></a> سازگار است. بنابراین هرکد Pydantic اضافی که داشته باشید، نیز کار خواهد کرد. + +از جمله کتابخانه های خارجی نیز مبتنی بر Pydantic میتوان به <abbr title="Object-Relational Mapper">ORM</abbr> و <abbr title="Object-Document Mapper">ODM</abbr> ها برای دیتابیس ها اشاره کرد. + +این همچنین به این معناست که در خیلی از موارد میتوانید همان ابجکتی که از request میگیرید را **مستقیما به دیتابیس** بفرستید زیرا همه چیز به طور خودکار تأیید میشود. + +همین امر برعکس نیز صدق می‌کند، در بسیاری از موارد شما می‌توانید ابجکتی را که از پایگاه داده دریافت می‌کنید را **مستقیماً به کاربر** ارسال کنید. + +با FastAPI شما تمام ویژگی های Pydantic را دراختیار دارید (زیرا FastAPI برای تمام بخش مدیریت دیتا بر اساس Pydantic عمل میکند): + +* **خبری از گیج شدن نیست**: + * هیچ <abbr title="micro-language">زبان خردی</abbr> برای یادگیری تعریف طرحواره های جدید وجود ندارد. + * اگر تایپ های پایتون را میشناسید، نحوه استفاده از Pydantic را نیز میدانید. +* به خوبی با **<abbr title="همان Integrated Development Environment, شبیه به ویرایشگر کد">IDE</abbr>/<abbr title="برنامه ای که خطاهای کد را بررسی می کند">linter</abbr>/مغز** شما عمل میکند: + * به این دلیل که ساختار داده Pydantic فقط نمونه هایی از کلاس هایی هستند که شما تعریف میکنید، تکمیل خودکار، mypy، linting و مشاهده شما باید به درستی با داده های معتبر شما کار کنند. +* اعتبار سنجی **ساختارهای پیچیده**: + * استفاده از مدل های سلسله مراتبی Pydantic, `List` و `Dict` کتابخانه `typing` پایتون و غیره. + * و اعتبارسنج ها اجازه میدهند که طرحواره های داده پیچیده به طور واضح و آسان تعریف، بررسی و بر پایه JSON مستند شوند. + * شما میتوانید ابجکت های عمیقا تودرتو JSON را که همگی تایید شده و annotated شده اند را داشته باشید. +* **قابل توسعه**: + * Pydantic اجازه میدهد تا data type های سفارشی تعریف شوند یا میتوانید اعتبارسنجی را با روش هایی به روی مدل ها با <abbr title="دکوریتور های اعتبارسنج">validator decorator</abbr> گسترش دهید. +* 100% پوشش با تست. From aa53a48fe364a7c9de92479437ae275448b6e456 Mon Sep 17 00:00:00 2001 From: Kay Jan <kayjanw@gmail.com> Date: Wed, 10 Jan 2024 00:21:54 +0800 Subject: [PATCH 1539/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`openapi-callbacks.md`=20(#10673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/openapi-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/openapi-callbacks.md b/docs/en/docs/advanced/openapi-callbacks.md index 37339eae5..03429b187 100644 --- a/docs/en/docs/advanced/openapi-callbacks.md +++ b/docs/en/docs/advanced/openapi-callbacks.md @@ -38,7 +38,7 @@ This part is pretty normal, most of the code is probably already familiar to you !!! tip The `callback_url` query parameter uses a Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/types/#urls" class="external-link" target="_blank">URL</a> type. -The only new thing is the `callbacks=messages_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next. +The only new thing is the `callbacks=invoices_callback_router.routes` as an argument to the *path operation decorator*. We'll see what that is next. ## Documenting the callback From 6efd537204f4b59e7ef7ba013c9506a97e09f01b Mon Sep 17 00:00:00 2001 From: Sumin Kim <42088290+Eeap@users.noreply.github.com> Date: Wed, 10 Jan 2024 01:23:09 +0900 Subject: [PATCH 1540/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20=20Update=20Pyth?= =?UTF-8?q?on=20version=20in=20`docs/ko/docs/index.md`=20(#10680)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index 7ce938106..c09b538cf 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -323,7 +323,7 @@ def update_item(item_id: int, item: Item): 새로운 문법, 특정 라이브러리의 메소드나 클래스 등을 배울 필요가 없습니다. -그저 표준 **Python 3.6+**입니다. +그저 표준 **Python 3.8+** 입니다. 예를 들어, `int`에 대해선: From ed3e79be77021edecb56041a558e80e8754db849 Mon Sep 17 00:00:00 2001 From: Clarence <clarencepenz@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:30:58 +0100 Subject: [PATCH 1541/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`/docs/reference/exceptions.md`=20and=20`/en/docs/reference/?= =?UTF-8?q?status.md`=20(#10809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/reference/exceptions.md | 2 +- docs/en/docs/reference/status.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/reference/exceptions.md b/docs/en/docs/reference/exceptions.md index adc9b91ce..7c4808349 100644 --- a/docs/en/docs/reference/exceptions.md +++ b/docs/en/docs/reference/exceptions.md @@ -3,7 +3,7 @@ These are the exceptions that you can raise to show errors to the client. When you raise an exception, as would happen with normal Python, the rest of the -excecution is aborted. This way you can raise these exceptions from anywhere in the +execution is aborted. This way you can raise these exceptions from anywhere in the code to abort a request and show the error to the client. You can use: diff --git a/docs/en/docs/reference/status.md b/docs/en/docs/reference/status.md index 54fba9387..a23800792 100644 --- a/docs/en/docs/reference/status.md +++ b/docs/en/docs/reference/status.md @@ -8,7 +8,7 @@ from fastapi import status `status` is provided directly by Starlette. -It containes a group of named constants (variables) with integer status codes. +It contains a group of named constants (variables) with integer status codes. For example: From 04dbcf416c881f4320b8263e840f188e7d494ca9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:31:04 +0000 Subject: [PATCH 1542/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 83cae1dda..345f85ed6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix typo in `fastapi/routing.py` . PR [#10520](https://github.com/tiangolo/fastapi/pull/10520) by [@sepsh](https://github.com/sepsh). * 📝 Replace HTTP code returned in case of existing user error in docs for testing. PR [#4482](https://github.com/tiangolo/fastapi/pull/4482) by [@TristanMarion](https://github.com/TristanMarion). * 📝 Add blog for FastAPI & Supabase. PR [#6018](https://github.com/tiangolo/fastapi/pull/6018) by [@theinfosecguy](https://github.com/theinfosecguy). * 📝 Update example source files for SQL databases with SQLAlchemy. PR [#9508](https://github.com/tiangolo/fastapi/pull/9508) by [@s-mustafa](https://github.com/s-mustafa). From c2dc0252b01e767fcafd80d9d6c1d03e4fd48f59 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:38:21 +0000 Subject: [PATCH 1543/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 345f85ed6..7115fa3f6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -18,6 +18,7 @@ hide: ### Translations +* 🌐 Add Persian translation for `docs/fa/docs/features.md`. PR [#5887](https://github.com/tiangolo/fastapi/pull/5887) by [@amirilf](https://github.com/amirilf). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/additional-responses.md`. PR [#10325](https://github.com/tiangolo/fastapi/pull/10325) by [@ShuibeiC](https://github.com/ShuibeiC). * 🌐 Fix typos in Russian translations for `docs/ru/docs/tutorial/background-tasks.md`, `docs/ru/docs/tutorial/body-nested-models.md`, `docs/ru/docs/tutorial/debugging.md`, `docs/ru/docs/tutorial/testing.md`. PR [#10311](https://github.com/tiangolo/fastapi/pull/10311) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-files.md`. PR [#10332](https://github.com/tiangolo/fastapi/pull/10332) by [@AlertRED](https://github.com/AlertRED). From 6c15776406ac238a7089b7644dca21affe4cf8d0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:50:06 +0000 Subject: [PATCH 1544/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7115fa3f6..deb2b285d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix typo in `openapi-callbacks.md`. PR [#10673](https://github.com/tiangolo/fastapi/pull/10673) by [@kayjan](https://github.com/kayjan). * ✏️ Fix typo in `fastapi/routing.py` . PR [#10520](https://github.com/tiangolo/fastapi/pull/10520) by [@sepsh](https://github.com/sepsh). * 📝 Replace HTTP code returned in case of existing user error in docs for testing. PR [#4482](https://github.com/tiangolo/fastapi/pull/4482) by [@TristanMarion](https://github.com/TristanMarion). * 📝 Add blog for FastAPI & Supabase. PR [#6018](https://github.com/tiangolo/fastapi/pull/6018) by [@theinfosecguy](https://github.com/theinfosecguy). From 5e5cabefe18e33c6087043af640d98e089884b9e Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 16:51:05 +0000 Subject: [PATCH 1545/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 deb2b285d..d6bacfe95 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -19,6 +19,7 @@ hide: ### Translations +* ✏️ Update Python version in `docs/ko/docs/index.md`. PR [#10680](https://github.com/tiangolo/fastapi/pull/10680) by [@Eeap](https://github.com/Eeap). * 🌐 Add Persian translation for `docs/fa/docs/features.md`. PR [#5887](https://github.com/tiangolo/fastapi/pull/5887) by [@amirilf](https://github.com/amirilf). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/additional-responses.md`. PR [#10325](https://github.com/tiangolo/fastapi/pull/10325) by [@ShuibeiC](https://github.com/ShuibeiC). * 🌐 Fix typos in Russian translations for `docs/ru/docs/tutorial/background-tasks.md`, `docs/ru/docs/tutorial/body-nested-models.md`, `docs/ru/docs/tutorial/debugging.md`, `docs/ru/docs/tutorial/testing.md`. PR [#10311](https://github.com/tiangolo/fastapi/pull/10311) by [@AlertRED](https://github.com/AlertRED). From 43489beb98555374949b451054e1cd02bca267eb Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 17:00:36 +0000 Subject: [PATCH 1546/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d6bacfe95..7b1b861b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix typos in `/docs/reference/exceptions.md` and `/en/docs/reference/status.md`. PR [#10809](https://github.com/tiangolo/fastapi/pull/10809) by [@clarencepenz](https://github.com/clarencepenz). * ✏️ Fix typo in `openapi-callbacks.md`. PR [#10673](https://github.com/tiangolo/fastapi/pull/10673) by [@kayjan](https://github.com/kayjan). * ✏️ Fix typo in `fastapi/routing.py` . PR [#10520](https://github.com/tiangolo/fastapi/pull/10520) by [@sepsh](https://github.com/sepsh). * 📝 Replace HTTP code returned in case of existing user error in docs for testing. PR [#4482](https://github.com/tiangolo/fastapi/pull/4482) by [@TristanMarion](https://github.com/TristanMarion). From e98689434449c4ec667df42b798bcf4b2359b399 Mon Sep 17 00:00:00 2001 From: Rostyslav <rostik1410@users.noreply.github.com> Date: Tue, 9 Jan 2024 19:04:42 +0200 Subject: [PATCH 1547/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Ukrainian=20tran?= =?UTF-8?q?slation=20for=20`docs/uk/docs/index.md`=20(#10362)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/uk/docs/index.md | 465 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 docs/uk/docs/index.md diff --git a/docs/uk/docs/index.md b/docs/uk/docs/index.md new file mode 100644 index 000000000..fad693f79 --- /dev/null +++ b/docs/uk/docs/index.md @@ -0,0 +1,465 @@ +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>Готовий до продакшину, високопродуктивний, простий у вивченні та швидкий для написання коду фреймворк</em> +</p> +<p align="center"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> +</a> +<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/fastapi" target="_blank"> + <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/fastapi.svg" alt="Coverage"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Supported Python versions"> +</a> +</p> + +--- + +**Документація**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**Програмний код**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- + +FastAPI - це сучасний, швидкий (високопродуктивний), вебфреймворк для створення API за допомогою Python 3.8+,в основі якого лежить стандартна анотація типів Python. + +Ключові особливості: + +* **Швидкий**: Дуже висока продуктивність, на рівні з **NodeJS** та **Go** (завдяки Starlette та Pydantic). [Один із найшвидших фреймворків](#performance). + +* **Швидке написання коду**: Пришвидшує розробку функціоналу приблизно на 200%-300%. * +* **Менше помилок**: Зменшить кількість помилок спричинених людиною (розробником) на 40%. * +* **Інтуїтивний**: Чудова підтримка редакторами коду. <abbr title="Також відоме як auto-complete, autocompletion, IntelliSense.">Доповнення</abbr> всюди. Зменште час на налагодження. +* **Простий**: Спроектований, для легкого використання та навчання. Знадобиться менше часу на читання документації. +* **Короткий**: Зведе до мінімуму дублювання коду. Кожен оголошений параметр може виконувати кілька функцій. +* **Надійний**: Ви матимете стабільний код готовий до продакшину з автоматичною інтерактивною документацією. +* **Стандартизований**: Оснований та повністю сумісний з відкритими стандартами для API: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (попередньо відомий як Swagger) та <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. + +<small>* оцінка на основі тестів внутрішньої команди розробників, створення продуктових застосунків.</small> + +## Спонсори + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Other sponsors</a> + +## Враження + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**, FastAPI CLI + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +Створюючи <abbr title="Command Line Interface">CLI</abbr> застосунок для використання в терміналі, замість веб-API зверніть увагу на <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. + +**Typer** є молодшим братом FastAPI. І це **FastAPI для CLI**. ⌨️ 🚀 + +## Вимоги + +Python 3.8+ + +FastAPI стоїть на плечах гігантів: + +* <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> для web частини. +* <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> для частини даних. + +## Вставновлення + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +Вам також знадобиться сервер ASGI для продакшину, наприклад <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> або <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>. + +<div class="termy"> + +```console +$ pip install uvicorn[standard] + +---> 100% +``` + +</div> + +## Приклад + +### Створіть + +* Створіть файл `main.py` з: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>Або використайте <code>async def</code>...</summary> + +Якщо ваш код використовує `async` / `await`, скористайтеся `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Примітка**: + +Стикнувшись з проблемами, не зайвим буде ознайомитися з розділом _"In a hurry?"_ про <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` та `await` у документації</a>. + +</details> + +### Запустіть + +Запустіть server з: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary>Про команди <code>uvicorn main:app --reload</code>...</summary> + +Команда `uvicorn main:app` посилається на: + +* `main`: файл `main.py` ("Модуль" Python). +* `app`: об’єкт створений усередині `main.py` рядком `app = FastAPI()`. +* `--reload`: перезапускає сервер після зміни коду. Використовуйте виключно для розробки. + +</details> + +### Перевірте + +Відкрийте браузер та введіть адресу <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. + +Ви побачите у відповідь подібний JSON: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Ви вже створили API, який: + +* Отримує HTTP запити за _шляхами_ `/` та `/items/{item_id}`. +* Обидва _шляхи_ приймають `GET` <em>операції</em> (також відомі як HTTP _методи_). +* _Шлях_ `/items/{item_id}` містить _параметр шляху_ `item_id` який має бути типу `int`. +* _Шлях_ `/items/{item_id}` містить необовʼязковий `str` _параметр запиту_ `q`. + +### Інтерактивні документації API + +Перейдемо сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +Ви побачите автоматичну інтерактивну API документацію (створену завдяки <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Альтернативні документації API + +Тепер перейдемо сюди <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. + +Ви побачите альтернативну автоматичну документацію (створену завдяки <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Приклад оновлення + +Тепер модифікуйте файл `main.py`, щоб отримати вміст запиту `PUT`. + +Оголошуйте вміст запиту за допомогою стандартних типів Python завдяки Pydantic. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Сервер повинен автоматично перезавантажуватися (тому що Ви додали `--reload` до `uvicorn` команди вище). + +### Оновлення інтерактивної API документації + +Тепер перейдемо сюди <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +* Інтерактивна документація API буде автоматично оновлена, включаючи новий вміст: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Натисніть кнопку "Try it out", це дозволить вам заповнити параметри та безпосередньо взаємодіяти з API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Потім натисніть кнопку "Execute", інтерфейс користувача зв'яжеться з вашим API, надішле параметри, у відповідь отримає результати та покаже їх на екрані: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Оновлення альтернативної API документації + +Зараз перейдемо <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. + +* Альтернативна документація також показуватиме новий параметр і вміст запиту: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Підсумки + +Таким чином, Ви **один раз** оголошуєте типи параметрів, тіла тощо, як параметри функції. + +Ви робите це за допомогою стандартних сучасних типів Python. + +Вам не потрібно вивчати новий синтаксис, методи чи класи конкретної бібліотеки тощо. + +Використовуючи стандартний **Python 3.8+**. + +Наприклад, для `int`: + +```Python +item_id: int +``` + +або для більш складної моделі `Item`: + +```Python +item: Item +``` + +...і з цим єдиним оголошенням Ви отримуєте: + +* Підтримку редактора, включаючи: + * Варіанти заповнення. + * Перевірку типів. +* Перевірку даних: + * Автоматичні та зрозумілі помилки, у разі некоректних даних. + * Перевірка навіть для JSON з високим рівнем вкладеності. +* <abbr title="також відомий як: serialization, parsing, marshalling">Перетворення</abbr> вхідних даних: з мережі до даних і типів Python. Читання з: + * JSON. + * Параметрів шляху. + * Параметрів запиту. + * Cookies. + * Headers. + * Forms. + * Файлів. +* <abbr title="також відомий як: serialization, parsing, marshalling">Перетворення</abbr> вихідних даних: з типів і даних Python до мережевих даних (як JSON): + * Конвертація Python типів (`str`, `int`, `float`, `bool`, `list`, тощо). + * `datetime` об'єкти. + * `UUID` об'єкти. + * Моделі бази даних. + * ...та багато іншого. +* Автоматичну інтерактивну документацію API, включаючи 2 альтернативні інтерфейси користувача: + * Swagger UI. + * ReDoc. + +--- + +Повертаючись до попереднього прикладу коду, **FastAPI**: + +* Підтвердить наявність `item_id` у шляху для запитів `GET` та `PUT`. +* Підтвердить, що `item_id` має тип `int` для запитів `GET` and `PUT`. + * Якщо це не так, клієнт побачить корисну, зрозумілу помилку. +* Перевірить, чи є необов'язковий параметр запиту з назвою `q` (а саме `http://127.0.0.1:8000/items/foo?q=somequery`) для запитів `GET`. + * Оскільки параметр `q` оголошено як `= None`, він необов'язковий. + * За відсутності `None` він був би обов'язковим (як і вміст у випадку з `PUT`). +* Для запитів `PUT` із `/items/{item_id}`, читає вміст як JSON: + * Перевірить, чи має обов'язковий атрибут `name` тип `str`. + * Перевірить, чи має обов'язковий атрибут `price` тип `float`. + * Перевірить, чи існує необов'язковий атрибут `is_offer` та чи має він тип `bool`. + * Усе це також працюватиме для глибоко вкладених об'єктів JSON. +* Автоматично конвертує із та в JSON. +* Документує все за допомогою OpenAPI, який може бути використано в: + * Інтерактивних системах документації. + * Системах автоматичної генерації клієнтського коду для багатьох мов. +* Надає безпосередньо 2 вебінтерфейси інтерактивної документації. + +--- + +Ми лише трішки доторкнулися до коду, але Ви вже маєте уявлення про те, як все працює. + +Спробуйте змінити рядок: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...із: + +```Python + ... "item_name": item.name ... +``` + +...на: + +```Python + ... "item_price": item.price ... +``` + +...і побачите, як ваш редактор автоматично заповнюватиме атрибути та знатиме їхні типи: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Для більш повного ознайомлення з додатковими функціями, перегляньте <a href="https://fastapi.tiangolo.com/tutorial/">Туторіал - Посібник Користувача</a>. + +**Spoiler alert**: туторіал - посібник користувача містить: + +* Оголошення **параметрів** з інших місць як: **headers**, **cookies**, **form fields** та **files**. +* Як встановити **перевірку обмежень** як `maximum_length` або `regex`. +* Дуже потужна і проста у використанні система **<abbr title="також відома як: components, resources, providers, services, injectables">Ін'єкція Залежностей</abbr>**. +* Безпека та автентифікація, включаючи підтримку **OAuth2** з **JWT tokens** та **HTTP Basic** автентифікацію. +* Досконаліші (але однаково прості) техніки для оголошення **глибоко вкладених моделей JSON** (завдяки Pydantic). +* Багато додаткових функцій (завдяки Starlette) як-от: + * **WebSockets** + * надзвичайно прості тести на основі HTTPX та `pytest` + * **CORS** + * **Cookie Sessions** + * ...та більше. + +## Продуктивність + +Незалежні тести TechEmpower показують що застосунки **FastAPI**, які працюють під керуванням Uvicorn <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">є одними з найшвидших серед доступних фреймворків в Python</a>, поступаючись лише Starlette та Uvicorn (які внутрішньо використовуються в FastAPI). (*) + +Щоб дізнатися більше про це, перегляньте розділ <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. + +## Необов'язкові залежності + +Pydantic використовує: + +* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - для валідації електронної пошти. +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - для управління налаштуваннями. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - для додаткових типів, що можуть бути використані з Pydantic. + + +Starlette використовує: + +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Необхідно, якщо Ви хочете використовувати `TestClient`. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Необхідно, якщо Ви хочете використовувати шаблони як конфігурацію за замовчуванням. +* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Необхідно, якщо Ви хочете підтримувати <abbr title="перетворення рядка, який надходить із запиту HTTP, на дані Python">"розбір"</abbr> форми за допомогою `request.form()`. +* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Необхідно для підтримки `SessionMiddleware`. +* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Необхідно для підтримки Starlette `SchemaGenerator` (ймовірно, вам це не потрібно з FastAPI). +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Необхідно, якщо Ви хочете використовувати `UJSONResponse`. + +FastAPI / Starlette використовують: + +* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - для сервера, який завантажує та обслуговує вашу програму. +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - Необхідно, якщо Ви хочете використовувати `ORJSONResponse`. + +Ви можете встановити все це за допомогою `pip install fastapi[all]`. + +## Ліцензія + +Цей проєкт ліцензовано згідно з умовами ліцензії MIT. From 33e57e6f022a24f4aec966a6735754c1c18d9245 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 9 Jan 2024 20:06:10 +0300 Subject: [PATCH 1548/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/request-forms-and-files.md?= =?UTF-8?q?`=20(#10347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- .../docs/tutorial/request-forms-and-files.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/ru/docs/tutorial/request-forms-and-files.md diff --git a/docs/ru/docs/tutorial/request-forms-and-files.md b/docs/ru/docs/tutorial/request-forms-and-files.md new file mode 100644 index 000000000..3f587c38a --- /dev/null +++ b/docs/ru/docs/tutorial/request-forms-and-files.md @@ -0,0 +1,69 @@ +# Файлы и формы в запросе + +Вы можете определять файлы и поля формы одновременно, используя `File` и `Form`. + +!!! info "Дополнительная информация" + Чтобы получать загруженные файлы и/или данные форм, сначала установите <a href="https://andrew-d.github.io/python-multipart/" class="external-link" target="_blank">`python-multipart`</a>. + + Например: `pip install python-multipart`. + +## Импортируйте `File` и `Form` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` + +## Определите параметры `File` и `Form` + +Создайте параметры файла и формы таким же образом, как для `Body` или `Query`: + +=== "Python 3.9+" + + ```Python hl_lines="10-12" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/request_forms_and_files/tutorial001_an.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="8" + {!> ../../../docs_src/request_forms_and_files/tutorial001.py!} + ``` + +Файлы и поля формы будут загружены в виде данных формы, и вы получите файлы и поля формы. + +Вы можете объявить некоторые файлы как `bytes`, а некоторые - как `UploadFile`. + +!!! warning "Внимание" + Вы можете объявить несколько параметров `File` и `Form` в операции *path*, но вы не можете также объявить поля `Body`, которые вы ожидаете получить в виде JSON, так как запрос будет иметь тело, закодированное с помощью `multipart/form-data` вместо `application/json`. + + Это не ограничение **Fast API**, это часть протокола HTTP. + +## Резюме + +Используйте `File` и `Form` вместе, когда необходимо получить данные и файлы в одном запросе. From d2c7ffb447f4007e35c3bc102ef2ce13695ea5d8 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 17:31:25 +0000 Subject: [PATCH 1549/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7b1b861b2..eb6435235 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Translations +* 🌐 Add Ukrainian translation for `docs/uk/docs/index.md`. PR [#10362](https://github.com/tiangolo/fastapi/pull/10362) by [@rostik1410](https://github.com/rostik1410). * ✏️ Update Python version in `docs/ko/docs/index.md`. PR [#10680](https://github.com/tiangolo/fastapi/pull/10680) by [@Eeap](https://github.com/Eeap). * 🌐 Add Persian translation for `docs/fa/docs/features.md`. PR [#5887](https://github.com/tiangolo/fastapi/pull/5887) by [@amirilf](https://github.com/amirilf). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/additional-responses.md`. PR [#10325](https://github.com/tiangolo/fastapi/pull/10325) by [@ShuibeiC](https://github.com/ShuibeiC). From d62b3ea69c70c77b570dafe175b6a4062ea545b2 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 17:32:21 +0000 Subject: [PATCH 1550/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 eb6435235..bea478c2b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -20,6 +20,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms-and-files.md`. PR [#10347](https://github.com/tiangolo/fastapi/pull/10347) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Ukrainian translation for `docs/uk/docs/index.md`. PR [#10362](https://github.com/tiangolo/fastapi/pull/10362) by [@rostik1410](https://github.com/rostik1410). * ✏️ Update Python version in `docs/ko/docs/index.md`. PR [#10680](https://github.com/tiangolo/fastapi/pull/10680) by [@Eeap](https://github.com/Eeap). * 🌐 Add Persian translation for `docs/fa/docs/features.md`. PR [#5887](https://github.com/tiangolo/fastapi/pull/5887) by [@amirilf](https://github.com/amirilf). From 6f43539d87406ea0afc52de3c54f352487024f6c Mon Sep 17 00:00:00 2001 From: Andrew Chang-DeWitt <11323923+andrew-chang-dewitt@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:45:52 -0600 Subject: [PATCH 1551/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20warning=20about?= =?UTF-8?q?=20lifecycle=20events=20with=20`AsyncClient`=20(#4167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> --- docs/en/docs/advanced/async-tests.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index 9b39d70fc..c79822d63 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -84,6 +84,9 @@ response = client.get('/') !!! tip Note that we're using async/await with the new `AsyncClient` - the request is asynchronous. +!!! warning + If your application relies on lifespan events, the `AsyncClient` won't trigger these events. To ensure they are triggered, use `LifespanManager` from <a href="florimondmanca/asgi-lifespan" class="external-link" target="_blank">https://github.com/florimondmanca/asgi-lifespan#usage</a>. + ## Other Asynchronous Function Calls As the testing function is now asynchronous, you can now also call (and `await`) other `async` functions apart from sending requests to your FastAPI application in your tests, exactly as you would call them anywhere else in your code. From 7dd944deda6433924712274b81743edc63f92bbb Mon Sep 17 00:00:00 2001 From: John Philip <developerphilo@gmail.com> Date: Tue, 9 Jan 2024 20:49:58 +0300 Subject: [PATCH 1552/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20article:=20"Buil?= =?UTF-8?q?ding=20a=20RESTful=20API=20with=20FastAPI:=20Secure=20Signup=20?= =?UTF-8?q?and=20Login=20Functionality=20Included"=20(#9733)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 35c9b6718..d53afd7f9 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: John Philip + author_link: https://medium.com/@amjohnphilip + link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106 + title: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included" - author: Keshav Malik author_link: https://theinfosecguy.xyz/ link: https://blog.theinfosecguy.xyz/building-a-crud-api-with-fastapi-and-supabase-a-step-by-step-guide From 809b21c849a4d84898fb59ea832bb77386ae3aec Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:12:12 +0000 Subject: [PATCH 1553/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bea478c2b..839d84e81 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add warning about lifecycle events with `AsyncClient`. PR [#4167](https://github.com/tiangolo/fastapi/pull/4167) by [@andrew-chang-dewitt](https://github.com/andrew-chang-dewitt). * ✏️ Fix typos in `/docs/reference/exceptions.md` and `/en/docs/reference/status.md`. PR [#10809](https://github.com/tiangolo/fastapi/pull/10809) by [@clarencepenz](https://github.com/clarencepenz). * ✏️ Fix typo in `openapi-callbacks.md`. PR [#10673](https://github.com/tiangolo/fastapi/pull/10673) by [@kayjan](https://github.com/kayjan). * ✏️ Fix typo in `fastapi/routing.py` . PR [#10520](https://github.com/tiangolo/fastapi/pull/10520) by [@sepsh](https://github.com/sepsh). From e628e1928e18643196e8ae9a02f2f5ce0817c914 Mon Sep 17 00:00:00 2001 From: Takuma Yamamoto <yamataku3831@gmail.com> Date: Wed, 10 Jan 2024 03:13:02 +0900 Subject: [PATCH 1554/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20Python?= =?UTF-8?q?=20version=20in=20`index.md`=20in=20several=20languages=20(#107?= =?UTF-8?q?11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/index.md | 4 ++-- docs/ko/docs/index.md | 2 +- docs/pl/docs/index.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ja/docs/index.md b/docs/ja/docs/index.md index f340fdb87..22c31e7ca 100644 --- a/docs/ja/docs/index.md +++ b/docs/ja/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 +FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.8 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。 主な特徴: @@ -317,7 +317,7 @@ def update_item(item_id: int, item: Item): 新しい構文や特定のライブラリのメソッドやクラスなどを覚える必要はありません。 -単なる標準的な**3.6 以降の Python**です。 +単なる標準的な**3.8 以降の Python**です。 例えば、`int`の場合: diff --git a/docs/ko/docs/index.md b/docs/ko/docs/index.md index c09b538cf..594b092f7 100644 --- a/docs/ko/docs/index.md +++ b/docs/ko/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.6+의 API를 빌드하기 위한 웹 프레임워크입니다. +FastAPI는 현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python3.8+의 API를 빌드하기 위한 웹 프레임워크입니다. 주요 특징으로: diff --git a/docs/pl/docs/index.md b/docs/pl/docs/index.md index 43a20383c..49f5c2b01 100644 --- a/docs/pl/docs/index.md +++ b/docs/pl/docs/index.md @@ -24,7 +24,7 @@ --- -FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.6+ bazujący na standardowym typowaniu Pythona. +FastAPI to nowoczesny, wydajny framework webowy do budowania API z użyciem Pythona 3.8+ bazujący na standardowym typowaniu Pythona. Kluczowe cechy: From cbd53f3bc8900b10b10d89dba895d8d7f15db7a5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:15:02 +0000 Subject: [PATCH 1555/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 839d84e81..cf686b02a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add article: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included". PR [#9733](https://github.com/tiangolo/fastapi/pull/9733) by [@dxphilo](https://github.com/dxphilo). * 📝 Add warning about lifecycle events with `AsyncClient`. PR [#4167](https://github.com/tiangolo/fastapi/pull/4167) by [@andrew-chang-dewitt](https://github.com/andrew-chang-dewitt). * ✏️ Fix typos in `/docs/reference/exceptions.md` and `/en/docs/reference/status.md`. PR [#10809](https://github.com/tiangolo/fastapi/pull/10809) by [@clarencepenz](https://github.com/clarencepenz). * ✏️ Fix typo in `openapi-callbacks.md`. PR [#10673](https://github.com/tiangolo/fastapi/pull/10673) by [@kayjan](https://github.com/kayjan). From f226040d28621175477ee6b1273044f775ceac93 Mon Sep 17 00:00:00 2001 From: Dmitry Volodin <mr.molkree@gmail.com> Date: Tue, 9 Jan 2024 22:19:59 +0400 Subject: [PATCH 1556/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`docs/en/docs/tutorial/dependencies/dependencies-with-yield.?= =?UTF-8?q?md`=20(#10834)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/tutorial/dependencies/dependencies-with-yield.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md index 4ead4682c..de87ba315 100644 --- a/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md +++ b/docs/en/docs/tutorial/dependencies/dependencies-with-yield.md @@ -160,7 +160,7 @@ The same way, you could raise an `HTTPException` or similar in the exit code, af {!> ../../../docs_src/dependencies/tutorial008b.py!} ``` -An alternative you could use to catch exceptions (and possibly also raise another `HTTPException`) is ot create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. +An alternative you could use to catch exceptions (and possibly also raise another `HTTPException`) is to create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}. ## Execution of dependencies with `yield` @@ -249,7 +249,7 @@ with open("./somefile.txt") as f: print(contents) ``` -Underneath, the `open("./somefile.txt")` creates an object that is a called a "Context Manager". +Underneath, the `open("./somefile.txt")` creates an object that is called a "Context Manager". When the `with` block finishes, it makes sure to close the file, even if there were exceptions. From 0108b002f38f5179b29cc579e4bf8e5c3c282a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 9 Jan 2024 22:20:37 +0400 Subject: [PATCH 1557/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#10871)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions <github-actions@github.com> --- docs/en/data/github_sponsors.yml | 159 +++++++++++++++---------- docs/en/data/people.yml | 192 +++++++++++++++---------------- 2 files changed, 190 insertions(+), 161 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 43b4b8c6b..713f229cf 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,19 +1,25 @@ sponsors: -- - login: codacy +- - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar + - login: codacy avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 url: https://github.com/codacy - login: bump-sh avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 url: https://github.com/bump-sh + - login: Alek99 + avatarUrl: https://avatars.githubusercontent.com/u/38776361?u=bd6c163fe787c2de1a26c881598e54b67e2482dd&v=4 + url: https://github.com/Alek99 - login: cryptapi avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 url: https://github.com/cryptapi - login: porter-dev avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 url: https://github.com/porter-dev - - login: fern-api - avatarUrl: https://avatars.githubusercontent.com/u/102944815?v=4 - url: https://github.com/fern-api + - login: andrew-propelauth + avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=1188c27cb744bbec36447a2cfd4453126b2ddb5c&v=4 + url: https://github.com/andrew-propelauth - - login: nihpo avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 url: https://github.com/nihpo @@ -21,7 +27,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI - - login: mikeckennedy - avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=1bb18268bcd4d9249e1f783a063c27df9a84c05b&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=ce6165b799ea3164cb6f5ff54ea08042057442af&v=4 url: https://github.com/mikeckennedy - login: ndimares avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 @@ -59,10 +65,7 @@ sponsors: - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai -- - login: HiredScore - avatarUrl: https://avatars.githubusercontent.com/u/3908850?v=4 - url: https://github.com/HiredScore - - login: Trivie +- - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie - - login: birkjernstrom @@ -80,27 +83,39 @@ sponsors: - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - - login: deployplex + - login: doseiai avatarUrl: https://avatars.githubusercontent.com/u/57115726?v=4 - url: https://github.com/deployplex + url: https://github.com/doseiai + - login: CanoaPBC + avatarUrl: https://avatars.githubusercontent.com/u/64223768?v=4 + url: https://github.com/CanoaPBC - - login: povilasb avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 url: https://github.com/povilasb - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io +- - login: upciti + avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 + url: https://github.com/upciti - - login: Kludex avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin + - login: koconder + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=582657b23622aaa3dfe68bd028a780f272f456fa&v=4 + url: https://github.com/koconder - login: jefftriplett avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 url: https://github.com/jefftriplett - login: jstanden avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 url: https://github.com/jstanden + - login: andreaso + avatarUrl: https://avatars.githubusercontent.com/u/285964?u=837265cc7562c0685f25b2d81cd9de0434fe107c&v=4 + url: https://github.com/andreaso - login: pamelafox avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 url: https://github.com/pamelafox @@ -116,9 +131,9 @@ sponsors: - login: falkben avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 url: https://github.com/falkben - - login: jqueguiner - avatarUrl: https://avatars.githubusercontent.com/u/690878?u=bd65cc1f228ce6455e56dfaca3ef47c33bc7c3b0&v=4 - url: https://github.com/jqueguiner + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse - login: tcsmith avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 url: https://github.com/tcsmith @@ -128,6 +143,9 @@ sponsors: - login: knallgelb avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 url: https://github.com/knallgelb + - login: johannquerne + avatarUrl: https://avatars.githubusercontent.com/u/2736484?u=9b3381546a25679913a2b08110e4373c98840821&v=4 + url: https://github.com/johannquerne - login: Shark009 avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 url: https://github.com/Shark009 @@ -147,7 +165,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 url: https://github.com/anomaly - login: jgreys - avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=c66ae617d614f6c886f1f1c1799d22100b3c848d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=096820d1ef89877d57d0f68e669ead8b0fde84df&v=4 url: https://github.com/jgreys - login: jaredtrog avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 @@ -212,42 +230,48 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa + - login: ehaca + avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 + url: https://github.com/ehaca + - login: timlrx + avatarUrl: https://avatars.githubusercontent.com/u/28362229?u=9a745ca31372ee324af682715ae88ce8522f9094&v=4 + url: https://github.com/timlrx - login: BrettskiPy avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 url: https://github.com/BrettskiPy - - login: mauroalejandrojm - avatarUrl: https://avatars.githubusercontent.com/u/31569442?u=cdada990a1527926a36e95f62c30a8b48bbc49a1&v=4 - url: https://github.com/mauroalejandrojm - login: Leay15 avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 url: https://github.com/Leay15 - - login: dvlpjrs - avatarUrl: https://avatars.githubusercontent.com/u/32254642?u=fbd6ad0324d4f1eb6231cf775be1c7bd4404e961&v=4 - url: https://github.com/dvlpjrs - login: ygorpontelo avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 url: https://github.com/ygorpontelo - login: ProteinQure avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 url: https://github.com/ProteinQure + - login: RafaelWO + avatarUrl: https://avatars.githubusercontent.com/u/38643099?u=56c676f024667ee416dc8b1cdf0c2611b9dc994f&v=4 + url: https://github.com/RafaelWO - login: arleybri18 avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 url: https://github.com/arleybri18 - login: thenickben avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 url: https://github.com/thenickben - - login: adtalos - avatarUrl: https://avatars.githubusercontent.com/u/40748353?v=4 - url: https://github.com/adtalos - - login: ybressler - avatarUrl: https://avatars.githubusercontent.com/u/40807730?u=41e2c00f1eebe3c402635f0325e41b4e6511462c&v=4 - url: https://github.com/ybressler - login: ddilidili avatarUrl: https://avatars.githubusercontent.com/u/42176885?u=c0a849dde06987434653197b5f638d3deb55fc6c&v=4 url: https://github.com/ddilidili + - login: ramonalmeidam + avatarUrl: https://avatars.githubusercontent.com/u/45269580?u=3358750b3a5854d7c3ed77aaca7dd20a0f529d32&v=4 + url: https://github.com/ramonalmeidam - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender + - login: Amirshox + avatarUrl: https://avatars.githubusercontent.com/u/56707784?u=2a2f8cc243d6f5b29cd63fd2772f7a97aadc6c6b&v=4 + url: https://github.com/Amirshox + - login: prodhype + avatarUrl: https://avatars.githubusercontent.com/u/60444672?u=3f278cff25ea37ead487d7861d4a984795de819e&v=4 + url: https://github.com/prodhype - login: yakkonaut avatarUrl: https://avatars.githubusercontent.com/u/60633704?u=90a71fd631aa998ba4a96480788f017c9904e07b&v=4 url: https://github.com/yakkonaut @@ -257,15 +281,18 @@ sponsors: - login: anthonycepeda avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 url: https://github.com/anthonycepeda + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: kaoru0310 + avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 + url: https://github.com/kaoru0310 - login: DelfinaCare avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 url: https://github.com/DelfinaCare - login: osawa-koki avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 url: https://github.com/osawa-koki - - login: pyt3h - avatarUrl: https://avatars.githubusercontent.com/u/99658549?v=4 - url: https://github.com/pyt3h - login: apitally avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 url: https://github.com/apitally @@ -281,9 +308,6 @@ sponsors: - login: bryanculbertson avatarUrl: https://avatars.githubusercontent.com/u/144028?u=defda4f90e93429221cc667500944abde60ebe4a&v=4 url: https://github.com/bryanculbertson - - login: yourkin - avatarUrl: https://avatars.githubusercontent.com/u/178984?u=b43a7e5f8818f7d9083d3b110118d9c27d48a794&v=4 - url: https://github.com/yourkin - login: slafs avatarUrl: https://avatars.githubusercontent.com/u/210173?v=4 url: https://github.com/slafs @@ -305,12 +329,12 @@ sponsors: - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - - login: janfilips - avatarUrl: https://avatars.githubusercontent.com/u/870699?u=80702ec63f14e675cd4cdcc6ce3821d2ed207fd7&v=4 - url: https://github.com/janfilips - login: dodo5522 avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 url: https://github.com/dodo5522 + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr - login: WillHogan avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 url: https://github.com/WillHogan @@ -326,15 +350,12 @@ sponsors: - login: anthonycorletti avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 url: https://github.com/anthonycorletti - - login: erhan - avatarUrl: https://avatars.githubusercontent.com/u/3872888?u=cd9a20fcd33c5598d9d7797a78dedfc9148592f6&v=4 - url: https://github.com/erhan - login: Alisa-lisa avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 url: https://github.com/Alisa-lisa - - login: piotrgredowski - avatarUrl: https://avatars.githubusercontent.com/u/4294480?v=4 - url: https://github.com/piotrgredowski + - login: gowikel + avatarUrl: https://avatars.githubusercontent.com/u/4339072?u=0e325ffcc539c38f89d9aa876bd87f9ec06ce0ee&v=4 + url: https://github.com/gowikel - login: danielunderwood avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 url: https://github.com/danielunderwood @@ -350,6 +371,9 @@ sponsors: - login: Baghdady92 avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 url: https://github.com/Baghdady92 + - login: jakeecolution + avatarUrl: https://avatars.githubusercontent.com/u/5884696?u=4a7c7883fb064b593b50cb6697b54687e6f7aafe&v=4 + url: https://github.com/jakeecolution - login: KentShikama avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 url: https://github.com/KentShikama @@ -369,7 +393,7 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/8574425?u=aad2a9674273c9275fe414d99269b7418d144089&v=4 url: https://github.com/albertkun - login: xncbf - avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=05cb2d7c797a02f666805ad4639d9582f31d432c&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/9462045?u=ee91e210ae93b9cdd8f248b21cb028316cc0b747&v=4 url: https://github.com/xncbf - login: DMantis avatarUrl: https://avatars.githubusercontent.com/u/9536869?v=4 @@ -389,27 +413,42 @@ sponsors: - login: pheanex avatarUrl: https://avatars.githubusercontent.com/u/10408624?u=5b6bab6ee174aa6e991333e06eb29f628741013d&v=4 url: https://github.com/pheanex + - login: dzoladz + avatarUrl: https://avatars.githubusercontent.com/u/10561752?u=5ee314d54aa79592c18566827ad8914debd5630d&v=4 + url: https://github.com/dzoladz - login: JimFawkes avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 url: https://github.com/JimFawkes + - login: artempronevskiy + avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 + url: https://github.com/artempronevskiy - login: giuliano-oliveira avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 url: https://github.com/giuliano-oliveira - login: TheR1D avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 url: https://github.com/TheR1D + - login: joshuatz + avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 + url: https://github.com/joshuatz - login: jangia avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 url: https://github.com/jangia - login: shuheng-liu avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 url: https://github.com/shuheng-liu + - login: salahelfarissi + avatarUrl: https://avatars.githubusercontent.com/u/23387408?u=73222a4be627c1a3dee9736e0da22224eccdc8f6&v=4 + url: https://github.com/salahelfarissi - login: pers0n4 avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 url: https://github.com/pers0n4 - login: kxzk avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 url: https://github.com/kxzk + - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota - login: nisutec avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 url: https://github.com/nisutec @@ -422,9 +461,6 @@ sponsors: - login: rlnchow avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 url: https://github.com/rlnchow - - login: mertguvencli - avatarUrl: https://avatars.githubusercontent.com/u/29762151?u=16a906d90df96c8cff9ea131a575c4bc171b1523&v=4 - url: https://github.com/mertguvencli - login: White-Mask avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 url: https://github.com/White-Mask @@ -435,14 +471,11 @@ sponsors: avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 url: https://github.com/engineerjoe440 - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=ea88e4bd668c984cff1bca3e71ab2deb37fafdc4&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=fa1dc8db3e920cf5c5636b97180a6f811fa01aaf&v=4 url: https://github.com/bnkc - login: declon avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 url: https://github.com/declon - - login: DSMilestone6538 - avatarUrl: https://avatars.githubusercontent.com/u/37230924?u=f299dce910366471523155e0cb213356d34aadc1&v=4 - url: https://github.com/DSMilestone6538 - login: curegit avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 url: https://github.com/curegit @@ -458,12 +491,12 @@ sponsors: - login: hgalytoby avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 url: https://github.com/hgalytoby - - login: eladgunders - avatarUrl: https://avatars.githubusercontent.com/u/52347338?u=83d454817cf991a035c8827d46ade050c813e2d6&v=4 - url: https://github.com/eladgunders - login: conservative-dude avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 url: https://github.com/conservative-dude + - login: Calesi19 + avatarUrl: https://avatars.githubusercontent.com/u/58052598?u=273d4fc364c004602c93dd6adeaf5cc915b93cd2&v=4 + url: https://github.com/Calesi19 - login: 0417taehyun avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 url: https://github.com/0417taehyun @@ -476,30 +509,30 @@ sponsors: - login: mbukeRepo avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=d2eb23e2b222a3b316c4183b05a3236b32819dc2&v=4 url: https://github.com/mbukeRepo + - login: adriiamontoto + avatarUrl: https://avatars.githubusercontent.com/u/75563346?u=eeb1350b82ecb4d96592f9b6cd1a16870c355e38&v=4 + url: https://github.com/adriiamontoto - login: PelicanQ avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 url: https://github.com/PelicanQ + - login: tahmarrrr23 + avatarUrl: https://avatars.githubusercontent.com/u/138208610?u=465a46b0ff72a74252d3e3a71ac7d2f1919cda28&v=4 + url: https://github.com/tahmarrrr23 - - login: ssbarnea avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 url: https://github.com/ssbarnea - login: Patechoc avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 url: https://github.com/Patechoc - - login: LanceMoe - avatarUrl: https://avatars.githubusercontent.com/u/18505474?u=7fd3ead4364bdf215b6d75cb122b3811c391ef6b&v=4 - url: https://github.com/LanceMoe + - login: DazzyMlv + avatarUrl: https://avatars.githubusercontent.com/u/23006212?u=df429da52882b0432e5ac81d4f1b489abc86433c&v=4 + url: https://github.com/DazzyMlv - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu - - login: msniezynski - avatarUrl: https://avatars.githubusercontent.com/u/27588547?u=0e3be5ac57dcfdf124f470bcdf74b5bf79af1b6c&v=4 - url: https://github.com/msniezynski - login: danburonline - avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=2cad4388c1544e539ecb732d656e42fb07b4ff2d&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=94935cccfbec58083ab1e535212d54f1bf2c978a&v=4 url: https://github.com/danburonline - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd - - login: IvanReyesO7 - avatarUrl: https://avatars.githubusercontent.com/u/74359151?u=4b2c368f71e1411b462a8c2290c920ad35dc1af8&v=4 - url: https://github.com/IvanReyesO7 diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2e84f1128..2877e7938 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,16 +1,16 @@ maintainers: - login: tiangolo answers: 1870 - prs: 508 + prs: 523 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 512 + count: 522 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu - count: 240 + count: 241 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu - login: Mause @@ -21,20 +21,20 @@ experts: count: 217 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd +- login: jgould22 + count: 205 + avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 + url: https://github.com/jgould22 - login: JarroVGIT count: 193 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 url: https://github.com/JarroVGIT -- login: jgould22 - count: 186 - avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 - url: https://github.com/jgould22 - login: euri10 count: 153 avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 - login: iudeen - count: 126 + count: 127 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: phy25 @@ -62,9 +62,17 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/516999?u=437c0c5038558c67e887ccd863c1ba0f846c03da&v=4 url: https://github.com/sm-Fifteen - login: yinziyan1206 - count: 45 + count: 48 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 +- login: insomnes + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 + url: https://github.com/insomnes +- login: adriangb + count: 45 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: acidjunk count: 45 avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 @@ -73,26 +81,22 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 url: https://github.com/Dustyposa -- login: adriangb - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 - url: https://github.com/adriangb -- login: insomnes - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 - url: https://github.com/insomnes -- login: frankie567 - count: 43 - avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 - url: https://github.com/frankie567 - login: odiseo0 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 +- login: frankie567 + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 + url: https://github.com/frankie567 - login: includeamin count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin +- login: n8sty + count: 40 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: chbndrhnns count: 38 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 @@ -101,10 +105,6 @@ experts: count: 37 avatarUrl: https://avatars.githubusercontent.com/u/5167622?u=de8f597c81d6336fcebc37b32dfd61a3f877160c&v=4 url: https://github.com/STeveShary -- login: n8sty - count: 36 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty - login: krishnardt count: 35 avatarUrl: https://avatars.githubusercontent.com/u/31960541?u=47f4829c77f4962ab437ffb7995951e41eeebe9b&v=4 @@ -113,6 +113,10 @@ experts: count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla +- login: JavierSanchezCastro + count: 30 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: prostomarkeloff count: 28 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -125,50 +129,46 @@ experts: count: 25 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 url: https://github.com/wshayes -- login: SirTelemak - count: 23 - avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 - url: https://github.com/SirTelemak - login: acnebs count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9054108?v=4 url: https://github.com/acnebs +- login: SirTelemak + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 + url: https://github.com/SirTelemak - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf -- login: chris-allnutt +- login: nymous count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 - url: https://github.com/chris-allnutt -- login: ebottos94 - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: nsidnev count: 20 avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 url: https://github.com/nsidnev -- login: JavierSanchezCastro +- login: chris-allnutt count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro + avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 + url: https://github.com/chris-allnutt - login: chrisK824 - count: 19 + count: 20 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 url: https://github.com/chrisK824 -- login: zoliknemet - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 - url: https://github.com/zoliknemet +- login: ebottos94 + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 + url: https://github.com/ebottos94 - login: retnikt count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 url: https://github.com/retnikt -- login: nymous - count: 17 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous +- login: zoliknemet + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/22326718?u=31ba446ac290e23e56eea8e4f0c558aaf0b40779&v=4 + url: https://github.com/zoliknemet - login: Hultner count: 17 avatarUrl: https://avatars.githubusercontent.com/u/2669034?u=115e53df959309898ad8dc9443fbb35fee71df07&v=4 @@ -193,39 +193,31 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli -- login: abhint +- login: ghost count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/25699289?u=b5d219277b4d001ac26fb8be357fddd88c29d51b&v=4 - url: https://github.com/abhint + avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 + url: https://github.com/ghost last_month_active: +- login: Ventura94 + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 + url: https://github.com/Ventura94 +- login: JavierSanchezCastro + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: jgould22 - count: 18 + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: Kludex - count: 10 + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: White-Mask - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 - url: https://github.com/White-Mask - login: n8sty - count: 4 + count: 3 avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 url: https://github.com/n8sty -- login: hasansezertasan - count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=e389634d494d503cca867f76c2d00cacc273a46e&v=4 - url: https://github.com/hasansezertasan -- login: pythonweb2 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/32141163?v=4 - url: https://github.com/pythonweb2 -- login: ebottos94 - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 - url: https://github.com/ebottos94 top_contributors: - login: waynerv count: 25 @@ -343,6 +335,10 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: adriangb + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 + url: https://github.com/adriangb - login: iudeen count: 4 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 @@ -365,21 +361,25 @@ top_reviewers: avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan - count: 82 + count: 86 avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 url: https://github.com/BilalAlpaslan - login: yezz123 - count: 80 + count: 82 avatarUrl: https://avatars.githubusercontent.com/u/52716203?u=d7062cbc6eb7671d5dc9cc0e32a24ae335e0f225&v=4 url: https://github.com/yezz123 - login: iudeen - count: 54 + count: 55 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: tokusumi count: 51 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi +- login: Xewus + count: 50 + avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 + url: https://github.com/Xewus - login: waynerv count: 47 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 @@ -388,10 +388,6 @@ top_reviewers: count: 47 avatarUrl: https://avatars.githubusercontent.com/u/59285379?v=4 url: https://github.com/Laineyzhang55 -- login: Xewus - count: 47 - avatarUrl: https://avatars.githubusercontent.com/u/85196001?u=f8e2dc7e5104f109cef944af79050ea8d1b8f914&v=4 - url: https://github.com/Xewus - login: ycd count: 45 avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 @@ -416,17 +412,17 @@ top_reviewers: count: 28 avatarUrl: https://avatars.githubusercontent.com/u/3127847?u=b0a652331da17efeb85cd6e3a4969182e5004804&v=4 url: https://github.com/cassiobotaro +- login: lsglucas + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 + url: https://github.com/lsglucas - login: komtaki count: 27 avatarUrl: https://avatars.githubusercontent.com/u/39375566?u=260ad6b1a4b34c07dbfa728da5e586f16f6d1824&v=4 url: https://github.com/komtaki -- login: lsglucas - count: 26 - avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 - url: https://github.com/lsglucas - login: Ryandaydev count: 25 - avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=ba0eea19429e7cf77cf2ab8ad2f3d3af202bc1cf&v=4 + avatarUrl: https://avatars.githubusercontent.com/u/4292423?u=48f68868db8886fce31a1d802c1003914c6cd7c6&v=4 url: https://github.com/Ryandaydev - login: LorhanSohaky count: 24 @@ -460,6 +456,10 @@ top_reviewers: count: 17 avatarUrl: https://avatars.githubusercontent.com/u/67154681?u=5d634834cc514028ea3f9115f7030b99a1f4d5a4&v=4 url: https://github.com/zy7y +- login: peidrao + count: 17 + avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 + url: https://github.com/peidrao - login: yanever count: 16 avatarUrl: https://avatars.githubusercontent.com/u/21978760?v=4 @@ -468,6 +468,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc +- login: nilslindemann + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann - login: axel584 count: 16 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 @@ -480,6 +484,10 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 url: https://github.com/DevDae +- login: hasansezertasan + count: 16 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -488,10 +496,6 @@ top_reviewers: count: 15 avatarUrl: https://avatars.githubusercontent.com/u/63476957?u=6c86e59b48e0394d4db230f37fc9ad4d7e2c27c7&v=4 url: https://github.com/delhi09 -- login: peidrao - count: 14 - avatarUrl: https://avatars.githubusercontent.com/u/32584628?u=a66902b40c13647d0ed0e573d598128240a4dd04&v=4 - url: https://github.com/peidrao - login: sh0nk count: 13 avatarUrl: https://avatars.githubusercontent.com/u/6478810?u=af15d724875cec682ed8088a86d36b2798f981c0&v=4 @@ -536,6 +540,10 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: romashevchenko + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 + url: https://github.com/romashevchenko - login: izaguerreiro count: 9 avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 @@ -544,15 +552,3 @@ top_reviewers: count: 9 avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 url: https://github.com/graingert -- login: PandaHun - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/13096845?u=646eba44db720e37d0dbe8e98e77ab534ea78a20&v=4 - url: https://github.com/PandaHun -- login: kty4119 - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/49435654?v=4 - url: https://github.com/kty4119 -- login: bezaca - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/69092910?u=4ac58eab99bd37d663f3d23551df96d4fbdbf760&v=4 - url: https://github.com/bezaca From f43fc82267f35586b2868ebdd2caf7859b964914 Mon Sep 17 00:00:00 2001 From: s111d <angry.kustomer@gmail.com> Date: Tue, 9 Jan 2024 21:22:46 +0300 Subject: [PATCH 1558/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`docs/en/docs/alternatives.md`=20and=20`docs/en/docs/tutoria?= =?UTF-8?q?l/dependencies/index.md`=20(#10906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/alternatives.md | 6 +++--- docs/en/docs/tutorial/dependencies/index.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index 0f074ccf3..e02b3b55a 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -185,7 +185,7 @@ It's a Flask plug-in, that ties together Webargs, Marshmallow and APISpec. It uses the information from Webargs and Marshmallow to automatically generate OpenAPI schemas, using APISpec. -It's a great tool, very under-rated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract. +It's a great tool, very underrated. It should be way more popular than many Flask plug-ins out there. It might be due to its documentation being too concise and abstract. This solved having to write YAML (another syntax) inside of Python docstrings. @@ -263,7 +263,7 @@ I discovered Molten in the first stages of building **FastAPI**. And it has quit It doesn't use a data validation, serialization and documentation third-party library like Pydantic, it has its own. So, these data type definitions would not be reusable as easily. -It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high-performance provided by tools like Uvicorn, Starlette and Sanic. +It requires a little bit more verbose configurations. And as it is based on WSGI (instead of ASGI), it is not designed to take advantage of the high performance provided by tools like Uvicorn, Starlette and Sanic. The dependency injection system requires pre-registration of the dependencies and the dependencies are solved based on the declared types. So, it's not possible to declare more than one "component" that provides a certain type. @@ -357,7 +357,7 @@ It is comparable to Marshmallow. Although it's faster than Marshmallow in benchm ### <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> -Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services. +Starlette is a lightweight <abbr title="The new standard for building asynchronous Python web applications">ASGI</abbr> framework/toolkit, which is ideal for building high-performance asyncio services. It is very simple and intuitive. It's designed to be easily extensible, and have modular components. diff --git a/docs/en/docs/tutorial/dependencies/index.md b/docs/en/docs/tutorial/dependencies/index.md index bc98cb26e..608ced407 100644 --- a/docs/en/docs/tutorial/dependencies/index.md +++ b/docs/en/docs/tutorial/dependencies/index.md @@ -287,9 +287,9 @@ Other common terms for this same idea of "dependency injection" are: ## **FastAPI** plug-ins -Integrations and "plug-in"s can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your *path operation functions*. +Integrations and "plug-ins" can be built using the **Dependency Injection** system. But in fact, there is actually **no need to create "plug-ins"**, as by using dependencies it's possible to declare an infinite number of integrations and interactions that become available to your *path operation functions*. -And dependencies can be created in a very simple and intuitive way that allow you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, *literally*. +And dependencies can be created in a very simple and intuitive way that allows you to just import the Python packages you need, and integrate them with your API functions in a couple of lines of code, *literally*. You will see examples of this in the next chapters, about relational and NoSQL databases, security, etc. From aa6586d51a7868a08b2971ee14957d6db7f47d2e Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:24:21 +0000 Subject: [PATCH 1559/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cf686b02a..59de429ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Translations +* ✏️ Update Python version in `index.md` in several languages. PR [#10711](https://github.com/tiangolo/fastapi/pull/10711) by [@tamago3keran](https://github.com/tamago3keran). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms-and-files.md`. PR [#10347](https://github.com/tiangolo/fastapi/pull/10347) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Ukrainian translation for `docs/uk/docs/index.md`. PR [#10362](https://github.com/tiangolo/fastapi/pull/10362) by [@rostik1410](https://github.com/rostik1410). * ✏️ Update Python version in `docs/ko/docs/index.md`. PR [#10680](https://github.com/tiangolo/fastapi/pull/10680) by [@Eeap](https://github.com/Eeap). From dd6cf5d71091f15b4cef40fe23f7f7d7344d6f93 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:32:18 +0000 Subject: [PATCH 1560/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 59de429ab..bb7c722ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix typos in `docs/en/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10834](https://github.com/tiangolo/fastapi/pull/10834) by [@Molkree](https://github.com/Molkree). * 📝 Add article: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included". PR [#9733](https://github.com/tiangolo/fastapi/pull/9733) by [@dxphilo](https://github.com/dxphilo). * 📝 Add warning about lifecycle events with `AsyncClient`. PR [#4167](https://github.com/tiangolo/fastapi/pull/4167) by [@andrew-chang-dewitt](https://github.com/andrew-chang-dewitt). * ✏️ Fix typos in `/docs/reference/exceptions.md` and `/en/docs/reference/status.md`. PR [#10809](https://github.com/tiangolo/fastapi/pull/10809) by [@clarencepenz](https://github.com/clarencepenz). From f73be1d59984f5af8330655de44f8305ed0ec784 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:33:22 +0000 Subject: [PATCH 1561/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bb7c722ea..d350124fc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -52,6 +52,7 @@ hide: ### Internal +* 👥 Update FastAPI People. PR [#10871](https://github.com/tiangolo/fastapi/pull/10871) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade custom GitHub Action comment-docs-preview-in-pr. PR [#10916](https://github.com/tiangolo/fastapi/pull/10916) by [@tiangolo](https://github.com/tiangolo). * ⬆️ Upgrade GitHub Action latest-changes. PR [#10915](https://github.com/tiangolo/fastapi/pull/10915) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade GitHub Action label-approved. PR [#10913](https://github.com/tiangolo/fastapi/pull/10913) by [@tiangolo](https://github.com/tiangolo). From 3b9a2bcb1b0b3ec234cf70adeb551d5655f774e9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 18:35:49 +0000 Subject: [PATCH 1562/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d350124fc..c7787053e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix typos in `docs/en/docs/alternatives.md` and `docs/en/docs/tutorial/dependencies/index.md`. PR [#10906](https://github.com/tiangolo/fastapi/pull/10906) by [@s111d](https://github.com/s111d). * ✏️ Fix typos in `docs/en/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10834](https://github.com/tiangolo/fastapi/pull/10834) by [@Molkree](https://github.com/Molkree). * 📝 Add article: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included". PR [#9733](https://github.com/tiangolo/fastapi/pull/9733) by [@dxphilo](https://github.com/dxphilo). * 📝 Add warning about lifecycle events with `AsyncClient`. PR [#4167](https://github.com/tiangolo/fastapi/pull/4167) by [@andrew-chang-dewitt](https://github.com/andrew-chang-dewitt). From 7eeacc9958565dc7f2279c206d0be0e5364dd99d Mon Sep 17 00:00:00 2001 From: Craig Blaszczyk <masterjakul@gmail.com> Date: Tue, 9 Jan 2024 20:37:09 +0000 Subject: [PATCH 1563/1881] =?UTF-8?q?=E2=9C=A8=20Generate=20automatic=20la?= =?UTF-8?q?nguage=20names=20for=20docs=20translations=20(#5354)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Craig Blaszczyk <craig@boughtbymany.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/mkdocs.yml | 29 ++++--- docs/language_names.yml | 182 ++++++++++++++++++++++++++++++++++++++++ scripts/docs.py | 26 +++--- 3 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 docs/language_names.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index a64eff269..92d081aa1 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -58,15 +58,18 @@ plugins: python: options: extensions: - - griffe_typingdoc + - griffe_typingdoc show_root_heading: true show_if_no_docstring: true - preload_modules: [httpx, starlette] + preload_modules: + - httpx + - starlette inherited_members: true members_order: source separate_signature: true unwrap_annotated: true - filters: ["!^_"] + filters: + - '!^_' merge_init_into_class: true docstring_section_style: spacy signature_crossrefs: true @@ -264,25 +267,25 @@ extra: - link: / name: en - English - link: /de/ - name: de - - link: /em/ - name: 😉 + name: de - Deutsch - link: /es/ name: es - español - link: /fa/ - name: fa + name: fa - فارسی - link: /fr/ name: fr - français - link: /he/ - name: he + name: he - עברית + - link: /hu/ + name: hu - magyar - link: /id/ - name: id + name: id - Bahasa Indonesia - link: /ja/ name: ja - 日本語 - link: /ko/ name: ko - 한국어 - link: /pl/ - name: pl + name: pl - Polski - link: /pt/ name: pt - português - link: /ru/ @@ -290,15 +293,17 @@ extra: - link: /tr/ name: tr - Türkçe - link: /uk/ - name: uk + name: uk - українська мова - link: /ur/ - name: ur + name: ur - اردو - link: /vi/ name: vi - Tiếng Việt - link: /yo/ name: yo - Yorùbá - link: /zh/ name: zh - 汉语 + - link: /em/ + name: 😉 extra_css: - css/termynal.css - css/custom.css diff --git a/docs/language_names.yml b/docs/language_names.yml new file mode 100644 index 000000000..fbbbde303 --- /dev/null +++ b/docs/language_names.yml @@ -0,0 +1,182 @@ +aa: Afaraf +ab: аҧсуа бызшәа +ae: avesta +af: Afrikaans +ak: Akan +am: አማርኛ +an: aragonés +ar: اللغة العربية +as: অসমীয়া +av: авар мацӀ +ay: aymar aru +az: azərbaycan dili +ba: башҡорт теле +be: беларуская мова +bg: български език +bh: भोजपुरी +bi: Bislama +bm: bamanankan +bn: বাংলা +bo: བོད་ཡིག +br: brezhoneg +bs: bosanski jezik +ca: Català +ce: нохчийн мотт +ch: Chamoru +co: corsu +cr: ᓀᐦᐃᔭᐍᐏᐣ +cs: čeština +cu: ѩзыкъ словѣньскъ +cv: чӑваш чӗлхи +cy: Cymraeg +da: dansk +de: Deutsch +dv: Dhivehi +dz: རྫོང་ཁ +ee: Eʋegbe +el: Ελληνικά +en: English +eo: Esperanto +es: español +et: eesti +eu: euskara +fa: فارسی +ff: Fulfulde +fi: suomi +fj: Vakaviti +fo: føroyskt +fr: français +fy: Frysk +ga: Gaeilge +gd: Gàidhlig +gl: galego +gu: ગુજરાતી +gv: Gaelg +ha: هَوُسَ +he: עברית +hi: हिन्दी +ho: Hiri Motu +hr: Hrvatski +ht: Kreyòl ayisyen +hu: magyar +hy: Հայերեն +hz: Otjiherero +ia: Interlingua +id: Bahasa Indonesia +ie: Interlingue +ig: Asụsụ Igbo +ii: ꆈꌠ꒿ Nuosuhxop +ik: Iñupiaq +io: Ido +is: Íslenska +it: italiano +iu: ᐃᓄᒃᑎᑐᑦ +ja: 日本語 +jv: basa Jawa +ka: ქართული +kg: Kikongo +ki: Gĩkũyũ +kj: Kuanyama +kk: қазақ тілі +kl: kalaallisut +km: ខេមរភាសា +kn: ಕನ್ನಡ +ko: 한국어 +kr: Kanuri +ks: कश्मीरी +ku: Kurdî +kv: коми кыв +kw: Kernewek +ky: Кыргызча +la: latine +lb: Lëtzebuergesch +lg: Luganda +li: Limburgs +ln: Lingála +lo: ພາສາ +lt: lietuvių kalba +lu: Tshiluba +lv: latviešu valoda +mg: fiteny malagasy +mh: Kajin M̧ajeļ +mi: te reo Māori +mk: македонски јазик +ml: മലയാളം +mn: Монгол хэл +mr: मराठी +ms: Bahasa Malaysia +mt: Malti +my: ဗမာစာ +na: Ekakairũ Naoero +nb: Norsk bokmål +nd: isiNdebele +ne: नेपाली +ng: Owambo +nl: Nederlands +nn: Norsk nynorsk +'no': Norsk +nr: isiNdebele +nv: Diné bizaad +ny: chiCheŵa +oc: occitan +oj: ᐊᓂᔑᓈᐯᒧᐎᓐ +om: Afaan Oromoo +or: ଓଡ଼ିଆ +os: ирон æвзаг +pa: ਪੰਜਾਬੀ +pi: पाऴि +pl: Polski +ps: پښتو +pt: português +qu: Runa Simi +rm: rumantsch grischun +rn: Ikirundi +ro: Română +ru: русский язык +rw: Ikinyarwanda +sa: संस्कृतम् +sc: sardu +sd: सिन्धी +se: Davvisámegiella +sg: yângâ tî sängö +si: සිංහල +sk: slovenčina +sl: slovenščina +sn: chiShona +so: Soomaaliga +sq: shqip +sr: српски језик +ss: SiSwati +st: Sesotho +su: Basa Sunda +sv: svenska +sw: Kiswahili +ta: தமிழ் +te: తెలుగు +tg: тоҷикӣ +th: ไทย +ti: ትግርኛ +tk: Türkmen +tl: Wikang Tagalog +tn: Setswana +to: faka Tonga +tr: Türkçe +ts: Xitsonga +tt: татар теле +tw: Twi +ty: Reo Tahiti +ug: ئۇيغۇرچە‎ +uk: українська мова +ur: اردو +uz: Ўзбек +ve: Tshivenḓa +vi: Tiếng Việt +vo: Volapük +wa: walon +wo: Wollof +xh: isiXhosa +yi: ייִדיש +yo: Yorùbá +za: Saɯ cueŋƅ +zh: 汉语 +zu: isiZulu diff --git a/scripts/docs.py b/scripts/docs.py index 73e1900ad..a6710d7a5 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -274,22 +274,24 @@ def live( def update_config() -> None: config = get_en_config() languages = [{"en": "/"}] - alternate: List[Dict[str, str]] = config["extra"].get("alternate", []) - alternate_dict = {alt["link"]: alt["name"] for alt in alternate} new_alternate: List[Dict[str, str]] = [] + # Language names sourced from https://quickref.me/iso-639-1 + # Contributors may wish to update or change these, e.g. to fix capitalization. + language_names_path = Path(__file__).parent / "../docs/language_names.yml" + local_language_names: Dict[str, str] = mkdocs.utils.yaml_load( + language_names_path.read_text(encoding="utf-8") + ) for lang_path in get_lang_paths(): - if lang_path.name == "en" or not lang_path.is_dir(): + if lang_path.name in {"en", "em"} or not lang_path.is_dir(): continue - name = lang_path.name - languages.append({name: f"/{name}/"}) + code = lang_path.name + languages.append({code: f"/{code}/"}) for lang_dict in languages: - name = list(lang_dict.keys())[0] - url = lang_dict[name] - if url not in alternate_dict: - new_alternate.append({"link": url, "name": name}) - else: - use_name = alternate_dict[url] - new_alternate.append({"link": url, "name": use_name}) + code = list(lang_dict.keys())[0] + url = lang_dict[code] + use_name = f"{code} - {local_language_names[code]}" + new_alternate.append({"link": url, "name": use_name}) + new_alternate.append({"link": "/em/", "name": "😉"}) config["extra"]["alternate"] = new_alternate en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), From 958425a899642d1853a1181a3b89dcb07aabea4f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 9 Jan 2024 20:37:29 +0000 Subject: [PATCH 1564/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c7787053e..8699fbddc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✨ Generate automatic language names for docs translations. PR [#5354](https://github.com/tiangolo/fastapi/pull/5354) by [@jakul](https://github.com/jakul). * ✏️ Fix typos in `docs/en/docs/alternatives.md` and `docs/en/docs/tutorial/dependencies/index.md`. PR [#10906](https://github.com/tiangolo/fastapi/pull/10906) by [@s111d](https://github.com/s111d). * ✏️ Fix typos in `docs/en/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10834](https://github.com/tiangolo/fastapi/pull/10834) by [@Molkree](https://github.com/Molkree). * 📝 Add article: "Building a RESTful API with FastAPI: Secure Signup and Login Functionality Included". PR [#9733](https://github.com/tiangolo/fastapi/pull/9733) by [@dxphilo](https://github.com/dxphilo). From 84cd488df17543089e0dbb1e1ab3c7bd4e976be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 10 Jan 2024 21:19:21 +0400 Subject: [PATCH 1565/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20FastAPI=20application=20monitoring=20made=20easy=20(#10917)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Gurcke <simon@gurcke.de> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index d53afd7f9..d9cfe3431 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Apitally + author_link: https://apitally.io + link: https://blog.apitally.io/fastapi-application-monitoring-made-easy + title: FastAPI application monitoring made easy - author: John Philip author_link: https://medium.com/@amjohnphilip link: https://python.plainenglish.io/building-a-restful-api-with-fastapi-secure-signup-and-login-functionality-included-45cdbcb36106 From 7e0cdf25100207880e9519f109b9ad5377a83e01 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 17:19:42 +0000 Subject: [PATCH 1566/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8699fbddc..6885ef68d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add External Link: FastAPI application monitoring made easy. PR [#10917](https://github.com/tiangolo/fastapi/pull/10917) by [@tiangolo](https://github.com/tiangolo). * ✨ Generate automatic language names for docs translations. PR [#5354](https://github.com/tiangolo/fastapi/pull/5354) by [@jakul](https://github.com/jakul). * ✏️ Fix typos in `docs/en/docs/alternatives.md` and `docs/en/docs/tutorial/dependencies/index.md`. PR [#10906](https://github.com/tiangolo/fastapi/pull/10906) by [@s111d](https://github.com/s111d). * ✏️ Fix typos in `docs/en/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10834](https://github.com/tiangolo/fastapi/pull/10834) by [@Molkree](https://github.com/Molkree). From 06bf7781df11213ce4bc39370566b733de4dfd07 Mon Sep 17 00:00:00 2001 From: Fahad Md Kamal <faahad.hossain@gmail.com> Date: Wed, 10 Jan 2024 23:43:35 +0600 Subject: [PATCH 1567/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Bengali=20transl?= =?UTF-8?q?ation=20for=20`docs/bn/docs/index.md`=20(#9177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/bn/docs/index.md | 464 ++++++++++++++++++++++++++++++++++++++++++ docs/bn/mkdocs.yml | 1 + 2 files changed, 465 insertions(+) create mode 100644 docs/bn/docs/index.md create mode 100644 docs/bn/mkdocs.yml diff --git a/docs/bn/docs/index.md b/docs/bn/docs/index.md new file mode 100644 index 000000000..4f778e873 --- /dev/null +++ b/docs/bn/docs/index.md @@ -0,0 +1,464 @@ +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>FastAPI উচ্চক্ষমতা সম্পন্ন, সহজে শেখার এবং দ্রুত কোড করে প্রোডাকশনের জন্য ফ্রামওয়ার্ক।</em> +</p> +<p align="center"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg" alt="Test"> +</a> +<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank"> + <img src="https://img.shields.io/codecov/c/github/tiangolo/fastapi?color=%2334D058" alt="Coverage"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> +</a> +</p> + +--- + +**নির্দেশিকা নথি**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**সোর্স কোড**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- + +FastAPI একটি আধুনিক, দ্রুত ( বেশি ক্ষমতা ) সম্পন্ন, Python 3.6+ দিয়ে API তৈরির জন্য স্ট্যান্ডার্ড পাইথন টাইপ ইঙ্গিত ভিত্তিক ওয়েব ফ্রেমওয়ার্ক। + +এর মূল বৈশিষ্ট্য গুলো হলঃ + +- **গতি**: এটি **NodeJS** এবং **Go** এর মত কার্যক্ষমতা সম্পন্ন (Starlette এবং Pydantic এর সাহায্যে)। [পাইথন এর দ্রুততম ফ্রেমওয়ার্ক গুলোর মধ্যে এটি একটি](#_11)। +- **দ্রুত কোড করা**:বৈশিষ্ট্য তৈরির গতি ২০০% থেকে ৩০০% বৃদ্ধি করে৷ \* +- **স্বল্প bugs**: মানুব (ডেভেলপার) সৃষ্ট ত্রুটির প্রায় ৪০% হ্রাস করে। \* +- **স্বজ্ঞাত**: দুর্দান্ত এডিটর সাহায্য <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> নামেও পরিচিত। দ্রুত ডিবাগ করা যায়। + +- **সহজ**: এটি এমন ভাবে সজানো হয়েছে যেন নির্দেশিকা নথি পড়ে সহজে শেখা এবং ব্যবহার করা যায়। +- **সংক্ষিপ্ত**: কোড পুনরাবৃত্তি কমানোর পাশাপাশি, bug কমায় এবং প্রতিটি প্যারামিটার ঘোষণা থেকে একাধিক ফিচার পাওয়া যায় । +- **জোরালো**: স্বয়ংক্রিয় ভাবে তৈরি ক্রিয়াশীল নির্দেশনা নথি (documentation) সহ উৎপাদন উপযোগি (Production-ready) কোড পাওয়া যায়। +- **মান-ভিত্তিক**: এর ভিত্তি <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (যা পুর্বে Swagger নামে পরিচিত ছিল) এবং <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a> এর আদর্শের মানের ওপর + +<small>\* উৎপাদনমুখি এপ্লিকেশন বানানোর এক দল ডেভেলপার এর মতামত ভিত্তিক ফলাফল।</small> + +## স্পনসর গণ + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">অন্যান্য স্পনসর গণ</a> + +## মতামত সমূহ + +"_আমি আজকাল **FastAPI** ব্যবহার করছি। [...] আমরা ভাবছি মাইক্রোসফ্টে **ML সার্ভিস** এ সকল দলের জন্য এটি ব্যবহার করব। যার মধ্যে কিছু পণ্য **Windows** এ সংযোযন হয় এবং কিছু **Office** এর সাথে সংযোযন হচ্ছে।_" + +<div style="text-align: right; margin-right: 10%;">কবির খান - <strong>মাইক্রোসফ্টে</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_আমরা **FastAPI** লাইব্রেরি গ্রহণ করেছি একটি **REST** সার্ভার তৈরি করতে, যা **ভবিষ্যদ্বাণী** পাওয়ার জন্য কুয়েরি করা যেতে পারে। [লুডউইগের জন্য]_" + +<div style="text-align: right; margin-right: 10%;">পিয়েরো মোলিনো, ইয়ারোস্লাভ দুদিন, এবং সাই সুমন্থ মিরিয়ালা - <strong>উবার</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** আমাদের **ক্রাইসিস ম্যানেজমেন্ট** অর্কেস্ট্রেশন ফ্রেমওয়ার্ক: **ডিসপ্যাচ** এর ওপেন সোর্স রিলিজ ঘোষণা করতে পেরে আনন্দিত! [যাকিনা **FastAPI** দিয়ে নির্মিত]_" + +<div style="text-align: right; margin-right: 10%;">কেভিন গ্লিসন, মার্ক ভিলানোভা, ফরেস্ট মনসেন - <strong>নেটফ্লিক্স</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_আমি **FastAPI** নিয়ে চাঁদের সমান উৎসাহিত। এটি খুবই মজার!_" + +<div style="text-align: right; margin-right: 10%;">ব্রায়ান ওকেন - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">পাইথন বাইটস</a> পডকাস্ট হোস্ট</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"\_সত্যিই, আপনি যা তৈরি করেছেন তা খুব মজবুত এবং পরিপূর্ন৷ অনেক উপায়ে, আমি যা **Hug** এ করতে চেয়েছিলাম - তা কাউকে তৈরি করতে দেখে আমি সত্যিই অনুপ্রানিত৷\_" + +<div style="text-align: right; margin-right: 10%;">টিমোথি ক্রসলে - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> স্রষ্টা</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"আপনি যদি REST API তৈরির জন্য একটি **আধুনিক ফ্রেমওয়ার্ক** শিখতে চান, তাহলে **FastAPI** দেখুন [...] এটি দ্রুত, ব্যবহার করা সহজ এবং শিখতেও সহজ [...]\_" + +"_আমরা আমাদের **APIs** [...] এর জন্য **FastAPI**- তে এসেছি [...] আমি মনে করি আপনিও এটি পছন্দ করবেন [...]_" + +<div style="text-align: right; margin-right: 10%;">ইনেস মন্টানি - ম্যাথিউ হোনিবাল - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> প্রতিষ্ঠাতা - <a href="https://spacy.io" target="_blank">spaCy</a> স্রষ্টা</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**, CLI এর জন্য FastAPI + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +আপনি যদি <abbr title="Command Line Interface">CLI</abbr> অ্যাপ বানাতে চান, যা কিনা ওয়েব API এর পরিবর্তে টার্মিনালে ব্যবহার হবে, তাহলে দেখুন<a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. + +**টাইপার** হল FastAPI এর ছোট ভাইয়ের মত। এবং এটির উদ্দেশ্য ছিল **CLIs এর FastAPI** হওয়া। ⌨️ 🚀 + +## প্রয়োজনীয়তা গুলো + +Python 3.7+ + +FastAPI কিছু দানবেদের কাঁধে দাঁড়িয়ে আছে: + +- <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> ওয়েব অংশের জন্য. +- <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> ডেটা অংশগুলির জন্য. + +## ইনস্টলেশন প্রক্রিয়া + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +আপনার একটি ASGI সার্ভারেরও প্রয়োজন হবে, প্রোডাকশনের জন্য <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> অথবা <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>. + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +</div> + +## উদাহরণ + +### তৈরি + +- `main.py` নামে একটি ফাইল তৈরি করুন: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>অথবা ব্যবহার করুন <code>async def</code>...</summary> + +যদি আপনার কোড `async` / `await`, ব্যবহার করে তাহলে `async def` ব্যবহার করুন: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**টীকা**: + +আপনি যদি না জানেন, _"তাড়াহুড়ো?"_ বিভাগটি দেখুন <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` এবং `await` নথির মধ্যে দেখুন </a>. + +</details> + +### এটি চালান + +সার্ভার চালু করুন: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary>নির্দেশনা সম্পর্কে <code>uvicorn main:app --reload</code>...</summary> + +`uvicorn main:app` নির্দেশনাটি দ্বারা বোঝায়: + +- `main`: ফাইল `main.py` (পাইথন "মডিউল")। +- `app`: `app = FastAPI()` লাইন দিয়ে `main.py` এর ভিতরে তৈরি করা অবজেক্ট। +- `--reload`: কোড পরিবর্তনের পরে সার্ভার পুনরায় চালু করুন। এটি শুধুমাত্র ডেভেলপমেন্ট এর সময় ব্যবহার করুন। + +</details> + +### এটা চেক করুন + +আপনার ব্রাউজার খুলুন <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a> এ। + +আপনি JSON রেসপন্স দেখতে পাবেন: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +আপনি ইতিমধ্যে একটি API তৈরি করেছেন যা: + +- `/` এবং `/items/{item_id}` _paths_ এ HTTP অনুরোধ গ্রহণ করে। +- উভয় *path*ই `GET` <em>অপারেশন</em> নেয় ( যা HTTP _methods_ নামেও পরিচিত)। +- _path_ `/items/{item_id}`-এ একটি _path প্যারামিটার_ `item_id` আছে যা কিনা `int` হতে হবে। +- _path_ `/items/{item_id}`-এর একটি ঐচ্ছিক `str` _query প্যারামিটার_ `q` আছে। + +### ক্রিয়াশীল API নির্দেশিকা নথি + +এখন যান <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +আপনি স্বয়ংক্রিয় ভাবে প্রস্তুত ক্রিয়াশীল API নির্দেশিকা নথি দেখতে পাবেন (<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> প্রদত্ত): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### বিকল্প API নির্দেশিকা নথি + +এবং এখন <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> এ যান. + +আপনি স্বয়ংক্রিয় ভাবে প্রস্তুত বিকল্প নির্দেশিকা নথি দেখতে পাবেন (<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> প্রদত্ত): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## উদাহরণস্বরূপ আপগ্রেড + +এখন `main.py` ফাইলটি পরিবর্তন করুন যেন এটি `PUT` রিকুয়েস্ট থেকে বডি পেতে পারে। + +Python স্ট্যান্ডার্ড লাইব্রেরি, Pydantic এর সাহায্যে বডি ঘোষণা করুন। + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +সার্ভারটি স্বয়ংক্রিয়ভাবে পুনরায় লোড হওয়া উচিত (কারণ আপনি উপরের `uvicorn` কমান্ডে `--reload` যোগ করেছেন)। + +### ক্রিয়াশীল API নির্দেশিকা নথি উন্নীতকরণ + +এখন <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> এডড্রেসে যান. + +- ক্রিয়াশীল API নির্দেশিকা নথিটি স্বয়ংক্রিয়ভাবে উন্নীত হযে যাবে, নতুন বডি সহ: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +- "Try it out" বাটনে চাপুন, এটি আপনাকে পেরামিটারগুলো পূরণ করতে এবং API এর সাথে সরাসরি ক্রিয়া-কলাপ করতে দিবে: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +- তারপরে "Execute" বাটনে চাপুন, ব্যবহারকারীর ইন্টারফেস আপনার API এর সাথে যোগাযোগ করবে, পেরামিটার পাঠাবে, ফলাফলগুলি পাবে এবং সেগুলি পর্রদায় দেখাবে: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### বিকল্প API নির্দেশিকা নথি আপগ্রেড + +এবং এখন <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> এ যান। + +- বিকল্প নির্দেশিকা নথিতেও নতুন কুয়েরি প্যারামিটার এবং বডি প্রতিফলিত হবে: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### সংক্ষিপ্তকরণ + +সংক্ষেপে, আপনি **শুধু একবার** প্যারামিটারের ধরন, বডি ইত্যাদি ফাংশন প্যারামিটার হিসেবে ঘোষণা করেন। + +আপনি সেটি আধুনিক পাইথনের সাথে করেন। + +আপনাকে নতুন করে নির্দিষ্ট কোন লাইব্রেরির বাক্য গঠন, ফাংশন বা ক্লাস কিছুই শিখতে হচ্ছে না। + +শুধুই আধুনিক **Python 3.6+** + +উদাহরণস্বরূপ, `int` এর জন্য: + +```Python +item_id: int +``` + +অথবা আরও জটিল `Item` মডেলের জন্য: + +```Python +item: Item +``` + +...এবং সেই একই ঘোষণার সাথে আপনি পাবেন: + +- এডিটর সাহায্য, যেমন + - সমাপ্তি। + - ধরণ যাচাই +- তথ্য যাচাইকরণ: + - ডেটা অবৈধ হলে স্বয়ংক্রিয় এবং পরিষ্কার ত্রুটির নির্দেশনা। + - এমনকি গভীরভাবে নেস্ট করা JSON অবজেক্টের জন্য বৈধতা। +- প্রেরিত তথ্য <abbr title="যা পরিচিত: serialization, parsing, marshalling">রূপান্তর</abbr>: যা নেটওয়ার্ক থেকে পাইথনের তথ্য এবং ধরনে আসে, এবং সেখান থেকে পড়া: + + - JSON। + - পাথ প্যারামিটার। + - কুয়েরি প্যারামিটার। + - কুকিজ + - হেডার + - ফর্ম + - ফাইল + +- আউটপুট ডেটার <abbr title="যা পরিচিত: serialization, parsing, marshalling">রূপান্তর</abbr>: পাইথন ডেটা এবং টাইপ থেকে নেটওয়ার্ক ডেটাতে রূপান্তর করা (JSON হিসাবে): + -পাইথন টাইপে রূপান্তর করুন (`str`, `int`, `float`, `bool`, `list`, ইত্যাদি)। + - `datetime` অবজেক্ট। + - `UUID` objeঅবজেক্টcts। + - ডাটাবেস মডেল। + - ...এবং আরো অনেক। +- স্বয়ংক্রিয় ক্রিয়াশীল API নির্দেশিকা নথি, 2টি বিকল্প ব্যবহারকারীর ইন্টারফেস সহ: + - সোয়াগার ইউ আই (Swagger UI)। + - রিডক (ReDoc)। + +--- + +পূর্ববর্তী কোড উদাহরণে ফিরে আসা যাক, **FastAPI** যা করবে: + +- `GET` এবং `PUT` অনুরোধের জন্য পথে `item_id` আছে কিনা তা যাচাই করবে। +- `GET` এবং `PUT` অনুরোধের জন্য `item_id` টাইপ `int` এর হতে হবে তা যাচাই করবে। + - যদি না হয় তবে ক্লায়েন্ট একটি উপযুক্ত, পরিষ্কার ত্রুটি দেখতে পাবেন। +- `GET` অনুরোধের জন্য একটি ঐচ্ছিক ক্যুয়েরি প্যারামিটার নামক `q` (যেমন `http://127.0.0.1:8000/items/foo?q=somequery`) আছে কি তা চেক করবে। + - যেহেতু `q` প্যারামিটারটি `= None` দিয়ে ঘোষণা করা হয়েছে, তাই এটি ঐচ্ছিক। + - `None` ছাড়া এটি প্রয়োজনীয় হতো (যেমন `PUT` এর ক্ষেত্রে হয়েছে)। +- `/items/{item_id}` এর জন্য `PUT` অনুরোধের বডি JSON হিসাবে পড়ুন: + - লক্ষ করুন, `name` একটি প্রয়োজনীয় অ্যাট্রিবিউট হিসাবে বিবেচনা করেছে এবং এটি `str` হতে হবে। + - লক্ষ করুন এখানে, `price` অ্যাট্রিবিউটটি আবশ্যক এবং এটি `float` হতে হবে। + - লক্ষ করুন `is_offer` একটি ঐচ্ছিক অ্যাট্রিবিউট এবং এটি `bool` হতে হবে যদি উপস্থিত থাকে। + - এই সবটি গভীরভাবে অবস্থানরত JSON অবজেক্টগুলিতেও কাজ করবে। +- স্বয়ংক্রিয়ভাবে JSON হতে এবং JSON থেকে কনভার্ট করুন। +- OpenAPI দিয়ে সবকিছু ডকুমেন্ট করুন, যা ব্যবহার করা যেতে পারে: + - ক্রিয়াশীল নির্দেশিকা নথি। + - অনেক ভাষার জন্য স্বয়ংক্রিয় ক্লায়েন্ট কোড তৈরির ব্যবস্থা। +- সরাসরি 2টি ক্রিয়াশীল নির্দেশিকা নথি ওয়েব পৃষ্ঠ প্রদান করা হয়েছে। + +--- + +আমরা এতক্ষন শুধু এর পৃষ্ঠ তৈরি করেছি, কিন্তু আপনি ইতমধ্যেই এটি কিভাবে কাজ করে তার ধারণাও পেয়ে গিয়েছেন। + +নিম্নোক্ত লাইন গুলো পরিবর্তন করার চেষ্টা করুন: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...পুর্বে: + +```Python + ... "item_name": item.name ... +``` + +...পরবর্তীতে: + +```Python + ... "item_price": item.price ... +``` + +...এবং দেখুন কিভাবে আপনার এডিটর উপাদানগুলোকে সয়ংক্রিয়ভাবে-সম্পন্ন করবে এবং তাদের ধরন জানতে পারবে: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +আরও বৈশিষ্ট্য সম্পন্ন উদাহরণের জন্য, দেখুন <a href="https://fastapi.tiangolo.com/tutorial/">টিউটোরিয়াল - ব্যবহারকারীর গাইড</a>. + +**স্পয়লার সতর্কতা**: টিউটোরিয়াল - ব্যবহারকারীর গাইড নিম্নোক্ত বিষয়গুলি অন্তর্ভুক্ত করে: + +- **হেডার**, **কুকিজ**, **ফর্ম ফিল্ড** এবং **ফাইলগুলি** এমন অন্যান্য জায়গা থেকে প্যারামিটার ঘোষণা করা। +- `maximum_length` বা `regex` এর মতো **যাচাইকরণ বাধামুক্তি** সেট করা হয় কিভাবে, তা নিয়ে আলোচনা করা হবে। +- একটি খুব শক্তিশালী এবং ব্যবহার করা সহজ <abbr title="also known as components, resources, providers, services, injectables">ডিপেন্ডেন্সি ইনজেকশন</abbr> পদ্ধতি +- **OAuth2** এবং **JWT টোকেন** এবং **HTTP Basic** auth সহ নিরাপত্তা এবং অনুমোদনপ্রাপ্তি সম্পর্কিত বিষয়সমূহের উপর। +- **গভীরভাবে অবস্থানরত JSON মডেল** ঘোষণা করার জন্য আরও উন্নত (কিন্তু সমান সহজ) কৌশল (Pydantic কে ধন্যবাদ)। +- আরো অতিরিক্ত বৈশিষ্ট্য (স্টারলেটকে ধন্যবাদ) হিসাবে: + - **WebSockets** + - **GraphQL** + - HTTPX এবং `pytest` ভিত্তিক অত্যন্ত সহজ পরীক্ষা + - **CORS** + - **Cookie Sessions** + - ...এবং আরো। + +## কর্মক্ষমতা + +স্বাধীন TechEmpower Benchmarks দেখায় যে **FastAPI** অ্যাপ্লিকেশনগুলি Uvicorn-এর অধীনে চলমান দ্রুততম<a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">পাইথন ফ্রেমওয়ার্কগুলির মধ্যে একটি,</a> শুধুমাত্র Starlette এবং Uvicorn-এর পর (FastAPI দ্বারা অভ্যন্তরীণভাবে ব্যবহৃত)। (\*) + +এটি সম্পর্কে আরও বুঝতে, দেখুন <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. + +## ঐচ্ছিক নির্ভরশীলতা + +Pydantic দ্বারা ব্যবহৃত: + +- <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - দ্রুত JSON এর জন্য <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>. +- <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - ইমেল যাচাইকরণের জন্য। + +স্টারলেট দ্বারা ব্যবহৃত: + +- <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - আপনি যদি `TestClient` ব্যবহার করতে চান তাহলে আবশ্যক। +- <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - আপনি যদি প্রদত্ত টেমপ্লেট রূপরেখা ব্যবহার করতে চান তাহলে প্রয়োজন। +- <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - আপনি যদি ফর্ম সহায়তা করতে চান তাহলে প্রয়োজন <abbr title="converting the string that comes from an HTTP request into Python data">"parsing"</abbr>, `request.form()` সহ। +- <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - `SessionMiddleware` সহায়তার জন্য প্রয়োজন। +- <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - স্টারলেটের SchemaGenerator সাপোর্ট এর জন্য প্রয়োজন (আপনার সম্ভাবত FastAPI প্রয়োজন নেই)। +- <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - `GraphQLApp` সহায়তার জন্য প্রয়োজন। +- <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - আপনি `UJSONResponse` ব্যবহার করতে চাইলে প্রয়োজন। + +FastAPI / Starlette দ্বারা ব্যবহৃত: + +- <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - সার্ভারের জন্য যা আপনার অ্যাপ্লিকেশন লোড করে এবং পরিবেশন করে। +- <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - আপনি `ORJSONResponse` ব্যবহার করতে চাইলে প্রয়োজন। + +আপনি এই সব ইনস্টল করতে পারেন `pip install fastapi[all]` দিয়ে. + +## লাইসেন্স + +এই প্রজেক্ট MIT লাইসেন্স নীতিমালার অধীনে শর্তায়িত। diff --git a/docs/bn/mkdocs.yml b/docs/bn/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/bn/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From 1cd23a1dbce8f2fd7f15ece57c3cd45a2cb04cac Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 17:43:56 +0000 Subject: [PATCH 1568/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6885ef68d..f3c70489b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -26,6 +26,7 @@ hide: ### Translations +* 🌐 Add Bengali translation for `docs/bn/docs/index.md`. PR [#9177](https://github.com/tiangolo/fastapi/pull/9177) by [@Fahad-Md-Kamal](https://github.com/Fahad-Md-Kamal). * ✏️ Update Python version in `index.md` in several languages. PR [#10711](https://github.com/tiangolo/fastapi/pull/10711) by [@tamago3keran](https://github.com/tamago3keran). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/request-forms-and-files.md`. PR [#10347](https://github.com/tiangolo/fastapi/pull/10347) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Ukrainian translation for `docs/uk/docs/index.md`. PR [#10362](https://github.com/tiangolo/fastapi/pull/10362) by [@rostik1410](https://github.com/rostik1410). From 843bc85155be86d7688ab126bb7ea266d410bf71 Mon Sep 17 00:00:00 2001 From: Sungyun Hur <ethan0311@gmail.com> Date: Thu, 11 Jan 2024 03:15:04 +0900 Subject: [PATCH 1569/1881] =?UTF-8?q?=F0=9F=93=9D=20Fix=20broken=20link=20?= =?UTF-8?q?in=20`docs/en/docs/tutorial/sql-databases.md`=20(#10765)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/docs/tutorial/sql-databases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 010244bbf..ce6507912 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -624,7 +624,7 @@ def read_user(user_id: int, db: Session = Depends(get_db)): ``` !!! info - If you need to connect to your relational database asynchronously, see [Async SQL (Relational) Databases](../advanced/async-sql-databases.md){.internal-link target=_blank}. + If you need to connect to your relational database asynchronously, see [Async SQL (Relational) Databases](../how-to/async-sql-encode-databases.md){.internal-link target=_blank}. !!! note "Very Technical Details" If you are curious and have a deep technical knowledge, you can check the very technical details of how this `async def` vs `def` is handled in the [Async](../async.md#very-technical-details){.internal-link target=_blank} docs. From 1334485435ce0e934a965b23912654baab5d861d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 18:15:28 +0000 Subject: [PATCH 1570/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f3c70489b..b0a6c8dc6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Fix broken link in `docs/en/docs/tutorial/sql-databases.md`. PR [#10765](https://github.com/tiangolo/fastapi/pull/10765) by [@HurSungYun](https://github.com/HurSungYun). * 📝 Add External Link: FastAPI application monitoring made easy. PR [#10917](https://github.com/tiangolo/fastapi/pull/10917) by [@tiangolo](https://github.com/tiangolo). * ✨ Generate automatic language names for docs translations. PR [#5354](https://github.com/tiangolo/fastapi/pull/5354) by [@jakul](https://github.com/jakul). * ✏️ Fix typos in `docs/en/docs/alternatives.md` and `docs/en/docs/tutorial/dependencies/index.md`. PR [#10906](https://github.com/tiangolo/fastapi/pull/10906) by [@s111d](https://github.com/s111d). From b584faffee11bfc08bea3bd2d66c304701b921d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 10 Jan 2024 23:13:55 +0400 Subject: [PATCH 1571/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20notes=20about=20?= =?UTF-8?q?Pydantic=20v2's=20new=20`.model=5Fdump()`=20(#10929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/how-to/async-sql-encode-databases.md | 5 +++++ docs/en/docs/tutorial/body-updates.md | 20 ++++++++++++++----- docs/en/docs/tutorial/extra-models.md | 5 +++++ docs/en/docs/tutorial/response-model.md | 5 +++++ docs/en/docs/tutorial/sql-databases.md | 5 +++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/en/docs/how-to/async-sql-encode-databases.md b/docs/en/docs/how-to/async-sql-encode-databases.md index 697167f79..0e2ccce78 100644 --- a/docs/en/docs/how-to/async-sql-encode-databases.md +++ b/docs/en/docs/how-to/async-sql-encode-databases.md @@ -114,6 +114,11 @@ Create the *path operation function* to create notes: {!../../../docs_src/async_sql_databases/tutorial001.py!} ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! Note Notice that as we communicate with the database using `await`, the *path operation function* is declared with `async`. diff --git a/docs/en/docs/tutorial/body-updates.md b/docs/en/docs/tutorial/body-updates.md index 3341f2d5d..39d133c55 100644 --- a/docs/en/docs/tutorial/body-updates.md +++ b/docs/en/docs/tutorial/body-updates.md @@ -59,9 +59,14 @@ This means that you can send only the data that you want to update, leaving the ### Using Pydantic's `exclude_unset` parameter -If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.dict()`. +If you want to receive partial updates, it's very useful to use the parameter `exclude_unset` in Pydantic's model's `.model_dump()`. -Like `item.dict(exclude_unset=True)`. +Like `item.model_dump(exclude_unset=True)`. + +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. That would generate a `dict` with only the data that was set when creating the `item` model, excluding default values. @@ -87,9 +92,14 @@ Then you can use this to generate a `dict` with only the data that was set (sent ### Using Pydantic's `update` parameter -Now, you can create a copy of the existing model using `.copy()`, and pass the `update` parameter with a `dict` containing the data to update. +Now, you can create a copy of the existing model using `.model_copy()`, and pass the `update` parameter with a `dict` containing the data to update. -Like `stored_item_model.copy(update=update_data)`: +!!! info + In Pydantic v1 the method was called `.copy()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_copy()`. + + The examples here use `.copy()` for compatibility with Pydantic v1, but you should use `.model_copy()` instead if you can use Pydantic v2. + +Like `stored_item_model.model_copy(update=update_data)`: === "Python 3.10+" @@ -120,7 +130,7 @@ In summary, to apply partial updates you would: * This way you can update only the values actually set by the user, instead of overriding values already stored with default values in your model. * Create a copy of the stored model, updating it's attributes with the received partial updates (using the `update` parameter). * Convert the copied model to something that can be stored in your DB (for example, using the `jsonable_encoder`). - * This is comparable to using the model's `.dict()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. + * This is comparable to using the model's `.model_dump()` method again, but it makes sure (and converts) the values to data types that can be converted to JSON, for example, `datetime` to `str`. * Save the data to your DB. * Return the updated model. diff --git a/docs/en/docs/tutorial/extra-models.md b/docs/en/docs/tutorial/extra-models.md index 590d095bd..d83b6bc85 100644 --- a/docs/en/docs/tutorial/extra-models.md +++ b/docs/en/docs/tutorial/extra-models.md @@ -29,6 +29,11 @@ Here's a general idea of how the models could look like with their password fiel {!> ../../../docs_src/extra_models/tutorial001.py!} ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + ### About `**user_in.dict()` #### Pydantic's `.dict()` diff --git a/docs/en/docs/tutorial/response-model.md b/docs/en/docs/tutorial/response-model.md index d6d3d61cb..d5683ac7f 100644 --- a/docs/en/docs/tutorial/response-model.md +++ b/docs/en/docs/tutorial/response-model.md @@ -377,6 +377,11 @@ So, if you send a request to that *path operation* for the item with ID `foo`, t } ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! info FastAPI uses Pydantic model's `.dict()` with <a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" class="external-link" target="_blank">its `exclude_unset` parameter</a> to achieve this. diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index ce6507912..1bc87a702 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -451,6 +451,11 @@ The steps are: {!../../../docs_src/sql_databases/sql_app/crud.py!} ``` +!!! info + In Pydantic v1 the method was called `.dict()`, it was deprecated (but still supported) in Pydantic v2, and renamed to `.model_dump()`. + + The examples here use `.dict()` for compatibility with Pydantic v1, but you should use `.model_dump()` instead if you can use Pydantic v2. + !!! tip The SQLAlchemy model for `User` contains a `hashed_password` that should contain a secure hashed version of the password. From 91d7fb6d255156a753108b4f762a4f12b8861961 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 19:14:15 +0000 Subject: [PATCH 1572/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b0a6c8dc6..ab64e33d0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix broken link in `docs/en/docs/tutorial/sql-databases.md`. PR [#10765](https://github.com/tiangolo/fastapi/pull/10765) by [@HurSungYun](https://github.com/HurSungYun). * 📝 Add External Link: FastAPI application monitoring made easy. PR [#10917](https://github.com/tiangolo/fastapi/pull/10917) by [@tiangolo](https://github.com/tiangolo). * ✨ Generate automatic language names for docs translations. PR [#5354](https://github.com/tiangolo/fastapi/pull/5354) by [@jakul](https://github.com/jakul). From 07f8d31ec9d6e2234e12515f3373f57020b1726d Mon Sep 17 00:00:00 2001 From: Aliaksei Urbanski <aliaksei.urbanski@gmail.com> Date: Wed, 10 Jan 2024 23:55:45 +0300 Subject: [PATCH 1573/1881] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20Pyt?= =?UTF-8?q?hon=203.12=20(#10666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .github/workflows/test.yml | 7 ++++++- docs_src/security/tutorial004.py | 6 +++--- docs_src/security/tutorial004_an.py | 6 +++--- docs_src/security/tutorial004_an_py310.py | 6 +++--- docs_src/security/tutorial004_an_py39.py | 6 +++--- docs_src/security/tutorial004_py310.py | 6 +++--- docs_src/security/tutorial005.py | 6 +++--- docs_src/security/tutorial005_an.py | 6 +++--- docs_src/security/tutorial005_an_py310.py | 6 +++--- docs_src/security/tutorial005_an_py39.py | 6 +++--- docs_src/security/tutorial005_py310.py | 6 +++--- docs_src/security/tutorial005_py39.py | 6 +++--- pyproject.toml | 10 ++++++++++ 13 files changed, 49 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7ebb80efd..032db9c9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: + - "3.12" + - "3.11" + - "3.10" + - "3.9" + - "3.8" pydantic-version: ["pydantic-v1", "pydantic-v2"] fail-fast: false steps: diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 64099abe9..134c15c5a 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Union from fastapi import Depends, FastAPI, HTTPException, status @@ -78,9 +78,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py index ca350343d..204151a56 100644 --- a/docs_src/security/tutorial004_an.py +++ b/docs_src/security/tutorial004_an.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Union from fastapi import Depends, FastAPI, HTTPException, status @@ -79,9 +79,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py index 8bf5f3b71..64dfa15c6 100644 --- a/docs_src/security/tutorial004_an_py310.py +++ b/docs_src/security/tutorial004_an_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, status @@ -78,9 +78,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py index a634e23de..631a8366e 100644 --- a/docs_src/security/tutorial004_an_py39.py +++ b/docs_src/security/tutorial004_an_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Annotated, Union from fastapi import Depends, FastAPI, HTTPException, status @@ -78,9 +78,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index 797d56d04..470f22e29 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -77,9 +77,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index bd0a33581..ece461bc8 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py index ec4fa1a07..c5b5609e5 100644 --- a/docs_src/security/tutorial005_an.py +++ b/docs_src/security/tutorial005_an.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -94,9 +94,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index 45f3fc0bd..5e81a50e1 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Annotated from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index ecb5ed516..ae9811c68 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Annotated, List, Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index ba756ef4f..0fcdda4c0 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from fastapi import Depends, FastAPI, HTTPException, Security, status from fastapi.security import ( @@ -92,9 +92,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 9e4dbcffb..d756c0b6b 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Union from fastapi import Depends, FastAPI, HTTPException, Security, status @@ -93,9 +93,9 @@ def authenticate_user(fake_db, username: str, password: str): def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: - expire = datetime.utcnow() + expires_delta + expire = datetime.now(timezone.utc) + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=15) + expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt diff --git a/pyproject.toml b/pyproject.toml index 38728d99e..2fde7553a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP :: HTTP Servers", "Topic :: Internet :: WWW/HTTP", ] @@ -111,6 +112,15 @@ filterwarnings = [ "ignore::trio.TrioDeprecationWarning", # TODO remove pytest-cov 'ignore::pytest.PytestDeprecationWarning:pytest_cov', + # TODO: remove after upgrading SQLAlchemy to a version that includes the following changes + # https://github.com/sqlalchemy/sqlalchemy/commit/59521abcc0676e936b31a523bd968fc157fef0c2 + 'ignore:datetime\.datetime\.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:sqlalchemy', + # TODO: remove after upgrading python-jose to a version that explicitly supports Python 3.12 + # also, if it won't receive an update, consider replacing python-jose with some alternative + # related issues: + # - https://github.com/mpdavis/python-jose/issues/332 + # - https://github.com/mpdavis/python-jose/issues/334 + 'ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:jose', ] [tool.coverage.run] From 21145d8e9f896502ae227678c63467fb00384cfa Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 20:56:59 +0000 Subject: [PATCH 1574/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index ab64e33d0..25219b328 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Features + +* ✨ Add support for Python 3.12. PR [#10666](https://github.com/tiangolo/fastapi/pull/10666) by [@Jamim](https://github.com/Jamim). + ### Docs * 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). From 135dcba746cdaf2b4726f4f10fad9eb8bb91e342 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Wed, 10 Jan 2024 22:00:32 +0100 Subject: [PATCH 1575/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20VS=20Code=20tuto?= =?UTF-8?q?rial=20link=20(#10592)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index d9cfe3431..f15560d1b 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Visual Studio Code Team + author_link: https://code.visualstudio.com/ + link: https://code.visualstudio.com/docs/python/tutorial-fastapi + title: FastAPI Tutorial in Visual Studio Code - author: Apitally author_link: https://apitally.io link: https://blog.apitally.io/fastapi-application-monitoring-made-easy From 0da980cb0b73c8cb5713d95d44620c41c0a29b5d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 10 Jan 2024 21:00:51 +0000 Subject: [PATCH 1576/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 25219b328..b523be75d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Add VS Code tutorial link. PR [#10592](https://github.com/tiangolo/fastapi/pull/10592) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix broken link in `docs/en/docs/tutorial/sql-databases.md`. PR [#10765](https://github.com/tiangolo/fastapi/pull/10765) by [@HurSungYun](https://github.com/HurSungYun). * 📝 Add External Link: FastAPI application monitoring made easy. PR [#10917](https://github.com/tiangolo/fastapi/pull/10917) by [@tiangolo](https://github.com/tiangolo). From 69cb005f61239a378a7e0715cc5e3ff4b713ab4d Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Thu, 11 Jan 2024 15:33:05 +0100 Subject: [PATCH 1577/1881] =?UTF-8?q?=F0=9F=93=9D=20Replace=20`email`=20wi?= =?UTF-8?q?th=20`username`=20in=20`docs=5Fsrc/security/tutorial007`=20code?= =?UTF-8?q?=20examples=20(#10649)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/security/http-basic-auth.md | 6 +++--- docs_src/security/tutorial007.py | 2 +- docs_src/security/tutorial007_an.py | 2 +- docs_src/security/tutorial007_an_py39.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/advanced/security/http-basic-auth.md b/docs/en/docs/advanced/security/http-basic-auth.md index 6f9002f60..680f4dff5 100644 --- a/docs/en/docs/advanced/security/http-basic-auth.md +++ b/docs/en/docs/advanced/security/http-basic-auth.md @@ -105,7 +105,7 @@ if "johndoe" == "stanleyjobson" and "love123" == "swordfish": ... ``` -But right at the moment Python compares the first `j` in `johndoe` to the first `s` in `stanleyjobson`, it will return `False`, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "incorrect user or password". +But right at the moment Python compares the first `j` in `johndoe` to the first `s` in `stanleyjobson`, it will return `False`, because it already knows that those two strings are not the same, thinking that "there's no need to waste more computation comparing the rest of the letters". And your application will say "Incorrect username or password". But then the attackers try with username `stanleyjobsox` and password `love123`. @@ -116,11 +116,11 @@ if "stanleyjobsox" == "stanleyjobson" and "love123" == "swordfish": ... ``` -Python will have to compare the whole `stanleyjobso` in both `stanleyjobsox` and `stanleyjobson` before realizing that both strings are not the same. So it will take some extra microseconds to reply back "incorrect user or password". +Python will have to compare the whole `stanleyjobso` in both `stanleyjobsox` and `stanleyjobson` before realizing that both strings are not the same. So it will take some extra microseconds to reply back "Incorrect username or password". #### The time to answer helps the attackers -At that point, by noticing that the server took some microseconds longer to send the "incorrect user or password" response, the attackers will know that they got _something_ right, some of the initial letters were right. +At that point, by noticing that the server took some microseconds longer to send the "Incorrect username or password" response, the attackers will know that they got _something_ right, some of the initial letters were right. And then they can try again knowing that it's probably something more similar to `stanleyjobsox` than to `johndoe`. diff --git a/docs_src/security/tutorial007.py b/docs_src/security/tutorial007.py index 790ee10bc..ac816eb0c 100644 --- a/docs_src/security/tutorial007.py +++ b/docs_src/security/tutorial007.py @@ -22,7 +22,7 @@ def get_current_username(credentials: HTTPBasicCredentials = Depends(security)): if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail="Incorrect username or password", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username diff --git a/docs_src/security/tutorial007_an.py b/docs_src/security/tutorial007_an.py index 5fb7c8e57..9e9c3cd70 100644 --- a/docs_src/security/tutorial007_an.py +++ b/docs_src/security/tutorial007_an.py @@ -25,7 +25,7 @@ def get_current_username( if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail="Incorrect username or password", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username diff --git a/docs_src/security/tutorial007_an_py39.py b/docs_src/security/tutorial007_an_py39.py index 17177dabf..3d9ea2726 100644 --- a/docs_src/security/tutorial007_an_py39.py +++ b/docs_src/security/tutorial007_an_py39.py @@ -25,7 +25,7 @@ def get_current_username( if not (is_correct_username and is_correct_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Incorrect email or password", + detail="Incorrect username or password", headers={"WWW-Authenticate": "Basic"}, ) return credentials.username From 5b1e6865c50207ad24d26b55f3577b5c3b8244b3 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 14:33:27 +0000 Subject: [PATCH 1578/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b523be75d..f95ccff56 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* 📝 Replace `email` with `username` in `docs_src/security/tutorial007` code examples. PR [#10649](https://github.com/tiangolo/fastapi/pull/10649) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add VS Code tutorial link. PR [#10592](https://github.com/tiangolo/fastapi/pull/10592) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). * 📝 Fix broken link in `docs/en/docs/tutorial/sql-databases.md`. PR [#10765](https://github.com/tiangolo/fastapi/pull/10765) by [@HurSungYun](https://github.com/HurSungYun). From c46eba8004cc688ebfb4d9bf4074ccc8c6f0ca3e Mon Sep 17 00:00:00 2001 From: s111d <angry.kustomer@gmail.com> Date: Thu, 11 Jan 2024 17:33:57 +0300 Subject: [PATCH 1579/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/en/docs/alternatives.md`=20(#10931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/docs/alternatives.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/alternatives.md b/docs/en/docs/alternatives.md index e02b3b55a..70bbcac91 100644 --- a/docs/en/docs/alternatives.md +++ b/docs/en/docs/alternatives.md @@ -191,7 +191,7 @@ This solved having to write YAML (another syntax) inside of Python docstrings. This combination of Flask, Flask-apispec with Marshmallow and Webargs was my favorite backend stack until building **FastAPI**. -Using it led to the creation of several Flask full-stack generators. These are the main stack I (and several external teams) have been using up to now: +Using it led to the creation of several Flask full-stack generators. These are the main stacks I (and several external teams) have been using up to now: * <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a> * <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a> @@ -211,7 +211,7 @@ This isn't even Python, NestJS is a JavaScript (TypeScript) NodeJS framework ins It achieves something somewhat similar to what can be done with Flask-apispec. -It has an integrated dependency injection system, inspired by Angular two. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition. +It has an integrated dependency injection system, inspired by Angular 2. It requires pre-registering the "injectables" (like all the other dependency injection systems I know), so, it adds to the verbosity and code repetition. As the parameters are described with TypeScript types (similar to Python type hints), editor support is quite good. From c3e062542375f1c8c9e645ca1889872e51e97ed4 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 14:35:15 +0000 Subject: [PATCH 1580/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f95ccff56..0ec3fd4f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,7 @@ hide: ### Docs +* ✏️ Fix typo in `docs/en/docs/alternatives.md`. PR [#10931](https://github.com/tiangolo/fastapi/pull/10931) by [@s111d](https://github.com/s111d). * 📝 Replace `email` with `username` in `docs_src/security/tutorial007` code examples. PR [#10649](https://github.com/tiangolo/fastapi/pull/10649) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add VS Code tutorial link. PR [#10592](https://github.com/tiangolo/fastapi/pull/10592) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add notes about Pydantic v2's new `.model_dump()`. PR [#10929](https://github.com/tiangolo/fastapi/pull/10929) by [@tiangolo](https://github.com/tiangolo). From 5e583199b3ad252a3539f1d865e36d3c9ae61318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Thu, 11 Jan 2024 19:29:54 +0400 Subject: [PATCH 1581/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20Starle?= =?UTF-8?q?tte=20to=20>=3D0.35.0,<0.36.0=20(#10938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2fde7553a..8e7f8bbbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.29.0,<0.33.0", + "starlette>=0.35.0,<0.36.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", ] @@ -121,6 +121,9 @@ filterwarnings = [ # - https://github.com/mpdavis/python-jose/issues/332 # - https://github.com/mpdavis/python-jose/issues/334 'ignore:datetime\.datetime\.utcnow\(\) is deprecated and scheduled for removal in a future version\..*:DeprecationWarning:jose', + # TODO: remove after upgrading Starlette to a version including https://github.com/encode/starlette/pull/2406 + # Probably Starlette 0.36.0 + "ignore: The 'method' parameter is not used, and it will be removed.:DeprecationWarning:starlette", ] [tool.coverage.run] From 7c1aeb5db21593421d96fa226461569c77f973eb Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 15:30:35 +0000 Subject: [PATCH 1582/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0ec3fd4f5..3395215af 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✨ Add support for Python 3.12. PR [#10666](https://github.com/tiangolo/fastapi/pull/10666) by [@Jamim](https://github.com/Jamim). +### Upgrades + +* ⬆️ Upgrade Starlette to >=0.35.0,<0.36.0. PR [#10938](https://github.com/tiangolo/fastapi/pull/10938) by [@tiangolo](https://github.com/tiangolo). + ### Docs * ✏️ Fix typo in `docs/en/docs/alternatives.md`. PR [#10931](https://github.com/tiangolo/fastapi/pull/10931) by [@s111d](https://github.com/s111d). From cb95d1cb8927292fc096834a62c3aa46af46e4ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Thu, 11 Jan 2024 16:32:00 +0100 Subject: [PATCH 1583/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?109.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 3395215af..d75d9e3bd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.109.0 + ### Features * ✨ Add support for Python 3.12. PR [#10666](https://github.com/tiangolo/fastapi/pull/10666) by [@Jamim](https://github.com/Jamim). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 02ac83b5e..f457fafd4 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.108.0" +__version__ = "0.109.0" from starlette import status as status From 6bda1326a47968a1e81888da39397c4460368bb8 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Thu, 11 Jan 2024 16:59:27 +0100 Subject: [PATCH 1584/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20location=20info?= =?UTF-8?q?=20to=20`tutorial/bigger-applications.md`=20(#10552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/tutorial/bigger-applications.md | 26 ++++++++--------- docs/en/docs/tutorial/bigger-applications.md | 30 ++++++++++---------- docs/zh/docs/tutorial/bigger-applications.md | 26 ++++++++--------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/em/docs/tutorial/bigger-applications.md b/docs/em/docs/tutorial/bigger-applications.md index 7b4694387..c30bba106 100644 --- a/docs/em/docs/tutorial/bigger-applications.md +++ b/docs/em/docs/tutorial/bigger-applications.md @@ -79,7 +79,7 @@ 👆 🗄 ⚫️ & ✍ "👐" 🎏 🌌 👆 🔜 ⏮️ 🎓 `FastAPI`: -```Python hl_lines="1 3" +```Python hl_lines="1 3" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -89,7 +89,7 @@ ⚙️ ⚫️ 🎏 🌌 👆 🔜 ⚙️ `FastAPI` 🎓: -```Python hl_lines="6 11 16" +```Python hl_lines="6 11 16" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -112,7 +112,7 @@ 👥 🔜 🔜 ⚙️ 🙅 🔗 ✍ 🛃 `X-Token` 🎚: -```Python hl_lines="1 4-6" +```Python hl_lines="1 4-6" title="app/dependencies.py" {!../../../docs_src/bigger_applications/app/dependencies.py!} ``` @@ -143,7 +143,7 @@ , ↩️ ❎ 🌐 👈 🔠 *➡ 🛠️*, 👥 💪 🚮 ⚫️ `APIRouter`. -```Python hl_lines="5-10 16 21" +```Python hl_lines="5-10 16 21" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -195,7 +195,7 @@ async def read_item(item_id: str): 👥 ⚙️ ⚖ 🗄 ⏮️ `..` 🔗: -```Python hl_lines="3" +```Python hl_lines="3" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -265,7 +265,7 @@ that 🔜 ⛓: ✋️ 👥 💪 🚮 _🌅_ `tags` 👈 🔜 ✔ 🎯 *➡ 🛠️*, & ➕ `responses` 🎯 👈 *➡ 🛠️*: -```Python hl_lines="30-31" +```Python hl_lines="30-31" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -290,7 +290,7 @@ that 🔜 ⛓: & 👥 💪 📣 [🌐 🔗](dependencies/global-dependencies.md){.internal-link target=_blank} 👈 🔜 🌀 ⏮️ 🔗 🔠 `APIRouter`: -```Python hl_lines="1 3 7" +```Python hl_lines="1 3 7" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -298,7 +298,7 @@ that 🔜 ⛓: 🔜 👥 🗄 🎏 🔁 👈 ✔️ `APIRouter`Ⓜ: -```Python hl_lines="5" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -360,7 +360,7 @@ from .routers.users import router , 💪 ⚙️ 👯‍♂️ 👫 🎏 📁, 👥 🗄 🔁 🔗: -```Python hl_lines="4" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -368,7 +368,7 @@ from .routers.users import router 🔜, ➡️ 🔌 `router`Ⓜ ⚪️➡️ 🔁 `users` & `items`: -```Python hl_lines="10-11" +```Python hl_lines="10-11" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -401,7 +401,7 @@ from .routers.users import router 👉 🖼 ⚫️ 🔜 💎 🙅. ✋️ ➡️ 💬 👈 ↩️ ⚫️ 💰 ⏮️ 🎏 🏗 🏢, 👥 🚫🔜 🔀 ⚫️ & 🚮 `prefix`, `dependencies`, `tags`, ♒️. 🔗 `APIRouter`: -```Python hl_lines="3" +```Python hl_lines="3" title="app/internal/admin.py" {!../../../docs_src/bigger_applications/app/internal/admin.py!} ``` @@ -409,7 +409,7 @@ from .routers.users import router 👥 💪 📣 🌐 👈 🍵 ✔️ 🔀 ⏮️ `APIRouter` 🚶‍♀️ 👈 🔢 `app.include_router()`: -```Python hl_lines="14-17" +```Python hl_lines="14-17" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -432,7 +432,7 @@ from .routers.users import router 📥 👥 ⚫️... 🎦 👈 👥 💪 🤷: -```Python hl_lines="21-23" +```Python hl_lines="21-23" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 1cf7e50e0..9ec3720c8 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -79,7 +79,7 @@ You can create the *path operations* for that module using `APIRouter`. You import it and create an "instance" the same way you would with the class `FastAPI`: -```Python hl_lines="1 3" +```Python hl_lines="1 3" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -89,7 +89,7 @@ And then you use it to declare your *path operations*. Use it the same way you would use the `FastAPI` class: -```Python hl_lines="6 11 16" +```Python hl_lines="6 11 16" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -114,13 +114,13 @@ We will now use a simple dependency to read a custom `X-Token` header: === "Python 3.9+" - ```Python hl_lines="3 6-8" + ```Python hl_lines="3 6-8" title="app/dependencies.py" {!> ../../../docs_src/bigger_applications/app_an_py39/dependencies.py!} ``` === "Python 3.8+" - ```Python hl_lines="1 5-7" + ```Python hl_lines="1 5-7" title="app/dependencies.py" {!> ../../../docs_src/bigger_applications/app_an/dependencies.py!} ``` @@ -129,7 +129,7 @@ We will now use a simple dependency to read a custom `X-Token` header: !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="1 4-6" + ```Python hl_lines="1 4-6" title="app/dependencies.py" {!> ../../../docs_src/bigger_applications/app/dependencies.py!} ``` @@ -160,7 +160,7 @@ We know all the *path operations* in this module have the same: So, instead of adding all that to each *path operation*, we can add it to the `APIRouter`. -```Python hl_lines="5-10 16 21" +```Python hl_lines="5-10 16 21" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -212,7 +212,7 @@ And we need to get the dependency function from the module `app.dependencies`, t So we use a relative import with `..` for the dependencies: -```Python hl_lines="3" +```Python hl_lines="3" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -282,7 +282,7 @@ We are not adding the prefix `/items` nor the `tags=["items"]` to each *path ope But we can still add _more_ `tags` that will be applied to a specific *path operation*, and also some extra `responses` specific to that *path operation*: -```Python hl_lines="30-31" +```Python hl_lines="30-31" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -307,7 +307,7 @@ You import and create a `FastAPI` class as normally. And we can even declare [global dependencies](dependencies/global-dependencies.md){.internal-link target=_blank} that will be combined with the dependencies for each `APIRouter`: -```Python hl_lines="1 3 7" +```Python hl_lines="1 3 7" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -315,7 +315,7 @@ And we can even declare [global dependencies](dependencies/global-dependencies.m Now we import the other submodules that have `APIRouter`s: -```Python hl_lines="5" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -377,7 +377,7 @@ The `router` from `users` would overwrite the one from `items` and we wouldn't b So, to be able to use both of them in the same file, we import the submodules directly: -```Python hl_lines="5" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -385,7 +385,7 @@ So, to be able to use both of them in the same file, we import the submodules di Now, let's include the `router`s from the submodules `users` and `items`: -```Python hl_lines="10-11" +```Python hl_lines="10-11" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -418,7 +418,7 @@ It contains an `APIRouter` with some admin *path operations* that your organizat For this example it will be super simple. But let's say that because it is shared with other projects in the organization, we cannot modify it and add a `prefix`, `dependencies`, `tags`, etc. directly to the `APIRouter`: -```Python hl_lines="3" +```Python hl_lines="3" title="app/internal/admin.py" {!../../../docs_src/bigger_applications/app/internal/admin.py!} ``` @@ -426,7 +426,7 @@ But we still want to set a custom `prefix` when including the `APIRouter` so tha We can declare all that without having to modify the original `APIRouter` by passing those parameters to `app.include_router()`: -```Python hl_lines="14-17" +```Python hl_lines="14-17" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -449,7 +449,7 @@ We can also add *path operations* directly to the `FastAPI` app. Here we do it... just to show that we can 🤷: -```Python hl_lines="21-23" +```Python hl_lines="21-23" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` diff --git a/docs/zh/docs/tutorial/bigger-applications.md b/docs/zh/docs/tutorial/bigger-applications.md index 9f0134f68..138959566 100644 --- a/docs/zh/docs/tutorial/bigger-applications.md +++ b/docs/zh/docs/tutorial/bigger-applications.md @@ -79,7 +79,7 @@ 你可以导入它并通过与 `FastAPI` 类相同的方式创建一个「实例」: -```Python hl_lines="1 3" +```Python hl_lines="1 3" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -89,7 +89,7 @@ 使用方式与 `FastAPI` 类相同: -```Python hl_lines="6 11 16" +```Python hl_lines="6 11 16" title="app/routers/users.py" {!../../../docs_src/bigger_applications/app/routers/users.py!} ``` @@ -112,7 +112,7 @@ 现在我们将使用一个简单的依赖项来读取一个自定义的 `X-Token` 请求首部: -```Python hl_lines="1 4-6" +```Python hl_lines="1 4-6" title="app/dependencies.py" {!../../../docs_src/bigger_applications/app/dependencies.py!} ``` @@ -143,7 +143,7 @@ 因此,我们可以将其添加到 `APIRouter` 中,而不是将其添加到每个路径操作中。 -```Python hl_lines="5-10 16 21" +```Python hl_lines="5-10 16 21" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -195,7 +195,7 @@ async def read_item(item_id: str): 因此,我们通过 `..` 对依赖项使用了相对导入: -```Python hl_lines="3" +```Python hl_lines="3" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -265,7 +265,7 @@ from ...dependencies import get_token_header 但是我们仍然可以添加*更多*将会应用于特定的*路径操作*的 `tags`,以及一些特定于该*路径操作*的额外 `responses`: -```Python hl_lines="30-31" +```Python hl_lines="30-31" title="app/routers/items.py" {!../../../docs_src/bigger_applications/app/routers/items.py!} ``` @@ -290,7 +290,7 @@ from ...dependencies import get_token_header 我们甚至可以声明[全局依赖项](dependencies/global-dependencies.md){.internal-link target=_blank},它会和每个 `APIRouter` 的依赖项组合在一起: -```Python hl_lines="1 3 7" +```Python hl_lines="1 3 7" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -298,7 +298,7 @@ from ...dependencies import get_token_header 现在,我们导入具有 `APIRouter` 的其他子模块: -```Python hl_lines="5" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -360,7 +360,7 @@ from .routers.users import router 因此,为了能够在同一个文件中使用它们,我们直接导入子模块: -```Python hl_lines="4" +```Python hl_lines="5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -368,7 +368,7 @@ from .routers.users import router 现在,让我们来包含来自 `users` 和 `items` 子模块的 `router`。 -```Python hl_lines="10-11" +```Python hl_lines="10-11" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -401,7 +401,7 @@ from .routers.users import router 对于此示例,它将非常简单。但是假设由于它是与组织中的其他项目所共享的,因此我们无法对其进行修改,以及直接在 `APIRouter` 中添加 `prefix`、`dependencies`、`tags` 等: -```Python hl_lines="3" +```Python hl_lines="3" title="app/internal/admin.py" {!../../../docs_src/bigger_applications/app/internal/admin.py!} ``` @@ -409,7 +409,7 @@ from .routers.users import router 我们可以通过将这些参数传递给 `app.include_router()` 来完成所有的声明,而不必修改原始的 `APIRouter`: -```Python hl_lines="14-17" +```Python hl_lines="14-17" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` @@ -432,7 +432,7 @@ from .routers.users import router 这里我们这样做了...只是为了表明我们可以做到🤷: -```Python hl_lines="21-23" +```Python hl_lines="21-23" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` From fedee4d028e8c5ff634f91ca742fb404641adb40 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 15:59:47 +0000 Subject: [PATCH 1585/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index d75d9e3bd..8d789b137 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). + ## 0.109.0 ### Features From 1369c45c2e047fe349c18fd0d4a29451de02106f Mon Sep 17 00:00:00 2001 From: Jeny Sadadia <jeny.sadadia@gmail.com> Date: Thu, 11 Jan 2024 21:37:05 +0530 Subject: [PATCH 1586/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Talk=20by=20Jeny=20Sadadia=20(#10265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jeny Sadadia <jeny.sadadia@gmail.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index f15560d1b..ed512b733 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -349,6 +349,10 @@ Podcasts: title: FastAPI on PythonBytes Talks: English: + - author: Jeny Sadadia + author_link: https://github.com/JenySadadia + link: https://www.youtube.com/watch?v=uZdTe8_Z6BQ + title: 'PyCon AU 2023: Testing asynchronous applications with FastAPI and pytest' - author: Sebastián Ramírez (tiangolo) author_link: https://twitter.com/tiangolo link: https://www.youtube.com/watch?v=PnpTY1f4k2U From facdc9162933acec437eecb0318d8d2bfdec2d6b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 16:07:29 +0000 Subject: [PATCH 1587/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 8d789b137..692890d73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). * 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). ## 0.109.0 From 838e9c964eaef85f47ec8b22b4d1feb124a3a039 Mon Sep 17 00:00:00 2001 From: malicious <38064672+malicious@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:31:18 +0000 Subject: [PATCH 1588/1881] =?UTF-8?q?=F0=9F=93=9D=20Reword=20in=20docs,=20?= =?UTF-8?q?from=20"have=20in=20mind"=20to=20"keep=20in=20mind"=20(#10376)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/docs/advanced/additional-responses.md | 2 +- docs/en/docs/advanced/behind-a-proxy.md | 4 ++-- docs/en/docs/advanced/custom-response.md | 2 +- docs/en/docs/advanced/dataclasses.md | 2 +- docs/en/docs/advanced/events.md | 2 +- docs/en/docs/advanced/response-change-status-code.md | 2 +- docs/en/docs/advanced/response-cookies.md | 2 +- docs/en/docs/advanced/response-headers.md | 2 +- docs/en/docs/advanced/websockets.md | 2 +- docs/en/docs/benchmarks.md | 2 +- docs/en/docs/deployment/concepts.md | 4 ++-- docs/en/docs/deployment/https.md | 2 +- docs/en/docs/deployment/index.md | 4 ++-- docs/en/docs/deployment/manually.md | 4 ++-- docs/en/docs/help-fastapi.md | 6 +++--- docs/en/docs/how-to/sql-databases-peewee.md | 4 ++-- docs/en/docs/release-notes.md | 2 +- docs/en/docs/tutorial/body-nested-models.md | 2 +- docs/en/docs/tutorial/middleware.md | 2 +- docs/en/docs/tutorial/path-params-numeric-validations.md | 4 ++-- docs/en/docs/tutorial/query-params-str-validations.md | 8 ++++---- docs/en/docs/tutorial/request-files.md | 2 +- docs/en/docs/tutorial/security/get-current-user.md | 2 +- docs/en/docs/tutorial/security/oauth2-jwt.md | 2 +- docs/en/docs/tutorial/sql-databases.md | 2 +- 25 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/en/docs/advanced/additional-responses.md b/docs/en/docs/advanced/additional-responses.md index 624036ce9..41b39c18e 100644 --- a/docs/en/docs/advanced/additional-responses.md +++ b/docs/en/docs/advanced/additional-responses.md @@ -28,7 +28,7 @@ For example, to declare another response with a status code `404` and a Pydantic ``` !!! note - Have in mind that you have to return the `JSONResponse` directly. + Keep in mind that you have to return the `JSONResponse` directly. !!! info The `model` key is not part of OpenAPI. diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index e7af77f3d..01998cc91 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -125,7 +125,7 @@ Passing the `root_path` to `FastAPI` would be the equivalent of passing the `--r ### About `root_path` -Have in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app. +Keep in mind that the server (Uvicorn) won't use that `root_path` for anything else than passing it to the app. But if you go with your browser to <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app</a> you will see the normal response: @@ -142,7 +142,7 @@ Uvicorn will expect the proxy to access Uvicorn at `http://127.0.0.1:8000/app`, ## About proxies with a stripped path prefix -Have in mind that a proxy with stripped path prefix is only one of the ways to configure it. +Keep in mind that a proxy with stripped path prefix is only one of the ways to configure it. Probably in many cases the default will be that the proxy doesn't have a stripped path prefix. diff --git a/docs/en/docs/advanced/custom-response.md b/docs/en/docs/advanced/custom-response.md index ce2619e8d..827776f5e 100644 --- a/docs/en/docs/advanced/custom-response.md +++ b/docs/en/docs/advanced/custom-response.md @@ -101,7 +101,7 @@ But as you passed the `HTMLResponse` in the `response_class` too, **FastAPI** wi Here are some of the available responses. -Have in mind that you can use `Response` to return anything else, or even create a custom sub-class. +Keep in mind that you can use `Response` to return anything else, or even create a custom sub-class. !!! note "Technical Details" You could also use `from starlette.responses import HTMLResponse`. diff --git a/docs/en/docs/advanced/dataclasses.md b/docs/en/docs/advanced/dataclasses.md index 72daca06a..ed1d5610f 100644 --- a/docs/en/docs/advanced/dataclasses.md +++ b/docs/en/docs/advanced/dataclasses.md @@ -21,7 +21,7 @@ And of course, it supports the same: This works the same way as with Pydantic models. And it is actually achieved in the same way underneath, using Pydantic. !!! info - Have in mind that dataclasses can't do everything Pydantic models can do. + Keep in mind that dataclasses can't do everything Pydantic models can do. So, you might still need to use Pydantic models. diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index 6b7de4130..6df1411d1 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -159,4 +159,4 @@ Underneath, in the ASGI technical specification, this is part of the <a href="ht ## Sub Applications -🚨 Have in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. +🚨 Keep in mind that these lifespan events (startup and shutdown) will only be executed for the main application, not for [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}. diff --git a/docs/en/docs/advanced/response-change-status-code.md b/docs/en/docs/advanced/response-change-status-code.md index 979cef3f0..b88d74a8a 100644 --- a/docs/en/docs/advanced/response-change-status-code.md +++ b/docs/en/docs/advanced/response-change-status-code.md @@ -30,4 +30,4 @@ And if you declared a `response_model`, it will still be used to filter and conv **FastAPI** will use that *temporal* response to extract the status code (also cookies and headers), and will put them in the final response that contains the value you returned, filtered by any `response_model`. -You can also declare the `Response` parameter in dependencies, and set the status code in them. But have in mind that the last one to be set will win. +You can also declare the `Response` parameter in dependencies, and set the status code in them. But keep in mind that the last one to be set will win. diff --git a/docs/en/docs/advanced/response-cookies.md b/docs/en/docs/advanced/response-cookies.md index 9178ef816..d53985dbb 100644 --- a/docs/en/docs/advanced/response-cookies.md +++ b/docs/en/docs/advanced/response-cookies.md @@ -31,7 +31,7 @@ Then set Cookies in it, and then return it: ``` !!! tip - Have in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly. + Keep in mind that if you return a response directly instead of using the `Response` parameter, FastAPI will return it directly. So, you will have to make sure your data is of the correct type. E.g. it is compatible with JSON, if you are returning a `JSONResponse`. diff --git a/docs/en/docs/advanced/response-headers.md b/docs/en/docs/advanced/response-headers.md index 758bd6455..49b5fe476 100644 --- a/docs/en/docs/advanced/response-headers.md +++ b/docs/en/docs/advanced/response-headers.md @@ -37,6 +37,6 @@ Create a response as described in [Return a Response Directly](response-directly ## Custom Headers -Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">using the 'X-' prefix</a>. +Keep in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">using the 'X-' prefix</a>. But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations (read more in [CORS (Cross-Origin Resource Sharing)](../tutorial/cors.md){.internal-link target=_blank}), using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette's CORS docs</a>. diff --git a/docs/en/docs/advanced/websockets.md b/docs/en/docs/advanced/websockets.md index 49b8fba89..b8dfab1d1 100644 --- a/docs/en/docs/advanced/websockets.md +++ b/docs/en/docs/advanced/websockets.md @@ -212,7 +212,7 @@ Client #1596980209979 left the chat !!! tip The app above is a minimal and simple example to demonstrate how to handle and broadcast messages to several WebSocket connections. - But have in mind that, as everything is handled in memory, in a single list, it will only work while the process is running, and will only work with a single process. + But keep in mind that, as everything is handled in memory, in a single list, it will only work while the process is running, and will only work with a single process. If you need something easy to integrate with FastAPI but that is more robust, supported by Redis, PostgreSQL or others, check <a href="https://github.com/encode/broadcaster" class="external-link" target="_blank">encode/broadcaster</a>. diff --git a/docs/en/docs/benchmarks.md b/docs/en/docs/benchmarks.md index e05fec840..d746b6d7c 100644 --- a/docs/en/docs/benchmarks.md +++ b/docs/en/docs/benchmarks.md @@ -2,7 +2,7 @@ Independent TechEmpower benchmarks show **FastAPI** applications running under Uvicorn as <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">one of the fastest Python frameworks available</a>, only below Starlette and Uvicorn themselves (used internally by FastAPI). (*) -But when checking benchmarks and comparisons you should have the following in mind. +But when checking benchmarks and comparisons you should keep the following in mind. ## Benchmarks and speed diff --git a/docs/en/docs/deployment/concepts.md b/docs/en/docs/deployment/concepts.md index 77419f8b0..cc01fb24e 100644 --- a/docs/en/docs/deployment/concepts.md +++ b/docs/en/docs/deployment/concepts.md @@ -258,7 +258,7 @@ And you will have to make sure that it's a single process running those previous 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. + Also, keep 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. 🤷 @@ -297,7 +297,7 @@ You can use simple tools like `htop` to see the CPU and RAM used in your server ## 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: +You have been reading here some of the main concepts that you would probably need to keep in mind when deciding how to deploy your application: * Security - HTTPS * Running on startup diff --git a/docs/en/docs/deployment/https.md b/docs/en/docs/deployment/https.md index 790976a71..5cf76c111 100644 --- a/docs/en/docs/deployment/https.md +++ b/docs/en/docs/deployment/https.md @@ -9,7 +9,7 @@ But it is way more complex than that. To **learn the basics of HTTPS**, from a consumer perspective, check <a href="https://howhttps.works/" class="external-link" target="_blank">https://howhttps.works/</a>. -Now, from a **developer's perspective**, here are several things to have in mind while thinking about HTTPS: +Now, from a **developer's perspective**, here are several things to keep 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". diff --git a/docs/en/docs/deployment/index.md b/docs/en/docs/deployment/index.md index 6c43d8abb..b43bd050a 100644 --- a/docs/en/docs/deployment/index.md +++ b/docs/en/docs/deployment/index.md @@ -16,6 +16,6 @@ There are several ways to do it depending on your specific use case and the tool 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). +I will show you some of the main concepts you should probably keep 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. ✨ +You will see more details to keep 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 d6892b2c1..b10a3686d 100644 --- a/docs/en/docs/deployment/manually.md +++ b/docs/en/docs/deployment/manually.md @@ -10,11 +10,11 @@ There are 3 main alternatives: ## Server Machine and Server Program -There's a small detail about names to have in mind. 💡 +There's a small detail about names to keep 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. +Just keep in mind that 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. diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 8199c9b9a..71c580409 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -106,7 +106,7 @@ In many cases they will only copy a fragment of the code, but that's not enough * You can ask them to provide a <a href="https://stackoverflow.com/help/minimal-reproducible-example" class="external-link" target="_blank">minimal, reproducible, example</a>, that you can **copy-paste** and run locally to see the same error or behavior they are seeing, or to understand their use case better. -* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just have in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. +* If you are feeling too generous, you can try to **create an example** like that yourself, just based on the description of the problem. Just keep in mind that this might take a lot of time and it might be better to ask them to clarify the problem first. ### Suggest solutions @@ -148,7 +148,7 @@ Again, please try your best to be kind. 🤗 --- -Here's what to have in mind and how to review a pull request: +Here's what to keep in mind and how to review a pull request: ### Understand the problem @@ -233,7 +233,7 @@ Join the 👥 <a href="https://discord.gg/VQjSZaeJmf" class="external-link" targ ### Don't use the chat for questions -Have in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. +Keep in mind that as chats allow more "free conversation", it's easy to ask questions that are too general and more difficult to answer, so, you might not receive answers. In GitHub, the template will guide you to write the right question so that you can more easily get a good answer, or even solve the problem yourself even before asking. And in GitHub I can make sure I always answer everything, even if it takes some time. I can't personally do that with the chat systems. 😅 diff --git a/docs/en/docs/how-to/sql-databases-peewee.md b/docs/en/docs/how-to/sql-databases-peewee.md index b0ab7c633..74a28b170 100644 --- a/docs/en/docs/how-to/sql-databases-peewee.md +++ b/docs/en/docs/how-to/sql-databases-peewee.md @@ -75,7 +75,7 @@ Let's first check all the normal Peewee code, create a Peewee database: ``` !!! tip - Have in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class. + Keep in mind that if you wanted to use a different database, like PostgreSQL, you couldn't just change the string. You would need to use a different Peewee database class. #### Note @@ -493,7 +493,7 @@ This means that, with Peewee's current implementation, multiple tasks could be u Python 3.7 has <a href="https://docs.python.org/3/library/contextvars.html" class="external-link" target="_blank">`contextvars`</a> that can create a local variable very similar to `threading.local`, but also supporting these async features. -There are several things to have in mind. +There are several things to keep in mind. The `ContextVar` has to be created at the top of the module, like: diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 692890d73..6431ed2c3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -3379,7 +3379,7 @@ Note: all the previous parameters are still there, so it's still possible to dec * Add OAuth2 redirect page for Swagger UI. This allows having delegated authentication in the Swagger UI docs. For this to work, you need to add `{your_origin}/docs/oauth2-redirect` to the allowed callbacks in your OAuth2 provider (in Auth0, Facebook, Google, etc). * For example, during development, it could be `http://localhost:8000/docs/oauth2-redirect`. - * Have in mind that this callback URL is independent of whichever one is used by your frontend. You might also have another callback at `https://yourdomain.com/login/callback`. + * Keep in mind that this callback URL is independent of whichever one is used by your frontend. You might also have another callback at `https://yourdomain.com/login/callback`. * This is only to allow delegated authentication in the API docs with Swagger UI. * PR [#198](https://github.com/tiangolo/fastapi/pull/198) by [@steinitzu](https://github.com/steinitzu). diff --git a/docs/en/docs/tutorial/body-nested-models.md b/docs/en/docs/tutorial/body-nested-models.md index 387f0de9a..7058d4ad0 100644 --- a/docs/en/docs/tutorial/body-nested-models.md +++ b/docs/en/docs/tutorial/body-nested-models.md @@ -361,7 +361,7 @@ In this case, you would accept any `dict` as long as it has `int` keys with `flo ``` !!! tip - Have in mind that JSON only supports `str` as keys. + Keep in mind that JSON only supports `str` as keys. But Pydantic has automatic data conversion. diff --git a/docs/en/docs/tutorial/middleware.md b/docs/en/docs/tutorial/middleware.md index 3c6868fe4..492a1b065 100644 --- a/docs/en/docs/tutorial/middleware.md +++ b/docs/en/docs/tutorial/middleware.md @@ -33,7 +33,7 @@ The middleware function receives: ``` !!! tip - Have in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">using the 'X-' prefix</a>. + Keep in mind that custom proprietary headers can be added <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">using the 'X-' prefix</a>. But if you have custom headers that you want a client in a browser to be able to see, you need to add them to your CORS configurations ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) using the parameter `expose_headers` documented in <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette's CORS docs</a>. diff --git a/docs/en/docs/tutorial/path-params-numeric-validations.md b/docs/en/docs/tutorial/path-params-numeric-validations.md index 57ad20b13..b5b13cfbe 100644 --- a/docs/en/docs/tutorial/path-params-numeric-validations.md +++ b/docs/en/docs/tutorial/path-params-numeric-validations.md @@ -126,7 +126,7 @@ So, you can declare your function as: {!> ../../../docs_src/path_params_numeric_validations/tutorial002.py!} ``` -But have in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. +But keep in mind that if you use `Annotated`, you won't have this problem, it won't matter as you're not using the function parameter default values for `Query()` or `Path()`. === "Python 3.9+" @@ -166,7 +166,7 @@ Python won't do anything with that `*`, but it will know that all the following ### Better with `Annotated` -Have in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and you probably won't need to use `*`. +Keep in mind that if you use `Annotated`, as you are not using function parameter default values, you won't have this problem, and you probably won't need to use `*`. === "Python 3.9+" diff --git a/docs/en/docs/tutorial/query-params-str-validations.md b/docs/en/docs/tutorial/query-params-str-validations.md index 91ae615ff..7a9bc4875 100644 --- a/docs/en/docs/tutorial/query-params-str-validations.md +++ b/docs/en/docs/tutorial/query-params-str-validations.md @@ -173,7 +173,7 @@ q: str | None = None But it declares it explicitly as being a query parameter. !!! info - Have in mind that the most important part to make a parameter optional is the part: + Keep in mind that the most important part to make a parameter optional is the part: ```Python = None @@ -199,7 +199,7 @@ This will validate the data, show a clear error when the data is not valid, and ### `Query` as the default value or in `Annotated` -Have in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. +Keep in mind that when using `Query` inside of `Annotated` you cannot use the `default` parameter for `Query`. Instead use the actual default value of the function parameter. Otherwise, it would be inconsistent. @@ -659,7 +659,7 @@ You can also use `list` directly instead of `List[str]` (or `list[str]` in Pytho ``` !!! note - Have in mind that in this case, FastAPI won't check the contents of the list. + Keep in mind that in this case, FastAPI won't check the contents of the list. For example, `List[int]` would check (and document) that the contents of the list are integers. But `list` alone wouldn't. @@ -670,7 +670,7 @@ You can add more information about the parameter. That information will be included in the generated OpenAPI and used by the documentation user interfaces and external tools. !!! note - Have in mind that different tools might have different levels of OpenAPI support. + Keep in mind that different tools might have different levels of OpenAPI support. Some of them might not show all the extra information declared yet, although in most of the cases, the missing feature is already planned for development. diff --git a/docs/en/docs/tutorial/request-files.md b/docs/en/docs/tutorial/request-files.md index c85a68ed6..8eb8ace64 100644 --- a/docs/en/docs/tutorial/request-files.md +++ b/docs/en/docs/tutorial/request-files.md @@ -71,7 +71,7 @@ The files will be uploaded as "form data". If you declare the type of your *path operation function* parameter as `bytes`, **FastAPI** will read the file for you and you will receive the contents as `bytes`. -Have in mind that this means that the whole contents will be stored in memory. This will work well for small files. +Keep in mind that this means that the whole contents will be stored in memory. This will work well for small files. But there are several cases in which you might benefit from using `UploadFile`. diff --git a/docs/en/docs/tutorial/security/get-current-user.md b/docs/en/docs/tutorial/security/get-current-user.md index e99a800c6..dc6d87c9c 100644 --- a/docs/en/docs/tutorial/security/get-current-user.md +++ b/docs/en/docs/tutorial/security/get-current-user.md @@ -227,7 +227,7 @@ Just use any kind of model, any kind of class, any kind of database that you nee ## Code size -This example might seem verbose. Have in mind that we are mixing security, data models, utility functions and *path operations* in the same file. +This example might seem verbose. Keep in mind that we are mixing security, data models, utility functions and *path operations* in the same file. But here's the key point. diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 0a347fed3..4159b3659 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -318,7 +318,7 @@ In those cases, several of those entities could have the same ID, let's say `foo So, to avoid ID collisions, when creating the JWT token for the user, you could prefix the value of the `sub` key, e.g. with `username:`. So, in this example, the value of `sub` could have been: `username:johndoe`. -The important thing to have in mind is that the `sub` key should have a unique identifier across the entire application, and it should be a string. +The important thing to keep in mind is that the `sub` key should have a unique identifier across the entire application, and it should be a string. ## Check it diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 1bc87a702..161d5491d 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -301,7 +301,7 @@ while Pydantic *models* declare the types using `:`, the new type annotation syn name: str ``` -Have it in mind, so you don't get confused when using `=` and `:` with them. +Keep these in mind, so you don't get confused when using `=` and `:` with them. ### Create Pydantic *models* / schemas for reading / returning From 6761fc1fa4ffce2cc0c73117aeadce81fa3a659c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 16:31:38 +0000 Subject: [PATCH 1589/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6431ed2c3..9cdb82e19 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). * 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). * 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). From abe7db6b2489052e73b2386d6c5372922f9a7cee Mon Sep 17 00:00:00 2001 From: Mikhail Rozhkov <mnrozhkov@users.noreply.github.com> Date: Thu, 11 Jan 2024 18:29:24 +0100 Subject: [PATCH 1590/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20ML=20serving=20and=20monitoring=20with=20FastAPI=20and=20Evi?= =?UTF-8?q?dently=20(#9701)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index ed512b733..aea400dfc 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Mikhail Rozhkov, Elena Samuylova + author_link: https://www.linkedin.com/in/mnrozhkov/ + link: https://www.evidentlyai.com/blog/fastapi-tutorial + title: ML serving and monitoring with FastAPI and Evidently - author: Visual Studio Code Team author_link: https://code.visualstudio.com/ link: https://code.visualstudio.com/docs/python/tutorial-fastapi From 3325635eed6ab45e81de31766863e63ab3a7662a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 17:29:48 +0000 Subject: [PATCH 1591/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9cdb82e19..ccb2d1593 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add External Link: ML serving and monitoring with FastAPI and Evidently. PR [#9701](https://github.com/tiangolo/fastapi/pull/9701) by [@mnrozhkov](https://github.com/mnrozhkov). * 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). * 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). * 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). From 0380ca3e69efe642149bda481d05906f99f4da69 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Thu, 11 Jan 2024 18:42:43 +0100 Subject: [PATCH 1592/1881] =?UTF-8?q?=F0=9F=93=9D=20Review=20and=20rewordi?= =?UTF-8?q?ng=20of=20`en/docs/contributing.md`=20(#10480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/contributing.md | 71 ++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/docs/en/docs/contributing.md b/docs/en/docs/contributing.md index 35bc1c501..2d308a9db 100644 --- a/docs/en/docs/contributing.md +++ b/docs/en/docs/contributing.md @@ -4,11 +4,11 @@ First, you might want to see the basic ways to [help FastAPI and get help](help- ## Developing -If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. +If you already cloned the <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">fastapi repository</a> and you want to deep dive in the code, here are some guidelines to set up your environment. ### Virtual environment with `venv` -You can create a virtual environment in a directory using Python's `venv` module: +You can create an isolated virtual local environment in a directory using Python's `venv` module. Let's do this in the cloned repository (where the `requirements.txt` is): <div class="termy"> @@ -18,7 +18,7 @@ $ python -m venv env </div> -That will create a directory `./env/` with the Python binaries and then you will be able to install packages for that isolated environment. +That will create a directory `./env/` with the Python binaries, and then you will be able to install packages for that local environment. ### Activate the environment @@ -84,7 +84,7 @@ To check it worked, use: If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 -Make sure you have the latest pip version on your virtual environment to avoid errors on the next steps: +Make sure you have the latest pip version on your local environment to avoid errors on the next steps: <div class="termy"> @@ -101,7 +101,7 @@ $ python -m pip install --upgrade pip This makes sure that if you use a terminal program installed by that package, you use the one from your local environment and not any other that could be installed globally. -### pip +### Install requirements using pip After activating the environment as described above: @@ -117,20 +117,20 @@ $ pip install -r requirements.txt It will install all the dependencies and your local FastAPI in your local environment. -#### Using your local FastAPI +### Using your local FastAPI -If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your local FastAPI source code. +If you create a Python file that imports and uses FastAPI, and run it with the Python from your local environment, it will use your cloned local FastAPI source code. And if you update that local FastAPI source code when you run that Python file again, it will use the fresh version of FastAPI you just edited. That way, you don't have to "install" your local version to be able to test every change. !!! note "Technical Details" - This only happens when you install using this included `requirements.txt` instead of installing `pip install fastapi` directly. + This only happens when you install using this included `requirements.txt` instead of running `pip install fastapi` directly. - That is because inside of the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. + That is because inside the `requirements.txt` file, the local version of FastAPI is marked to be installed in "editable" mode, with the `-e` option. -### Format +### Format the code There is a script that you can run that will format and clean all your code: @@ -227,15 +227,13 @@ And those Python files are included/injected in the documentation when generatin Most of the tests actually run against the example source files in the documentation. -This helps making sure that: +This helps to make sure that: -* The documentation is up to date. +* The documentation is up-to-date. * The documentation examples can be run as is. * Most of the features are covered by the documentation, ensured by test coverage. - - -### Apps and docs at the same time +#### Apps and docs at the same time If you run the examples with, e.g.: @@ -259,7 +257,9 @@ Here are the steps to help with translations. #### Tips and guidelines -* Check the currently <a href="https://github.com/tiangolo/fastapi/pulls" class="external-link" target="_blank">existing pull requests</a> for your language and add reviews requesting changes or approving them. +* Check the currently <a href="https://github.com/tiangolo/fastapi/pulls" class="external-link" target="_blank">existing pull requests</a> for your language. You can filter the pull requests by the ones with the label for your language. For example, for Spanish, the label is <a href="https://github.com/tiangolo/fastapi/pulls?q=is%3Aopen+sort%3Aupdated-desc+label%3Alang-es+label%3Aawaiting-review" class="external-link" target="_blank">`lang-es`</a>. + +* Review those pull requests, requesting changes or approving them. For the languages I don't speak, I'll wait for several others to review the translation before merging. !!! tip You can <a href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request" class="external-link" target="_blank">add comments with change suggestions</a> to existing pull requests. @@ -268,19 +268,9 @@ Here are the steps to help with translations. * Check if there's a <a href="https://github.com/tiangolo/fastapi/discussions/categories/translations" class="external-link" target="_blank">GitHub Discussion</a> to coordinate translations for your language. You can subscribe to it, and when there's a new pull request to review, an automatic comment will be added to the discussion. -* Add a single pull request per page translated. That will make it much easier for others to review it. +* If you translate pages, add a single pull request per page translated. That will make it much easier for others to review it. -For the languages I don't speak, I'll wait for several others to review the translation before merging. - -* You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it. - * You could check in the <a href="https://github.com/tiangolo/fastapi/discussions/categories/translations" class="external-link" target="_blank">GitHub Discussions</a> for your language. - * Or you can filter the existing PRs by the ones with the label for your language, for example, for Spanish, the label is <a href="https://github.com/tiangolo/fastapi/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3Alang-es+label%3A%22awaiting+review%22" class="external-link" target="_blank">`lang-es`</a>. - -* Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work. - -* Use the same images, file names, and links. You don't have to change anything for it to work. - -* To check the 2-letter code for the language you want to translate you can use the table <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" class="external-link" target="_blank">List of ISO 639-1 codes</a>. +* To check the 2-letter code for the language you want to translate, you can use the table <a href="https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes" class="external-link" target="_blank">List of ISO 639-1 codes</a>. #### Existing language @@ -323,7 +313,7 @@ $ python ./scripts/docs.py live es Now you can go to <a href="http://127.0.0.1:8008" class="external-link" target="_blank">http://127.0.0.1:8008</a> and see your changes live. -You will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. +You will see that every language has all the pages. But some pages are not translated and have an info box at the top, about the missing translation. Now let's say that you want to add a translation for the section [Features](features.md){.internal-link target=_blank}. @@ -342,7 +332,7 @@ docs/es/docs/features.md !!! tip Notice that the only change in the path and file name is the language code, from `en` to `es`. -If you go to your browser you will see that now the docs show your new section. 🎉 +If you go to your browser you will see that now the docs show your new section (the info box at the top is gone). 🎉 Now you can translate it all and see how it looks as you save the file. @@ -386,7 +376,7 @@ You can make the first pull request with those two files, `docs/ht/mkdocs.yml` a #### Preview the result -You can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). +As already mentioned above, you can use the `./scripts/docs.py` with the `live` command to preview the results (or `mkdocs serve`). Once you are done, you can also test it all as it would look online, including all the other languages. @@ -423,6 +413,25 @@ Serving at: http://127.0.0.1:8008 </div> +#### Translation specific tips and guidelines + +* Translate only the Markdown documents (`.md`). Do not translate the code examples at `./docs_src`. + +* In code blocks within the Markdown document, translate comments (`# a comment`), but leave the rest unchanged. + +* Do not change anything enclosed in "``" (inline code). + +* In lines starting with `===` or `!!!`, translate only the ` "... Text ..."` part. Leave the rest unchanged. + +* You can translate info boxes like `!!! warning` with for example `!!! warning "Achtung"`. But do not change the word immediately after the `!!!`, it determines the color of the info box. + +* Do not change the paths in links to images, code files, Markdown documents. + +* However, when a Markdown document is translated, the `#hash-parts` in links to its headings may change. Update these links if possible. + * Search for such links in the translated document using the regex `#[^# ]`. + * Search in all documents already translated into your language for `your-translated-document.md`. For example VS Code has an option "Edit" -> "Find in Files". + * When translating a document, do not "pre-translate" `#hash-parts` that link to headings in untranslated documents. + ## Tests There is a script that you can run locally to test all the code and generate coverage reports in HTML: From 0be64abac748116f12d68ca766915ec46ff879df Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 17:43:08 +0000 Subject: [PATCH 1593/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ccb2d1593..65a81f7d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Review and rewording of `en/docs/contributing.md`. PR [#10480](https://github.com/tiangolo/fastapi/pull/10480) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add External Link: ML serving and monitoring with FastAPI and Evidently. PR [#9701](https://github.com/tiangolo/fastapi/pull/9701) by [@mnrozhkov](https://github.com/mnrozhkov). * 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). * 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). From e6759aa6044929d1aaaea8280dd2e576a2339dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B3=20Lino?= <nico.lino1991@gmail.com> Date: Thu, 11 Jan 2024 20:52:15 +0100 Subject: [PATCH 1594/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Instrument=20a=20FastAPI=20service=20adding=20tracing=20with?= =?UTF-8?q?=20OpenTelemetry=20and=20send/show=20traces=20in=20Grafana=20Te?= =?UTF-8?q?mpo=20(#9440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carol Willing <carolcode@willingconsulting.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index aea400dfc..412156442 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Nicoló Lino + author_link: https://www.nlino.com + link: https://github.com/softwarebloat/python-tracing-demo + title: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo - author: Mikhail Rozhkov, Elena Samuylova author_link: https://www.linkedin.com/in/mnrozhkov/ link: https://www.evidentlyai.com/blog/fastapi-tutorial From 4dde172a969644e4e4f88b5d6c29b1d4d2e95303 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 19:52:37 +0000 Subject: [PATCH 1595/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 65a81f7d3..23dbeb5b3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add External Link: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo. PR [#9440](https://github.com/tiangolo/fastapi/pull/9440) by [@softwarebloat](https://github.com/softwarebloat). * 📝 Review and rewording of `en/docs/contributing.md`. PR [#10480](https://github.com/tiangolo/fastapi/pull/10480) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add External Link: ML serving and monitoring with FastAPI and Evidently. PR [#9701](https://github.com/tiangolo/fastapi/pull/9701) by [@mnrozhkov](https://github.com/mnrozhkov). * 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). From f74aeb00674d35d10e4f9f0ecd34a8e36a0df131 Mon Sep 17 00:00:00 2001 From: Hungtsetse <33526088+hungtsetse@users.noreply.github.com> Date: Fri, 12 Jan 2024 03:56:09 +0800 Subject: [PATCH 1596/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20hyperlink=20to?= =?UTF-8?q?=20`docs/en/docs/tutorial/static-files.md`=20(#10243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/static-files.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/static-files.md b/docs/en/docs/tutorial/static-files.md index 7a0c36af3..311d2b1c8 100644 --- a/docs/en/docs/tutorial/static-files.md +++ b/docs/en/docs/tutorial/static-files.md @@ -22,7 +22,7 @@ You can serve static files automatically from a directory using `StaticFiles`. This is different from using an `APIRouter` as a mounted application is completely independent. The OpenAPI and docs from your main application won't include anything from the mounted application, etc. -You can read more about this in the **Advanced User Guide**. +You can read more about this in the [Advanced User Guide](../advanced/index.md){.internal-link target=_blank}. ## Details From 99769b966975b85321a8213b48a57828fac9453c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 19:57:48 +0000 Subject: [PATCH 1597/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 23dbeb5b3..66b7f260e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add hyperlink to `docs/en/docs/tutorial/static-files.md`. PR [#10243](https://github.com/tiangolo/fastapi/pull/10243) by [@hungtsetse](https://github.com/hungtsetse). * 📝 Add External Link: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo. PR [#9440](https://github.com/tiangolo/fastapi/pull/9440) by [@softwarebloat](https://github.com/softwarebloat). * 📝 Review and rewording of `en/docs/contributing.md`. PR [#10480](https://github.com/tiangolo/fastapi/pull/10480) by [@nilslindemann](https://github.com/nilslindemann). * 📝 Add External Link: ML serving and monitoring with FastAPI and Evidently. PR [#9701](https://github.com/tiangolo/fastapi/pull/9701) by [@mnrozhkov](https://github.com/mnrozhkov). From b62e379a55488d523c42718616f0ad7eea526850 Mon Sep 17 00:00:00 2001 From: Ankit Anchlia <aanchlia@gmail.com> Date: Thu, 11 Jan 2024 13:59:29 -0600 Subject: [PATCH 1598/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Explore=20How=20to=20Effectively=20Use=20JWT=20With=20FastAP?= =?UTF-8?q?I=20(#10212)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ankit <aanchlia@bluemoonforms.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 412156442..b4b8687c4 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Ankit Anchlia + author_link: https://linkedin.com/in/aanchlia21 + link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi + title: Explore How to Effectively Use JWT With FastAPI - author: Nicoló Lino author_link: https://www.nlino.com link: https://github.com/softwarebloat/python-tracing-demo From cbcd3fe863a4ee537facb65acf0e8ef9e2b6da23 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 20:01:57 +0000 Subject: [PATCH 1599/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 66b7f260e..b4b137a30 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add External Link: Explore How to Effectively Use JWT With FastAPI. PR [#10212](https://github.com/tiangolo/fastapi/pull/10212) by [@aanchlia](https://github.com/aanchlia). * 📝 Add hyperlink to `docs/en/docs/tutorial/static-files.md`. PR [#10243](https://github.com/tiangolo/fastapi/pull/10243) by [@hungtsetse](https://github.com/hungtsetse). * 📝 Add External Link: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo. PR [#9440](https://github.com/tiangolo/fastapi/pull/9440) by [@softwarebloat](https://github.com/softwarebloat). * 📝 Review and rewording of `en/docs/contributing.md`. PR [#10480](https://github.com/tiangolo/fastapi/pull/10480) by [@nilslindemann](https://github.com/nilslindemann). From d192ddacec3ffc2d0ff5ec43bc7f816358ab769c Mon Sep 17 00:00:00 2001 From: Pedro Augusto de Paula Barbosa <papb1996@gmail.com> Date: Thu, 11 Jan 2024 17:18:07 -0300 Subject: [PATCH 1600/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Update=20highlig?= =?UTF-8?q?hted=20line=20in=20`docs/en/docs/tutorial/bigger-applications.m?= =?UTF-8?q?d`=20(#5490)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/docs/tutorial/bigger-applications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/tutorial/bigger-applications.md b/docs/en/docs/tutorial/bigger-applications.md index 9ec3720c8..b2d928405 100644 --- a/docs/en/docs/tutorial/bigger-applications.md +++ b/docs/en/docs/tutorial/bigger-applications.md @@ -315,7 +315,7 @@ And we can even declare [global dependencies](dependencies/global-dependencies.m Now we import the other submodules that have `APIRouter`s: -```Python hl_lines="5" title="app/main.py" +```Python hl_lines="4-5" title="app/main.py" {!../../../docs_src/bigger_applications/app/main.py!} ``` From 53a3dd740826362dd874c92d5c08fb5f607e2bc0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 20:18:31 +0000 Subject: [PATCH 1601/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b4b137a30..80a581865 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Update highlighted line in `docs/en/docs/tutorial/bigger-applications.md`. PR [#5490](https://github.com/tiangolo/fastapi/pull/5490) by [@papb](https://github.com/papb). * 📝 Add External Link: Explore How to Effectively Use JWT With FastAPI. PR [#10212](https://github.com/tiangolo/fastapi/pull/10212) by [@aanchlia](https://github.com/aanchlia). * 📝 Add hyperlink to `docs/en/docs/tutorial/static-files.md`. PR [#10243](https://github.com/tiangolo/fastapi/pull/10243) by [@hungtsetse](https://github.com/hungtsetse). * 📝 Add External Link: Instrument a FastAPI service adding tracing with OpenTelemetry and send/show traces in Grafana Tempo. PR [#9440](https://github.com/tiangolo/fastapi/pull/9440) by [@softwarebloat](https://github.com/softwarebloat). From fd97e8efe43baced1c040cac3627904b37f2380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Szaci=C5=82owski?= <44623605+piotrszacilowski@users.noreply.github.com> Date: Thu, 11 Jan 2024 22:21:35 +0100 Subject: [PATCH 1602/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20usage=20of=20?= =?UTF-8?q?Token=20model=20in=20security=20docs=20(#9313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra Sánchez <ing.alejandrasanchezv@gmail.com> Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../em/docs/advanced/security/oauth2-scopes.md | 6 +++--- docs/em/docs/tutorial/security/oauth2-jwt.md | 4 ++-- .../en/docs/advanced/security/oauth2-scopes.md | 18 +++++++++--------- docs/en/docs/tutorial/security/oauth2-jwt.md | 4 ++-- docs/ja/docs/tutorial/security/oauth2-jwt.md | 2 +- docs/zh/docs/tutorial/security/oauth2-jwt.md | 2 +- docs_src/security/tutorial004.py | 8 +++++--- docs_src/security/tutorial004_an.py | 6 +++--- docs_src/security/tutorial004_an_py310.py | 6 +++--- docs_src/security/tutorial004_an_py39.py | 6 +++--- docs_src/security/tutorial004_py310.py | 8 +++++--- docs_src/security/tutorial005.py | 8 +++++--- docs_src/security/tutorial005_an.py | 6 +++--- docs_src/security/tutorial005_an_py310.py | 6 +++--- docs_src/security/tutorial005_an_py39.py | 6 +++--- docs_src/security/tutorial005_py310.py | 8 +++++--- docs_src/security/tutorial005_py39.py | 8 +++++--- 17 files changed, 61 insertions(+), 51 deletions(-) diff --git a/docs/em/docs/advanced/security/oauth2-scopes.md b/docs/em/docs/advanced/security/oauth2-scopes.md index a4684352c..d82fe152b 100644 --- a/docs/em/docs/advanced/security/oauth2-scopes.md +++ b/docs/em/docs/advanced/security/oauth2-scopes.md @@ -56,7 +56,7 @@ Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. 🥇, ➡️ 🔜 👀 🍕 👈 🔀 ⚪️➡️ 🖼 👑 **🔰 - 👩‍💻 🦮** [Oauth2️⃣ ⏮️ 🔐 (& 🔁), 📨 ⏮️ 🥙 🤝](../../tutorial/security/oauth2-jwt.md){.internal-link target=_blank}. 🔜 ⚙️ Oauth2️⃣ ↔: -```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" +```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" {!../../../docs_src/security/tutorial005.py!} ``` @@ -93,7 +93,7 @@ Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. ✋️ 👆 🈸, 💂‍♂, 👆 🔜 ⚒ 💭 👆 🕴 🚮 ↔ 👈 👩‍💻 🤙 💪 ✔️, ⚖️ 🕐 👆 ✔️ 🔁. -```Python hl_lines="153" +```Python hl_lines="155" {!../../../docs_src/security/tutorial005.py!} ``` @@ -118,7 +118,7 @@ Oauth2️⃣ 🔧 🔬 "↔" 📇 🎻 🎏 🚀. 👥 🔨 ⚫️ 📥 🎦 ❔ **FastAPI** 🍵 ↔ 📣 🎏 🎚. -```Python hl_lines="4 139 166" +```Python hl_lines="4 139 168" {!../../../docs_src/security/tutorial005.py!} ``` diff --git a/docs/em/docs/tutorial/security/oauth2-jwt.md b/docs/em/docs/tutorial/security/oauth2-jwt.md index bc207c566..bc3c943f8 100644 --- a/docs/em/docs/tutorial/security/oauth2-jwt.md +++ b/docs/em/docs/tutorial/security/oauth2-jwt.md @@ -192,13 +192,13 @@ $ openssl rand -hex 32 === "🐍 3️⃣.6️⃣ & 🔛" - ```Python hl_lines="115-128" + ```Python hl_lines="115-130" {!> ../../../docs_src/security/tutorial004.py!} ``` === "🐍 3️⃣.1️⃣0️⃣ & 🔛" - ```Python hl_lines="114-127" + ```Python hl_lines="114-129" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index 304a46090..b93d2991c 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -79,7 +79,7 @@ First, let's quickly see the parts that change from the examples in the main **T !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 152" + ```Python hl_lines="3 7 11 45 63 104 106-114 120-123 127-133 138 154" {!> ../../../docs_src/security/tutorial005_py310.py!} ``` @@ -88,7 +88,7 @@ First, let's quickly see the parts that change from the examples in the main **T !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" {!> ../../../docs_src/security/tutorial005_py39.py!} ``` @@ -97,7 +97,7 @@ First, let's quickly see the parts that change from the examples in the main **T !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 153" + ```Python hl_lines="2 4 8 12 46 64 105 107-115 121-124 128-134 139 155" {!> ../../../docs_src/security/tutorial005.py!} ``` @@ -199,7 +199,7 @@ And we return the scopes as part of the JWT token. !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="152" + ```Python hl_lines="154" {!> ../../../docs_src/security/tutorial005_py310.py!} ``` @@ -208,7 +208,7 @@ And we return the scopes as part of the JWT token. !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="153" + ```Python hl_lines="155" {!> ../../../docs_src/security/tutorial005_py39.py!} ``` @@ -217,7 +217,7 @@ And we return the scopes as part of the JWT token. !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="153" + ```Python hl_lines="155" {!> ../../../docs_src/security/tutorial005.py!} ``` @@ -265,7 +265,7 @@ In this case, it requires the scope `me` (it could require more than one scope). !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="3 138 165" + ```Python hl_lines="3 138 167" {!> ../../../docs_src/security/tutorial005_py310.py!} ``` @@ -274,7 +274,7 @@ In this case, it requires the scope `me` (it could require more than one scope). !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="4 139 166" + ```Python hl_lines="4 139 168" {!> ../../../docs_src/security/tutorial005_py39.py!} ``` @@ -283,7 +283,7 @@ In this case, it requires the scope `me` (it could require more than one scope). !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="4 139 166" + ```Python hl_lines="4 139 168" {!> ../../../docs_src/security/tutorial005.py!} ``` diff --git a/docs/en/docs/tutorial/security/oauth2-jwt.md b/docs/en/docs/tutorial/security/oauth2-jwt.md index 4159b3659..1c792e3d9 100644 --- a/docs/en/docs/tutorial/security/oauth2-jwt.md +++ b/docs/en/docs/tutorial/security/oauth2-jwt.md @@ -285,7 +285,7 @@ Create a real JWT access token and return it !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="114-127" + ```Python hl_lines="114-129" {!> ../../../docs_src/security/tutorial004_py310.py!} ``` @@ -294,7 +294,7 @@ Create a real JWT access token and return it !!! tip Prefer to use the `Annotated` version if possible. - ```Python hl_lines="115-128" + ```Python hl_lines="115-130" {!> ../../../docs_src/security/tutorial004.py!} ``` diff --git a/docs/ja/docs/tutorial/security/oauth2-jwt.md b/docs/ja/docs/tutorial/security/oauth2-jwt.md index 348ffda01..d5b179aa0 100644 --- a/docs/ja/docs/tutorial/security/oauth2-jwt.md +++ b/docs/ja/docs/tutorial/security/oauth2-jwt.md @@ -167,7 +167,7 @@ JWTトークンの署名に使用するアルゴリズム`"HS256"`を指定し JWTアクセストークンを作成し、それを返します。 -```Python hl_lines="115-128" +```Python hl_lines="115-130" {!../../../docs_src/security/tutorial004.py!} ``` diff --git a/docs/zh/docs/tutorial/security/oauth2-jwt.md b/docs/zh/docs/tutorial/security/oauth2-jwt.md index 054198545..33a4d7fc7 100644 --- a/docs/zh/docs/tutorial/security/oauth2-jwt.md +++ b/docs/zh/docs/tutorial/security/oauth2-jwt.md @@ -170,7 +170,7 @@ $ openssl rand -hex 32 创建并返回真正的 JWT 访问令牌。 -```Python hl_lines="115-128" +```Python hl_lines="115-130" {!../../../docs_src/security/tutorial004.py!} ``` diff --git a/docs_src/security/tutorial004.py b/docs_src/security/tutorial004.py index 134c15c5a..044eec700 100644 --- a/docs_src/security/tutorial004.py +++ b/docs_src/security/tutorial004.py @@ -112,8 +112,10 @@ async def get_current_active_user(current_user: User = Depends(get_current_user) return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -125,7 +127,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial004_an.py b/docs_src/security/tutorial004_an.py index 204151a56..c78e8496c 100644 --- a/docs_src/security/tutorial004_an.py +++ b/docs_src/security/tutorial004_an.py @@ -115,10 +115,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -130,7 +130,7 @@ async def login_for_access_token( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial004_an_py310.py b/docs_src/security/tutorial004_an_py310.py index 64dfa15c6..36dbc677e 100644 --- a/docs_src/security/tutorial004_an_py310.py +++ b/docs_src/security/tutorial004_an_py310.py @@ -114,10 +114,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -129,7 +129,7 @@ async def login_for_access_token( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial004_an_py39.py b/docs_src/security/tutorial004_an_py39.py index 631a8366e..23fc04a72 100644 --- a/docs_src/security/tutorial004_an_py39.py +++ b/docs_src/security/tutorial004_an_py39.py @@ -114,10 +114,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -129,7 +129,7 @@ async def login_for_access_token( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial004_py310.py b/docs_src/security/tutorial004_py310.py index 470f22e29..8363d45ab 100644 --- a/docs_src/security/tutorial004_py310.py +++ b/docs_src/security/tutorial004_py310.py @@ -111,8 +111,10 @@ async def get_current_active_user(current_user: User = Depends(get_current_user) return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -124,7 +126,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index ece461bc8..b16bf440a 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -143,8 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -153,7 +155,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py index c5b5609e5..95e406b32 100644 --- a/docs_src/security/tutorial005_an.py +++ b/docs_src/security/tutorial005_an.py @@ -144,10 +144,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -156,7 +156,7 @@ async def login_for_access_token( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index 5e81a50e1..c6116a5ed 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -143,10 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -155,7 +155,7 @@ async def login_for_access_token( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index ae9811c68..af51c08b5 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -143,10 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) +@app.post("/token") async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()] -): +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -155,7 +155,7 @@ async def login_for_access_token( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index 0fcdda4c0..37a22c709 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -142,8 +142,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -152,7 +154,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index d756c0b6b..c27580763 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -143,8 +143,10 @@ async def get_current_active_user( return current_user -@app.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): +@app.post("/token") +async def login_for_access_token( + form_data: OAuth2PasswordRequestForm = Depends() +) -> Token: user = authenticate_user(fake_users_db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect username or password") @@ -153,7 +155,7 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( data={"sub": user.username, "scopes": form_data.scopes}, expires_delta=access_token_expires, ) - return {"access_token": access_token, "token_type": "bearer"} + return Token(access_token=access_token, token_type="bearer") @app.get("/users/me/", response_model=User) From 22e68b151d92aff6d6e0cdf001b08a792e20020d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 21:21:57 +0000 Subject: [PATCH 1603/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 80a581865..945d342d9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update usage of Token model in security docs. PR [#9313](https://github.com/tiangolo/fastapi/pull/9313) by [@piotrszacilowski](https://github.com/piotrszacilowski). * ✏️ Update highlighted line in `docs/en/docs/tutorial/bigger-applications.md`. PR [#5490](https://github.com/tiangolo/fastapi/pull/5490) by [@papb](https://github.com/papb). * 📝 Add External Link: Explore How to Effectively Use JWT With FastAPI. PR [#10212](https://github.com/tiangolo/fastapi/pull/10212) by [@aanchlia](https://github.com/aanchlia). * 📝 Add hyperlink to `docs/en/docs/tutorial/static-files.md`. PR [#10243](https://github.com/tiangolo/fastapi/pull/10243) by [@hungtsetse](https://github.com/hungtsetse). From 0c796747a3d652f7d5d7fc59c5fbb68512b64ccf Mon Sep 17 00:00:00 2001 From: Ezzeddin Abdullah <ezzeddinabdullah@gmail.com> Date: Fri, 12 Jan 2024 00:25:37 +0200 Subject: [PATCH 1604/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20template=20do?= =?UTF-8?q?cs=20with=20more=20info=20about=20`url=5Ffor`=20(#5937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/advanced/templates.md | 48 +++++++++++++++++-- docs_src/templates/templates/item.html | 2 +- .../test_templates/test_tutorial001.py | 5 +- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/advanced/templates.md b/docs/en/docs/advanced/templates.md index 583abda7f..6055b3017 100644 --- a/docs/en/docs/advanced/templates.md +++ b/docs/en/docs/advanced/templates.md @@ -46,21 +46,61 @@ $ pip install jinja2 ## Writing templates -Then you can write a template at `templates/item.html` with: +Then you can write a template at `templates/item.html` with, for example: ```jinja hl_lines="7" {!../../../docs_src/templates/templates/item.html!} ``` -It will show the `id` taken from the "context" `dict` you passed: +### Template Context Values + +In the HTML that contains: + +{% raw %} + +```jinja +Item ID: {{ id }} +``` + +{% endraw %} + +...it will show the `id` taken from the "context" `dict` you passed: ```Python -{"request": request, "id": id} +{"id": id} +``` + +For example, with an ID of `42`, this would render: + +```html +Item ID: 42 +``` + +### Template `url_for` Arguments + +You can also use `url_for()` inside of the template, it takes as arguments the same arguments that would be used by your *path operation function*. + +So, the section with: + +{% raw %} + +```jinja +<a href="{{ url_for('read_item', id=id) }}"> +``` + +{% endraw %} + +...will generate a link to the same URL that would be handled by the *path operation function* `read_item(id=id)`. + +For example, with an ID of `42`, this would render: + +```html +<a href="/items/42"> ``` ## Templates and static files -You can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted. +You can also use `url_for()` inside of the template, and use it, for example, with the `StaticFiles` you mounted with the `name="static"`. ```jinja hl_lines="4" {!../../../docs_src/templates/templates/item.html!} diff --git a/docs_src/templates/templates/item.html b/docs_src/templates/templates/item.html index a70287e77..27994ca99 100644 --- a/docs_src/templates/templates/item.html +++ b/docs_src/templates/templates/item.html @@ -4,6 +4,6 @@ <link href="{{ url_for('static', path='/styles.css') }}" rel="stylesheet"> </head> <body> - <h1>Item ID: {{ id }}</h1> + <h1><a href="{{ url_for('read_item', id=id) }}">Item ID: {{ id }}</a></h1> </body> </html> diff --git a/tests/test_tutorial/test_templates/test_tutorial001.py b/tests/test_tutorial/test_templates/test_tutorial001.py index bfee5c090..4d4729425 100644 --- a/tests/test_tutorial/test_templates/test_tutorial001.py +++ b/tests/test_tutorial/test_templates/test_tutorial001.py @@ -16,7 +16,10 @@ def test_main(): client = TestClient(app) response = client.get("/items/foo") assert response.status_code == 200, response.text - assert b"<h1>Item ID: foo</h1>" in response.content + assert ( + b'<h1><a href="http://testserver/items/foo">Item ID: foo</a></h1>' + in response.content + ) response = client.get("/static/styles.css") assert response.status_code == 200, response.text assert b"color: green;" in response.content From 5f37d3870bd101bdda65e6dbda06ca969f6ef510 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 11 Jan 2024 22:25:58 +0000 Subject: [PATCH 1605/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 945d342d9..af3d2e2b2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Update template docs with more info about `url_for`. PR [#5937](https://github.com/tiangolo/fastapi/pull/5937) by [@EzzEddin](https://github.com/EzzEddin). * 📝 Update usage of Token model in security docs. PR [#9313](https://github.com/tiangolo/fastapi/pull/9313) by [@piotrszacilowski](https://github.com/piotrszacilowski). * ✏️ Update highlighted line in `docs/en/docs/tutorial/bigger-applications.md`. PR [#5490](https://github.com/tiangolo/fastapi/pull/5490) by [@papb](https://github.com/papb). * 📝 Add External Link: Explore How to Effectively Use JWT With FastAPI. PR [#10212](https://github.com/tiangolo/fastapi/pull/10212) by [@aanchlia](https://github.com/aanchlia). From ea84587a2f0431a7fad42395fd1dadee3dae3fed Mon Sep 17 00:00:00 2001 From: Turabek Gaybullaev <43612265+Torabek@users.noreply.github.com> Date: Fri, 12 Jan 2024 20:10:55 +0900 Subject: [PATCH 1606/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Remove=20broken?= =?UTF-8?q?=20links=20from=20`external=5Flinks.yml`=20(#10943)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index b4b8687c4..00d6f696d 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -40,10 +40,6 @@ Articles: author_link: https://dev.to/ link: https://dev.to/teresafds/authorization-on-fastapi-with-casbin-41og title: Authorization on FastAPI with Casbin - - author: WayScript - author_link: https://www.wayscript.com - link: https://blog.wayscript.com/fast-api-quickstart/ - title: Quickstart Guide to Build and Host Responsive APIs with Fast API and WayScript - author: New Relic author_link: https://newrelic.com link: https://newrelic.com/instant-observability/fastapi/e559ec64-f765-4470-a15f-1901fcebb468 @@ -96,10 +92,6 @@ Articles: author_link: https://dev.to/factorlive link: https://dev.to/factorlive/python-facebook-messenger-webhook-with-fastapi-on-glitch-4n90 title: Python Facebook messenger webhook with FastAPI on Glitch - - author: Dom Patmore - author_link: https://twitter.com/dompatmore - link: https://dompatmore.com/blog/authenticate-your-fastapi-app-with-auth0 - title: Authenticate Your FastAPI App with auth0 - author: Valon Januzaj author_link: https://www.linkedin.com/in/valon-januzaj-b02692187/ link: https://valonjanuzaj.medium.com/deploy-a-dockerized-fastapi-application-to-aws-cc757830ba1b @@ -112,10 +104,6 @@ Articles: author_link: https://twitter.com/louis_guitton link: https://guitton.co/posts/fastapi-monitoring/ title: How to monitor your FastAPI service - - author: Julien Harbulot - author_link: https://julienharbulot.com/ - link: https://julienharbulot.com/notification-server.html - title: HTTP server to display desktop notifications - author: Precious Ndubueze author_link: https://medium.com/@gabbyprecious2000 link: https://medium.com/@gabbyprecious2000/creating-a-crud-app-with-fastapi-part-one-7c049292ad37 @@ -164,18 +152,10 @@ Articles: author_link: https://wuilly.com/ link: https://wuilly.com/2019/10/real-time-notifications-with-python-and-postgres/ title: Real-time Notifications with Python and Postgres - - author: Benjamin Ramser - author_link: https://iwpnd.pw - link: https://iwpnd.pw/articles/2020-03/apache-kafka-fastapi-geostream - title: Apache Kafka producer and consumer with FastAPI and aiokafka - author: Navule Pavan Kumar Rao author_link: https://www.linkedin.com/in/navule/ link: https://www.tutlinks.com/create-and-deploy-fastapi-app-to-heroku/ title: Create and Deploy FastAPI app to Heroku without using Docker - - author: Benjamin Ramser - author_link: https://iwpnd.pw - link: https://iwpnd.pw/articles/2020-01/deploy-fastapi-to-aws-lambda - title: How to continuously deploy a FastAPI to AWS Lambda with AWS SAM - author: Arthur Henrique author_link: https://twitter.com/arthurheinrique link: https://medium.com/@arthur393/another-boilerplate-to-fastapi-azure-pipeline-ci-pytest-3c8d9a4be0bb @@ -200,10 +180,6 @@ Articles: author_link: https://dev.to/dbanty link: https://dev.to/dbanty/why-i-m-leaving-flask-3ki6 title: Why I'm Leaving Flask - - author: Rob Wagner - author_link: https://robwagner.dev/ - link: https://robwagner.dev/tortoise-fastapi-setup/ - title: Setting up Tortoise ORM with FastAPI - author: Mike Moritz author_link: https://medium.com/@mike.p.moritz link: https://medium.com/@mike.p.moritz/using-docker-compose-to-deploy-a-lightweight-python-rest-api-with-a-job-queue-37e6072a209b From e0eaaee7496e3f95cd0e1c47c5473c989f390ed6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 11:11:15 +0000 Subject: [PATCH 1607/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 af3d2e2b2..671e63216 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Remove broken links from `external_links.yml`. PR [#10943](https://github.com/tiangolo/fastapi/pull/10943) by [@Torabek](https://github.com/Torabek). * 📝 Update template docs with more info about `url_for`. PR [#5937](https://github.com/tiangolo/fastapi/pull/5937) by [@EzzEddin](https://github.com/EzzEddin). * 📝 Update usage of Token model in security docs. PR [#9313](https://github.com/tiangolo/fastapi/pull/9313) by [@piotrszacilowski](https://github.com/piotrszacilowski). * ✏️ Update highlighted line in `docs/en/docs/tutorial/bigger-applications.md`. PR [#5490](https://github.com/tiangolo/fastapi/pull/5490) by [@papb](https://github.com/papb). From 3ca38568c1a31628c3f5863fb67a99b6b44cec9d Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Fri, 12 Jan 2024 14:12:19 +0300 Subject: [PATCH 1608/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/dependencies/classes-as-de?= =?UTF-8?q?pendencies.md`=20(#10410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vladislav Kramorenko <85196001+Xewus@users.noreply.github.com> --- .../dependencies/classes-as-dependencies.md | 478 ++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md diff --git a/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..b6ad25daf --- /dev/null +++ b/docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,478 @@ +# Классы как зависимости + +Прежде чем углубиться в систему **Внедрения Зависимостей**, давайте обновим предыдущий пример. + +## `Словарь` из предыдущего примера + +В предыдущем примере мы возвращали `словарь` из нашей зависимости: + +=== "Python 3.10+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +Но затем мы получаем `словарь` в параметре `commons` *функции операции пути*. И мы знаем, что редакторы не могут обеспечить достаточную поддержку для `словаря`, поскольку они не могут знать их ключи и типы значений. + +Мы можем сделать лучше... + +## Что делает зависимость + +До сих пор вы видели зависимости, объявленные как функции. + +Но это не единственный способ объявления зависимостей (хотя, вероятно, более распространенный). + +Ключевым фактором является то, что зависимость должна быть "вызываемой". + +В Python "**вызываемый**" - это все, что Python может "вызвать", как функцию. + +Так, если у вас есть объект `something` (который может _не_ быть функцией) и вы можете "вызвать" его (выполнить) как: + +```Python +something() +``` + +или + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +в таком случае он является "вызываемым". + +## Классы как зависимости + +Вы можете заметить, что для создания экземпляра класса в Python используется тот же синтаксис. + +Например: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +В данном случае `fluffy` является экземпляром класса `Cat`. + +А чтобы создать `fluffy`, вы "вызываете" `Cat`. + +Таким образом, класс в Python также является **вызываемым**. + +Тогда в **FastAPI** в качестве зависимости можно использовать класс Python. + +На самом деле FastAPI проверяет, что переданный объект является "вызываемым" (функция, класс или что-либо еще) и указаны необходимые для его вызова параметры. + +Если вы передаёте что-то, что можно "вызывать" в качестве зависимости в **FastAPI**, то он будет анализировать параметры, необходимые для "вызова" этого объекта и обрабатывать их так же, как параметры *функции операции пути*. Включая подзависимости. + +Это относится и к вызываемым объектам без параметров. Работа с ними происходит точно так же, как и для *функций операции пути* без параметров. + +Теперь мы можем изменить зависимость `common_parameters`, указанную выше, на класс `CommonQueryParams`: + +=== "Python 3.10+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="12-16" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9-13" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="11-15" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +Обратите внимание на метод `__init__`, используемый для создания экземпляра класса: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="13" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="12" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +...имеет те же параметры, что и ранее используемая функция `common_parameters`: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="10" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="6" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="9" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +Эти параметры и будут использоваться **FastAPI** для "решения" зависимости. + +В обоих случаях она будет иметь: + +* Необязательный параметр запроса `q`, представляющий собой `str`. +* Параметр запроса `skip`, представляющий собой `int`, по умолчанию `0`. +* Параметр запроса `limit`, представляющий собой `int`, по умолчанию равный `100`. + +В обоих случаях данные будут конвертированы, валидированы, документированы по схеме OpenAPI и т.д. + +## Как это использовать + +Теперь вы можете объявить свою зависимость, используя этот класс. + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial002_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial002_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial002.py!} + ``` + +**FastAPI** вызывает класс `CommonQueryParams`. При этом создается "экземпляр" этого класса, который будет передан в качестве параметра `commons` в вашу функцию. + +## Аннотация типа или `Depends` + +Обратите внимание, что в приведенном выше коде мы два раза пишем `CommonQueryParams`: + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +Последний параметр `CommonQueryParams`, в: + +```Python +... Depends(CommonQueryParams) +``` + +...это то, что **FastAPI** будет использовать, чтобы узнать, что является зависимостью. + +Из него FastAPI извлечёт объявленные параметры и именно их будет вызывать. + +--- + +В этом случае первый `CommonQueryParams`, в: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, ... + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams ... + ``` + +...не имеет никакого специального значения для **FastAPI**. FastAPI не будет использовать его для преобразования данных, валидации и т.д. (поскольку для этого используется `Depends(CommonQueryParams)`). + +На самом деле можно написать просто: + +=== "Python 3.6+" + + ```Python + commons: Annotated[Any, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons = Depends(CommonQueryParams) + ``` + +...как тут: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial003_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial003.py!} + ``` + +Но объявление типа приветствуется, так как в этом случае ваш редактор будет знать, что будет передано в качестве параметра `commons`, и тогда он сможет помочь вам с автодополнением, проверкой типов и т.д: + +<img src="/img/tutorial/dependencies/image02.png"> + +## Сокращение + +Но вы видите, что здесь мы имеем некоторое повторение кода, дважды написав `CommonQueryParams`: + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +Для случаев, когда зависимостью является *конкретный* класс, который **FastAPI** "вызовет" для создания экземпляра этого класса, можно использовать укороченную запись. + + +Вместо того чтобы писать: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends(CommonQueryParams) + ``` + +...следует написать: + +=== "Python 3.6+" + + ```Python + commons: Annotated[CommonQueryParams, Depends()] + ``` + +=== "Python 3.6 без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python + commons: CommonQueryParams = Depends() + ``` + +Вы объявляете зависимость как тип параметра и используете `Depends()` без какого-либо параметра, вместо того чтобы *снова* писать полный класс внутри `Depends(CommonQueryParams)`. + +Аналогичный пример будет выглядеть следующим образом: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004_an_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="20" + {!> ../../../docs_src/dependencies/tutorial004_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="17" + {!> ../../../docs_src/dependencies/tutorial004_py310.py!} + ``` + +=== "Python 3.6+ без Annotated" + + !!! tip "Подсказка" + Рекомендуется использовать версию с `Annotated` если возможно. + + ```Python hl_lines="19" + {!> ../../../docs_src/dependencies/tutorial004.py!} + ``` + +...и **FastAPI** будет знать, что делать. + +!!! tip "Подсказка" + Если это покажется вам более запутанным, чем полезным, не обращайте внимания, это вам не *нужно*. + + Это просто сокращение. Потому что **FastAPI** заботится о том, чтобы помочь вам свести к минимуму повторение кода. From 4c231854dc2dc7336f8ad3ed399b806f6dc7d498 Mon Sep 17 00:00:00 2001 From: HiemalBeryl <63165207+HiemalBeryl@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:13:04 +0800 Subject: [PATCH 1609/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20in?= =?UTF-8?q?=20`docs/zh/docs/tutorial/extra-data-types.md`=20(#10727)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/tutorial/extra-data-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/zh/docs/tutorial/extra-data-types.md b/docs/zh/docs/tutorial/extra-data-types.md index a74efa61b..f4a77050c 100644 --- a/docs/zh/docs/tutorial/extra-data-types.md +++ b/docs/zh/docs/tutorial/extra-data-types.md @@ -44,11 +44,11 @@ * 产生的模式将指定那些 `set` 的值是唯一的 (使用 JSON 模式的 `uniqueItems`)。 * `bytes`: * 标准的 Python `bytes`。 - * 在请求和相应中被当作 `str` 处理。 + * 在请求和响应中被当作 `str` 处理。 * 生成的模式将指定这个 `str` 是 `binary` "格式"。 * `Decimal`: * 标准的 Python `Decimal`。 - * 在请求和相应中被当做 `float` 一样处理。 + * 在请求和响应中被当做 `float` 一样处理。 * 您可以在这里检查所有有效的pydantic数据类型: <a href="https://pydantic-docs.helpmanual.io/usage/types" class="external-link" target="_blank">Pydantic data types</a>. ## 例子 From 6a4aed45f05c7e0c9db635e74b29e76ecae6ca5c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 11:13:22 +0000 Subject: [PATCH 1610/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 671e63216..457bc5e98 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,10 @@ hide: * 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). * 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). +### Translations + +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). + ## 0.109.0 ### Features From 753c8136d8b5ecffe27f7b2ac18e02687f2c269b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 11:15:04 +0000 Subject: [PATCH 1611/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 457bc5e98..628e18b10 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -24,6 +24,7 @@ hide: ### Translations +* ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). ## 0.109.0 From f1329abf9930165f4c53cb760fd9f809b4ed6266 Mon Sep 17 00:00:00 2001 From: theoohoho <31537466+theoohoho@users.noreply.github.com> Date: Fri, 12 Jan 2024 21:39:54 +0800 Subject: [PATCH 1612/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20broken=20l?= =?UTF-8?q?ink=20in=20`docs/tutorial/sql-databases.md`=20in=20several=20la?= =?UTF-8?q?nguages=20(#10716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/em/docs/tutorial/sql-databases.md | 2 +- docs/en/docs/tutorial/sql-databases.md | 2 +- docs/zh/docs/tutorial/sql-databases.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/em/docs/tutorial/sql-databases.md b/docs/em/docs/tutorial/sql-databases.md index 9d46c2460..e3ced7ef4 100644 --- a/docs/em/docs/tutorial/sql-databases.md +++ b/docs/em/docs/tutorial/sql-databases.md @@ -501,7 +501,7 @@ current_user.items "🛠️" ⚒ 🔁 💪 🕐❔ 👆 🔀 📊 👆 🇸🇲 🏷, 🚮 🆕 🔢, ♒️. 🔁 👈 🔀 💽, 🚮 🆕 🏓, 🆕 🏓, ♒️. -👆 💪 🔎 🖼 ⚗ FastAPI 🏗 📄 ⚪️➡️ [🏗 ⚡ - 📄](../project-generation.md){.internal-link target=_blank}. 🎯 <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/alembic/" class="external-link" target="_blank"> `alembic` 📁 ℹ 📟</a>. +👆 💪 🔎 🖼 ⚗ FastAPI 🏗 📄 ⚪️➡️ [🏗 ⚡ - 📄](../project-generation.md){.internal-link target=_blank}. 🎯 <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/src/backend/app/alembic/" class="external-link" target="_blank"> `alembic` 📁 ℹ 📟</a>. ### ✍ 🔗 diff --git a/docs/en/docs/tutorial/sql-databases.md b/docs/en/docs/tutorial/sql-databases.md index 161d5491d..70d9482df 100644 --- a/docs/en/docs/tutorial/sql-databases.md +++ b/docs/en/docs/tutorial/sql-databases.md @@ -513,7 +513,7 @@ And you would also use Alembic for "migrations" (that's its main job). A "migration" is the set of steps needed whenever you change the structure of your SQLAlchemy models, add a new attribute, etc. to replicate those changes in the database, add a new column, a new table, etc. -You can find an example of Alembic in a FastAPI project in the templates from [Project Generation - Template](../project-generation.md){.internal-link target=_blank}. Specifically in <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/alembic/" class="external-link" target="_blank">the `alembic` directory in the source code</a>. +You can find an example of Alembic in a FastAPI project in the templates from [Project Generation - Template](../project-generation.md){.internal-link target=_blank}. Specifically in <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/src/backend/app/alembic" class="external-link" target="_blank">the `alembic` directory in the source code</a>. ### Create a dependency diff --git a/docs/zh/docs/tutorial/sql-databases.md b/docs/zh/docs/tutorial/sql-databases.md index a936eb27b..c49374971 100644 --- a/docs/zh/docs/tutorial/sql-databases.md +++ b/docs/zh/docs/tutorial/sql-databases.md @@ -499,7 +499,7 @@ current_user.items “迁移”是每当您更改 SQLAlchemy 模型的结构、添加新属性等以在数据库中复制这些更改、添加新列、新表等时所需的一组步骤。 -您可以在[Project Generation - Template](https://fastapi.tiangolo.com/zh/project-generation/)的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在[`alembic`代码目录中](https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/%7B%7Bcookiecutter.project_slug%7D%7D/backend/app/alembic/)。 +您可以在[Project Generation - Template](https://fastapi.tiangolo.com/zh/project-generation/)的模板中找到一个 FastAPI 项目中的 Alembic 示例。具体在[`alembic`代码目录中](https://github.com/tiangolo/full-stack-fastapi-postgresql/tree/master/src/backend/app/alembic/)。 ### 创建依赖项 From dc704036a2dd646c30fb9d58ec8a707236135f84 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 13:40:15 +0000 Subject: [PATCH 1613/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 628e18b10..55b6a6894 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* ✏️ Fix broken link in `docs/tutorial/sql-databases.md` in several languages. PR [#10716](https://github.com/tiangolo/fastapi/pull/10716) by [@theoohoho](https://github.com/theoohoho). * ✏️ Remove broken links from `external_links.yml`. PR [#10943](https://github.com/tiangolo/fastapi/pull/10943) by [@Torabek](https://github.com/Torabek). * 📝 Update template docs with more info about `url_for`. PR [#5937](https://github.com/tiangolo/fastapi/pull/5937) by [@EzzEddin](https://github.com/EzzEddin). * 📝 Update usage of Token model in security docs. PR [#9313](https://github.com/tiangolo/fastapi/pull/9313) by [@piotrszacilowski](https://github.com/piotrszacilowski). From 7e0e16fa3669d83a9992ff5aee669135ebb41fc1 Mon Sep 17 00:00:00 2001 From: Jacob McDonald <48448372+jacob-indigo@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:03:25 -0500 Subject: [PATCH 1614/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20warning=20about?= =?UTF-8?q?=20lifespan=20functions=20and=20backwards=20compatibility=20wit?= =?UTF-8?q?h=20events=20(#10734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alejandra <90076947+alejsdev@users.noreply.github.com> --- docs/en/docs/advanced/events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/events.md b/docs/en/docs/advanced/events.md index 6df1411d1..ca9d86ae4 100644 --- a/docs/en/docs/advanced/events.md +++ b/docs/en/docs/advanced/events.md @@ -92,7 +92,7 @@ The `lifespan` parameter of the `FastAPI` app takes an **async context manager** ## Alternative Events (deprecated) !!! warning - The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above. + The recommended way to handle the *startup* and *shutdown* is using the `lifespan` parameter of the `FastAPI` app as described above. If you provide a `lifespan` parameter, `startup` and `shutdown` event handlers will no longer be called. It's all `lifespan` or all events, not both. You can probably skip this part. From 38915783fc355a4eb49310182f353778982c70e1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 14:03:51 +0000 Subject: [PATCH 1615/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 55b6a6894..01eefb052 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Docs +* 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). * ✏️ Fix broken link in `docs/tutorial/sql-databases.md` in several languages. PR [#10716](https://github.com/tiangolo/fastapi/pull/10716) by [@theoohoho](https://github.com/theoohoho). * ✏️ Remove broken links from `external_links.yml`. PR [#10943](https://github.com/tiangolo/fastapi/pull/10943) by [@Torabek](https://github.com/Torabek). * 📝 Update template docs with more info about `url_for`. PR [#5937](https://github.com/tiangolo/fastapi/pull/5937) by [@EzzEddin](https://github.com/EzzEddin). From 58e2f8b1d9265a90aa0aecd7e7eb1ca8c19bf1de Mon Sep 17 00:00:00 2001 From: Delitel-WEB <57365921+Delitel-WEB@users.noreply.github.com> Date: Fri, 12 Jan 2024 17:10:31 +0300 Subject: [PATCH 1616/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`docs/ru/docs/index.md`=20(#10672)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ru/docs/index.md b/docs/ru/docs/index.md index 97a3947bd..6c99f623d 100644 --- a/docs/ru/docs/index.md +++ b/docs/ru/docs/index.md @@ -321,7 +321,7 @@ def update_item(item_id: int, item: Item): Таким образом, вы объявляете **один раз** типы параметров, тело и т. д. в качестве параметров функции. -Вы делаете это испльзуя стандартную современную типизацию Python. +Вы делаете это используя стандартную современную типизацию Python. Вам не нужно изучать новый синтаксис, методы или классы конкретной библиотеки и т. д. From ca33b6edac847e27a9543c0a8dad95c1fcfc65fc Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 14:10:54 +0000 Subject: [PATCH 1617/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 01eefb052..4e795b8cc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -26,6 +26,7 @@ hide: ### Translations +* ✏️ Fix typo in `docs/ru/docs/index.md`. PR [#10672](https://github.com/tiangolo/fastapi/pull/10672) by [@Delitel-WEB](https://github.com/Delitel-WEB). * ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). From c81ab17a594e140e66424addfe8374d72550728a Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Fri, 12 Jan 2024 15:15:29 +0100 Subject: [PATCH 1618/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/background-tasks.md`=20(#10?= =?UTF-8?q?566)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/background-tasks.md | 126 ++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/de/docs/tutorial/background-tasks.md diff --git a/docs/de/docs/tutorial/background-tasks.md b/docs/de/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..a7bfd55a7 --- /dev/null +++ b/docs/de/docs/tutorial/background-tasks.md @@ -0,0 +1,126 @@ +# Hintergrundtasks + +Sie können Hintergrundtasks (Hintergrund-Aufgaben) definieren, die *nach* der Rückgabe einer Response ausgeführt werden sollen. + +Das ist nützlich für Vorgänge, die nach einem Request ausgeführt werden müssen, bei denen der Client jedoch nicht unbedingt auf den Abschluss des Vorgangs warten muss, bevor er die Response erhält. + +Hierzu zählen beispielsweise: + +* E-Mail-Benachrichtigungen, die nach dem Ausführen einer Aktion gesendet werden: + * Da die Verbindung zu einem E-Mail-Server und das Senden einer E-Mail in der Regel „langsam“ ist (einige Sekunden), können Sie die Response sofort zurücksenden und die E-Mail-Benachrichtigung im Hintergrund senden. +* Daten verarbeiten: + * Angenommen, Sie erhalten eine Datei, die einen langsamen Prozess durchlaufen muss. Sie können als Response „Accepted“ (HTTP 202) zurückgeben und die Datei im Hintergrund verarbeiten. + +## `BackgroundTasks` verwenden + +Importieren Sie zunächst `BackgroundTasks` und definieren Sie einen Parameter in Ihrer *Pfadoperation-Funktion* mit der Typdeklaration `BackgroundTasks`: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** erstellt für Sie das Objekt vom Typ `BackgroundTasks` und übergibt es als diesen Parameter. + +## Eine Taskfunktion erstellen + +Erstellen Sie eine Funktion, die als Hintergrundtask ausgeführt werden soll. + +Es handelt sich schlicht um eine Standard-Funktion, die Parameter empfangen kann. + +Es kann sich um eine `async def`- oder normale `def`-Funktion handeln. **FastAPI** weiß, wie damit zu verfahren ist. + +In diesem Fall schreibt die Taskfunktion in eine Datei (den Versand einer E-Mail simulierend). + +Und da der Schreibvorgang nicht `async` und `await` verwendet, definieren wir die Funktion mit normalem `def`: + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## Den Hintergrundtask hinzufügen + +Übergeben Sie innerhalb Ihrer *Pfadoperation-Funktion* Ihre Taskfunktion mit der Methode `.add_task()` an das *Hintergrundtasks*-Objekt: + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` erhält als Argumente: + +* Eine Taskfunktion, die im Hintergrund ausgeführt wird (`write_notification`). +* Eine beliebige Folge von Argumenten, die der Reihe nach an die Taskfunktion übergeben werden sollen (`email`). +* Alle Schlüsselwort-Argumente, die an die Taskfunktion übergeben werden sollen (`message="some notification"`). + +## Dependency Injection + +Die Verwendung von `BackgroundTasks` funktioniert auch mit dem <abbr title="Einbringen von Abhängigkeiten">Dependency Injection</abbr> System. Sie können einen Parameter vom Typ `BackgroundTasks` auf mehreren Ebenen deklarieren: in einer *Pfadoperation-Funktion*, in einer Abhängigkeit (Dependable), in einer Unterabhängigkeit usw. + +**FastAPI** weiß, was jeweils zu tun ist und wie dasselbe Objekt wiederverwendet werden kann, sodass alle Hintergrundtasks zusammengeführt und anschließend im Hintergrund ausgeführt werden: + +=== "Python 3.10+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14 16 23 26" + {!> ../../../docs_src/background_tasks/tutorial002_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +In obigem Beispiel werden die Nachrichten, *nachdem* die Response gesendet wurde, in die Datei `log.txt` geschrieben. + +Wenn im Request ein Query-Parameter enthalten war, wird dieser in einem Hintergrundtask in das Log geschrieben. + +Und dann schreibt ein weiterer Hintergrundtask, der in der *Pfadoperation-Funktion* erstellt wird, eine Nachricht unter Verwendung des Pfad-Parameters `email`. + +## Technische Details + +Die Klasse `BackgroundTasks` stammt direkt von <a href="https://www.starlette.io/background/" class="external-link" target="_blank">`starlette.background`</a>. + +Sie wird direkt in FastAPI importiert/inkludiert, sodass Sie sie von `fastapi` importieren können und vermeiden, versehentlich das alternative `BackgroundTask` (ohne das `s` am Ende) von `starlette.background` zu importieren. + +Indem Sie nur `BackgroundTasks` (und nicht `BackgroundTask`) verwenden, ist es dann möglich, es als *Pfadoperation-Funktion*-Parameter zu verwenden und **FastAPI** den Rest für Sie erledigen zu lassen, genau wie bei der direkten Verwendung des `Request`-Objekts. + +Es ist immer noch möglich, `BackgroundTask` allein in FastAPI zu verwenden, aber Sie müssen das Objekt in Ihrem Code erstellen und eine Starlette-`Response` zurückgeben, die es enthält. + +Weitere Details finden Sie in der <a href="https://www.starlette.io/background/" class="external-link" target="_blank">offiziellen Starlette-Dokumentation für Hintergrundtasks</a>. + +## Vorbehalt + +Wenn Sie umfangreiche Hintergrundberechnungen durchführen müssen und diese nicht unbedingt vom selben Prozess ausgeführt werden müssen (z. B. müssen Sie Speicher, Variablen, usw. nicht gemeinsam nutzen), könnte die Verwendung anderer größerer Tools wie z. B. <a href="https://docs.celeryq.dev" class="external-link" target="_blank">Celery</a> von Vorteil sein. + +Sie erfordern in der Regel komplexere Konfigurationen und einen Nachrichten-/Job-Queue-Manager wie RabbitMQ oder Redis, ermöglichen Ihnen jedoch die Ausführung von Hintergrundtasks in mehreren Prozessen und insbesondere auf mehreren Servern. + +Um ein Beispiel zu sehen, sehen Sie sich die [Projektgeneratoren](../project-generation.md){.internal-link target=_blank} an. Sie alle enthalten Celery, bereits konfiguriert. + +Wenn Sie jedoch über dieselbe **FastAPI**-Anwendung auf Variablen und Objekte zugreifen oder kleine Hintergrundtasks ausführen müssen (z. B. das Senden einer E-Mail-Benachrichtigung), können Sie einfach `BackgroundTasks` verwenden. + +## Zusammenfassung + +Importieren und verwenden Sie `BackgroundTasks` mit Parametern in *Pfadoperation-Funktionen* und Abhängigkeiten, um Hintergrundtasks hinzuzufügen. From 4f9ad80f5d48377dae978130692dc4965fa19a2a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 14:15:52 +0000 Subject: [PATCH 1619/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4e795b8cc..b69ad9e78 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -26,6 +26,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/background-tasks.md`. PR [#10566](https://github.com/tiangolo/fastapi/pull/10566) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo in `docs/ru/docs/index.md`. PR [#10672](https://github.com/tiangolo/fastapi/pull/10672) by [@Delitel-WEB](https://github.com/Delitel-WEB). * ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). From 0aee526de9fa908027c100237a692407a5e49818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 12 Jan 2024 15:38:17 +0100 Subject: [PATCH 1620/1881] =?UTF-8?q?=F0=9F=94=A7=20=20Add=20support=20for?= =?UTF-8?q?=20translations=20to=20languages=20with=20a=20longer=20code=20n?= =?UTF-8?q?ame,=20like=20`zh-hant`=20(#10950)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/language_names.yml | 1 + scripts/docs.py | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/language_names.yml b/docs/language_names.yml index fbbbde303..7c37ff2b1 100644 --- a/docs/language_names.yml +++ b/docs/language_names.yml @@ -179,4 +179,5 @@ yi: ייִדיש yo: Yorùbá za: Saɯ cueŋƅ zh: 汉语 +zh-hant: 繁體中文 zu: isiZulu diff --git a/scripts/docs.py b/scripts/docs.py index a6710d7a5..37a7a3477 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -53,9 +53,6 @@ def get_lang_paths() -> List[Path]: def lang_callback(lang: Optional[str]) -> Union[str, None]: if lang is None: return None - if not lang.isalpha() or len(lang) != 2: - typer.echo("Use a 2 letter language code, like: es") - raise typer.Abort() lang = lang.lower() return lang @@ -289,6 +286,12 @@ def update_config() -> None: for lang_dict in languages: code = list(lang_dict.keys())[0] url = lang_dict[code] + if code not in local_language_names: + print( + f"Missing language name for: {code}, " + "update it in docs/language_names.yml" + ) + raise typer.Abort() use_name = f"{code} - {local_language_names[code]}" new_alternate.append({"link": url, "name": use_name}) new_alternate.append({"link": "/em/", "name": "😉"}) From 44f3ebce6edc3926876ac0c0370fa8b69e2dea19 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 14:38:40 +0000 Subject: [PATCH 1621/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b69ad9e78..eb78fe8cf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -31,6 +31,10 @@ hide: * ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#10410](https://github.com/tiangolo/fastapi/pull/10410) by [@AlertRED](https://github.com/AlertRED). +### Internal + +* 🔧 Add support for translations to languages with a longer code name, like `zh-hant`. PR [#10950](https://github.com/tiangolo/fastapi/pull/10950) by [@tiangolo](https://github.com/tiangolo). + ## 0.109.0 ### Features From be0bd344463c04ce095051a1fd6bf209165b3e94 Mon Sep 17 00:00:00 2001 From: ooknimm <68068775+ooknimm@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:52:00 +0900 Subject: [PATCH 1622/1881] =?UTF-8?q?=E2=9C=85=20Re-enable=20test=20in=20`?= =?UTF-8?q?tests/test=5Ftutorial/test=5Fheader=5Fparams/test=5Ftutorial003?= =?UTF-8?q?.py`=20after=20fix=20in=20Starlette=20(#10904)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_tutorial/test_header_params/test_tutorial003.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_tutorial/test_header_params/test_tutorial003.py b/tests/test_tutorial/test_header_params/test_tutorial003.py index 268df7a3e..6f7de8ed4 100644 --- a/tests/test_tutorial/test_header_params/test_tutorial003.py +++ b/tests/test_tutorial/test_header_params/test_tutorial003.py @@ -12,8 +12,12 @@ client = TestClient(app) [ ("/items", None, 200, {"X-Token values": None}), ("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}), - # TODO: fix this, is it a bug? - # ("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}), + ( + "/items", + [("x-token", "foo"), ("x-token", "bar")], + 200, + {"X-Token values": ["foo", "bar"]}, + ), ], ) def test(path, headers, expected_status, expected_response): From 22e5d9e27fd3079e693cda8ae5c5a44c53b61ca2 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 14:52:20 +0000 Subject: [PATCH 1623/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index eb78fe8cf..2f82b70d6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Refactors + +* ✅ Re-enable test in `tests/test_tutorial/test_header_params/test_tutorial003.py` after fix in Starlette. PR [#10904](https://github.com/tiangolo/fastapi/pull/10904) by [@ooknimm](https://github.com/ooknimm). + ### Docs * 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). From 91666b3556aedbaae2c84112ac272d23f312e5cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:00:18 -0500 Subject: [PATCH 1624/1881] =?UTF-8?q?=E2=AC=86=20Bump=20dawidd6/action-dow?= =?UTF-8?q?nload-artifact=20from=202.28.0=20to=203.0.0=20(#10777)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 2.28.0 to 3.0.0. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v2.28.0...v3.0.0) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 155ebd0a8..2bec6682c 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -21,7 +21,7 @@ jobs: mkdir ./site - name: Download Artifact Docs id: download - uses: dawidd6/action-download-artifact@v2.28.0 + uses: dawidd6/action-download-artifact@v3.0.0 with: if_no_artifact_found: ignore github_token: ${{ secrets.FASTAPI_PREVIEW_DOCS_DOWNLOAD_ARTIFACTS }} diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 38b44c413..229f56a9f 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -24,7 +24,7 @@ jobs: - run: pip install smokeshow - - uses: dawidd6/action-download-artifact@v2.28.0 + - uses: dawidd6/action-download-artifact@v3.0.0 with: github_token: ${{ secrets.FASTAPI_SMOKESHOW_DOWNLOAD_ARTIFACTS }} workflow: test.yml From 0ce4f80ac9aea70af3425bd337c875a85b2d4625 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 15:00:39 +0000 Subject: [PATCH 1625/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2f82b70d6..156c48bbe 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -37,6 +37,7 @@ hide: ### Internal +* ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.0.0. PR [#10777](https://github.com/tiangolo/fastapi/pull/10777) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Add support for translations to languages with a longer code name, like `zh-hant`. PR [#10950](https://github.com/tiangolo/fastapi/pull/10950) by [@tiangolo](https://github.com/tiangolo). ## 0.109.0 From 25646a5070064053309259f1e69c98015c5ec633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jiri=20Dan=C4=9Bk?= <jdanek@redhat.com> Date: Fri, 12 Jan 2024 16:01:06 +0100 Subject: [PATCH 1626/1881] =?UTF-8?q?=F0=9F=94=A7=20Fix=20Ruff=20configura?= =?UTF-8?q?tion=20unintentionally=20enabling=20and=20re-disabling=20mccabe?= =?UTF-8?q?=20complexity=20check=20(#10893)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix mistake in Ruff configuration unintentionally enabling mccabe complexity check Enabling "C" turns on complexity checks (C90, mccabe), which is unintended Instead, enable "C4" to get flake8-comprehensions checks See docs at https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8e7f8bbbe..3e43f35e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,15 +145,14 @@ select = [ "W", # pycodestyle warnings "F", # pyflakes "I", # isort - "C", # flake8-comprehensions "B", # flake8-bugbear + "C4", # flake8-comprehensions "UP", # pyupgrade ] ignore = [ "E501", # line too long, handled by black "B008", # do not perform function calls in argument defaults - "C901", # too complex - "W191", # indentation contains tabs + "W191", # indentation contains tabs ] [tool.ruff.per-file-ignores] From a2937099982b7da564279619f0cb257df33521f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:01:52 -0500 Subject: [PATCH 1627/1881] =?UTF-8?q?=E2=AC=86=20Bump=20pypa/gh-action-pyp?= =?UTF-8?q?i-publish=20from=201.8.10=20to=201.8.11=20(#10731)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.10 to 1.8.11. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.10...v1.8.11) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8cbd01b92..d053be0a0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,7 @@ jobs: - name: Build distribution run: python -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.8.10 + uses: pypa/gh-action-pypi-publish@v1.8.11 with: password: ${{ secrets.PYPI_API_TOKEN }} - name: Dump GitHub context From c9e46ae12cfe5e32a25a78dca7499db1fae55059 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 15:02:38 +0000 Subject: [PATCH 1628/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 156c48bbe..558bd10b1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,7 @@ hide: ### Refactors +* 🔧 Fix Ruff configuration unintentionally enabling and re-disabling mccabe complexity check. PR [#10893](https://github.com/tiangolo/fastapi/pull/10893) by [@jiridanek](https://github.com/jiridanek). * ✅ Re-enable test in `tests/test_tutorial/test_header_params/test_tutorial003.py` after fix in Starlette. PR [#10904](https://github.com/tiangolo/fastapi/pull/10904) by [@ooknimm](https://github.com/ooknimm). ### Docs From bc7d026b6ca487598a758b20c5935ccde1eace11 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 15:04:40 +0000 Subject: [PATCH 1629/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 558bd10b1..65128b29a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -38,6 +38,7 @@ hide: ### Internal +* ⬆ Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11. PR [#10731](https://github.com/tiangolo/fastapi/pull/10731) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.0.0. PR [#10777](https://github.com/tiangolo/fastapi/pull/10777) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Add support for translations to languages with a longer code name, like `zh-hant`. PR [#10950](https://github.com/tiangolo/fastapi/pull/10950) by [@tiangolo](https://github.com/tiangolo). From b0cd4f915bce79575e890e4ae0cccd5b15f2e38f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 16:13:58 +0100 Subject: [PATCH 1630/1881] =?UTF-8?q?=E2=AC=86=20Bump=20actions/setup-pyth?= =?UTF-8?q?on=20from=204=20to=205=20(#10764)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-docs.yml | 4 ++-- .github/workflows/publish.yml | 2 +- .github/workflows/smokeshow.yml | 2 +- .github/workflows/test.yml | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 51c069d9e..abf2b90f6 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -39,7 +39,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - uses: actions/cache@v3 @@ -80,7 +80,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - uses: actions/cache@v3 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d053be0a0..8ebb28a80 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" # Issue ref: https://github.com/actions/setup-python/issues/436 diff --git a/.github/workflows/smokeshow.yml b/.github/workflows/smokeshow.yml index 229f56a9f..10bff67ae 100644 --- a/.github/workflows/smokeshow.yml +++ b/.github/workflows/smokeshow.yml @@ -18,7 +18,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.9' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 032db9c9c..b6b173685 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" # Issue ref: https://github.com/actions/setup-python/issues/436 @@ -57,7 +57,7 @@ jobs: run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} # Issue ref: https://github.com/actions/setup-python/issues/436 @@ -98,7 +98,7 @@ jobs: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.8' # Issue ref: https://github.com/actions/setup-python/issues/436 From 26e57903d134742ba0ee8e65ce7985cd398afdea Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 12 Jan 2024 15:14:18 +0000 Subject: [PATCH 1631/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 65128b29a..19b4d8a35 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -38,6 +38,7 @@ hide: ### Internal +* ⬆ Bump actions/setup-python from 4 to 5. PR [#10764](https://github.com/tiangolo/fastapi/pull/10764) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11. PR [#10731](https://github.com/tiangolo/fastapi/pull/10731) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.0.0. PR [#10777](https://github.com/tiangolo/fastapi/pull/10777) by [@dependabot[bot]](https://github.com/apps/dependabot). * 🔧 Add support for translations to languages with a longer code name, like `zh-hant`. PR [#10950](https://github.com/tiangolo/fastapi/pull/10950) by [@tiangolo](https://github.com/tiangolo). From fad1a464e73003feb8d5ae47f5d676faf353dd3e Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski <marcelotryle@gmail.com> Date: Sat, 13 Jan 2024 02:00:31 +0100 Subject: [PATCH 1632/1881] =?UTF-8?q?=F0=9F=94=A7=20=20Group=20dependencie?= =?UTF-8?q?s=20on=20dependabot=20updates=20(#10952)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cd972a0ba..0a59adbd6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,6 +11,10 @@ updates: - package-ecosystem: "pip" directory: "/" schedule: - interval: "daily" + interval: "monthly" + groups: + python-packages: + patterns: + - "*" commit-message: prefix: ⬆ From 1ce27fd743cf95901ca44fcf24805b596615581a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 01:00:55 +0000 Subject: [PATCH 1633/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 19b4d8a35..13efb84e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -38,6 +38,7 @@ hide: ### Internal +* 🔧 Group dependencies on dependabot updates. PR [#10952](https://github.com/tiangolo/fastapi/pull/10952) by [@Kludex](https://github.com/Kludex). * ⬆ Bump actions/setup-python from 4 to 5. PR [#10764](https://github.com/tiangolo/fastapi/pull/10764) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11. PR [#10731](https://github.com/tiangolo/fastapi/pull/10731) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump dawidd6/action-download-artifact from 2.28.0 to 3.0.0. PR [#10777](https://github.com/tiangolo/fastapi/pull/10777) by [@dependabot[bot]](https://github.com/apps/dependabot). From 1caee0f105d64f475e369a82f9cac90472e54d61 Mon Sep 17 00:00:00 2001 From: Ahmed Ashraf <104530599+ahmedabdou14@users.noreply.github.com> Date: Sat, 13 Jan 2024 14:49:05 +0300 Subject: [PATCH 1634/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20Pydantic?= =?UTF-8?q?=20method=20name=20in=20`docs/en/docs/advanced/path-operation-a?= =?UTF-8?q?dvanced-configuration.md`=20(#10826)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ahmed Ashraf <root@xps> --- docs/en/docs/advanced/path-operation-advanced-configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/path-operation-advanced-configuration.md b/docs/en/docs/advanced/path-operation-advanced-configuration.md index 7ca88d43e..8b79bfe22 100644 --- a/docs/en/docs/advanced/path-operation-advanced-configuration.md +++ b/docs/en/docs/advanced/path-operation-advanced-configuration.md @@ -163,7 +163,7 @@ For example, in this application we don't use FastAPI's integrated functionality ``` !!! info - In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`. + In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_json_schema()`. Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML. From cc5711e6f105251f8e1952c0a10c660a258a0ed3 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 11:49:51 +0000 Subject: [PATCH 1635/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 13efb84e9..973073205 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +* ✏️ Fix Pydantic method name in `docs/en/docs/advanced/path-operation-advanced-configuration.md`. PR [#10826](https://github.com/tiangolo/fastapi/pull/10826) by [@ahmedabdou14](https://github.com/ahmedabdou14). + ### Refactors * 🔧 Fix Ruff configuration unintentionally enabling and re-disabling mccabe complexity check. PR [#10893](https://github.com/tiangolo/fastapi/pull/10893) by [@jiridanek](https://github.com/jiridanek). From c3e2aa9dc2b1fbdb48009ed532410e1e75c2f231 Mon Sep 17 00:00:00 2001 From: fhabers21 <58401847+fhabers21@users.noreply.github.com> Date: Sat, 13 Jan 2024 12:50:36 +0100 Subject: [PATCH 1636/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/index.md`=20(#9502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/de/docs/tutorial/index.md | 80 ++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docs/de/docs/tutorial/index.md diff --git a/docs/de/docs/tutorial/index.md b/docs/de/docs/tutorial/index.md new file mode 100644 index 000000000..dd7ed43bd --- /dev/null +++ b/docs/de/docs/tutorial/index.md @@ -0,0 +1,80 @@ +# Tutorial - Benutzerhandbuch - Intro + +Diese Anleitung zeigt Ihnen Schritt für Schritt, wie Sie **FastAPI** mit den meisten Funktionen nutzen können. + +Jeder Abschnitt baut schrittweise auf den vorhergehenden auf. Diese Abschnitte sind aber nach einzelnen Themen gegliedert, sodass Sie direkt zu einem bestimmten Thema übergehen können, um Ihre speziellen API-Anforderungen zu lösen. + +Außerdem dienen diese als zukünftige Referenz. + +Dadurch können Sie jederzeit zurückkommen und sehen genau das, was Sie benötigen. + +## Den Code ausführen + +Alle Codeblöcke können kopiert und direkt verwendet werden (da es sich um getestete Python-Dateien handelt). + +Um eines der Beispiele auszuführen, kopieren Sie den Code in die Datei `main.py`, und starten Sie `uvicorn` mit: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +<span style="color: green;">INFO</span>: Started reloader process [28720] +<span style="color: green;">INFO</span>: Started server process [28722] +<span style="color: green;">INFO</span>: Waiting for application startup. +<span style="color: green;">INFO</span>: Application startup complete. +``` + +</div> + +Es wird **ausdrücklich empfohlen**, dass Sie den Code schreiben oder kopieren, ihn bearbeiten und lokal ausführen. + +Die Verwendung in Ihrem eigenen Editor zeigt Ihnen die Vorteile von FastAPI am besten, wenn Sie sehen, wie wenig Code Sie schreiben müssen, all die Typprüfungen, die automatische Vervollständigung usw. + +--- + +## FastAPI installieren + +Der erste Schritt besteht aus der Installation von FastAPI. + +Für dieses Tutorial empfiehlt es sich, FastAPI mit allen optionalen Abhängigkeiten und Funktionen zu installieren: + +<div class="termy"> + +```console +$ pip install "fastapi[all]" + +---> 100% +``` + +</div> + +...dies beinhaltet auch `uvicorn`, das Sie als Server verwenden können, auf dem Ihr Code läuft. + +!!! Hinweis + Sie können die Installation auch in einzelnen Schritten ausführen. + + Dies werden Sie wahrscheinlich tun, wenn Sie Ihre Anwendung produktiv einsetzen möchten: + + ``` + pip install fastapi + ``` + + Installieren Sie auch `uvicorn`, dies arbeitet als Server: + + ``` + pip install "uvicorn[standard]" + ``` + + Dasselbe gilt für jede der optionalen Abhängigkeiten, die Sie verwenden möchten. + +## Erweitertes Benutzerhandbuch + +Zusätzlich gibt es ein **Erweitertes Benutzerhandbuch**, dies können Sie später nach diesem **Tutorial - Benutzerhandbuch** lesen. + +Das **Erweiterte Benutzerhandbuch** baut auf dieses Tutorial auf, verwendet dieselben Konzepte und bringt Ihnen zusätzliche Funktionen bei. + +Allerdings sollten Sie zuerst das **Tutorial - Benutzerhandbuch** lesen (was Sie gerade lesen). + +Es ist so konzipiert, dass Sie nur mit dem **Tutorial - Benutzerhandbuch** eine vollständige Anwendung erstellen können und diese dann je nach Bedarf mit einigen der zusätzlichen Ideen aus dem **Erweiterten Benutzerhandbuch** erweitern können. From 4299e712fb600b6460f85f551a2dd1d75ca7be05 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 11:51:55 +0000 Subject: [PATCH 1637/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 973073205..66f14ef1e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -33,6 +33,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). * 🌐 Add German translation for `docs/de/docs/tutorial/background-tasks.md`. PR [#10566](https://github.com/tiangolo/fastapi/pull/10566) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo in `docs/ru/docs/index.md`. PR [#10672](https://github.com/tiangolo/fastapi/pull/10672) by [@Delitel-WEB](https://github.com/Delitel-WEB). * ✏️ Fix typos in `docs/zh/docs/tutorial/extra-data-types.md`. PR [#10727](https://github.com/tiangolo/fastapi/pull/10727) by [@HiemalBeryl](https://github.com/HiemalBeryl). From a37ac3819ee9b8d19d045d53d742779109d2f711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20L=C3=B3pez=20Lira?= <jlopezlira@gmail.com> Date: Sat, 13 Jan 2024 06:57:27 -0500 Subject: [PATCH 1638/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typos=20fo?= =?UTF-8?q?r=20Spanish=20documentation=20(#10957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/advanced/additional-status-codes.md | 2 +- docs/es/docs/advanced/response-directly.md | 2 +- docs/es/docs/features.md | 14 +++++++------- docs/es/docs/index.md | 8 ++++---- docs/es/docs/python-types.md | 16 ++++++++-------- docs/es/docs/tutorial/first-steps.md | 2 +- docs/es/docs/tutorial/index.md | 2 +- docs/es/docs/tutorial/path-params.md | 6 +++--- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/es/docs/advanced/additional-status-codes.md b/docs/es/docs/advanced/additional-status-codes.md index 1f28ea85b..eaa3369eb 100644 --- a/docs/es/docs/advanced/additional-status-codes.md +++ b/docs/es/docs/advanced/additional-status-codes.md @@ -23,7 +23,7 @@ Para conseguir esto importa `JSONResponse` y devuelve ahí directamente tu conte No será serializado con el modelo, etc. - Asegurate de que la respuesta tenga los datos que quieras, y que los valores sean JSON válidos (si estás usando `JSONResponse`). + Asegúrate de que la respuesta tenga los datos que quieras, y que los valores sean JSON válidos (si estás usando `JSONResponse`). !!! note "Detalles Técnicos" También podrías utilizar `from starlette.responses import JSONResponse`. diff --git a/docs/es/docs/advanced/response-directly.md b/docs/es/docs/advanced/response-directly.md index 54dadf576..dee44ac08 100644 --- a/docs/es/docs/advanced/response-directly.md +++ b/docs/es/docs/advanced/response-directly.md @@ -21,7 +21,7 @@ Y cuando devuelves una `Response`, **FastAPI** la pasará directamente. No hará ninguna conversión de datos con modelos Pydantic, no convertirá el contenido a ningún tipo, etc. -Esto te da mucha flexibilidad. Puedes devolver cualquier tipo de dato, sobrescribir cualquer declaración de datos o validación, etc. +Esto te da mucha flexibilidad. Puedes devolver cualquier tipo de dato, sobrescribir cualquier declaración de datos o validación, etc. ## Usando el `jsonable_encoder` en una `Response` diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index d05c4f73e..d68791d63 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -13,7 +13,7 @@ ### Documentación automática -Documentación interactiva de la API e interfaces web de exploración. Hay múltiples opciones, dos incluídas por defecto, porque el framework está basado en OpenAPI. +Documentación interactiva de la API e interfaces web de exploración. Hay múltiples opciones, dos incluidas por defecto, porque el framework está basado en OpenAPI. * <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>, con exploración interactiva, llama y prueba tu API directamente desde tu navegador. @@ -25,7 +25,7 @@ Documentación interactiva de la API e interfaces web de exploración. Hay múlt ### Simplemente Python moderno -Todo está basado en las declaraciones de tipo de **Python 3.8** estándar (gracias a Pydantic). No necesitas aprender una sintáxis nueva, solo Python moderno. +Todo está basado en las declaraciones de tipo de **Python 3.8** estándar (gracias a Pydantic). No necesitas aprender una sintaxis nueva, solo Python moderno. Si necesitas un repaso de 2 minutos de cómo usar los tipos de Python (así no uses FastAPI) prueba el tutorial corto: [Python Types](python-types.md){.internal-link target=_blank}. @@ -72,9 +72,9 @@ my_second_user: User = User(**second_user_data) El framework fue diseñado en su totalidad para ser fácil e intuitivo de usar. Todas las decisiones fueron probadas en múltiples editores antes de comenzar el desarrollo para asegurar la mejor experiencia de desarrollo. -En la última encuesta a desarrolladores de Python fue claro que <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">la característica más usada es el "autocompletado"</a>. +En la última encuesta a desarrolladores de Python fue claro que <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">la característica más usada es el "auto-completado"</a>. -El framework **FastAPI** está creado para satisfacer eso. El autocompletado funciona en todas partes. +El framework **FastAPI** está creado para satisfacer eso. El auto-completado funciona en todas partes. No vas a tener que volver a la documentación seguido. @@ -140,13 +140,13 @@ FastAPI incluye un sistema de <abbr title='En español: Inyección de Dependenci * Todas las dependencias pueden requerir datos de los requests y aumentar las restricciones del *path operation* y la documentación automática. * **Validación automática** inclusive para parámetros del *path operation* definidos en las dependencias. * Soporte para sistemas complejos de autenticación de usuarios, **conexiones con bases de datos**, etc. -* **Sin comprometerse** con bases de datos, frontends, etc. Pero permitiendo integración fácil con todos ellos. +* **Sin comprometerse** con bases de datos, frontend, etc. Pero permitiendo integración fácil con todos ellos. ### "Plug-ins" ilimitados O dicho de otra manera, no hay necesidad para "plug-ins". Importa y usa el código que necesites. -Cualquier integración está diseñada para que sea tan sencilla de usar (con dependencias) que puedas crear un "plug-in" para tu aplicación en dos líneas de código usando la misma estructura y sintáxis que usaste para tus *path operations*. +Cualquier integración está diseñada para que sea tan sencilla de usar (con dependencias) que puedas crear un "plug-in" para tu aplicación en dos líneas de código usando la misma estructura y sintaxis que usaste para tus *path operations*. ### Probado @@ -181,7 +181,7 @@ Esto incluye a librerías externas basadas en Pydantic como <abbr title="Object- Esto también significa que en muchos casos puedes pasar el mismo objeto que obtuviste de un request **directamente a la base de datos**, dado que todo es validado automáticamente. -Lo mismo aplica para el sentido contrario. En muchos casos puedes pasarle el objeto que obtienes de la base de datos **directamente al cliente**. +Lo mismo aplica para el sentido contrario. En muchos casos puedes pasar el objeto que obtienes de la base de datos **directamente al cliente**. Con **FastAPI** obtienes todas las características de **Pydantic** (dado que FastAPI está basado en Pydantic para todo el manejo de datos): diff --git a/docs/es/docs/index.md b/docs/es/docs/index.md index 30a575577..df8342357 100644 --- a/docs/es/docs/index.md +++ b/docs/es/docs/index.md @@ -37,7 +37,7 @@ Sus características principales son: * **Robusto**: Crea código listo para producción con documentación automática interactiva. * **Basado en estándares**: Basado y totalmente compatible con los estándares abiertos para APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (conocido previamente como Swagger) y <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. -<small>* Esta estimación está basada en pruebas con un equipo de desarrollo interno contruyendo aplicaciones listas para producción.</small> +<small>* Esta estimación está basada en pruebas con un equipo de desarrollo interno construyendo aplicaciones listas para producción.</small> ## Sponsors @@ -295,11 +295,11 @@ Ahora ve a <a href="http://127.0.0.1:8000/docs" class="external-link" target="_b ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* Haz clíck en el botón de "Try it out" que te permite llenar los parámetros e interactuar directamente con la API: +* Haz click en el botón de "Try it out" que te permite llenar los parámetros e interactuar directamente con la API: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* Luego haz clíck en el botón de "Execute". La interfaz de usuario se comunicará con tu API, enviará los parámetros y recibirá los resultados para mostrarlos en pantalla: +* Luego haz click en el botón de "Execute". La interfaz de usuario se comunicará con tu API, enviará los parámetros y recibirá los resultados para mostrarlos en pantalla: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) @@ -317,7 +317,7 @@ En resumen, declaras los tipos de parámetros, body, etc. **una vez** como pará Lo haces con tipos modernos estándar de Python. -No tienes que aprender una sintáxis nueva, los métodos o clases de una library específica, etc. +No tienes que aprender una sintaxis nueva, los métodos o clases de una library específica, etc. Solo **Python 3.8+** estándar. diff --git a/docs/es/docs/python-types.md b/docs/es/docs/python-types.md index e9fd61629..b83cbe3f5 100644 --- a/docs/es/docs/python-types.md +++ b/docs/es/docs/python-types.md @@ -2,7 +2,7 @@ **Python 3.6+** tiene soporte para <abbr title="en español, anotaciones de tipo. En inglés también se conocen como: type annotations">"type hints"</abbr> opcionales. -Estos **type hints** son una nueva sintáxis, desde Python 3.6+, que permite declarar el <abbr title="por ejemplo: str, int, float, bool">tipo</abbr> de una variable. +Estos **type hints** son una nueva sintaxis, desde Python 3.6+, que permite declarar el <abbr title="por ejemplo: str, int, float, bool">tipo</abbr> de una variable. Usando las declaraciones de tipos para tus variables, los editores y otras herramientas pueden proveerte un soporte mejor. @@ -33,7 +33,7 @@ La función hace lo siguiente: * Toma un `first_name` y un `last_name`. * Convierte la primera letra de cada uno en una letra mayúscula con `title()`. -* Las <abbr title="las junta como si fuesen una. Con el contenido de una después de la otra. En inlgés: concatenate.">concatena</abbr> con un espacio en la mitad. +* Las <abbr title="las junta como si fuesen una. Con el contenido de una después de la otra. En inglés: concatenate.">concatena</abbr> con un espacio en la mitad. ```Python hl_lines="2" {!../../../docs_src/python_types/tutorial001.py!} @@ -51,9 +51,9 @@ Pero, luego tienes que llamar "ese método que convierte la primera letra en una Era `upper`? O era `uppercase`? `first_uppercase`? `capitalize`? -Luego lo intentas con el viejo amigo de los programadores, el autocompletado del editor. +Luego lo intentas con el viejo amigo de los programadores, el auto-completado del editor. -Escribes el primer parámetro de la función `first_name`, luego un punto (`.`) y luego presionas `Ctrl+Space` para iniciar el autocompletado. +Escribes el primer parámetro de la función `first_name`, luego un punto (`.`) y luego presionas `Ctrl+Space` para iniciar el auto-completado. Tristemente, no obtienes nada útil: @@ -97,7 +97,7 @@ Añadir los type hints normalmente no cambia lo que sucedería si ellos no estuv Pero ahora imagina que nuevamente estás creando la función, pero con los type hints. -En el mismo punto intentas iniciar el autocompletado con `Ctrl+Space` y ves: +En el mismo punto intentas iniciar el auto-completado con `Ctrl+Space` y ves: <img src="https://fastapi.tiangolo.com/img/python-types/image02.png"> @@ -113,7 +113,7 @@ Mira esta función que ya tiene type hints: {!../../../docs_src/python_types/tutorial003.py!} ``` -Como el editor conoce el tipo de las variables no solo obtienes autocompletado, si no que también obtienes chequeo de errores: +Como el editor conoce el tipo de las variables no solo obtienes auto-completado, si no que también obtienes chequeo de errores: <img src="https://fastapi.tiangolo.com/img/python-types/image04.png"> @@ -162,7 +162,7 @@ De `typing`, importa `List` (con una `L` mayúscula): {!../../../docs_src/python_types/tutorial006.py!} ``` -Declara la variable con la misma sintáxis de los dos puntos (`:`). +Declara la variable con la misma sintaxis de los dos puntos (`:`). Pon `List` como el tipo. @@ -176,7 +176,7 @@ Esto significa: la variable `items` es una `list` y cada uno de los ítems en es Con esta declaración tu editor puede proveerte soporte inclusive mientras está procesando ítems de la lista. -Sin tipos el autocompletado en este tipo de estructura es casi imposible de lograr: +Sin tipos el auto-completado en este tipo de estructura es casi imposible de lograr: <img src="https://fastapi.tiangolo.com/img/python-types/image05.png"> diff --git a/docs/es/docs/tutorial/first-steps.md b/docs/es/docs/tutorial/first-steps.md index efa61f994..2cb7e6308 100644 --- a/docs/es/docs/tutorial/first-steps.md +++ b/docs/es/docs/tutorial/first-steps.md @@ -303,7 +303,7 @@ En este caso es una función `async`. --- -También podrías definirla como una función normal, en vez de `async def`: +También podrías definirla como una función estándar en lugar de `async def`: ```Python hl_lines="7" {!../../../docs_src/first_steps/tutorial003.py!} diff --git a/docs/es/docs/tutorial/index.md b/docs/es/docs/tutorial/index.md index 1cff8b4e3..f0dff02b4 100644 --- a/docs/es/docs/tutorial/index.md +++ b/docs/es/docs/tutorial/index.md @@ -28,7 +28,7 @@ $ uvicorn main:app --reload Se **RECOMIENDA** que escribas o copies el código, lo edites y lo ejecutes localmente. -Usarlo en tu editor de código es lo que realmente te muestra los beneficios de FastAPI, al ver la poca cantidad de código que tienes que escribir, todas las verificaciones de tipo, autocompletado, etc. +Usarlo en tu editor de código es lo que realmente te muestra los beneficios de FastAPI, al ver la poca cantidad de código que tienes que escribir, todas las verificaciones de tipo, auto-completado, etc. --- diff --git a/docs/es/docs/tutorial/path-params.md b/docs/es/docs/tutorial/path-params.md index 6432de1cd..765ae4140 100644 --- a/docs/es/docs/tutorial/path-params.md +++ b/docs/es/docs/tutorial/path-params.md @@ -25,7 +25,7 @@ Puedes declarar el tipo de un parámetro de path en la función usando las anota En este caso, `item_id` es declarado como un `int`. !!! check "Revisa" - Esto te dará soporte en el editor dentro de tu función, con chequeos de errores, autocompletado, etc. + Esto te dará soporte en el editor dentro de tu función, con chequeo de errores, auto-completado, etc. ## <abbr title="también conocido en inglés como: serialization, parsing, marshalling">Conversión</abbr> de datos @@ -135,7 +135,7 @@ Luego crea atributos de clase con valores fijos, que serán los valores disponib Las <a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">Enumerations (o enums) están disponibles en Python</a> desde la versión 3.4. !!! tip "Consejo" - Si lo estás dudando, "AlexNet", "ResNet", y "LeNet" son solo nombres de <abbr title="Tecnicamente, arquitecturas de modelos de Deep Learning">modelos</abbr> de Machine Learning. + Si lo estás dudando, "AlexNet", "ResNet", y "LeNet" son solo nombres de <abbr title="Técnicamente, arquitecturas de modelos de Deep Learning">modelos</abbr> de Machine Learning. ### Declara un *parámetro de path* @@ -234,7 +234,7 @@ Entonces lo puedes usar con: Con **FastAPI**, usando declaraciones de tipo de Python intuitivas y estándares, obtienes: -* Soporte en el editor: chequeos de errores, auto-completado, etc. +* Soporte en el editor: chequeo de errores, auto-completado, etc. * "<abbr title="convertir el string que viene de un HTTP request a datos de Python">Parsing</abbr>" de datos * Validación de datos * Anotación de la API y documentación automática From 5377c594da2287bbdd273df37dd7c00d5ce4518b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 12:00:11 +0000 Subject: [PATCH 1639/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 66f14ef1e..c632cc0b0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Docs +* ✏️ Fix typos for Spanish documentation. PR [#10957](https://github.com/tiangolo/fastapi/pull/10957) by [@jlopezlira](https://github.com/jlopezlira). * 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). * ✏️ Fix broken link in `docs/tutorial/sql-databases.md` in several languages. PR [#10716](https://github.com/tiangolo/fastapi/pull/10716) by [@theoohoho](https://github.com/theoohoho). * ✏️ Remove broken links from `external_links.yml`. PR [#10943](https://github.com/tiangolo/fastapi/pull/10943) by [@Torabek](https://github.com/Torabek). From bc13faa15d379b9bc5e2475992866e8ca66002ee Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Sat, 13 Jan 2024 13:07:15 +0100 Subject: [PATCH 1640/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20link=20in?= =?UTF-8?q?=20`docs/en/docs/advanced/async-tests.md`=20(#10960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/async-tests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/async-tests.md b/docs/en/docs/advanced/async-tests.md index c79822d63..f9c82e6ab 100644 --- a/docs/en/docs/advanced/async-tests.md +++ b/docs/en/docs/advanced/async-tests.md @@ -85,7 +85,7 @@ response = client.get('/') Note that we're using async/await with the new `AsyncClient` - the request is asynchronous. !!! warning - If your application relies on lifespan events, the `AsyncClient` won't trigger these events. To ensure they are triggered, use `LifespanManager` from <a href="florimondmanca/asgi-lifespan" class="external-link" target="_blank">https://github.com/florimondmanca/asgi-lifespan#usage</a>. + If your application relies on lifespan events, the `AsyncClient` won't trigger these events. To ensure they are triggered, use `LifespanManager` from <a href="https://github.com/florimondmanca/asgi-lifespan#usage" class="external-link" target="_blank">florimondmanca/asgi-lifespan</a>. ## Other Asynchronous Function Calls From b24c4870d8f8f0547d956ac8c1d2c751f1f66b69 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 12:10:29 +0000 Subject: [PATCH 1641/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c632cc0b0..2234624f0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Docs +* ✏️ Fix link in `docs/en/docs/advanced/async-tests.md`. PR [#10960](https://github.com/tiangolo/fastapi/pull/10960) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typos for Spanish documentation. PR [#10957](https://github.com/tiangolo/fastapi/pull/10957) by [@jlopezlira](https://github.com/jlopezlira). * 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). * ✏️ Fix broken link in `docs/tutorial/sql-databases.md` in several languages. PR [#10716](https://github.com/tiangolo/fastapi/pull/10716) by [@theoohoho](https://github.com/theoohoho). From 83e386519d30d8c732249983ffdf7200e8af5a5d Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Sat, 13 Jan 2024 13:16:22 +0100 Subject: [PATCH 1642/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20A=20few=20tweaks?= =?UTF-8?q?=20in=20`docs/de/docs/tutorial/first-steps.md`=20(#10959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/first-steps.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/de/docs/tutorial/first-steps.md b/docs/de/docs/tutorial/first-steps.md index 5997f138f..27ba3ec16 100644 --- a/docs/de/docs/tutorial/first-steps.md +++ b/docs/de/docs/tutorial/first-steps.md @@ -43,7 +43,7 @@ Diese Zeile zeigt die URL, unter der Ihre Anwendung auf Ihrem lokalen Computer b Öffnen Sie Ihren Browser unter <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000.</a> -Sie werden folgende JSON-Antwort sehen: +Sie werden folgende JSON-Response sehen: ```JSON {"message": "Hello World"} @@ -81,7 +81,7 @@ Diese Schemadefinition enthält Ihre API-Pfade, die möglichen Parameter, welche #### Daten-„Schema“ -Der Begriff „Schema“ kann sich auch auf die Form von Daten beziehen, wie z.B. einen JSON-Inhalt. +Der Begriff „Schema“ kann sich auch auf die Form von Daten beziehen, wie z. B. einen JSON-Inhalt. In diesem Fall sind die JSON-Attribute und deren Datentypen, usw. gemeint. @@ -328,6 +328,6 @@ Es gibt viele andere Objekte und Modelle, die automatisch zu JSON konvertiert we * Importieren Sie `FastAPI`. * Erstellen Sie eine `app` Instanz. -* Schreiben Sie einen **Pfadoperation-Dekorator** (wie z.B. `@app.get("/")`). -* Schreiben Sie eine **Pfadoperation-Funktion** (wie z.B. oben `def root(): ...`). -* Starten Sie den Entwicklungsserver (z.B. `uvicorn main:app --reload`). +* Schreiben Sie einen **Pfadoperation-Dekorator** (wie z. B. `@app.get("/")`). +* Schreiben Sie eine **Pfadoperation-Funktion** (wie z. B. oben `def root(): ...`). +* Starten Sie den Entwicklungsserver (z. B. `uvicorn main:app --reload`). From cca6203c18552aa95c7ad0f7fd972fd6e86d0d77 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 12:19:28 +0000 Subject: [PATCH 1643/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2234624f0..510b842ba 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -16,6 +16,7 @@ hide: ### Docs +* ✏️ A few tweaks in `docs/de/docs/tutorial/first-steps.md`. PR [#10959](https://github.com/tiangolo/fastapi/pull/10959) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix link in `docs/en/docs/advanced/async-tests.md`. PR [#10960](https://github.com/tiangolo/fastapi/pull/10960) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typos for Spanish documentation. PR [#10957](https://github.com/tiangolo/fastapi/pull/10957) by [@jlopezlira](https://github.com/jlopezlira). * 📝 Add warning about lifespan functions and backwards compatibility with events. PR [#10734](https://github.com/tiangolo/fastapi/pull/10734) by [@jacob-indigo](https://github.com/jacob-indigo). From de0126d145c920c992c605538e63fe0e46b508d5 Mon Sep 17 00:00:00 2001 From: Evgenii <ekublin@gmail.com> Date: Sat, 13 Jan 2024 17:31:38 +0300 Subject: [PATCH 1644/1881] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20strin?= =?UTF-8?q?g=20format=20with=20f-strings=20in=20`fastapi/utils.py`=20(#105?= =?UTF-8?q?76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- fastapi/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index f8463dda2..0019c2153 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -173,17 +173,17 @@ def generate_operation_id_for_path( DeprecationWarning, stacklevel=2, ) - operation_id = name + path + operation_id = f"{name}{path}" operation_id = re.sub(r"\W", "_", operation_id) - operation_id = operation_id + "_" + method.lower() + operation_id = f"{operation_id}_{method.lower()}" return operation_id def generate_unique_id(route: "APIRoute") -> str: - operation_id = route.name + route.path_format + operation_id = f"{route.name}{route.path_format}" operation_id = re.sub(r"\W", "_", operation_id) assert route.methods - operation_id = operation_id + "_" + list(route.methods)[0].lower() + operation_id = f"{operation_id}_{list(route.methods)[0].lower()}" return operation_id From 61a08d0c60cbe29784bb64cdbdea5d613a38b2e5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 14:31:58 +0000 Subject: [PATCH 1645/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 510b842ba..1fc1a1695 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,7 @@ hide: ### Refactors +* ♻️ Simplify string format with f-strings in `fastapi/utils.py`. PR [#10576](https://github.com/tiangolo/fastapi/pull/10576) by [@eukub](https://github.com/eukub). * 🔧 Fix Ruff configuration unintentionally enabling and re-disabling mccabe complexity check. PR [#10893](https://github.com/tiangolo/fastapi/pull/10893) by [@jiridanek](https://github.com/jiridanek). * ✅ Re-enable test in `tests/test_tutorial/test_header_params/test_tutorial003.py` after fix in Starlette. PR [#10904](https://github.com/tiangolo/fastapi/pull/10904) by [@ooknimm](https://github.com/ooknimm). From f18eadb7de142d6bf37eff900731329a541758f5 Mon Sep 17 00:00:00 2001 From: Emmett Butler <723615+emmettbutler@users.noreply.github.com> Date: Sat, 13 Jan 2024 07:10:26 -0800 Subject: [PATCH 1646/1881] =?UTF-8?q?=E2=9C=85=20Refactor=20tests=20for=20?= =?UTF-8?q?duplicate=20operation=20ID=20generation=20for=20compatibility?= =?UTF-8?q?=20with=20other=20tools=20running=20the=20FastAPI=20test=20suit?= =?UTF-8?q?e=20(#10876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- tests/test_generate_unique_id_function.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_generate_unique_id_function.py b/tests/test_generate_unique_id_function.py index c5ef5182b..5aeec6636 100644 --- a/tests/test_generate_unique_id_function.py +++ b/tests/test_generate_unique_id_function.py @@ -1626,6 +1626,9 @@ def test_warn_duplicate_operation_id(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") client.get("/openapi.json") - assert len(w) == 2 - assert issubclass(w[-1].category, UserWarning) - assert "Duplicate Operation ID" in str(w[-1].message) + assert len(w) >= 2 + duplicate_warnings = [ + warning for warning in w if issubclass(warning.category, UserWarning) + ] + assert len(duplicate_warnings) > 0 + assert "Duplicate Operation ID" in str(duplicate_warnings[0].message) From e90fc7bed4213fbf42195830a1a8f54288be4fb8 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 13 Jan 2024 15:10:47 +0000 Subject: [PATCH 1647/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1fc1a1695..5e02e2352 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,7 @@ hide: ### Refactors +* ✅ Refactor tests for duplicate operation ID generation for compatibility with other tools running the FastAPI test suite. PR [#10876](https://github.com/tiangolo/fastapi/pull/10876) by [@emmettbutler](https://github.com/emmettbutler). * ♻️ Simplify string format with f-strings in `fastapi/utils.py`. PR [#10576](https://github.com/tiangolo/fastapi/pull/10576) by [@eukub](https://github.com/eukub). * 🔧 Fix Ruff configuration unintentionally enabling and re-disabling mccabe complexity check. PR [#10893](https://github.com/tiangolo/fastapi/pull/10893) by [@jiridanek](https://github.com/jiridanek). * ✅ Re-enable test in `tests/test_tutorial/test_header_params/test_tutorial003.py` after fix in Starlette. PR [#10904](https://github.com/tiangolo/fastapi/pull/10904) by [@ooknimm](https://github.com/ooknimm). From dcc952d6990c507956669e6fc5cddba0530c79d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 15 Jan 2024 11:32:16 +0100 Subject: [PATCH 1648/1881] =?UTF-8?q?=E2=9C=A8=20=20Include=20HTTP=20205?= =?UTF-8?q?=20in=20status=20codes=20with=20no=20body=20(#10969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/utils.py b/fastapi/utils.py index 0019c2153..53b2fa0c3 100644 --- a/fastapi/utils.py +++ b/fastapi/utils.py @@ -53,7 +53,7 @@ def is_body_allowed_for_status_code(status_code: Union[int, str, None]) -> bool: }: return True current_status_code = int(status_code) - return not (current_status_code < 200 or current_status_code in {204, 304}) + return not (current_status_code < 200 or current_status_code in {204, 205, 304}) def get_path_param_names(path: str) -> Set[str]: From 69dc735fc2339f8b39a9f1b01ced7974df9c4a65 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 10:32:42 +0000 Subject: [PATCH 1649/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 5e02e2352..7b09977e7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -9,6 +9,10 @@ hide: * ✏️ Fix Pydantic method name in `docs/en/docs/advanced/path-operation-advanced-configuration.md`. PR [#10826](https://github.com/tiangolo/fastapi/pull/10826) by [@ahmedabdou14](https://github.com/ahmedabdou14). +### Features + +* ✨ Include HTTP 205 in status codes with no body. PR [#10969](https://github.com/tiangolo/fastapi/pull/10969) by [@tiangolo](https://github.com/tiangolo). + ### Refactors * ✅ Refactor tests for duplicate operation ID generation for compatibility with other tools running the FastAPI test suite. PR [#10876](https://github.com/tiangolo/fastapi/pull/10876) by [@emmettbutler](https://github.com/emmettbutler). From 63e5396a78a470c39558a37d1fefbbe1bcbf4db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 15 Jan 2024 16:13:48 +0100 Subject: [PATCH 1650/1881] =?UTF-8?q?=F0=9F=91=B7=20Add=20changes-requeste?= =?UTF-8?q?d=20handling=20in=20GitHub=20Action=20issue=20manager=20(#10971?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index bb967fa11..d1aad28fd 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -31,5 +31,9 @@ jobs: "answered": { "delay": 864000, "message": "Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs." + }, + "changes-requested": { + "delay": 2628000, + "message": "As this PR had requested changes to be applied but has been inactive for a while, it's now going to be closed. But if there's anyone interested, feel free to create a new PR." } } From 2b6f12a5d00717f40b1fa0fa5e882fe021862559 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:14:34 +0000 Subject: [PATCH 1651/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7b09977e7..c7ef17d65 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -50,6 +50,7 @@ hide: ### Internal +* 👷 Add changes-requested handling in GitHub Action issue manager. PR [#10971](https://github.com/tiangolo/fastapi/pull/10971) by [@tiangolo](https://github.com/tiangolo). * 🔧 Group dependencies on dependabot updates. PR [#10952](https://github.com/tiangolo/fastapi/pull/10952) by [@Kludex](https://github.com/Kludex). * ⬆ Bump actions/setup-python from 4 to 5. PR [#10764](https://github.com/tiangolo/fastapi/pull/10764) by [@dependabot[bot]](https://github.com/apps/dependabot). * ⬆ Bump pypa/gh-action-pypi-publish from 1.8.10 to 1.8.11. PR [#10731](https://github.com/tiangolo/fastapi/pull/10731) by [@dependabot[bot]](https://github.com/apps/dependabot). From cf01195555ea0111a9540bccc1444b9d802587da Mon Sep 17 00:00:00 2001 From: Pedro Augusto de Paula Barbosa <papb1996@gmail.com> Date: Mon, 15 Jan 2024 12:17:34 -0300 Subject: [PATCH 1652/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20`HTTPExceptio?= =?UTF-8?q?n`=20details=20in=20`docs/en/docs/tutorial/handling-errors.md`?= =?UTF-8?q?=20(#5418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/docs/tutorial/handling-errors.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index a03029e81..7d521696d 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -234,9 +234,7 @@ You will receive a response telling you that the data is invalid containing the And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class. -The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response. - -This is needed/used internally for OAuth 2.0 and some security utilities. +The only difference is that **FastAPI**'s `HTTPException` accepts any JSON-able data for the `detail` field, while Starlette's `HTTPException` only accepts strings for it. So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code. From 32ae9497233a9dc859a17f642c6b9bca0260f9ca Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:17:54 +0000 Subject: [PATCH 1653/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c7ef17d65..771d286a1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Update `HTTPException` details in `docs/en/docs/tutorial/handling-errors.md`. PR [#5418](https://github.com/tiangolo/fastapi/pull/5418) by [@papb](https://github.com/papb). * ✏️ A few tweaks in `docs/de/docs/tutorial/first-steps.md`. PR [#10959](https://github.com/tiangolo/fastapi/pull/10959) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix link in `docs/en/docs/advanced/async-tests.md`. PR [#10960](https://github.com/tiangolo/fastapi/pull/10960) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typos for Spanish documentation. PR [#10957](https://github.com/tiangolo/fastapi/pull/10957) by [@jlopezlira](https://github.com/jlopezlira). From 15429a9c395df0378aa58fdee00c9b63a7a40358 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:33:28 +0900 Subject: [PATCH 1654/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/body-fields.md`=20(#1923)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/body-fields.md | 48 ++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/ja/docs/tutorial/body-fields.md diff --git a/docs/ja/docs/tutorial/body-fields.md b/docs/ja/docs/tutorial/body-fields.md new file mode 100644 index 000000000..8f01e8216 --- /dev/null +++ b/docs/ja/docs/tutorial/body-fields.md @@ -0,0 +1,48 @@ +# ボディ - フィールド + +`Query`や`Path`、`Body`を使って *path operation関数* のパラメータに追加のバリデーションやメタデータを宣言するのと同じように、Pydanticの`Field`を使ってPydanticモデルの内部でバリデーションやメタデータを宣言することができます。 + +## `Field`のインポート + +まず、以下のようにインポートします: + +```Python hl_lines="4" +{!../../../docs_src/body_fields/tutorial001.py!} +``` + +!!! warning "注意" + `Field`は他の全てのもの(`Query`、`Path`、`Body`など)とは違い、`fastapi`からではなく、`pydantic`から直接インポートされていることに注意してください。 + +## モデルの属性の宣言 + +以下のように`Field`をモデルの属性として使用することができます: + +```Python hl_lines="11 12 13 14" +{!../../../docs_src/body_fields/tutorial001.py!} +``` + +`Field`は`Query`や`Path`、`Body`と同じように動作し、全く同様のパラメータなどを持ちます。 + +!!! note "技術詳細" + 実際には次に見る`Query`や`Path`などは、共通の`Param`クラスのサブクラスのオブジェクトを作成しますが、それ自体はPydanticの`FieldInfo`クラスのサブクラスです。 + + また、Pydanticの`Field`は`FieldInfo`のインスタンスも返します。 + + `Body`は`FieldInfo`のサブクラスのオブジェクトを直接返すこともできます。そして、他にも`Body`クラスのサブクラスであるものがあります。 + + `fastapi`から`Query`や`Path`などをインポートする場合、これらは実際には特殊なクラスを返す関数であることに注意してください。 + +!!! tip "豆知識" + 型、デフォルト値、`Field`を持つ各モデルの属性が、`Path`や`Query`、`Body`の代わりに`Field`を持つ、*path operation 関数の*パラメータと同じ構造になっていることに注目してください。 + +## 追加情報の追加 + +追加情報は`Field`や`Query`、`Body`などで宣言することができます。そしてそれは生成されたJSONスキーマに含まれます。 + +後に例を用いて宣言を学ぶ際に、追加情報を句悪方法を学べます。 + +## まとめ + +Pydanticの`Field`を使用して、モデルの属性に追加のバリデーションやメタデータを宣言することができます。 + +追加のキーワード引数を使用して、追加のJSONスキーマのメタデータを渡すこともできます。 From 467ab2a5756245cc53a8c0ec4fd467ffbef7d347 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:33:51 +0000 Subject: [PATCH 1655/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 771d286a1..2d73401ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). * 🌐 Add German translation for `docs/de/docs/tutorial/background-tasks.md`. PR [#10566](https://github.com/tiangolo/fastapi/pull/10566) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo in `docs/ru/docs/index.md`. PR [#10672](https://github.com/tiangolo/fastapi/pull/10672) by [@Delitel-WEB](https://github.com/Delitel-WEB). From 88f19be7c38b1c904d453dff3f0f1f97ebdcaec7 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:34:57 +0900 Subject: [PATCH 1656/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/body-nested-models.md`=20?= =?UTF-8?q?(#1930)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/body-nested-models.md | 244 ++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 docs/ja/docs/tutorial/body-nested-models.md diff --git a/docs/ja/docs/tutorial/body-nested-models.md b/docs/ja/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..7f916c47a --- /dev/null +++ b/docs/ja/docs/tutorial/body-nested-models.md @@ -0,0 +1,244 @@ +# ボディ - ネストされたモデル + +**FastAPI** を使用すると、深くネストされた任意のモデルを定義、検証、文書化、使用することができます(Pydanticのおかげです)。 + +## リストのフィールド + +属性をサブタイプとして定義することができます。例えば、Pythonの`list`は以下のように定義できます: + +```Python hl_lines="12" +{!../../../docs_src/body_nested_models/tutorial001.py!} +``` + +これにより、各項目の型は宣言されていませんが、`tags`はある項目のリストになります。 + +## タイプパラメータを持つリストのフィールド + +しかし、Pythonには型や「タイプパラメータ」を使ってリストを宣言する方法があります: + +### typingの`List`をインポート + +まず、Pythonの標準の`typing`モジュールから`List`をインポートします: + +```Python hl_lines="1" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### タイプパラメータを持つ`List`の宣言 + +`list`や`dict`、`tuple`のようなタイプパラメータ(内部の型)を持つ型を宣言するには: + +* `typing`モジュールからそれらをインストールします。 +* 角括弧(`[`と`]`)を使って「タイプパラメータ」として内部の型を渡します: + +```Python +from typing import List + +my_list: List[str] +``` + +型宣言の標準的なPythonの構文はこれだけです。 + +内部の型を持つモデルの属性にも同じ標準の構文を使用してください。 + +そのため、以下の例では`tags`を具体的な「文字列のリスト」にすることができます: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +## セット型 + +しかし、よく考えてみると、タグは繰り返すべきではなく、おそらくユニークな文字列になるのではないかと気付いたとします。 + +そして、Pythonにはユニークな項目のセットのための特別なデータ型`set`があります。 + +そのため、以下のように、`Set`をインポートして`str`の`set`として`tags`を宣言することができます: + +```Python hl_lines="1 14" +{!../../../docs_src/body_nested_models/tutorial003.py!} +``` + +これを使えば、データが重複しているリクエストを受けた場合でも、ユニークな項目のセットに変換されます。 + +そして、そのデータを出力すると、たとえソースに重複があったとしても、固有の項目のセットとして出力されます。 + +また、それに応じて注釈をつけたり、文書化したりします。 + +## ネストされたモデル + +Pydanticモデルの各属性には型があります。 + +しかし、その型はそれ自体が別のPydanticモデルである可能性があります。 + +そのため、特定の属性名、型、バリデーションを指定して、深くネストしたJSON`object`を宣言することができます。 + +すべては、任意のネストにされています。 + +### サブモデルの定義 + +例えば、`Image`モデルを定義することができます: + +```Python hl_lines="9 10 11" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +### サブモデルを型として使用 + +そして、それを属性の型として使用することができます: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +これは **FastAPI** が以下のようなボディを期待することを意味します: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +繰り返しになりますが、**FastAPI** を使用して、その宣言を行うだけで以下のような恩恵を受けられます: + +* ネストされたモデルでも対応可能なエディタのサポート(補完など) +* データ変換 +* データの検証 +* 自動文書化 + +## 特殊な型とバリデーション + +`str`や`int`、`float`のような通常の単数型の他にも、`str`を継承したより複雑な単数型を使うこともできます。 + +すべてのオプションをみるには、<a href="https://pydantic-docs.helpmanual.io/usage/types/" class="external-link" target="_blank">Pydanticのエキゾチック な型</a>のドキュメントを確認してください。次の章でいくつかの例をみることができます。 + +例えば、`Image`モデルのように`url`フィールドがある場合、`str`の代わりにPydanticの`HttpUrl`を指定することができます: + +```Python hl_lines="4 10" +{!../../../docs_src/body_nested_models/tutorial005.py!} +``` + +文字列は有効なURLであることが確認され、そのようにJSONスキーマ・OpenAPIで文書化されます。 + +## サブモデルのリストを持つ属性 + +Pydanticモデルを`list`や`set`などのサブタイプとして使用することもできます: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial006.py!} +``` + +これは、次のようなJSONボディを期待します(変換、検証、ドキュメントなど): + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info "情報" + `images`キーが画像オブジェクトのリストを持つようになったことに注目してください。 + +## 深くネストされたモデル + +深くネストされた任意のモデルを定義することができます: + +```Python hl_lines="9 14 20 23 27" +{!../../../docs_src/body_nested_models/tutorial007.py!} +``` + +!!! info "情報" + `Offer`は`Item`のリストであり、オプションの`Image`のリストを持っていることに注目してください。 + +## 純粋なリストのボディ + +期待するJSONボディのトップレベルの値がJSON`array`(Pythonの`list`)であれば、Pydanticモデルと同じように、関数のパラメータで型を宣言することができます: + +```Python +images: List[Image] +``` + +以下のように: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial008.py!} +``` + +## あらゆる場所でのエディタサポート + +エディタのサポートもどこでも受けることができます。 + +以下のようにリストの中の項目でも: + +<img src="https://fastapi.tiangolo.com/img/tutorial/body-nested-models/image01.png"> + +Pydanticモデルではなく、`dict`を直接使用している場合はこのようなエディタのサポートは得られません。 + +しかし、それらについて心配する必要はありません。入力された辞書は自動的に変換され、出力も自動的にJSONに変換されます。 + +## 任意の`dict`のボディ + +また、ある型のキーと別の型の値を持つ`dict`としてボディを宣言することもできます。 + +有効なフィールド・属性名を事前に知る必要がありません(Pydanticモデルの場合のように)。 + +これは、まだ知らないキーを受け取りたいときに便利だと思います。 + +--- + +他にも、`int`のように他の型のキーを持ちたい場合などに便利です。 + +それをここで見ていきましょう。 + +この場合、`int`のキーと`float`の値を持つものであれば、どんな`dict`でも受け入れることができます: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial009.py!} +``` + +!!! tip "豆知識" + JSONはキーとして`str`しかサポートしていないことに注意してください。 + + しかしPydanticには自動データ変換機能があります。 + + これは、APIクライアントがキーとして文字列しか送信できなくても、それらの文字列に純粋な整数が含まれている限り、Pydanticが変換して検証することを意味します。 + + そして、`weights`として受け取る`dict`は、実際には`int`のキーと`float`の値を持つことになります。 + +## まとめ + +**FastAPI** を使用すると、Pydanticモデルが提供する最大限の柔軟性を持ちながら、コードをシンプルに短く、エレガントに保つことができます。 + +以下のような利点があります: + +* エディタのサポート(どこでも補完!) +* データ変換(別名:構文解析・シリアライズ) +* データの検証 +* スキーマ文書 +* 自動文書化 From 2619bbd7cde8251e955512f560dc632a96f72fe8 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:35:25 +0900 Subject: [PATCH 1657/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20tranl?= =?UTF-8?q?sation=20for=20`docs/ja/docs/tutorial/schema-extra-example.md`?= =?UTF-8?q?=20(#1931)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/schema-extra-example.md | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs/ja/docs/tutorial/schema-extra-example.md diff --git a/docs/ja/docs/tutorial/schema-extra-example.md b/docs/ja/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..3102a4936 --- /dev/null +++ b/docs/ja/docs/tutorial/schema-extra-example.md @@ -0,0 +1,58 @@ +# スキーマの追加 - 例 + +JSON Schemaに追加する情報を定義することができます。 + +一般的なユースケースはこのドキュメントで示されているように`example`を追加することです。 + +JSON Schemaの追加情報を宣言する方法はいくつかあります。 + +## Pydanticの`schema_extra` + +<a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydanticのドキュメント: スキーマのカスタマイズ</a>で説明されているように、`Config`と`schema_extra`を使ってPydanticモデルの例を宣言することができます: + +```Python hl_lines="15 16 17 18 19 20 21 22 23" +{!../../../docs_src/schema_extra_example/tutorial001.py!} +``` + +その追加情報はそのまま出力され、JSON Schemaに追加されます。 + +## `Field`の追加引数 + +後述する`Field`、`Path`、`Query`、`Body`などでは、任意の引数を関数に渡すことでJSON Schemaの追加情報を宣言することもできます: + +```Python hl_lines="4 10 11 12 13" +{!../../../docs_src/schema_extra_example/tutorial002.py!} +``` + +!!! warning "注意" + これらの追加引数が渡されても、文書化のためのバリデーションは追加されず、注釈だけが追加されることを覚えておいてください。 + +## `Body`の追加引数 + +追加情報を`Field`に渡すのと同じように、`Path`、`Query`、`Body`などでも同じことができます。 + +例えば、`Body`にボディリクエストの`example`を渡すことができます: + +```Python hl_lines="21 22 23 24 25 26" +{!../../../docs_src/schema_extra_example/tutorial003.py!} +``` + +## ドキュメントのUIの例 + +上記のいずれの方法でも、`/docs`の中では以下のようになります: + +<img src="https://fastapi.tiangolo.com/img/tutorial/body-fields/image01.png"> + +## 技術詳細 + +`example` と `examples`について... + +JSON Schemaの最新バージョンでは<a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>というフィールドを定義していますが、OpenAPIは`examples`を持たない古いバージョンのJSON Schemaをベースにしています。 + +そのため、OpenAPIでは同じ目的のために<a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a>を独自に定義しており(`examples`ではなく`example`として)、それがdocs UI(Swagger UIを使用)で使用されています。 + +つまり、`example`はJSON Schemaの一部ではありませんが、OpenAPIの一部であり、それがdocs UIで使用されることになります。 + +## その他の情報 + +同じように、フロントエンドのユーザーインターフェースなどをカスタマイズするために、各モデルのJSON Schemaに追加される独自の追加情報を追加することができます。 From 79dbb11867f5217e090d3b498d91d9566be2fd95 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:35:41 +0000 Subject: [PATCH 1658/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2d73401ef..0b820dce1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). * 🌐 Add German translation for `docs/de/docs/tutorial/background-tasks.md`. PR [#10566](https://github.com/tiangolo/fastapi/pull/10566) by [@nilslindemann](https://github.com/nilslindemann). From c238292b44340f55df15ea48eb324288b922e85a Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:36:32 +0900 Subject: [PATCH 1659/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/extra-models.md`=20(#1941?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/extra-models.md | 195 ++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 docs/ja/docs/tutorial/extra-models.md diff --git a/docs/ja/docs/tutorial/extra-models.md b/docs/ja/docs/tutorial/extra-models.md new file mode 100644 index 000000000..aa2e5ffdc --- /dev/null +++ b/docs/ja/docs/tutorial/extra-models.md @@ -0,0 +1,195 @@ +# モデル - より詳しく + +先ほどの例に続き、複数の関連モデルを持つことが一般的です。 + +これはユーザーモデルの場合は特にそうです。なぜなら: + +* **入力モデル** にはパスワードが必要です。 +* **出力モデル**はパスワードをもつべきではありません。 +* **データベースモデル**はおそらくハッシュ化されたパスワードが必要になるでしょう。 + +!!! danger "危険" + ユーザーの平文のパスワードは絶対に保存しないでください。常に認証に利用可能な「安全なハッシュ」を保存してください。 + + 知らない方は、[セキュリティの章](security/simple-oauth2.md#password-hashing){.internal-link target=_blank}で「パスワードハッシュ」とは何かを学ぶことができます。 + +## 複数のモデル + +ここでは、パスワードフィールドをもつモデルがどのように見えるのか、また、どこで使われるのか、大まかなイメージを紹介します: + +```Python hl_lines="9 11 16 22 24 29-30 33-35 40-41" +{!../../../docs_src/extra_models/tutorial001.py!} +``` + +### `**user_in.dict()`について + +#### Pydanticの`.dict()` + +`user_in`は`UserIn`クラスのPydanticモデルです。 + +Pydanticモデルには、モデルのデータを含む`dict`を返す`.dict()`メソッドがあります。 + +そこで、以下のようなPydanticオブジェクト`user_in`を作成すると: + +```Python +user_in = UserIn(username="john", password="secret", email="john.doe@example.com") +``` + +そして呼び出すと: + +```Python +user_dict = user_in.dict() +``` + +これで変数`user_dict`のデータを持つ`dict`ができました。(これはPydanticモデルのオブジェクトの代わりに`dict`です)。 + +そして呼び出すと: + +```Python +print(user_dict) +``` + +以下のようなPythonの`dict`を得ることができます: + +```Python +{ + 'username': 'john', + 'password': 'secret', + 'email': 'john.doe@example.com', + 'full_name': None, +} +``` + +#### `dict`の展開 + +`user_dict`のような`dict`を受け取り、それを`**user_dict`を持つ関数(またはクラス)に渡すと、Pythonはそれを「展開」します。これは`user_dict`のキーと値を直接キー・バリューの引数として渡します。 + +そこで上述の`user_dict`の続きを以下のように書くと: + +```Python +UserInDB(**user_dict) +``` + +以下と同等の結果になります: + +```Python +UserInDB( + username="john", + password="secret", + email="john.doe@example.com", + full_name=None, +) +``` + +もっと正確に言えば、`user_dict`を将来的にどんな内容であっても直接使用することになります: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], +) +``` + +#### 別のモデルからつくるPydanticモデル + +上述の例では`user_in.dict()`から`user_dict`をこのコードのように取得していますが: + +```Python +user_dict = user_in.dict() +UserInDB(**user_dict) +``` + +これは以下と同等です: + +```Python +UserInDB(**user_in.dict()) +``` + +...なぜなら`user_in.dict()`は`dict`であり、`**`を付与して`UserInDB`を渡してPythonに「展開」させているからです。 + +そこで、別のPydanticモデルのデータからPydanticモデルを取得します。 + +#### `dict`の展開と追加引数 + +そして、追加のキーワード引数`hashed_password=hashed_password`を以下のように追加すると: + +```Python +UserInDB(**user_in.dict(), hashed_password=hashed_password) +``` + +...以下のようになります: + +```Python +UserInDB( + username = user_dict["username"], + password = user_dict["password"], + email = user_dict["email"], + full_name = user_dict["full_name"], + hashed_password = hashed_password, +) +``` + +!!! warning "注意" + サポートしている追加機能は、データの可能な流れをデモするだけであり、もちろん本当のセキュリティを提供しているわけではありません。 + +## 重複の削減 + +コードの重複を減らすことは、**FastAPI**の中核的なアイデアの1つです。 + +コードの重複が増えると、バグやセキュリティの問題、コードの非同期化問題(ある場所では更新しても他の場所では更新されない場合)などが発生する可能性が高くなります。 + +そして、これらのモデルは全てのデータを共有し、属性名や型を重複させています。 + +もっと良い方法があります。 + +他のモデルのベースとなる`UserBase`モデルを宣言することができます。そして、そのモデルの属性(型宣言、検証など)を継承するサブクラスを作ることができます。 + +データの変換、検証、文書化などはすべて通常通りに動作します。 + +このようにして、モデル間の違いだけを宣言することができます: + +```Python hl_lines="9 15 16 19 20 23 24" +{!../../../docs_src/extra_models/tutorial002.py!} +``` + +## `Union`または`anyOf` + +レスポンスを2つの型の`Union`として宣言することができます。 + +OpenAPIでは`anyOf`で定義されます。 + +そのためには、標準的なPythonの型ヒント<a href="https://docs.python.org/3/library/typing.html#typing.Union" class="external-link" target="_blank">`typing.Union`</a>を使用します: + +```Python hl_lines="1 14 15 18 19 20 33" +{!../../../docs_src/extra_models/tutorial003.py!} +``` + +## モデルのリスト + +同じように、オブジェクトのリストのレスポンスを宣言することができます。 + +そのためには、標準のPythonの`typing.List`を使用する: + +```Python hl_lines="1 20" +{!../../../docs_src/extra_models/tutorial004.py!} +``` + +## 任意の`dict`を持つレスポンス + +また、Pydanticモデルを使用せずに、キーと値の型だけを定義した任意の`dict`を使ってレスポンスを宣言することもできます。 + +これは、有効なフィールド・属性名(Pydanticモデルに必要なもの)を事前に知らない場合に便利です。 + +この場合、`typing.Dict`を使用することができます: + +```Python hl_lines="1 8" +{!../../../docs_src/extra_models/tutorial005.py!} +``` + +## まとめ + +複数のPydanticモデルを使用し、ケースごとに自由に継承します。 + +エンティティが異なる「状態」を持たなければならない場合は、エンティティごとに単一のデータモデルを持つ必要はありません。`password` や `password_hash` やパスワードなしなどのいくつかの「状態」をもつユーザー「エンティティ」の場合の様にすれば良いです。 From f386011d64e68c63f397834eda1c937b660f0d75 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:38:18 +0000 Subject: [PATCH 1660/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0b820dce1..bca167cd1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add German translation for `docs/de/docs/tutorial/index.md`. PR [#9502](https://github.com/tiangolo/fastapi/pull/9502) by [@fhabers21](https://github.com/fhabers21). From 17511f776891d6bbdaac2a6ba7157b24259210a4 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:38:58 +0000 Subject: [PATCH 1661/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bca167cd1..33caa2ac7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-fields.md`. PR [#1923](https://github.com/tiangolo/fastapi/pull/1923) by [@SwftAlpc](https://github.com/SwftAlpc). From 88225ae231731ff266f964f3bf5818a4397db223 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:42:08 +0900 Subject: [PATCH 1662/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/response-status-code.md`?= =?UTF-8?q?=20(#1942)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/response-status-code.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/ja/docs/tutorial/response-status-code.md diff --git a/docs/ja/docs/tutorial/response-status-code.md b/docs/ja/docs/tutorial/response-status-code.md new file mode 100644 index 000000000..ead2addda --- /dev/null +++ b/docs/ja/docs/tutorial/response-status-code.md @@ -0,0 +1,89 @@ +# レスポンスステータスコード + +レスポンスモデルを指定するのと同じ方法で、レスポンスに使用されるHTTPステータスコードを以下の*path operations*のいずれかの`status_code`パラメータで宣言することもできます。 + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* など。 + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +!!! note "備考" + `status_code`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数*のものではありません。 + +`status_code`パラメータはHTTPステータスコードを含む数値を受け取ります。 + +!!! info "情報" + `status_code`は代わりに、Pythonの<a href="https://docs.python.org/3/library/http.html#http.HTTPStatus" class="external-link" target="_blank">`http.HTTPStatus`</a>のように、`IntEnum`を受け取ることもできます。 + +これは: + +* レスポンスでステータスコードを返します。 +* OpenAPIスキーマ(およびユーザーインターフェース)に以下のように文書化します: + +<img src="https://fastapi.tiangolo.com/img/tutorial/response-status-code/image01.png"> + +!!! note "備考" + いくつかのレスポンスコード(次のセクションを参照)は、レスポンスにボディがないことを示しています。 + + FastAPIはこれを知っていて、レスポンスボディがないというOpenAPIドキュメントを生成します。 + +## HTTPステータスコードについて + +!!! note "備考" + すでにHTTPステータスコードが何であるかを知っている場合は、次のセクションにスキップしてください。 + +HTTPでは、レスポンスの一部として3桁の数字のステータスコードを送信します。 + +これらのステータスコードは、それらを認識するために関連付けられた名前を持っていますが、重要な部分は番号です。 + +つまり: + +* `100`以上は「情報」のためのものです。。直接使うことはほとんどありません。これらのステータスコードを持つレスポンスはボディを持つことができません。 +* **`200`** 以上は「成功」のレスポンスのためのものです。これらは最も利用するであろうものです。 + * `200`はデフォルトのステータスコードで、すべてが「OK」であったことを意味します。 + * 別の例としては、`201`(Created)があります。これはデータベースに新しいレコードを作成した後によく使用されます。 + * 特殊なケースとして、`204`(No Content)があります。このレスポンスはクライアントに返すコンテンツがない場合に使用されます。そしてこのレスポンスはボディを持つことはできません。 +* **`300`** 以上は「リダイレクト」のためのものです。これらのステータスコードを持つレスポンスは`304`(Not Modified)を除き、ボディを持つことも持たないこともできます。 +* **`400`** 以上は「クライアントエラー」のレスポンスのためのものです。これらは、おそらく最も多用するであろう2番目のタイプです。 + * 例えば、`404`は「Not Found」レスポンスです。 + * クライアントからの一般的なエラーについては、`400`を使用することができます。 +* `500`以上はサーバーエラーのためのものです。これらを直接使うことはほとんどありません。アプリケーションコードやサーバーのどこかで何か問題が発生した場合、これらのステータスコードのいずれかが自動的に返されます。 + +!!! tip "豆知識" + それぞれのステータスコードとどのコードが何のためのコードなのかについて詳細は<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" class="external-link" target="_blank"><abbr title="Mozilla Developer Network">MDN</abbr> HTTP レスポンスステータスコードについてのドキュメント</a>を参照してください。 + +## 名前を覚えるための近道 + +先ほどの例をもう一度見てみましょう: + +```Python hl_lines="6" +{!../../../docs_src/response_status_code/tutorial001.py!} +``` + +`201`は「作成完了」のためのステータスコードです。 + +しかし、それぞれのコードの意味を暗記する必要はありません。 + +`fastapi.status`の便利な変数を利用することができます。 + +```Python hl_lines="1 6" +{!../../../docs_src/response_status_code/tutorial002.py!} +``` + +それらは便利です。それらは同じ番号を保持しており、その方法ではエディタの自動補完を使用してそれらを見つけることができます。 + +<img src="https://fastapi.tiangolo.com/img/tutorial/response-status-code/image02.png"> + +!!! note "技術詳細" + また、`from starlette import status`を使うこともできます。 + + **FastAPI** は、`開発者の利便性を考慮して、fastapi.status`と同じ`starlette.status`を提供しています。しかし、これはStarletteから直接提供されています。 + +## デフォルトの変更 + +後に、[高度なユーザーガイド](../advanced/response-change-status-code.md){.internal-link target=_blank}で、ここで宣言しているデフォルトとは異なるステータスコードを返す方法を見ていきます。 From 217bff20cabbd47dfef43394bae72f807a44e9a7 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:43:45 +0900 Subject: [PATCH 1663/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/handling-errors.md`=20(#1?= =?UTF-8?q?953)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/handling-errors.md | 265 +++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 docs/ja/docs/tutorial/handling-errors.md diff --git a/docs/ja/docs/tutorial/handling-errors.md b/docs/ja/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..ec36e9880 --- /dev/null +++ b/docs/ja/docs/tutorial/handling-errors.md @@ -0,0 +1,265 @@ +# エラーハンドリング + +APIを使用しているクライアントにエラーを通知する必要がある状況はたくさんあります。 + +このクライアントは、フロントエンドを持つブラウザ、誰かのコード、IoTデバイスなどが考えられます。 + +クライアントに以下のようなことを伝える必要があるかもしれません: + +* クライアントにはその操作のための十分な権限がありません。 +* クライアントはそのリソースにアクセスできません。 +* クライアントがアクセスしようとしていた項目が存在しません。 +* など + +これらの場合、通常は **400**(400から499)の範囲内の **HTTPステータスコード** を返すことになります。 + +これは200のHTTPステータスコード(200から299)に似ています。これらの「200」ステータスコードは、何らかの形でリクエスト「成功」であったことを意味します。 + +400の範囲にあるステータスコードは、クライアントからのエラーがあったことを意味します。 + +**"404 Not Found"** のエラー(およびジョーク)を覚えていますか? + +## `HTTPException`の使用 + +HTTPレスポンスをエラーでクライアントに返すには、`HTTPException`を使用します。 + +### `HTTPException`のインポート + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### コード内での`HTTPException`の発生 + +`HTTPException`は通常のPythonの例外であり、APIに関連するデータを追加したものです。 + +Pythonの例外なので、`return`ではなく、`raise`です。 + +これはまた、*path operation関数*の内部で呼び出しているユーティリティ関数の内部から`HTTPException`を発生させた場合、*path operation関数*の残りのコードは実行されず、そのリクエストを直ちに終了させ、`HTTPException`からのHTTPエラーをクライアントに送信することを意味します。 + +値を返す`return`よりも例外を発生させることの利点は、「依存関係とセキュリティ」のセクションでより明確になります。 + +この例では、クライアントが存在しないIDでアイテムを要求した場合、`404`のステータスコードを持つ例外を発生させます: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### レスポンス結果 + +クライアントが`http://example.com/items/foo`(`item_id` `"foo"`)をリクエストすると、HTTPステータスコードが200で、以下のJSONレスポンスが返されます: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +しかし、クライアントが`http://example.com/items/bar`(存在しない`item_id` `"bar"`)をリクエストした場合、HTTPステータスコード404("not found"エラー)と以下のJSONレスポンスが返されます: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip "豆知識" + `HTTPException`を発生させる際には、`str`だけでなく、JSONに変換できる任意の値を`detail`パラメータとして渡すことができます。 + + `dist`や`list`などを渡すことができます。 + + これらは **FastAPI** によって自動的に処理され、JSONに変換されます。 + +## カスタムヘッダーの追加 + +例えば、いくつかのタイプのセキュリティのために、HTTPエラーにカスタムヘッダを追加できると便利な状況がいくつかあります。 + +おそらくコードの中で直接使用する必要はないでしょう。 + +しかし、高度なシナリオのために必要な場合には、カスタムヘッダーを追加することができます: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## カスタム例外ハンドラのインストール + +カスタム例外ハンドラは<a href="https://www.starlette.io/exceptions/" class="external-link" target="_blank">Starletteと同じ例外ユーティリティ</a>を使用して追加することができます。 + +あなた(または使用しているライブラリ)が`raise`するかもしれないカスタム例外`UnicornException`があるとしましょう。 + +そして、この例外をFastAPIでグローバルに処理したいと思います。 + +カスタム例外ハンドラを`@app.exception_handler()`で追加することができます: + +```Python hl_lines="5 6 7 13 14 15 16 17 18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +ここで、`/unicorns/yolo`をリクエストすると、*path operation*は`UnicornException`を`raise`します。 + +しかし、これは`unicorn_exception_handler`で処理されます。 + +そのため、HTTPステータスコードが`418`で、JSONの内容が以下のような明確なエラーを受け取ることになります: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "技術詳細" + また、`from starlette.requests import Request`と`from starlette.responses import JSONResponse`を使用することもできます。 + + **FastAPI** は開発者の利便性を考慮して、`fastapi.responses`と同じ`starlette.responses`を提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。これは`Request`と同じです。 + +## デフォルトの例外ハンドラのオーバーライド + +**FastAPI** にはいくつかのデフォルトの例外ハンドラがあります。 + +これらのハンドラは、`HTTPException`を`raise`させた場合や、リクエストに無効なデータが含まれている場合にデフォルトのJSONレスポンスを返す役割を担っています。 + +これらの例外ハンドラを独自のものでオーバーライドすることができます。 + +### リクエスト検証の例外のオーバーライド + +リクエストに無効なデータが含まれている場合、**FastAPI** は内部的に`RequestValidationError`を発生させます。 + +また、そのためのデフォルトの例外ハンドラも含まれています。 + +これをオーバーライドするには`RequestValidationError`をインポートして`@app.exception_handler(RequestValidationError)`と一緒に使用して例外ハンドラをデコレートします。 + +この例外ハンドラは`Requset`と例外を受け取ります。 + +```Python hl_lines="2 14 15 16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +これで、`/items/foo`にアクセスすると、デフォルトのJSONエラーの代わりに以下が返されます: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +以下のようなテキスト版を取得します: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +#### `RequestValidationError`と`ValidationError` + +!!! warning "注意" + これらは今のあなたにとって重要でない場合は省略しても良い技術的な詳細です。 + +`RequestValidationError`はPydanticの<a href="https://pydantic-docs.helpmanual.io/#error-handling" class="external-link" target="_blank">`ValidationError`</a>のサブクラスです。 + +**FastAPI** は`response_model`でPydanticモデルを使用していて、データにエラーがあった場合、ログにエラーが表示されるようにこれを使用しています。 + +しかし、クライアントやユーザーはそれを見ることはありません。その代わりに、クライアントはHTTPステータスコード`500`の「Internal Server Error」を受け取ります。 + +*レスポンス*やコードのどこか(クライアントの*リクエスト*ではなく)にPydanticの`ValidationError`がある場合、それは実際にはコードのバグなのでこのようにすべきです。 + +また、あなたがそれを修正している間は、セキュリティの脆弱性が露呈する場合があるため、クライアントやユーザーがエラーに関する内部情報にアクセスできないようにしてください。 + +### エラーハンドラ`HTTPException`のオーバーライド + +同様に、`HTTPException`ハンドラをオーバーライドすることもできます。 + +例えば、これらのエラーに対しては、JSONではなくプレーンテキストを返すようにすることができます: + +```Python hl_lines="3 4 9 10 11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "技術詳細" + また、`from starlette.responses import PlainTextResponse`を使用することもできます。 + + **FastAPI** は開発者の利便性を考慮して、`fastapi.responses`と同じ`starlette.responses`を提供しています。しかし、利用可能なレスポンスのほとんどはStarletteから直接提供されます。 + +### `RequestValidationError`のボディの使用 + +`RequestValidationError`には無効なデータを含む`body`が含まれています。 + +アプリ開発中に本体のログを取ってデバッグしたり、ユーザーに返したりなどに使用することができます。 + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial005.py!} +``` + +ここで、以下のような無効な項目を送信してみてください: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +受信したボディを含むデータが無効であることを示すレスポンスが表示されます: + +```JSON hl_lines="12 13 14 15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### FastAPIの`HTTPException`とStarletteの`HTTPException` + +**FastAPI**は独自の`HTTPException`を持っています。 + +また、 **FastAPI**のエラークラス`HTTPException`はStarletteのエラークラス`HTTPException`を継承しています。 + +唯一の違いは、**FastAPI** の`HTTPException`はレスポンスに含まれるヘッダを追加できることです。 + +これはOAuth 2.0といくつかのセキュリティユーティリティのために内部的に必要とされ、使用されています。 + +そのため、コード内では通常通り **FastAPI** の`HTTPException`を発生させ続けることができます。 + +しかし、例外ハンドラを登録する際には、Starletteの`HTTPException`を登録しておく必要があります。 + +これにより、Starletteの内部コードやStarletteの拡張機能やプラグインの一部が`HTTPException`を発生させた場合、ハンドラがそれをキャッチして処理することができるようになります。 + +以下の例では、同じコード内で両方の`HTTPException`を使用できるようにするために、Starletteの例外の名前を`StarletteHTTPException`に変更しています: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### **FastAPI** の例外ハンドラの再利用 + +また、何らかの方法で例外を使用することもできますが、**FastAPI** から同じデフォルトの例外ハンドラを使用することもできます。 + +デフォルトの例外ハンドラを`fastapi.exception_handlers`からインポートして再利用することができます: + +```Python hl_lines="2 3 4 5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +この例では、非常に表現力のあるメッセージでエラーを`print`しています。 + +しかし、例外を使用して、デフォルトの例外ハンドラを再利用することができるということが理解できます。 From efac3a293fecc06a4cbb933f829877cc914a95f1 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:45:27 +0900 Subject: [PATCH 1664/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/python-types.md`=20(#1899)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/python-types.md | 315 +++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 docs/ja/docs/python-types.md diff --git a/docs/ja/docs/python-types.md b/docs/ja/docs/python-types.md new file mode 100644 index 000000000..bbfef2adf --- /dev/null +++ b/docs/ja/docs/python-types.md @@ -0,0 +1,315 @@ +# Pythonの型の紹介 + +**Python 3.6以降** では「型ヒント」オプションがサポートされています。 + +これらの **"型ヒント"** は変数の<abbr title="例: str, int, float, bool">型</abbr>を宣言することができる新しい構文です。(Python 3.6以降) + +変数に型を宣言することでエディターやツールがより良いサポートを提供することができます。 + +ここではPythonの型ヒントについての **クイックチュートリアル/リフレッシュ** で、**FastAPI**でそれらを使用するために必要な最低限のことだけをカバーしています。...実際には本当に少ないです。 + +**FastAPI** はすべてこれらの型ヒントに基づいており、多くの強みと利点を与えてくれます。 + +しかしたとえまったく **FastAPI** を使用しない場合でも、それらについて少し学ぶことで利点を得ることができるでしょう。 + +!!! note "備考" + もしあなたがPythonの専門家で、すでに型ヒントについてすべて知っているのであれば、次の章まで読み飛ばしてください。 + +## 動機 + +簡単な例から始めてみましょう: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +このプログラムを実行すると以下が出力されます: + +``` +John Doe +``` + +この関数は以下のようなことを行います: + +* `first_name`と`last_name`を取得します。 +* `title()`を用いて、それぞれの最初の文字を大文字に変換します。 +* 真ん中にスペースを入れて<abbr title="次から次へと中身を入れて一つにまとめる">連結</abbr>します。 + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### 編集 + +これはとても簡単なプログラムです。 + +しかし、今、あなたがそれを一から書いていたと想像してみてください。 + +パラメータの準備ができていたら、そのとき、関数の定義を始めていたことでしょう... + +しかし、そうすると「最初の文字を大文字に変換するあのメソッド」を呼び出す必要があります。 + +それは`upper`でしたか?`uppercase`でしたか?それとも`first_uppercase`?または`capitalize`? + +そして、古くからプログラマーの友人であるエディタで自動補完を試してみます。 + +関数の最初のパラメータ`first_name`を入力し、ドット(`.`)を入力してから、`Ctrl+Space`を押すと補完が実行されます。 + +しかし、悲しいことに、これはなんの役にも立ちません: + +<img src="https://fastapi.tiangolo.com/img/python-types/image01.png"> + +### 型の追加 + +先ほどのコードから一行変更してみましょう。 + +以下の関数のパラメータ部分を: + +```Python + first_name, last_name +``` + +以下へ変更します: + +```Python + first_name: str, last_name: str +``` + +これだけです。 + +それが「型ヒント」です: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +これは、以下のようにデフォルト値を宣言するのと同じではありません: + +```Python + first_name="john", last_name="doe" +``` + +それとは別物です。 + +イコール(`=`)ではなく、コロン(`:`)を使用します。 + +そして、通常、型ヒントを追加しても、それらがない状態と起こることは何も変わりません。 + +しかし今、あなたが再びその関数を作成している最中に、型ヒントを使っていると想像してみて下さい。 + +同じタイミングで`Ctrl+Space`で自動補完を実行すると、以下のようになります: + +<img src="https://fastapi.tiangolo.com/img/python-types/image02.png"> + +これであれば、あなたは「ベルを鳴らす」一つを見つけるまで、オプションを見て、スクロールすることができます: + +<img src="https://fastapi.tiangolo.com/img/python-types/image03.png"> + +## より強い動機 + +この関数を見てください。すでに型ヒントを持っています: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +エディタは変数の型を知っているので、補完だけでなく、エラーチェックをすることもできます。 + +<img src="https://fastapi.tiangolo.com/img/python-types/image04.png"> + +これで`age`を`str(age)`で文字列に変換して修正する必要があることがわかります: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 型の宣言 + +関数のパラメータとして、型ヒントを宣言している主な場所を確認しました。 + +これは **FastAPI** で使用する主な場所でもあります。 + +### 単純な型 + +`str`だけでなく、Pythonの標準的な型すべてを宣言することができます。 + +例えば、以下を使用可能です: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 型パラメータを持つジェネリック型 + +データ構造の中には、`dict`、`list`、`set`、そして`tuple`のように他の値を含むことができるものがあります。また内部の値も独自の型を持つことができます。 + +これらの型や内部の型を宣言するには、Pythonの標準モジュール`typing`を使用します。 + +これらの型ヒントをサポートするために特別に存在しています。 + +#### `List` + +例えば、`str`の`list`の変数を定義してみましょう。 + +`typing`から`List`をインポートします(大文字の`L`を含む): + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +同じようにコロン(`:`)の構文で変数を宣言します。 + +型として、`List`を入力します。 + +リストはいくつかの内部の型を含む型なので、それらを角括弧で囲んでいます。 + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +!!! tip "豆知識" + 角括弧内の内部の型は「型パラメータ」と呼ばれています。 + + この場合、`str`は`List`に渡される型パラメータです。 + +つまり: 変数`items`は`list`であり、このリストの各項目は`str`です。 + +そうすることで、エディタはリストの項目を処理している間にもサポートを提供できます。 + +<img src="https://fastapi.tiangolo.com/img/python-types/image05.png"> + +タイプがなければ、それはほぼ不可能です。 + +変数`item`はリスト`items`の要素の一つであることに注意してください。 + +それでも、エディタはそれが`str`であることを知っていて、そのためのサポートを提供しています。 + +#### `Tuple` と `Set` + +`tuple`と`set`の宣言も同様です: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +つまり: + +* 変数`items_t`は`int`、`int`、`str`の3つの項目を持つ`tuple`です + +* 変数`items_s`はそれぞれの項目が`bytes`型である`set`です。 + +#### `Dict` + +`dict`を宣言するためには、カンマ区切りで2つの型パラメータを渡します。 + +最初の型パラメータは`dict`のキーです。 + +2番目の型パラメータは`dict`の値です。 + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +つまり: + +* 変数`prices`は`dict`であり: + * この`dict`のキーは`str`型です。(つまり、各項目の名前) + * この`dict`の値は`float`型です。(つまり、各項目の価格) + +#### `Optional` + +また、`Optional`を使用して、変数が`str`のような型を持つことを宣言することもできますが、それは「オプション」であり、`None`にすることもできます。 + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +ただの`str`の代わりに`Optional[str]`を使用することで、エディタは値が常に`str`であると仮定している場合に実際には`None`である可能性があるエラーを検出するのに役立ちます。 + +#### ジェネリック型 + +以下のように角括弧で型パラメータを取る型を: + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Optional` +* ...など + +**ジェネリック型** または **ジェネリクス** と呼びます。 + +### 型としてのクラス + +変数の型としてクラスを宣言することもできます。 + +例えば、`Person`クラスという名前のクラスがあるとしましょう: + +```Python hl_lines="1 2 3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +変数の型を`Person`として宣言することができます: + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +そして、再び、すべてのエディタのサポートを得ることができます: + +<img src="https://fastapi.tiangolo.com/img/python-types/image06.png"> + +## Pydanticのモデル + +<a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> はデータ検証を行うためのPythonライブラリです。 + +データの「形」を属性付きのクラスとして宣言します。 + +そして、それぞれの属性は型を持ちます。 + +さらに、いくつかの値を持つクラスのインスタンスを作成すると、その値を検証し、適切な型に変換して(もしそうであれば)全てのデータを持つオブジェクトを提供してくれます。 + +また、その結果のオブジェクトですべてのエディタのサポートを受けることができます。 + +Pydanticの公式ドキュメントから引用: + +```Python +{!../../../docs_src/python_types/tutorial011.py!} +``` + +!!! info "情報" + Pydanticについてより学びたい方は<a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">ドキュメントを参照してください</a>. + +**FastAPI** はすべてPydanticをベースにしています。 + +すべてのことは[チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で実際に見ることができます。 + +## **FastAPI**での型ヒント + +**FastAPI** はこれらの型ヒントを利用していくつかのことを行います。 + +**FastAPI** では型ヒントを使って型パラメータを宣言すると以下のものが得られます: + +* **エディタサポート**. +* **型チェック**. + +...そして **FastAPI** は同じように宣言をすると、以下のことを行います: + +* **要件の定義**: リクエストパスパラメータ、クエリパラメータ、ヘッダー、ボディ、依存関係などから要件を定義します。 +* **データの変換**: リクエストのデータを必要な型に変換します。 +* **データの検証**: リクエストごとに: + * データが無効な場合にクライアントに返される **自動エラー** を生成します。 +* **ドキュメント** OpenAPIを使用したAPI: + * 自動的に対話型ドキュメントのユーザーインターフェイスで使用されます。 + +すべてが抽象的に聞こえるかもしれません。心配しないでください。 この全ての動作は [チュートリアル - ユーザーガイド](tutorial/index.md){.internal-link target=_blank}で見ることができます。 + +重要なのは、Pythonの標準的な型を使うことで、(クラスやデコレータなどを追加するのではなく)1つの場所で **FastAPI** が多くの作業を代わりにやってくれているということです。 + +!!! info "情報" + すでにすべてのチュートリアルを終えて、型についての詳細を見るためにこのページに戻ってきた場合は、<a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy`のチートシートを参照してください</a> From b21599bab0a7ab38102c9e42364d1e224ad07fe3 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:46:12 +0000 Subject: [PATCH 1665/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 33caa2ac7..997d8b5aa 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-nested-models.md`. PR [#1930](https://github.com/tiangolo/fastapi/pull/1930) by [@SwftAlpc](https://github.com/SwftAlpc). From b73de83ca2edb35122624c4ec6db4120cf5e4496 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:46:32 +0900 Subject: [PATCH 1666/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/path-params-numeric-valid?= =?UTF-8?q?ations.md`=20(#1902)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../path-params-numeric-validations.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 docs/ja/docs/tutorial/path-params-numeric-validations.md diff --git a/docs/ja/docs/tutorial/path-params-numeric-validations.md b/docs/ja/docs/tutorial/path-params-numeric-validations.md new file mode 100644 index 000000000..551aeabb3 --- /dev/null +++ b/docs/ja/docs/tutorial/path-params-numeric-validations.md @@ -0,0 +1,122 @@ +# パスパラメータと数値の検証 + +クエリパラメータに対して`Query`でより多くのバリデーションとメタデータを宣言できるのと同じように、パスパラメータに対しても`Path`で同じ種類のバリデーションとメタデータを宣言することができます。 + +## Pathのインポート + +まず初めに、`fastapi`から`Path`をインポートします: + +```Python hl_lines="1" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +## メタデータの宣言 + +パラメータは`Query`と同じものを宣言することができます。 + +例えば、パスパラメータ`item_id`に対して`title`のメタデータを宣言するには以下のようにします: + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial001.py!} +``` + +!!! note "備考" + パスの一部でなければならないので、パスパラメータは常に必須です。 + + そのため、`...`を使用して必須と示す必要があります。 + + それでも、`None`で宣言しても、デフォルト値を設定しても、何の影響もなく、常に必要とされていることに変わりはありません。 + +## 必要に応じてパラメータを並び替える + +クエリパラメータ`q`を必須の`str`として宣言したいとしましょう。 + +また、このパラメータには何も宣言する必要がないので、`Query`を使う必要はありません。 + +しかし、パスパラメータ`item_id`のために`Path`を使用する必要があります。 + +Pythonは「デフォルト」を持たない値の前に「デフォルト」を持つ値を置くことができません。 + +しかし、それらを並び替えることができ、デフォルト値を持たない値(クエリパラメータ`q`)を最初に持つことができます。 + +**FastAPI**では関係ありません。パラメータは名前、型、デフォルトの宣言(`Query`、`Path`など)で検出され、順番は気にしません。 + +そのため、以下のように関数を宣言することができます: + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial002.py!} +``` + +## 必要に応じてパラメータを並び替えるトリック + +クエリパラメータ`q`を`Query`やデフォルト値なしで宣言し、パスパラメータ`item_id`を`Path`を用いて宣言し、それらを別の順番に並びたい場合、Pythonには少し特殊な構文が用意されています。 + +関数の最初のパラメータとして`*`を渡します。 + +Pythonはその`*`で何かをすることはありませんが、それ以降のすべてのパラメータがキーワード引数(キーと値のペア)として呼ばれるべきものであると知っているでしょう。それは<abbr title="From: K-ey W-ord Arg-uments"><code>kwargs</code></abbr>としても知られています。たとえデフォルト値がなくても。 + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial003.py!} +``` + +## 数値の検証: 以上 + +`Query`と`Path`(、そして後述する他のもの)を用いて、文字列の制約を宣言することができますが、数値の制約も同様に宣言できます。 + +ここで、`ge=1`の場合、`item_id`は`1`「より大きい`g`か、同じ`e`」整数でなれけばなりません。 + +```Python hl_lines="8" +{!../../../docs_src/path_params_numeric_validations/tutorial004.py!} +``` + +## 数値の検証: より大きいと小なりイコール + +以下も同様です: + +* `gt`: より大きい(`g`reater `t`han) +* `le`: 小なりイコール(`l`ess than or `e`qual) + +```Python hl_lines="9" +{!../../../docs_src/path_params_numeric_validations/tutorial005.py!} +``` + +## 数値の検証: 浮動小数点、 大なり小なり + +数値のバリデーションは`float`の値に対しても有効です。 + +ここで重要になってくるのは<abbr title="より大きい"><code>gt</code></abbr>だけでなく<abbr title="以下"><code>ge</code></abbr>も宣言できることです。これと同様に、例えば、値が`1`より小さくても`0`より大きくなければならないことを要求することができます。 + +したがって、`0.5`は有効な値ですが、`0.0`や`0`はそうではありません。 + +これは<abbr title="未満"><code>lt</code></abbr>も同じです。 + +```Python hl_lines="11" +{!../../../docs_src/path_params_numeric_validations/tutorial006.py!} +``` + +## まとめ + +`Query`と`Path`(そしてまだ見たことない他のもの)では、[クエリパラメータと文字列の検証](query-params-str-validations.md){.internal-link target=_blank}と同じようにメタデータと文字列の検証を宣言することができます。 + +また、数値のバリデーションを宣言することもできます: + +* `gt`: より大きい(`g`reater `t`han) +* `ge`: 以上(`g`reater than or `e`qual) +* `lt`: より小さい(`l`ess `t`han) +* `le`: 以下(`l`ess than or `e`qual) + +!!! info "情報" + `Query`、`Path`などは後に共通の`Param`クラスのサブクラスを見ることになります。(使う必要はありません) + + そして、それらすべては、これまで見てきた追加のバリデーションとメタデータと同じパラメータを共有しています。 + +!!! note "技術詳細" + `fastapi`から`Query`、`Path`などをインポートすると、これらは実際には関数です。 + + 呼び出されると、同じ名前のクラスのインスタンスを返します。 + + そのため、関数である`Query`をインポートし、それを呼び出すと、`Query`という名前のクラスのインスタンスが返されます。 + + これらの関数は(クラスを直接使うのではなく)エディタが型についてエラーとしないようにするために存在します。 + + この方法によって、これらのエラーを無視するための設定を追加することなく、通常のエディタやコーディングツールを使用することができます。 From eed57df6f685b73e3ac0a01060ed2eb81d71fa92 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:46:53 +0000 Subject: [PATCH 1667/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 997d8b5aa..68b2f0a07 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese tranlsation for `docs/ja/docs/tutorial/schema-extra-example.md`. PR [#1931](https://github.com/tiangolo/fastapi/pull/1931) by [@SwftAlpc](https://github.com/SwftAlpc). From a14907a47d723a21c46b06d73bf568a2643657e4 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 00:48:41 +0900 Subject: [PATCH 1668/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/body-multiple-params.md`?= =?UTF-8?q?=20(#1903)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/body-multiple-params.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/ja/docs/tutorial/body-multiple-params.md diff --git a/docs/ja/docs/tutorial/body-multiple-params.md b/docs/ja/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..2ba10c583 --- /dev/null +++ b/docs/ja/docs/tutorial/body-multiple-params.md @@ -0,0 +1,169 @@ +# ボディ - 複数のパラメータ + +これまで`Path`と`Query`をどう使うかを見てきましたが、リクエストボディの宣言のより高度な使い方を見てみましょう。 + +## `Path`、`Query`とボディパラメータを混ぜる + +まず、もちろん、`Path`と`Query`とリクエストボディのパラメータの宣言は自由に混ぜることができ、 **FastAPI** は何をするべきかを知っています。 + +また、デフォルトの`None`を設定することで、ボディパラメータをオプションとして宣言することもできます: + +```Python hl_lines="19 20 21" +{!../../../docs_src/body_multiple_params/tutorial001.py!} +``` + +!!! note "備考" + この場合、ボディから取得する`item`はオプションであることに注意してください。デフォルト値は`None`です。 + +## 複数のボディパラメータ + +上述の例では、*path operations*は`item`の属性を持つ以下のようなJSONボディを期待していました: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +しかし、`item`と`user`のように複数のボディパラメータを宣言することもできます: + +```Python hl_lines="22" +{!../../../docs_src/body_multiple_params/tutorial002.py!} +``` + +この場合、**FastAPI**は関数内に複数のボディパラメータ(Pydanticモデルである2つのパラメータ)があることに気付きます。 + +そのため、パラメータ名をボディのキー(フィールド名)として使用し、以下のようなボディを期待しています: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note "備考" + 以前と同じように`item`が宣言されていたにもかかわらず、`item`はキー`item`を持つボディの内部にあることが期待されていることに注意してください。 + +**FastAPI** はリクエストから自動で変換を行い、パラメータ`item`が特定の内容を受け取り、`user`も同じように特定の内容を受け取ります。 + +複合データの検証を行い、OpenAPIスキーマや自動ドキュメントのように文書化してくれます。 + +## ボディ内の単数値 + +クエリとパスパラメータの追加データを定義するための `Query` と `Path` があるのと同じように、 **FastAPI** は同等の `Body` を提供します。 + +例えば、前のモデルを拡張して、同じボディに `item` と `user` の他にもう一つのキー `importance` を入れたいと決めることができます。 + +単数値なのでそのまま宣言すると、**FastAPI** はそれがクエリパラメータであるとみなします。 + +しかし、`Body`を使用して、**FastAPI** に別のボディキーとして扱うように指示することができます: + + +```Python hl_lines="23" +{!../../../docs_src/body_multiple_params/tutorial003.py!} +``` + +この場合、**FastAPI** は以下のようなボディを期待します: + + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +繰り返しになりますが、データ型の変換、検証、文書化などを行います。 + +## 複数のボディパラメータとクエリ + +もちろん、ボディパラメータに加えて、必要に応じて追加のクエリパラメータを宣言することもできます。 + +デフォルトでは、単数値はクエリパラメータとして解釈されるので、明示的に `Query` を追加する必要はありません。 + +```Python +q: str = None +``` + +以下において: + +```Python hl_lines="27" +{!../../../docs_src/body_multiple_params/tutorial004.py!} +``` + +!!! info "情報" + `Body`もまた、後述する `Query` や `Path` などと同様に、すべての検証パラメータとメタデータパラメータを持っています。 + + +## 単一のボディパラメータの埋め込み + +Pydanticモデル`Item`のボディパラメータ`item`を1つだけ持っているとしましょう。 + +デフォルトでは、**FastAPI**はそのボディを直接期待します。 + +しかし、追加のボディパラメータを宣言したときのように、キー `item` を持つ JSON とその中のモデルの内容を期待したい場合は、特別な `Body` パラメータ `embed` を使うことができます: + +```Python +item: Item = Body(..., embed=True) +``` + +以下において: + +```Python hl_lines="17" +{!../../../docs_src/body_multiple_params/tutorial005.py!} +``` + +この場合、**FastAPI** は以下のようなボディを期待します: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +以下の代わりに: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## まとめ + +リクエストが単一のボディしか持てない場合でも、*path operation関数*に複数のボディパラメータを追加することができます。 + +しかし、**FastAPI** はそれを処理し、関数内の正しいデータを与え、*path operation*内の正しいスキーマを検証し、文書化します。 + +また、ボディの一部として受け取る単数値を宣言することもできます。 + +また、単一のパラメータしか宣言されていない場合でも、ボディをキーに埋め込むように **FastAPI** に指示することができます。 From 1cf1ee42fe6469b9120257b8be4a9f7bcb117177 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:48:57 +0000 Subject: [PATCH 1669/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 68b2f0a07..ebe8e1719 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/python-types.md`. PR [#1899](https://github.com/tiangolo/fastapi/pull/1899) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-models.md`. PR [#1941](https://github.com/tiangolo/fastapi/pull/1941) by [@SwftAlpc](https://github.com/SwftAlpc). From 997281bf83d3a08716c044c7cf374103bd9fc575 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:51:30 +0000 Subject: [PATCH 1670/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ebe8e1719..9ceee1fa0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#1902](https://github.com/tiangolo/fastapi/pull/1902) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/python-types.md`. PR [#1899](https://github.com/tiangolo/fastapi/pull/1899) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-status-code.md`. PR [#1942](https://github.com/tiangolo/fastapi/pull/1942) by [@SwftAlpc](https://github.com/SwftAlpc). From bf9489c0ad54634c2ea6595d0e1cfdf97b1e1a6d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 15:53:17 +0000 Subject: [PATCH 1671/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9ceee1fa0..33cd064e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-multiple-params.md`. PR [#1903](https://github.com/tiangolo/fastapi/pull/1903) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#1902](https://github.com/tiangolo/fastapi/pull/1902) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/python-types.md`. PR [#1899](https://github.com/tiangolo/fastapi/pull/1899) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/handling-errors.md`. PR [#1953](https://github.com/tiangolo/fastapi/pull/1953) by [@SwftAlpc](https://github.com/SwftAlpc). From 5c71522974e9dcec378165b64364b8c1deeecb16 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 01:01:54 +0900 Subject: [PATCH 1672/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/response-model.md`=20(#19?= =?UTF-8?q?38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: atsumi <atsumi.tatsuya@gmail.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/response-model.md | 208 ++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 docs/ja/docs/tutorial/response-model.md diff --git a/docs/ja/docs/tutorial/response-model.md b/docs/ja/docs/tutorial/response-model.md new file mode 100644 index 000000000..749b33061 --- /dev/null +++ b/docs/ja/docs/tutorial/response-model.md @@ -0,0 +1,208 @@ +# レスポンスモデル + +*path operations* のいずれにおいても、`response_model`パラメータを使用して、レスポンスのモデルを宣言することができます: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* など。 + +```Python hl_lines="17" +{!../../../docs_src/response_model/tutorial001.py!} +``` + +!!! note "備考" + `response_model`は「デコレータ」メソッド(`get`、`post`など)のパラメータであることに注意してください。すべてのパラメータやボディのように、*path operation関数* のパラメータではありません。 + +Pydanticモデルの属性に対して宣言するのと同じ型を受け取るので、Pydanticモデルになることもできますが、例えば、`List[Item]`のようなPydanticモデルの`list`になることもできます。 + +FastAPIは`response_model`を使って以下のことをします: + +* 出力データを型宣言に変換します。 +* データを検証します。 +* OpenAPIの *path operation* で、レスポンス用のJSON Schemaを追加します。 +* 自動ドキュメントシステムで使用されます。 + +しかし、最も重要なのは: + +* 出力データをモデルのデータに限定します。これがどのように重要なのか以下で見ていきましょう。 + +!!! note "技術詳細" + レスポンスモデルは、関数の戻り値のアノテーションではなく、このパラメータで宣言されています。なぜなら、パス関数は実際にはそのレスポンスモデルを返すのではなく、`dict`やデータベースオブジェクト、あるいは他のモデルを返し、`response_model`を使用してフィールドの制限やシリアライズを行うからです。 + +## 同じ入力データの返却 + +ここでは`UserIn`モデルを宣言しています。それには平文のパスワードが含まれています: + +```Python hl_lines="9 11" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +そして、このモデルを使用して入力を宣言し、同じモデルを使って出力を宣言しています: + +```Python hl_lines="17 18" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +これで、ブラウザがパスワードを使ってユーザーを作成する際に、APIがレスポンスで同じパスワードを返すようになりました。 + +この場合、ユーザー自身がパスワードを送信しているので問題ないかもしれません。 + +しかし、同じモデルを別の*path operation*に使用すると、すべてのクライアントにユーザーのパスワードを送信してしまうことになります。 + +!!! danger "危険" + ユーザーの平文のパスワードを保存したり、レスポンスで送信したりすることは絶対にしないでください。 + +## 出力モデルの追加 + +代わりに、平文のパスワードを持つ入力モデルと、パスワードを持たない出力モデルを作成することができます: + +```Python hl_lines="9 11 16" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +ここでは、*path operation関数*がパスワードを含む同じ入力ユーザーを返しているにもかかわらず: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +...`response_model`を`UserOut`と宣言したことで、パスワードが含まれていません: + +```Python hl_lines="22" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +そのため、**FastAPI** は出力モデルで宣言されていない全てのデータをフィルタリングしてくれます(Pydanticを使用)。 + +## ドキュメントを見る + +自動ドキュメントを見ると、入力モデルと出力モデルがそれぞれ独自のJSON Schemaを持っていることが確認できます。 + +<img src="https://fastapi.tiangolo.com/img/tutorial/response-model/image01.png"> + +そして、両方のモデルは、対話型のAPIドキュメントに使用されます: + +<img src="https://fastapi.tiangolo.com/img/tutorial/response-model/image02.png"> + +## レスポンスモデルのエンコーディングパラメータ + +レスポンスモデルにはデフォルト値を設定することができます: + +```Python hl_lines="11 13 14" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +* `description: str = None`は`None`がデフォルト値です。 +* `tax: float = 10.5`は`10.5`がデフォルト値です。 +* `tags: List[str] = []` は空のリスト(`[]`)がデフォルト値です。 + +しかし、実際に保存されていない場合には結果からそれらを省略した方が良いかもしれません。 + +例えば、NoSQLデータベースに多くのオプション属性を持つモデルがあるが、デフォルト値でいっぱいの非常に長いJSONレスポンスを送信したくない場合です。 + +### `response_model_exclude_unset`パラメータの使用 + +*path operation デコレータ*に`response_model_exclude_unset=True`パラメータを設定することができます: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +そして、これらのデフォルト値はレスポンスに含まれず、実際に設定された値のみが含まれます。 + +そのため、*path operation*にID`foo`が設定されたitemのリクエストを送ると、レスポンスは以下のようになります(デフォルト値を含まない): + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "情報" + FastAPIはこれをするために、Pydanticモデルの`.dict()`を<a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" class="external-link" target="_blank">その`exclude_unset`パラメータ</a>で使用しています。 + +!!! info "情報" + 以下も使用することができます: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + `exclude_defaults`と`exclude_none`については、<a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" class="external-link" target="_blank">Pydanticのドキュメント</a>で説明されている通りです。 + +#### デフォルト値を持つフィールドの値を持つデータ + +しかし、ID`bar`のitemのように、デフォルト値が設定されているモデルのフィールドに値が設定されている場合: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +それらはレスポンスに含まれます。 + +#### デフォルト値と同じ値を持つデータ + +ID`baz`のitemのようにデフォルト値と同じ値を持つデータの場合: + +```Python hl_lines="3 5 6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +FastAPIは十分に賢いので(実際には、Pydanticが十分に賢い)`description`や`tax`、`tags`はデフォルト値と同じ値を持っているにもかかわらず、明示的に設定されていることを理解しています。(デフォルトから取得するのではなく) + +そのため、それらはJSONレスポンスに含まれることになります。 + +!!! tip "豆知識" + デフォルト値は`None`だけでなく、なんでも良いことに注意してください。 + 例えば、リスト(`[]`)や`10.5`の`float`などです。 + +### `response_model_include`と`response_model_exclude` + +*path operationデコレータ*として`response_model_include`と`response_model_exclude`も使用することができます。 + +属性名を持つ`str`の`set`を受け取り、含める(残りを省略する)か、除外(残りを含む)します。 + +これは、Pydanticモデルが1つしかなく、出力からいくつかのデータを削除したい場合のクイックショートカットとして使用することができます。 + +!!! tip "豆知識" + それでも、これらのパラメータではなく、複数のクラスを使用して、上記のようなアイデアを使うことをおすすめします。 + + これは`response_model_include`や`response_mode_exclude`を使用していくつかの属性を省略しても、アプリケーションのOpenAPI(とドキュメント)で生成されたJSON Schemaが完全なモデルになるからです。 + + 同様に動作する`response_model_by_alias`にも当てはまります。 + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial005.py!} +``` + +!!! tip "豆知識" + `{"name", "description"}`の構文はこれら2つの値をもつ`set`を作成します。 + + これは`set(["name", "description"])`と同等です。 + +#### `set`の代わりに`list`を使用する + +もし`set`を使用することを忘れて、代わりに`list`や`tuple`を使用しても、FastAPIはそれを`set`に変換して正しく動作します: + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial006.py!} +``` + +## まとめ + +*path operationデコレータの*`response_model`パラメータを使用して、レスポンスモデルを定義し、特にプライベートデータがフィルタリングされていることを保証します。 + +明示的に設定された値のみを返すには、`response_model_exclude_unset`を使用します。 From 39bb4bbdfc654eab57ce2bc57286c3ea2ca39c7d Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 01:08:16 +0900 Subject: [PATCH 1673/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/dependencies/index.md`=20?= =?UTF-8?q?and=20`docs/ja/docs/tutorial/dependencies/classes-as-dependenci?= =?UTF-8?q?es.md`=20(#1958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .../dependencies/classes-as-dependencies.md | 191 ++++++++++++++++ docs/ja/docs/tutorial/dependencies/index.md | 209 ++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md create mode 100644 docs/ja/docs/tutorial/dependencies/index.md diff --git a/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md new file mode 100644 index 000000000..5c150d00c --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md @@ -0,0 +1,191 @@ +# 依存関係としてのクラス + +**依存性注入** システムを深く掘り下げる前に、先ほどの例をアップグレードしてみましょう。 + +## 前の例の`dict` + +前の例では、依存関係("dependable")から`dict`を返していました: + +```Python hl_lines="9" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +しかし、*path operation関数*のパラメータ`commons`に`dict`が含まれています。 + +また、エディタは`dict`のキーと値の型を知ることができないため、多くのサポート(補完のような)を提供することができません。 + +もっとうまくやれるはずです...。 + +## 依存関係を作るもの + +これまでは、依存関係が関数として宣言されているのを見てきました。 + +しかし、依存関係を定義する方法はそれだけではありません(その方が一般的かもしれませんが)。 + +重要なのは、依存関係が「呼び出し可能」なものであることです。 + +Pythonにおける「**呼び出し可能**」とは、Pythonが関数のように「呼び出す」ことができるものを指します。 + +そのため、`something`オブジェクト(関数ではないかもしれませんが)を持っていて、それを次のように「呼び出す」(実行する)ことができるとします: + +```Python +something() +``` + +または + +```Python +something(some_argument, some_keyword_argument="foo") +``` + +これを「呼び出し可能」なものと呼びます。 + +## 依存関係としてのクラス + +Pythonのクラスのインスタンスを作成する際に、同じ構文を使用していることに気づくかもしれません。 + +例えば: + +```Python +class Cat: + def __init__(self, name: str): + self.name = name + + +fluffy = Cat(name="Mr Fluffy") +``` + +この場合、`fluffy`は`Cat`クラスのインスタンスです。 + +そして`fluffy`を作成するために、`Cat`を「呼び出している」ことになります。 + +そのため、Pythonのクラスもまた「呼び出し可能」です。 + +そして、**FastAPI** では、Pythonのクラスを依存関係として使用することができます。 + +FastAPIが実際にチェックしているのは、それが「呼び出し可能」(関数、クラス、その他なんでも)であり、パラメータが定義されているかどうかということです。 + +**FastAPI** の依存関係として「呼び出し可能なもの」を渡すと、その「呼び出し可能なもの」のパラメータを解析し、サブ依存関係も含めて、*path operation関数*のパラメータと同じように処理します。 + +それは、パラメータが全くない呼び出し可能なものにも適用されます。パラメータのない*path operation関数*と同じように。 + +そこで、上で紹介した依存関係の`common_parameters`を`CommonQueryParams`クラスに変更します: + +```Python hl_lines="11 12 13 14 15" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +クラスのインスタンスを作成するために使用される`__init__`メソッドに注目してください: + +```Python hl_lines="12" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +...以前の`common_parameters`と同じパラメータを持っています: + +```Python hl_lines="8" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +これらのパラメータは **FastAPI** が依存関係を「解決」するために使用するものです。 + +どちらの場合も以下を持っています: + +* オプショナルの`q`クエリパラメータ。 +* `skip`クエリパラメータ、デフォルトは`0`。 +* `limit`クエリパラメータ、デフォルトは`100`。 + +どちらの場合も、データは変換され、検証され、OpenAPIスキーマなどで文書化されます。 + +## 使用 + +これで、このクラスを使用して依存関係を宣言することができます。 + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial002.py!} +``` + +**FastAPI** は`CommonQueryParams`クラスを呼び出します。これにより、そのクラスの「インスタンス」が作成され、インスタンスはパラメータ`commons`として関数に渡されます。 + +## 型注釈と`Depends` + +上のコードでは`CommonQueryParams`を2回書いていることに注目してください: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +以下にある最後の`CommonQueryParams`: + +```Python +... = Depends(CommonQueryParams) +``` + +...は、**FastAPI** が依存関係を知るために実際に使用するものです。 + +そこからFastAPIが宣言されたパラメータを抽出し、それが実際にFastAPIが呼び出すものです。 + +--- + +この場合、以下にある最初の`CommonQueryParams`: + +```Python +commons: CommonQueryParams ... +``` + +...は **FastAPI** に対して特別な意味をもちません。FastAPIはデータ変換や検証などには使用しません(それらのためには`= Depends(CommonQueryParams)`を使用しています)。 + +実際には以下のように書けばいいだけです: + +```Python +commons = Depends(CommonQueryParams) +``` + +以下にあるように: + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial003.py!} +``` + +しかし、型を宣言することは推奨されています。そうすれば、エディタは`commons`のパラメータとして何が渡されるかを知ることができ、コードの補完や型チェックなどを行うのに役立ちます: + +<img src="https://fastapi.tiangolo.com/img/tutorial/dependencies/image02.png"> + +## ショートカット + +しかし、ここでは`CommonQueryParams`を2回書くというコードの繰り返しが発生していることがわかります: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +依存関係が、クラス自体のインスタンスを作成するために**FastAPI**が「呼び出す」*特定の*クラスである場合、**FastAPI** はこれらのケースのショートカットを提供しています。 + +それらの具体的なケースについては以下のようにします: + +以下のように書く代わりに: + +```Python +commons: CommonQueryParams = Depends(CommonQueryParams) +``` + +...以下のように書きます: + +```Python +commons: CommonQueryParams = Depends() +``` + +パラメータの型として依存関係を宣言し、`Depends()`の中でパラメータを指定せず、`Depends()`をその関数のパラメータの「デフォルト」値(`=`のあとの値)として使用することで、`Depends(CommonQueryParams)`の中でクラス全体を*もう一度*書かなくてもよくなります。 + +同じ例では以下のようになります: + +```Python hl_lines="19" +{!../../../docs_src/dependencies/tutorial004.py!} +``` + +...そして **FastAPI** は何をすべきか知っています。 + +!!! tip "豆知識" + 役に立つというよりも、混乱するようであれば無視してください。それをする*必要*はありません。 + + それは単なるショートカットです。なぜなら **FastAPI** はコードの繰り返しを最小限に抑えることに気を使っているからです。 diff --git a/docs/ja/docs/tutorial/dependencies/index.md b/docs/ja/docs/tutorial/dependencies/index.md new file mode 100644 index 000000000..ec563a16d --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/index.md @@ -0,0 +1,209 @@ +# 依存関係 - 最初のステップ + +** FastAPI** は非常に強力でありながら直感的な **<abbr title="コンポーネント、リソース、プロバイダ、サービス、インジェクタブルとしても知られている">依存性注入</abbr>** システムを持っています。 + +それは非常にシンプルに使用できるように設計されており、開発者が他のコンポーネント **FastAPI** と統合するのが非常に簡単になるように設計されています。 + +## 「依存性注入」とは + +**「依存性注入」** とは、プログラミングにおいて、コード(この場合は、*path operation関数*)が動作したり使用したりするために必要なもの(「依存関係」)を宣言する方法があることを意味します: + +そして、そのシステム(この場合は、**FastAPI**)は、必要な依存関係をコードに提供するために必要なことは何でも行います(依存関係を「注入」します)。 + +これは以下のようなことが必要な時にとても便利です: + +* ロジックを共有している。(同じコードロジックを何度も繰り返している)。 +* データベース接続を共有する。 +* セキュリティ、認証、ロール要件などを強制する。 +* そのほかにも多くのこと... + +これらすべてを、コードの繰り返しを最小限に抑えながら行います。 + +## 最初のステップ + +非常にシンプルな例を見てみましょう。あまりにもシンプルなので、今のところはあまり参考にならないでしょう。 + +しかし、この方法では **依存性注入** システムがどのように機能するかに焦点を当てることができます。 + +### 依存関係の作成 + +まずは依存関係に注目してみましょう。 + +以下のように、*path operation関数*と同じパラメータを全て取ることができる関数にすぎません: + +```Python hl_lines="8 9" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +これだけです。 + +**2行**。 + +そして、それはすべての*path operation関数*が持っているのと同じ形と構造を持っています。 + +「デコレータ」を含まない(`@app.get("/some-path")`を含まない)*path operation関数*と考えることもできます。 + +そして何でも返すことができます。 + +この場合、この依存関係は以下を期待しています: + +* オプショナルのクエリパラメータ`q`は`str`です。 +* オプショナルのクエリパラメータ`skip`は`int`で、デフォルトは`0`です。 +* オプショナルのクエリパラメータ`limit`は`int`で、デフォルトは`100`です。 + +そして、これらの値を含む`dict`を返します。 + +### `Depends`のインポート + +```Python hl_lines="3" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +### "dependant"での依存関係の宣言 + +*path operation関数*のパラメータに`Body`や`Query`などを使用するのと同じように、新しいパラメータに`Depends`を使用することができます: + +```Python hl_lines="13 18" +{!../../../docs_src/dependencies/tutorial001.py!} +``` + +関数のパラメータに`Depends`を使用するのは`Body`や`Query`などと同じですが、`Depends`の動作は少し異なります。 + +`Depends`は1つのパラメータしか与えられません。 + +このパラメータは関数のようなものである必要があります。 + +そして、その関数は、*path operation関数*が行うのと同じ方法でパラメータを取ります。 + +!!! tip "豆知識" + 次の章では、関数以外の「もの」が依存関係として使用できるものを見ていきます。 + +新しいリクエストが到着するたびに、**FastAPI** が以下のような処理を行います: + +* 依存関係("dependable")関数を正しいパラメータで呼び出します。 +* 関数の結果を取得します。 +* *path operation関数*のパラメータにその結果を代入してください。 + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +この方法では、共有されるコードを一度書き、**FastAPI** が*path operations*のための呼び出しを行います。 + +!!! check "確認" + 特別なクラスを作成してどこかで **FastAPI** に渡して「登録」する必要はないことに注意してください。 + + `Depends`を渡すだけで、**FastAPI** が残りの処理をしてくれます。 + +## `async`にするかどうか + +依存関係は **FastAPI**(*path operation関数*と同じ)からも呼び出されるため、関数を定義する際にも同じルールが適用されます。 + +`async def`や通常の`def`を使用することができます。 + +また、通常の`def`*path operation関数*の中に`async def`を入れて依存関係を宣言したり、`async def`*path operation関数*の中に`def`を入れて依存関係を宣言したりすることなどができます。 + +それは重要ではありません。**FastAPI** は何をすべきかを知っています。 + +!!! note "備考" + わからない場合は、ドキュメントの[Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank}の中の`async`と`await`についてのセクションを確認してください。 + +## OpenAPIとの統合 + +依存関係(およびサブ依存関係)のすべてのリクエスト宣言、検証、および要件は、同じOpenAPIスキーマに統合されます。 + +つまり、対話型ドキュメントにはこれらの依存関係から得られる全ての情報も含まれているということです: + +<img src="https://fastapi.tiangolo.com/img/tutorial/dependencies/image01.png"> + +## 簡単な使い方 + +見てみると、*path*と*operation*が一致した時に*path operation関数*が宣言されていて、**FastAPI** が正しいパラメータで関数を呼び出してリクエストからデータを抽出する処理をしています。 + +実は、すべての(あるいはほとんどの)Webフレームワークは、このように動作します。 + +これらの関数を直接呼び出すことはありません。これらの関数はフレームワーク(この場合は、**FastAPI**)によって呼び出されます。 + +依存性注入システムでは、**FastAPI** に*path operation*もまた、*path operation関数*の前に実行されるべき他の何かに「依存」していることを伝えることができ、**FastAPI** がそれを実行し、結果を「注入」することを引き受けます。 + +他にも、「依存性注入」と同じような考えの一般的な用語があります: + +* リソース +* プロバイダ +* サービス +* インジェクタブル +* コンポーネント + +## **FastAPI** プラグイン + +統合や「プラグイン」は **依存性注入** システムを使って構築することができます。しかし、実際には、**「プラグイン」を作成する必要はありません**。依存関係を使用することで、無限の数の統合やインタラクションを宣言することができ、それが**path operation関数*で利用可能になるからです。 + +依存関係は非常にシンプルで直感的な方法で作成することができ、必要なPythonパッケージをインポートするだけで、*文字通り*数行のコードでAPI関数と統合することができます。 + +次の章では、リレーショナルデータベースやNoSQLデータベース、セキュリティなどについて、その例を見ていきます。 + +## **FastAPI** 互換性 + +依存性注入システムがシンプルなので、**FastAPI** は以下のようなものと互換性があります: + +* すべてのリレーショナルデータベース +* NoSQLデータベース +* 外部パッケージ +* 外部API +* 認証・認可システム +* API利用状況監視システム +* レスポンスデータ注入システム +* など。 + +## シンプルでパワフル + +階層依存性注入システムは、定義や使用方法が非常にシンプルであるにもかかわらず、非常に強力なものとなっています。 + +依存関係事態を定義する依存関係を定義することができます。 + +最終的には、依存関係の階層ツリーが構築され、**依存性注入**システムが、これらの依存関係(およびそのサブ依存関係)をすべて解決し、各ステップで結果を提供(注入)します。 + +例えば、4つのAPIエンドポイント(*path operations*)があるとします: + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +そして、依存関係とサブ依存関係だけで、それぞれに異なるパーミッション要件を追加することができます: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## **OpenAPI** との統合 + +これら全ての依存関係は、要件を宣言すると同時に、*path operations*にパラメータやバリデーションを追加します。 + +**FastAPI** はそれをすべてOpenAPIスキーマに追加して、対話型のドキュメントシステムに表示されるようにします。 From 8ad62bd837c0d098c6d55c35f414710946c18628 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:10:30 +0000 Subject: [PATCH 1674/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 33cd064e9..a23d367e9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-model.md`. PR [#1938](https://github.com/tiangolo/fastapi/pull/1938) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-multiple-params.md`. PR [#1903](https://github.com/tiangolo/fastapi/pull/1903) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#1902](https://github.com/tiangolo/fastapi/pull/1902) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/python-types.md`. PR [#1899](https://github.com/tiangolo/fastapi/pull/1899) by [@SwftAlpc](https://github.com/SwftAlpc). From 289fbc83badcd60c9a91a2a7c1fc0e43f951d497 Mon Sep 17 00:00:00 2001 From: tokusumi <41147016+tokusumi@users.noreply.github.com> Date: Tue, 16 Jan 2024 01:12:39 +0900 Subject: [PATCH 1675/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/background-tasks.md`=20(#?= =?UTF-8?q?2668)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/ja/docs/tutorial/background-tasks.md | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 docs/ja/docs/tutorial/background-tasks.md diff --git a/docs/ja/docs/tutorial/background-tasks.md b/docs/ja/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..6094c370f --- /dev/null +++ b/docs/ja/docs/tutorial/background-tasks.md @@ -0,0 +1,94 @@ +# バックグラウンドタスク + +レスポンスを返した *後に* 実行されるバックグラウンドタスクを定義できます。 + +これは、リクエスト後に処理を開始する必要があるが、クライアントがレスポンスを受け取る前に処理を終える必要のない操作に役立ちます。 + +これには、たとえば次のものが含まれます。 + +* 作業実行後のメール通知: + * メールサーバーへの接続とメールの送信は「遅い」(数秒) 傾向があるため、すぐにレスポンスを返し、バックグラウンドでメール通知ができます。 +* データ処理: + * たとえば、時間のかかる処理を必要とするファイル受信時には、「受信済み」(HTTP 202) のレスポンスを返し、バックグラウンドで処理できます。 + +## `BackgroundTasks` の使用 + +まず初めに、`BackgroundTasks` をインポートし、` BackgroundTasks` の型宣言と共に、*path operation 関数* のパラメーターを定義します: + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** は、`BackgroundTasks` 型のオブジェクトを作成し、そのパラメーターに渡します。 + +## タスク関数の作成 + +バックグラウンドタスクとして実行される関数を作成します。 + +これは、パラメーターを受け取ることができる単なる標準的な関数です。 + +これは `async def` または通常の `def` 関数であり、**FastAPI** はこれを正しく処理します。 + +ここで、タスク関数はファイル書き込みを実行します (メール送信のシミュレーション)。 + +また、書き込み操作では `async` と `await` を使用しないため、通常の `def` で関数を定義します。 + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## バックグラウンドタスクの追加 + +*path operations 関数* 内で、`.add_task()` メソッドを使用してタスク関数を *background tasks* オブジェクトに渡します。 + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` は以下の引数を受け取ります: + +* バックグラウンドで実行されるタスク関数 (`write_notification`)。 +* タスク関数に順番に渡す必要のある引数の列 (`email`)。 +* タスク関数に渡す必要のあるキーワード引数 (`message="some notification"`)。 + +## 依存性注入 + +`BackgroundTasks` の使用は依存性注入システムでも機能し、様々な階層 (*path operations 関数*、依存性 (依存可能性)、サブ依存性など) で `BackgroundTasks` 型のパラメーターを宣言できます。 + +**FastAPI** は、それぞれの場合の処理​​方法と同じオブジェクトの再利用方法を知っているため、すべてのバックグラウンドタスクがマージされ、バックグラウンドで後で実行されます。 + +```Python hl_lines="13 15 22 25" +{!../../../docs_src/background_tasks/tutorial002.py!} +``` + +この例では、レスポンスが送信された *後* にメッセージが `log.txt` ファイルに書き込まれます。 + +リクエストにクエリがあった場合、バックグラウンドタスクでログに書き込まれます。 + +そして、*path operations 関数* で生成された別のバックグラウンドタスクは、`email` パスパラメータを使用してメッセージを書き込みます。 + +## 技術的な詳細 + +`BackgroundTasks` クラスは、<a href="https://www.starlette.io/background/" class="external-link" target="_blank">`starlette.background`</a>から直接取得されます。 + +これは、FastAPI に直接インポート/インクルードされるため、`fastapi` からインポートできる上に、`starlette.background`から別の `BackgroundTask` (末尾に `s` がない) を誤ってインポートすることを回避できます。 + +`BackgroundTasks`のみを使用することで (`BackgroundTask` ではなく)、`Request` オブジェクトを直接使用する場合と同様に、それを *path operations 関数* パラメーターとして使用し、**FastAPI** に残りの処理を任せることができます。 + +それでも、FastAPI で `BackgroundTask` を単独で使用することは可能ですが、コード内でオブジェクトを作成し、それを含むStarlette `Response` を返す必要があります。 + +詳細については、<a href="https://www.starlette.io/background/" class="external-link" target="_blank">バックグラウンドタスクに関する Starlette の公式ドキュメント</a>を参照して下さい。 + +## 警告 + +大量のバックグラウンド計算が必要であり、必ずしも同じプロセスで実行する必要がない場合 (たとえば、メモリや変数などを共有する必要がない場合)、<a href="https://www.celeryproject.org/" class="external-link" target="_blank">Celery</a> のようなより大きな他のツールを使用するとメリットがあるかもしれません。 + +これらは、より複雑な構成、RabbitMQ や Redis などのメッセージ/ジョブキューマネージャーを必要とする傾向がありますが、複数のプロセス、特に複数のサーバーでバックグラウンドタスクを実行できます。 + +例を確認するには、[Project Generators](../project-generation.md){.internal-link target=_blank} を参照してください。これらにはすべて、Celery が構築済みです。 + +ただし、同じ **FastAPI** アプリから変数とオブジェクトにアクセスする必要がある場合、または小さなバックグラウンドタスク (電子メール通知の送信など) を実行する必要がある場合は、単に `BackgroundTasks` を使用できます。 + +## まとめ + +`BackgroundTasks` をインポートして、*path operations 関数* や依存関係のパラメータに `BackgroundTasks`を使用し、バックグラウンドタスクを追加して下さい。 From 94404fc1a087fdc6f645b56af596abbf12269bd1 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:16:10 +0000 Subject: [PATCH 1676/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a23d367e9..e8fd93cac 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/index.md` and `docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#1958](https://github.com/tiangolo/fastapi/pull/1958) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-model.md`. PR [#1938](https://github.com/tiangolo/fastapi/pull/1938) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-multiple-params.md`. PR [#1903](https://github.com/tiangolo/fastapi/pull/1903) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/path-params-numeric-validations.md`. PR [#1902](https://github.com/tiangolo/fastapi/pull/1902) by [@SwftAlpc](https://github.com/SwftAlpc). From 6f3a134f6d5798ce9da6e74f4669056cc92f77d3 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:18:40 +0000 Subject: [PATCH 1677/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e8fd93cac..ba357b127 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/background-tasks.md`. PR [#2668](https://github.com/tiangolo/fastapi/pull/2668) by [@tokusumi](https://github.com/tokusumi). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/index.md` and `docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#1958](https://github.com/tiangolo/fastapi/pull/1958) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-model.md`. PR [#1938](https://github.com/tiangolo/fastapi/pull/1938) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/body-multiple-params.md`. PR [#1903](https://github.com/tiangolo/fastapi/pull/1903) by [@SwftAlpc](https://github.com/SwftAlpc). From c68836ae46e7d78646bb4ba26d9822d21815782b Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 01:43:41 +0900 Subject: [PATCH 1678/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/dependencies/sub-dependen?= =?UTF-8?q?cies.md`=20(#1959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .../tutorial/dependencies/sub-dependencies.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/ja/docs/tutorial/dependencies/sub-dependencies.md diff --git a/docs/ja/docs/tutorial/dependencies/sub-dependencies.md b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md new file mode 100644 index 000000000..8848ac79e --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/sub-dependencies.md @@ -0,0 +1,86 @@ +# サブ依存関係 + +**サブ依存関係** を持つ依存関係を作成することができます。 + +それらは必要なだけ **深く** することができます。 + +**FastAPI** はそれらを解決してくれます。 + +### 最初の依存関係「依存可能なもの」 + +以下のような最初の依存関係(「依存可能なもの」)を作成することができます: + +```Python hl_lines="8 9" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +これはオプショナルのクエリパラメータ`q`を`str`として宣言し、それを返すだけです。 + +これは非常にシンプルです(あまり便利ではありません)が、サブ依存関係がどのように機能するかに焦点を当てるのに役立ちます。 + +### 第二の依存関係 「依存可能なもの」と「依存」 + +そして、別の依存関数(「依存可能なもの」)を作成して、同時にそれ自身の依存関係を宣言することができます(つまりそれ自身も「依存」です): + +```Python hl_lines="13" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +宣言されたパラメータに注目してみましょう: + +* この関数は依存関係(「依存可能なもの」)そのものであるにもかかわらず、別の依存関係を宣言しています(何か他のものに「依存」しています)。 + * これは`query_extractor`に依存しており、それが返す値をパラメータ`q`に代入します。 +* また、オプショナルの`last_query`クッキーを`str`として宣言します。 + * ユーザーがクエリ`q`を提供しなかった場合、クッキーに保存していた最後に使用したクエリを使用します。 + +### 依存関係の使用 + +以下のように依存関係を使用することができます: + +```Python hl_lines="21" +{!../../../docs_src/dependencies/tutorial005.py!} +``` + +!!! info "情報" + *path operation関数*の中で宣言している依存関係は`query_or_cookie_extractor`の1つだけであることに注意してください。 + + しかし、**FastAPI** は`query_extractor`を最初に解決し、その結果を`query_or_cookie_extractor`を呼び出す時に渡す必要があることを知っています。 + +```mermaid +graph TB + +query_extractor(["query_extractor"]) +query_or_cookie_extractor(["query_or_cookie_extractor"]) + +read_query["/items/"] + +query_extractor --> query_or_cookie_extractor --> read_query +``` + +## 同じ依存関係の複数回の使用 + +依存関係の1つが同じ*path operation*に対して複数回宣言されている場合、例えば、複数の依存関係が共通のサブ依存関係を持っている場合、**FastAPI** はリクエストごとに1回だけそのサブ依存関係を呼び出します。 + +そして、返された値を<abbr title="計算された値・生成された値を保存するユーティリティまたはシステム、再計算する代わりに再利用するためのもの">「キャッシュ」</abbr>に保存し、同じリクエストに対して依存関係を何度も呼び出す代わりに、特定のリクエストでそれを必要とする全ての「依存関係」に渡すことになります。 + +高度なシナリオでは、「キャッシュされた」値を使うのではなく、同じリクエストの各ステップ(おそらく複数回)で依存関係を呼び出す必要があることがわかっている場合、`Depens`を使用する際に、`use_cache=False`というパラメータを設定することができます。 + +```Python hl_lines="1" +async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)): + return {"fresh_value": fresh_value} +``` + +## まとめ + +ここで使われている派手な言葉は別にして、**依存性注入** システムは非常にシンプルです。 + +*path operation関数*と同じように見えるただの関数です。 + +しかし、それでも非常に強力で、任意の深くネストされた依存関係「グラフ」(ツリー)を宣言することができます。 + +!!! tip "豆知識" + これらの単純な例では、全てが役に立つとは言えないかもしれません。 + + しかし、**security** についての章で、それがどれほど有用であるかがわかるでしょう。 + + そして、あなたを救うコードの量もみることになるでしょう。 From 082eb21ba031ea2d432acba34f62c2c9ebfa6026 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:44:02 +0000 Subject: [PATCH 1679/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ba357b127..19a33ab73 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/sub-dependencies.md`. PR [#1959](https://github.com/tiangolo/fastapi/pull/1959) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/background-tasks.md`. PR [#2668](https://github.com/tiangolo/fastapi/pull/2668) by [@tokusumi](https://github.com/tokusumi). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/index.md` and `docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#1958](https://github.com/tiangolo/fastapi/pull/1958) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/response-model.md`. PR [#1938](https://github.com/tiangolo/fastapi/pull/1938) by [@SwftAlpc](https://github.com/SwftAlpc). From b518241c590027e2367366b89cd95eaba2c05705 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 01:44:28 +0900 Subject: [PATCH 1680/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/dependencies/dependencies?= =?UTF-8?q?-in-path-operation-decorators.md`=20(#1960)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- ...pendencies-in-path-operation-decorators.md | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md new file mode 100644 index 000000000..1684d9ca1 --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md @@ -0,0 +1,62 @@ +# path operationデコレータの依存関係 + +場合によっては*path operation関数*の中で依存関係の戻り値を本当に必要としないこともあります。 + +もしくは、依存関係が値を返さない場合もあります。 + +しかし、それでも実行・解決する必要があります。 + +このような場合、*path operation関数*のパラメータを`Depends`で宣言する代わりに、*path operation decorator*に`dependencies`の`list`を追加することができます。 + +## *path operationデコレータ*への`dependencies`の追加 + +*path operationデコレータ*はオプショナルの引数`dependencies`を受け取ります。 + +それは`Depends()`の`list`であるべきです: + +```Python hl_lines="17" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +これらの依存関係は、通常の依存関係と同様に実行・解決されます。しかし、それらの値(何かを返す場合)は*path operation関数*には渡されません。 + +!!! tip "豆知識" + エディタによっては、未使用の関数パラメータをチェックしてエラーとして表示するものもあります。 + + `dependencies`を`path operationデコレータ`で使用することで、エディタやツールのエラーを回避しながら確実に実行することができます。 + + また、コードの未使用のパラメータがあるのを見て、それが不要だと思ってしまうような新しい開発者の混乱を避けるのにも役立つかもしれません。 + +## 依存関係のエラーと戻り値 + +通常使用している依存関係の*関数*と同じものを使用することができます。 + +### 依存関係の要件 + +これらはリクエストの要件(ヘッダのようなもの)やその他のサブ依存関係を宣言することができます: + +```Python hl_lines="6 11" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 例外の発生 + +これらの依存関係は通常の依存関係と同じように、例外を`raise`発生させることができます: + +```Python hl_lines="8 13" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +### 戻り値 + +そして、値を返すことも返さないこともできますが、値は使われません。 + +つまり、すでにどこかで使っている通常の依存関係(値を返すもの)を再利用することができ、値は使われなくても依存関係は実行されます: + +```Python hl_lines="9 14" +{!../../../docs_src/dependencies/tutorial006.py!} +``` + +## *path operations*のグループに対する依存関係 + +後で、より大きなアプリケーションの構造([Bigger Applications - Multiple Files](../../tutorial/bigger-applications.md){.internal-link target=_blank})について読む時に、おそらく複数のファイルを使用して、*path operations*のグループに対して単一の`dependencies`パラメータを宣言する方法を学ぶでしょう。 From 7b462b2e69dee36db320b9977d815cdf1f8d1d2d Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 16 Jan 2024 01:45:09 +0900 Subject: [PATCH 1681/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/dependencies/dependencies?= =?UTF-8?q?-with-yield.md`=20(#1961)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ryusuke.miyaji <bluce826@gmail.com> Co-authored-by: ryuckel <36391432+ryuckel@users.noreply.github.com> Co-authored-by: tokusumi <tksmtoms@gmail.com> Co-authored-by: T. Tokusumi <41147016+tokusumi@users.noreply.github.com> Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .../dependencies/dependencies-with-yield.md | 225 ++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md diff --git a/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..2a89e51d2 --- /dev/null +++ b/docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,225 @@ +# yieldを持つ依存関係 + +FastAPIは、いくつかの<abbr title='時々"exit"、"cleanup"、"teardown"、"close"、"context managers"、 ...のように呼ばれる'>終了後の追加のステップ</abbr>を行う依存関係をサポートしています。 + +これを行うには、`return`の代わりに`yield`を使い、その後に追加のステップを書きます。 + +!!! tip "豆知識" + `yield`は必ず一度だけ使用するようにしてください。 + +!!! info "情報" + これを動作させるには、**Python 3.7** 以上を使用するか、**Python 3.6** では"backports"をインストールする必要があります: + + ``` + pip install async-exit-stack async-generator + ``` + + これにより<a href="https://github.com/sorcio/async_exit_stack" class="external-link" target="_blank">async-exit-stack</a>と<a href="https://github.com/python-trio/async_generator" class="external-link" target="_blank">async-generator</a>がインストールされます。 + +!!! note "技術詳細" + 以下と一緒に使用できる関数なら何でも有効です: + + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a>または + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> + + これらは **FastAPI** の依存関係として使用するのに有効です。 + + 実際、FastAPIは内部的にこれら2つのデコレータを使用しています。 + +## `yield`を持つデータベースの依存関係 + +例えば、これを使ってデータベースセッションを作成し、終了後にそれを閉じることができます。 + +レスポンスを送信する前に`yield`文を含む前のコードのみが実行されます。 + +```Python hl_lines="2 3 4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +生成された値は、*path operations*や他の依存関係に注入されるものです: + +```Python hl_lines="4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +`yield`文に続くコードは、レスポンスが送信された後に実行されます: + +```Python hl_lines="5 6" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +!!! tip "豆知識" + `async`や通常の関数を使用することができます。 + + **FastAPI** は、通常の依存関係と同じように、それぞれで正しいことを行います。 + +## `yield`と`try`を持つ依存関係 + +`yield`を持つ依存関係で`try`ブロックを使用した場合、その依存関係を使用した際に発生した例外を受け取ることになります。 + +例えば、途中のどこかの時点で、別の依存関係や*path operation*の中で、データベーストランザクションを「ロールバック」したり、その他のエラーを作成したりするコードがあった場合、依存関係の中で例外を受け取ることになります。 + +そのため、依存関係の中にある特定の例外を`except SomeException`で探すことができます。 + +同様に、`finally`を用いて例外があったかどうかにかかわらず、終了ステップを確実に実行することができます。 + +```Python hl_lines="3 5" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +## `yield`を持つサブ依存関係 + +任意の大きさや形のサブ依存関係やサブ依存関係の「ツリー」を持つことができ、その中で`yield`を使用することができます。 + +**FastAPI** は、`yield`を持つ各依存関係の「終了コード」が正しい順番で実行されていることを確認します。 + +例えば、`dependency_c`は`dependency_b`と`dependency_b`に依存する`dependency_a`に、依存することができます: + +```Python hl_lines="4 12 20" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +そして、それらはすべて`yield`を使用することができます。 + +この場合、`dependency_c`は終了コードを実行するために、`dependency_b`(ここでは`dep_b`という名前)の値がまだ利用可能である必要があります。 + +そして、`dependency_b`は`dependency_a`(ここでは`dep_a`という名前)の値を終了コードで利用できるようにする必要があります。 + +```Python hl_lines="16 17 24 25" +{!../../../docs_src/dependencies/tutorial008.py!} +``` + +同様に、`yield`と`return`が混在した依存関係を持つこともできます。 + +また、単一の依存関係を持っていて、`yield`などの他の依存関係をいくつか必要とすることもできます。 + +依存関係の組み合わせは自由です。 + +**FastAPI** は、全てが正しい順序で実行されていることを確認します。 + +!!! note "技術詳細" + これはPythonの<a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">Context Managers</a>のおかげで動作します。 + + **FastAPI** はこれを実現するために内部的に使用しています。 + +## `yield`と`HTTPException`を持つ依存関係 + +`yield`と例外をキャッチする`try`ブロックを持つことができる依存関係を使用することができることがわかりました。 + +`yield`の後の終了コードで`HTTPException`などを発生させたくなるかもしれません。しかし**それはうまくいきません** + +`yield`を持つ依存関係の終了コードは[例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}の*後に*実行されます。依存関係によって投げられた例外を終了コード(`yield`の後)でキャッチするものはなにもありません。 + +つまり、`yield`の後に`HTTPException`を発生させた場合、`HTTTPException`をキャッチしてHTTP 400のレスポンスを返すデフォルトの(あるいは任意のカスタムの)例外ハンドラは、その例外をキャッチすることができなくなります。 + +これは、依存関係に設定されているもの(例えば、DBセッション)を、例えば、バックグラウンドタスクで使用できるようにするものです。 + +バックグラウンドタスクはレスポンスが送信された*後*に実行されます。そのため、*すでに送信されている*レスポンスを変更する方法すらないので、`HTTPException`を発生させる方法はありません。 + +しかし、バックグラウンドタスクがDBエラーを発生させた場合、少なくとも`yield`で依存関係のセッションをロールバックしたり、きれいに閉じたりすることができ、エラーをログに記録したり、リモートのトラッキングシステムに報告したりすることができます。 + +例外が発生する可能性があるコードがある場合は、最も普通の「Python流」なことをして、コードのその部分に`try`ブロックを追加してください。 + +レスポンスを返したり、レスポンスを変更したり、`HTTPException`を発生させたりする*前に*処理したいカスタム例外がある場合は、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成してください。 + +!!! tip "豆知識" + `HTTPException`を含む例外は、`yield`の*前*でも発生させることができます。ただし、後ではできません。 + +実行の順序は多かれ少なかれ以下の図のようになります。時間は上から下へと流れていきます。そして、各列はコードを相互作用させたり、実行したりしている部分の一つです。 + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> handler: Raise HTTPException + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +!!! info "情報" + **1つのレスポンス** だけがクライアントに送信されます。それはエラーレスポンスの一つかもしれませんし、*path operation*からのレスポンスかもしれません。 + + いずれかのレスポンスが送信された後、他のレスポンスを送信することはできません。 + +!!! tip "豆知識" + この図は`HTTPException`を示していますが、[カスタム例外ハンドラ](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}を作成することで、他の例外を発生させることもできます。そして、その例外は依存関係の終了コードではなく、そのカスタム例外ハンドラによって処理されます。 + + しかし例外ハンドラで処理されない例外を発生させた場合は、依存関係の終了コードで処理されます。 + +## コンテキストマネージャ + +### 「コンテキストマネージャ」とは + +「コンテキストマネージャ」とは、`with`文の中で使用できるPythonオブジェクトのことです。 + +例えば、<a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank">ファイルを読み込むには`with`を使用することができます</a>: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +その後の`open("./somefile.txt")`は「コンテキストマネージャ」と呼ばれるオブジェクトを作成します。 + +`with`ブロックが終了すると、例外があったとしてもファイルを確かに閉じます。 + +`yield`を依存関係を作成すると、**FastAPI** は内部的にそれをコンテキストマネージャに変換し、他の関連ツールと組み合わせます。 + +### `yield`を持つ依存関係でのコンテキストマネージャの使用 + +!!! warning "注意" + これは多かれ少なかれ、「高度な」発想です。 + + **FastAPI** を使い始めたばかりの方は、とりあえずスキップした方がよいかもしれません。 + +Pythonでは、<a href="https://docs.python.org/3/reference/datamodel.html#context-managers" class="external-link" target="_blank">以下の2つのメソッドを持つクラスを作成する: `__enter__()`と`__exit__()`</a>ことでコンテキストマネージャを作成することができます。 + +また、依存関数の中で`with`や`async with`文を使用することによって`yield`を持つ **FastAPI** の依存関係の中でそれらを使用することができます: + +```Python hl_lines="1 2 3 4 5 6 7 8 9 13" +{!../../../docs_src/dependencies/tutorial010.py!} +``` + +!!! tip "豆知識" + コンテキストマネージャを作成するもう一つの方法はwithです: + + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> または + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> + + これらを使って、関数を単一の`yield`でデコレートすることができます。 + + これは **FastAPI** が内部的に`yield`を持つ依存関係のために使用しているものです。 + + しかし、FastAPIの依存関係にデコレータを使う必要はありません(そして使うべきではありません)。 + + FastAPIが内部的にやってくれます。 From e500f994035b0caa675d2bd65c3ae11d9b848a95 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:45:17 +0000 Subject: [PATCH 1682/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 19a33ab73..baa10754b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#1960](https://github.com/tiangolo/fastapi/pull/1960) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/sub-dependencies.md`. PR [#1959](https://github.com/tiangolo/fastapi/pull/1959) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/background-tasks.md`. PR [#2668](https://github.com/tiangolo/fastapi/pull/2668) by [@tokusumi](https://github.com/tokusumi). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/index.md` and `docs/ja/docs/tutorial/dependencies/classes-as-dependencies.md`. PR [#1958](https://github.com/tiangolo/fastapi/pull/1958) by [@SwftAlpc](https://github.com/SwftAlpc). From 2c670325af38a738d7cd8eecd622be77f900c6d8 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 16:47:21 +0000 Subject: [PATCH 1683/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 baa10754b..5d6c55709 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -43,6 +43,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#1961](https://github.com/tiangolo/fastapi/pull/1961) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#1960](https://github.com/tiangolo/fastapi/pull/1960) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/sub-dependencies.md`. PR [#1959](https://github.com/tiangolo/fastapi/pull/1959) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/background-tasks.md`. PR [#2668](https://github.com/tiangolo/fastapi/pull/2668) by [@tokusumi](https://github.com/tokusumi). From 8450dc204d806bac021c6a2432d7b4a0749e77cd Mon Sep 17 00:00:00 2001 From: Rafal Skolasinski <r.j.skolasinski@gmail.com> Date: Mon, 15 Jan 2024 20:41:47 +0000 Subject: [PATCH 1684/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20in?= =?UTF-8?q?=20`fastapi/security/oauth2.py`=20(#10972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/security/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 9281dfb64..be3e18cd8 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -353,7 +353,7 @@ class OAuth2(SecurityBase): bool, Doc( """ - By default, if no HTTP Auhtorization header is provided, required for + By default, if no HTTP Authorization header is provided, required for OAuth2 authentication, it will automatically cancel the request and send the client an error. From 2ce4c102fb64efd3e59b84359ddcebaaa21003ce Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 20:42:07 +0000 Subject: [PATCH 1685/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5d6c55709..ed9fdf03b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* ✏️ Fix typo in `fastapi/security/oauth2.py`. PR [#10972](https://github.com/tiangolo/fastapi/pull/10972) by [@RafalSkolasinski](https://github.com/RafalSkolasinski). * 📝 Update `HTTPException` details in `docs/en/docs/tutorial/handling-errors.md`. PR [#5418](https://github.com/tiangolo/fastapi/pull/5418) by [@papb](https://github.com/papb). * ✏️ A few tweaks in `docs/de/docs/tutorial/first-steps.md`. PR [#10959](https://github.com/tiangolo/fastapi/pull/10959) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix link in `docs/en/docs/advanced/async-tests.md`. PR [#10960](https://github.com/tiangolo/fastapi/pull/10960) by [@nilslindemann](https://github.com/nilslindemann). From 75ea31c79e9e744832570ad0f42ff8572e9fd0dd Mon Sep 17 00:00:00 2001 From: ChanHaeng Lee <61987505+2chanhaeng@users.noreply.github.com> Date: Tue, 16 Jan 2024 06:40:57 +0900 Subject: [PATCH 1686/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20typo=20err?= =?UTF-8?q?or=20in=20`docs/ko/docs/tutorial/path-params.md`=20(#10758)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/path-params.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ko/docs/tutorial/path-params.md b/docs/ko/docs/tutorial/path-params.md index 5cf397e7a..8ebd0804e 100644 --- a/docs/ko/docs/tutorial/path-params.md +++ b/docs/ko/docs/tutorial/path-params.md @@ -101,7 +101,7 @@ ## 순서 문제 -*경로 동작*을 만들때 고정 경로를 갖고 있는 상황들을 맞닦뜨릴 수 있습니다. +*경로 동작*을 만들때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. `/users/me`처럼, 현재 사용자의 데이터를 가져온다고 합시다. From ae92e563b1ba0d00f1aaf2b4a472f619038bf24c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 15 Jan 2024 21:41:21 +0000 Subject: [PATCH 1687/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ed9fdf03b..01fc71d3d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -44,6 +44,7 @@ hide: ### Translations +* ✏️ Fix typo error in `docs/ko/docs/tutorial/path-params.md`. PR [#10758](https://github.com/tiangolo/fastapi/pull/10758) by [@2chanhaeng](https://github.com/2chanhaeng). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#1961](https://github.com/tiangolo/fastapi/pull/1961) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#1960](https://github.com/tiangolo/fastapi/pull/1960) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/sub-dependencies.md`. PR [#1959](https://github.com/tiangolo/fastapi/pull/1959) by [@SwftAlpc](https://github.com/SwftAlpc). From d1e533e3705f742c7f6abaf1d68f3af75d5036e0 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 16 Jan 2024 13:11:15 +0100 Subject: [PATCH 1688/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Tweak=20the=20ge?= =?UTF-8?q?rman=20translation=20of=20`docs/de/docs/tutorial/index.md`=20(#?= =?UTF-8?q?10962)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/index.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/de/docs/tutorial/index.md b/docs/de/docs/tutorial/index.md index dd7ed43bd..93a30d1b3 100644 --- a/docs/de/docs/tutorial/index.md +++ b/docs/de/docs/tutorial/index.md @@ -1,6 +1,6 @@ -# Tutorial - Benutzerhandbuch - Intro +# Tutorial – Benutzerhandbuch -Diese Anleitung zeigt Ihnen Schritt für Schritt, wie Sie **FastAPI** mit den meisten Funktionen nutzen können. +Dieses Tutorial zeigt Ihnen Schritt für Schritt, wie Sie **FastAPI** und die meisten seiner Funktionen verwenden können. Jeder Abschnitt baut schrittweise auf den vorhergehenden auf. Diese Abschnitte sind aber nach einzelnen Themen gegliedert, sodass Sie direkt zu einem bestimmten Thema übergehen können, um Ihre speziellen API-Anforderungen zu lösen. @@ -12,7 +12,7 @@ Dadurch können Sie jederzeit zurückkommen und sehen genau das, was Sie benöt Alle Codeblöcke können kopiert und direkt verwendet werden (da es sich um getestete Python-Dateien handelt). -Um eines der Beispiele auszuführen, kopieren Sie den Code in die Datei `main.py`, und starten Sie `uvicorn` mit: +Um eines der Beispiele auszuführen, kopieren Sie den Code in eine Datei `main.py`, und starten Sie `uvicorn` mit: <div class="termy"> @@ -50,31 +50,31 @@ $ pip install "fastapi[all]" </div> -...dies beinhaltet auch `uvicorn`, das Sie als Server verwenden können, auf dem Ihr Code läuft. +... das beinhaltet auch `uvicorn`, welchen Sie als Server verwenden können, der ihren Code ausführt. -!!! Hinweis - Sie können die Installation auch in einzelnen Schritten ausführen. +!!! note "Hinweis" + Sie können die einzelnen Teile auch separat installieren. - Dies werden Sie wahrscheinlich tun, wenn Sie Ihre Anwendung produktiv einsetzen möchten: + Das folgende würden Sie wahrscheinlich tun, wenn Sie Ihre Anwendung in der Produktion einsetzen: ``` pip install fastapi ``` - Installieren Sie auch `uvicorn`, dies arbeitet als Server: + Installieren Sie auch `uvicorn` als Server: ``` pip install "uvicorn[standard]" ``` - Dasselbe gilt für jede der optionalen Abhängigkeiten, die Sie verwenden möchten. + Das gleiche gilt für jede der optionalen Abhängigkeiten, die Sie verwenden möchten. -## Erweitertes Benutzerhandbuch +## Handbuch für fortgeschrittene Benutzer -Zusätzlich gibt es ein **Erweitertes Benutzerhandbuch**, dies können Sie später nach diesem **Tutorial - Benutzerhandbuch** lesen. +Es gibt auch ein **Handbuch für fortgeschrittene Benutzer**, welches Sie später nach diesem **Tutorial – Benutzerhandbuch** lesen können. -Das **Erweiterte Benutzerhandbuch** baut auf dieses Tutorial auf, verwendet dieselben Konzepte und bringt Ihnen zusätzliche Funktionen bei. +Das **Handbuch für fortgeschrittene Benutzer** baut auf diesem Tutorial auf, verwendet dieselben Konzepte und bringt Ihnen einige zusätzliche Funktionen bei. -Allerdings sollten Sie zuerst das **Tutorial - Benutzerhandbuch** lesen (was Sie gerade lesen). +Allerdings sollten Sie zuerst das **Tutorial – Benutzerhandbuch** lesen (was Sie hier gerade tun). -Es ist so konzipiert, dass Sie nur mit dem **Tutorial - Benutzerhandbuch** eine vollständige Anwendung erstellen können und diese dann je nach Bedarf mit einigen der zusätzlichen Ideen aus dem **Erweiterten Benutzerhandbuch** erweitern können. +Die Dokumentation ist so konzipiert, dass Sie mit dem **Tutorial – Benutzerhandbuch** eine vollständige Anwendung erstellen können und diese dann je nach Bedarf mit einigen der zusätzlichen Ideen aus dem **Handbuch für fortgeschrittene Benutzer** vervollständigen können. From d761a29908aed90703d7c561f446469104903679 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 16 Jan 2024 12:11:43 +0000 Subject: [PATCH 1689/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 01fc71d3d..e42e1baf4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -44,6 +44,7 @@ hide: ### Translations +* ✏️ Tweak the german translation of `docs/de/docs/tutorial/index.md`. PR [#10962](https://github.com/tiangolo/fastapi/pull/10962) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo error in `docs/ko/docs/tutorial/path-params.md`. PR [#10758](https://github.com/tiangolo/fastapi/pull/10758) by [@2chanhaeng](https://github.com/2chanhaeng). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#1961](https://github.com/tiangolo/fastapi/pull/1961) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md`. PR [#1960](https://github.com/tiangolo/fastapi/pull/1960) by [@SwftAlpc](https://github.com/SwftAlpc). From 950d9ce74dd2efd73572dfb6d0631ce9687ce14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 16 Jan 2024 14:23:25 +0100 Subject: [PATCH 1690/1881] =?UTF-8?q?=F0=9F=93=9D=20Deprecate=20old=20tuto?= =?UTF-8?q?rials:=20Peewee,=20Couchbase,=20encode/databases=20(#10979)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/how-to/async-sql-encode-databases.md | 5 ++++- docs/en/docs/how-to/nosql-databases-couchbase.md | 5 ++++- docs/en/docs/how-to/sql-databases-peewee.md | 5 ++++- docs/en/mkdocs.yml | 7 ++++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/en/docs/how-to/async-sql-encode-databases.md b/docs/en/docs/how-to/async-sql-encode-databases.md index 0e2ccce78..c7b340d67 100644 --- a/docs/en/docs/how-to/async-sql-encode-databases.md +++ b/docs/en/docs/how-to/async-sql-encode-databases.md @@ -1,4 +1,4 @@ -# Async SQL (Relational) Databases with Encode/Databases +# ~~Async SQL (Relational) Databases with Encode/Databases~~ (deprecated) !!! info These docs are about to be updated. 🎉 @@ -7,6 +7,9 @@ The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> once it is updated to use Pydantic v2 as well. +!!! warning "Deprecated" + This tutorial is deprecated and will be removed in a future version. + You can also use <a href="https://github.com/encode/databases" class="external-link" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`. It is compatible with: diff --git a/docs/en/docs/how-to/nosql-databases-couchbase.md b/docs/en/docs/how-to/nosql-databases-couchbase.md index ae6ad604b..563318984 100644 --- a/docs/en/docs/how-to/nosql-databases-couchbase.md +++ b/docs/en/docs/how-to/nosql-databases-couchbase.md @@ -1,4 +1,4 @@ -# NoSQL (Distributed / Big Data) Databases with Couchbase +# ~~NoSQL (Distributed / Big Data) Databases with Couchbase~~ (deprecated) !!! info These docs are about to be updated. 🎉 @@ -7,6 +7,9 @@ The new docs will hopefully use Pydantic v2 and will use <a href="https://art049.github.io/odmantic/" class="external-link" target="_blank">ODMantic</a> with MongoDB. +!!! warning "Deprecated" + This tutorial is deprecated and will be removed in a future version. + **FastAPI** can also be integrated with any <abbr title="Distributed database (Big Data), also 'Not Only SQL'">NoSQL</abbr>. Here we'll see an example using **<a href="https://www.couchbase.com/" class="external-link" target="_blank">Couchbase</a>**, a <abbr title="Document here refers to a JSON object (a dict), with keys and values, and those values can also be other JSON objects, arrays (lists), numbers, strings, booleans, etc.">document</abbr> based NoSQL database. diff --git a/docs/en/docs/how-to/sql-databases-peewee.md b/docs/en/docs/how-to/sql-databases-peewee.md index 74a28b170..7211f7ed3 100644 --- a/docs/en/docs/how-to/sql-databases-peewee.md +++ b/docs/en/docs/how-to/sql-databases-peewee.md @@ -1,4 +1,7 @@ -# SQL (Relational) Databases with Peewee +# ~~SQL (Relational) Databases with Peewee~~ (deprecated) + +!!! warning "Deprecated" + This tutorial is deprecated and will be removed in a future version. !!! warning If you are just starting, the tutorial [SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank} that uses SQLAlchemy should be enough. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 92d081aa1..b8d27a35b 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -174,9 +174,6 @@ nav: - How To - Recipes: - how-to/index.md - how-to/general.md - - how-to/sql-databases-peewee.md - - how-to/async-sql-encode-databases.md - - how-to/nosql-databases-couchbase.md - how-to/graphql.md - how-to/custom-request-and-route.md - how-to/conditional-openapi.md @@ -184,6 +181,9 @@ nav: - how-to/separate-openapi-schemas.md - how-to/custom-docs-ui-assets.md - how-to/configure-swagger-ui.md + - how-to/sql-databases-peewee.md + - how-to/async-sql-encode-databases.md + - how-to/nosql-databases-couchbase.md - Reference (Code API): - reference/index.md - reference/fastapi.md @@ -242,6 +242,7 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format '' pymdownx.tabbed: alternate_style: true + pymdownx.tilde: attr_list: null md_in_html: null extra: From fc8ea413eb8a6370c3b41de7ccad6003bf37ab13 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 16 Jan 2024 13:23:49 +0000 Subject: [PATCH 1691/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 e42e1baf4..a96954527 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Deprecate old tutorials: Peewee, Couchbase, encode/databases. PR [#10979](https://github.com/tiangolo/fastapi/pull/10979) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/security/oauth2.py`. PR [#10972](https://github.com/tiangolo/fastapi/pull/10972) by [@RafalSkolasinski](https://github.com/RafalSkolasinski). * 📝 Update `HTTPException` details in `docs/en/docs/tutorial/handling-errors.md`. PR [#5418](https://github.com/tiangolo/fastapi/pull/5418) by [@papb](https://github.com/papb). * ✏️ A few tweaks in `docs/de/docs/tutorial/first-steps.md`. PR [#10959](https://github.com/tiangolo/fastapi/pull/10959) by [@nilslindemann](https://github.com/nilslindemann). From df09e0a3f6061de4bf63b8ab7ea61b6cdd70d4fd Mon Sep 17 00:00:00 2001 From: Max Su <a0025071@gmail.com> Date: Thu, 18 Jan 2024 01:15:27 +0800 Subject: [PATCH 1692/1881] =?UTF-8?q?=F0=9F=8C=90=20Initialize=20translati?= =?UTF-8?q?ons=20for=20Traditional=20Chinese=20(#10505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/mkdocs.yml | 2 + docs/zh-hant/docs/index.md | 470 +++++++++++++++++++++++++++++++++++++ docs/zh-hant/mkdocs.yml | 1 + scripts/docs.py | 2 - 4 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 docs/zh-hant/docs/index.md create mode 100644 docs/zh-hant/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index b8d27a35b..fcac555eb 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -303,6 +303,8 @@ extra: name: yo - Yorùbá - link: /zh/ name: zh - 汉语 + - link: /zh-hant/ + name: zh - 繁體中文 - link: /em/ name: 😉 extra_css: diff --git a/docs/zh-hant/docs/index.md b/docs/zh-hant/docs/index.md new file mode 100644 index 000000000..e7a2efec9 --- /dev/null +++ b/docs/zh-hant/docs/index.md @@ -0,0 +1,470 @@ +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>FastAPI 框架,高效能,易於學習,快速開發,適用於生產環境</em> +</p> +<p align="center"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> +</a> +<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/fastapi" target="_blank"> + <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/fastapi.svg" alt="Coverage"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Package version"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Supported Python versions"> +</a> +</p> + +--- + +**文件**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**程式碼**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- + +FastAPI 是一個現代、快速(高效能)的 web 框架,用於 Python 3.8+ 並採用標準 Python 型別提示。 + +主要特點包含: + +- **快速**: 非常高的效能,可與 **NodeJS** 和 **Go** 效能相當 (歸功於 Starlette and Pydantic)。 [FastAPI 是最快的 Python web 框架之一](#performance)。 +- **極速開發**: 提高開發功能的速度約 200% 至 300%。 \* +- **更少的 Bug**: 減少約 40% 的人為(開發者)導致的錯誤。 \* +- **直覺**: 具有出色的編輯器支援,處處都有<abbr title="也被稱為自動完成、IntelliSense">自動補全</abbr>以減少偵錯時間。 +- **簡單**: 設計上易於使用和學習,大幅減少閱讀文件的時間。 +- **簡潔**: 最小化程式碼重複性。可以通過不同的參數聲明來實現更豐富的功能,和更少的錯誤。 +- **穩健**: 立即獲得生產級可用的程式碼,還有自動生成互動式文件。 +- **標準化**: 基於 (且完全相容於) OpenAPIs 的相關標準:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>(之前被稱為 Swagger)和<a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>。 + +<small>\* 基於內部開發團隊在建立生產應用程式時的測試預估。</small> + +## 贊助 + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">其他贊助商</a> + +## 評價 + +"_[...] 近期大量的使用 **FastAPI**。 [...] 目前正在計畫在**微軟**團隊的**機器學習**服務中導入。其中一些正在整合到核心的 **Windows** 產品和一些 **Office** 產品。_" + +<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_我們使用 **FastAPI** 來建立產生**預測**結果的 **REST** 伺服器。 [for Ludwig]_" + +<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** 很榮幸地宣布開源**危機管理**協調框架: **Dispatch**! [是使用 **FastAPI** 建構]_" + +<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_我對 **FastAPI** 興奮得不得了。它太有趣了!_" + +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"_老實說,你建造的東西看起來非常堅固和精緻。在很多方面,這就是我想要的,看到有人建造它真的很鼓舞人心。_" + +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"_如果您想學習一種用於構建 REST API 的**現代框架**,不能錯過 **FastAPI** [...] 它非常快速、且易於使用和學習 [...]_" + +"_我們的 **APIs** 已經改用 **FastAPI** [...] 我想你會喜歡它 [...]_" + +<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> 創辦人 - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +"_如果有人想要建立一個生產環境的 Python API,我強烈推薦 **FastAPI**,它**設計精美**,**使用簡單**且**高度可擴充**,它已成為我們 API 優先開發策略中的**關鍵組件**,並且驅動了許多自動化服務,例如我們的 Virtual TAC Engineer。_" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**,命令列中的 FastAPI + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +如果你不是在開發網頁 API,而是正在開發一個在終端機中運行的<abbr title="Command Line Interface">命令列</abbr>應用程式,不妨嘗試 <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>。 + +**Typer** 是 FastAPI 的小兄弟。他立志成為命令列的 **FastAPI**。 ⌨️ 🚀 + +## 安裝需求 + +Python 3.8+ + +FastAPI 是站在以下巨人的肩膀上: + +- <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> 負責網頁的部分 +- <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> 負責資料的部分 + +## 安裝 + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +你同時也會需要 ASGI 伺服器用於生產環境,像是 <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> 或 <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>。 + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +</div> + +## 範例 + +### 建立 + +- 建立一個 python 檔案 `main.py`,並寫入以下程式碼: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>或可以使用 <code>async def</code>...</summary> + +如果你的程式使用 `async` / `await`,請使用 `async def`: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**注意**: + +如果你不知道是否會用到,可以查看 _"In a hurry?"_ 章節中,關於 <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` 和 `await` 的部分</a>。 + +</details> + +### 運行 + +使用以下指令運行伺服器: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary>關於指令 <code>uvicorn main:app --reload</code>...</summary> + +該指令 `uvicorn main:app` 指的是: + +- `main`:`main.py` 檔案(一個 python 的 "模組")。 +- `app`:在 `main.py` 檔案中,使用 `app = FastAPI()` 建立的物件。 +- `--reload`:程式碼更改後會自動重新啟動,請僅在開發時使用此參數。 + +</details> + +### 檢查 + +使用瀏覽器開啟 <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>。 + +你將會看到以下的 JSON 回應: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +你已經建立了一個具有以下功能的 API: + +- 透過路徑 `/` 和 `/items/{item_id}` 接受 HTTP 請求。 +- 以上路經都接受 `GET` <em>請求</em>(也被稱為 HTTP _方法_)。 +- 路徑 `/items/{item_id}` 有一個 `int` 型別的 `item_id` 參數。 +- 路徑 `/items/{item_id}` 有一個 `str` 型別的查詢參數 `q`。 + +### 互動式 API 文件 + +使用瀏覽器開啟 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。 + +你會看到自動生成的互動式 API 文件(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> 生成): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### ReDoc API 文件 + +使用瀏覽器開啟 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。 + +你將看到 ReDoc 文件 (由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 生成): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 範例升級 + +現在繼續修改 `main.py` 檔案,來接收一個帶有 body 的 `PUT` 請求。 + +我們使用 Pydantic 來使用標準的 Python 型別聲明請求。 + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +伺服器將自動重新載入(因為在上一步中,你向 `uvicorn` 指令添加了 `--reload` 的選項)。 + +### 互動式 API 文件升級 + +使用瀏覽器開啟 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>。 + +- 互動式 API 文件會自動更新,並加入新的 body 請求: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +- 點擊 "Try it out" 按鈕, 你可以填寫參數並直接與 API 互動: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +- 然後點擊 "Execute" 按鈕,使用者介面將會向 API 發送請求,並將結果顯示在螢幕上: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### ReDoc API 文件升級 + +使用瀏覽器開啟 <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>。 + +- ReDoc API 文件會自動更新,並加入新的參數和 body 請求: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 總結 + +總結來說, 你就像宣告函式的參數型別一樣,只宣告了一次請求參數和請求主體參數等型別。 + +你使用 Python 標準型別來完成聲明。 + +你不需要學習新的語法、類別、方法或函式庫等等。 + +只需要使用 **Python 3.8 以上的版本**。 + +舉個範例,比如宣告 int 的型別: + +```Python +item_id: int +``` + +或是一個更複雜的 `Item` 模型: + +```Python +item: Item +``` + +在進行一次宣告後,你將獲得: + +- 編輯器支援: + - 自動補全 + - 型別檢查 +- 資料驗證: + - 驗證失敗時自動生成清楚的錯誤訊息 + - 可驗證多層巢狀的 JSON 物件 +- <abbr title="也被稱為: 序列化或解析">轉換</abbr>輸入的資料: 轉換來自網路請求到 Python 資料型別。包含以下數據: + - JSON + - 路徑參數 + - 查詢參數 + - Cookies + - 請求標頭 + - 表單 + - 文件 +- <abbr title="也被稱為: 序列化或解析">轉換</abbr>輸出的資料: 轉換 Python 資料型別到網路傳輸的 JSON: + - 轉換 Python 型別 (`str`、 `int`、 `float`、 `bool`、 `list` 等) + - `datetime` 物件 + - `UUID` 物件 + - 數據模型 + - ...還有其他更多 +- 自動生成的 API 文件,包含 2 種不同的使用介面: + - Swagger UI + - ReDoc + +--- + +回到前面的的程式碼範例,**FastAPI** 還會: + +- 驗證 `GET` 和 `PUT` 請求路徑中是否包含 `item_id`。 +- 驗證 `GET` 和 `PUT` 請求中的 `item_id` 是否是 `int` 型別。 + - 如果驗證失敗,將會返回清楚有用的錯誤訊息。 +- 查看 `GET` 請求中是否有命名為 `q` 的查詢參數 (例如 `http://127.0.0.1:8000/items/foo?q=somequery`)。 + - 因為 `q` 參數被宣告為 `= None`,所以是選填的。 + - 如果沒有宣告 `None`,則此參數將會是必填 (例如 `PUT` 範例的請求 body)。 +- 對於 `PUT` 的請求 `/items/{item_id}`,將會讀取 body 為 JSON: + - 驗證是否有必填屬性 `name` 且型別是 `str`。 + - 驗證是否有必填屬性 `price` 且型別是 `float`。 + - 驗證是否有選填屬性 `is_offer` 且型別是 `bool`。 + - 以上驗證都適用於多層次巢狀 JSON 物件。 +- 自動轉換 JSON 格式。 +- 透過 OpenAPI 文件來記錄所有內容,可以被用於: + - 互動式文件系統。 + - 自動為多種程式語言生成用戶端的程式碼。 +- 提供兩種交互式文件介面。 + +--- + +雖然我們只敘述了表面的功能,但其實你已經理解了它是如何執行。 + +試著修改以下程式碼: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +從: + +```Python + ... "item_name": item.name ... +``` + +修改為: + +```Python + ... "item_price": item.price ... +``` + +然後觀察你的編輯器,會自動補全並且還知道他們的型別: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +有關更多功能的完整範例,可以參考 <a href="https://fastapi.tiangolo.com/tutorial/">教學 - 使用者指南</a>。 + +**劇透警告**: 教學 - 使用者指南內容有: + +- 對來自不同地方的**參數**進行宣告:像是 **headers**, **cookies**, **form 表單**以及**上傳檔案**。 +- 如何設定 **驗證限制** 像是 `maximum_length` or `regex`。 +- 簡單且非常容易使用的 **<abbr title="也被稱為元件、資源、提供者、服務或是注入">依賴注入</abbr>** 系統。 +- 安全性和身份驗證,包含提供支援 **OAuth2**、**JWT tokens** 和 **HTTP Basic** 驗證。 +- 更進階 (但同樣簡單) 的宣告 **多層次的巢狀 JSON 格式** (感謝 Pydantic)。 +- **GraphQL** 與 <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> 以及其他的相關函式庫進行整合。 +- 更多其他的功能 (感謝 Starlette) 像是: + - **WebSockets** + - 於 HTTPX 和 `pytest` 的非常簡單測試 + - **CORS** + - **Cookie Sessions** + - ...以及更多 + +## 效能 + +來自獨立機構 TechEmpower 的測試結果,顯示在 Uvicorn 執行下的 **FastAPI** 是 <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">最快的 Python 框架之一</a>, 僅次於 Starlette 和 Uvicorn 本身 (兩者是 FastAPI 的底層)。 (\*) + +想了解更多訊息,可以參考 <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">測試結果</a>。 + +## 可選的依賴套件 + +用於 Pydantic: + +- <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - 用於電子郵件驗證。 +- <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - 用於設定管理。 +- <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - 用於與 Pydantic 一起使用的額外型別。 + +用於 Starlette: + +- <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - 使用 `TestClient`時必須安裝。 +- <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - 使用預設的模板配置時必須安裝。 +- <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - 需要使用 `request.form()` 對表單進行<abbr title="轉換來自表單的 HTTP 請求到 Python 資料型別"> "解析" </abbr>時安裝。 +- <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - 需要使用 `SessionMiddleware` 支援時安裝。 +- <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - 用於支援 Starlette 的 `SchemaGenerator` (如果你使用 FastAPI,可能不需要它)。 +- <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - 使用 `UJSONResponse` 時必須安裝。 + +用於 FastAPI / Starlette: + +- <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - 用於加載和運行應用程式的服務器。 +- <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - 使用 `ORJSONResponse`時必須安裝。 + +你可以使用 `pip install "fastapi[all]"` 來安裝這些所有依賴套件。 + +## 授權 + +該項目遵循 MIT 許可協議。 diff --git a/docs/zh-hant/mkdocs.yml b/docs/zh-hant/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/zh-hant/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/scripts/docs.py b/scripts/docs.py index 37a7a3477..0643e414f 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -76,8 +76,6 @@ def callback() -> None: def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): """ Generate a new docs translation directory for the language LANG. - - LANG should be a 2-letter language code, like: en, es, de, pt, etc. """ new_path: Path = Path("docs") / lang if new_path.exists(): From d74b3b25659b42233a669f032529880de8bd6c2d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 17 Jan 2024 17:15:47 +0000 Subject: [PATCH 1693/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a96954527..d5117ccd1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Initialize translations for Traditional Chinese. PR [#10505](https://github.com/tiangolo/fastapi/pull/10505) by [@hsuanchi](https://github.com/hsuanchi). * ✏️ Tweak the german translation of `docs/de/docs/tutorial/index.md`. PR [#10962](https://github.com/tiangolo/fastapi/pull/10962) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo error in `docs/ko/docs/tutorial/path-params.md`. PR [#10758](https://github.com/tiangolo/fastapi/pull/10758) by [@2chanhaeng](https://github.com/2chanhaeng). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#1961](https://github.com/tiangolo/fastapi/pull/1961) by [@SwftAlpc](https://github.com/SwftAlpc). From c3019096e7cc7605a192712c6f7c1bafa1b3b57f Mon Sep 17 00:00:00 2001 From: Kani Kim <kkh5428@gmail.com> Date: Sat, 20 Jan 2024 08:04:42 +0900 Subject: [PATCH 1694/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/learn/index.md`=20(#10977)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/ko/docs/learn/index.md diff --git a/docs/ko/docs/learn/index.md b/docs/ko/docs/learn/index.md new file mode 100644 index 000000000..7ac3a99b6 --- /dev/null +++ b/docs/ko/docs/learn/index.md @@ -0,0 +1,5 @@ +# 배우기 + +여기 **FastAPI**를 배우기 위한 입문 자료와 자습서가 있습니다. + +여러분은 FastAPI를 배우기 위해 **책**, **강의**, **공식 자료** 그리고 추천받은 방법을 고려할 수 있습니다. 😎 From 510c7a56a412302270c34ddfbbdd345a96870d23 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 19 Jan 2024 23:05:10 +0000 Subject: [PATCH 1695/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d5117ccd1..7ca9c17a5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/learn/index.md`. PR [#10977](https://github.com/tiangolo/fastapi/pull/10977) by [@KaniKim](https://github.com/KaniKim). * 🌐 Initialize translations for Traditional Chinese. PR [#10505](https://github.com/tiangolo/fastapi/pull/10505) by [@hsuanchi](https://github.com/hsuanchi). * ✏️ Tweak the german translation of `docs/de/docs/tutorial/index.md`. PR [#10962](https://github.com/tiangolo/fastapi/pull/10962) by [@nilslindemann](https://github.com/nilslindemann). * ✏️ Fix typo error in `docs/ko/docs/tutorial/path-params.md`. PR [#10758](https://github.com/tiangolo/fastapi/pull/10758) by [@2chanhaeng](https://github.com/2chanhaeng). From 62e6c888b79e0ad93656dbdfc0b8310de06ae7b5 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:43:10 -0500 Subject: [PATCH 1696/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20config=20in?= =?UTF-8?q?=20`label-approved.yml`=20to=20accept=20translations=20with=201?= =?UTF-8?q?=20reviewer=20(#11007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/label-approved.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/label-approved.yml b/.github/workflows/label-approved.yml index 62daf2608..51be2413d 100644 --- a/.github/workflows/label-approved.yml +++ b/.github/workflows/label-approved.yml @@ -17,3 +17,11 @@ jobs: - uses: docker://tiangolo/label-approved:0.0.4 with: token: ${{ secrets.FASTAPI_LABEL_APPROVED }} + config: > + { + "approved-1": + { + "number": 1, + "await_label": "awaiting-review" + } + } From 60ea8f85a1fdac21f907ba5e21a09d935829b79a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 18:43:31 +0000 Subject: [PATCH 1697/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7ca9c17a5..270d5e46a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -72,6 +72,7 @@ hide: ### Internal +* 🔧 Update config in `label-approved.yml` to accept translations with 1 reviewer. PR [#11007](https://github.com/tiangolo/fastapi/pull/11007) by [@alejsdev](https://github.com/alejsdev). * 👷 Add changes-requested handling in GitHub Action issue manager. PR [#10971](https://github.com/tiangolo/fastapi/pull/10971) by [@tiangolo](https://github.com/tiangolo). * 🔧 Group dependencies on dependabot updates. PR [#10952](https://github.com/tiangolo/fastapi/pull/10952) by [@Kludex](https://github.com/Kludex). * ⬆ Bump actions/setup-python from 4 to 5. PR [#10764](https://github.com/tiangolo/fastapi/pull/10764) by [@dependabot[bot]](https://github.com/apps/dependabot). From 2fe1a1387b1c9bbcbb7f701e15472e491295bf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Mon, 22 Jan 2024 20:26:14 +0100 Subject: [PATCH 1698/1881] =?UTF-8?q?=F0=9F=94=A8=20Verify=20`mkdocs.yml`?= =?UTF-8?q?=20languages=20in=20CI,=20update=20`docs.py`=20(#11009)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build-docs.yml | 4 ++-- docs/en/mkdocs.yml | 6 ++++-- scripts/build-docs.sh | 2 +- scripts/docs.py | 32 +++++++++++++++++++++++++++++++- 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index abf2b90f6..7783161b9 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -57,8 +57,8 @@ jobs: pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/squidfunk/mkdocs-material-insiders.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/griffe-typing-deprecated.git pip install git+https://${{ secrets.FASTAPI_MKDOCS_MATERIAL_INSIDERS }}@github.com/pawamoy-insiders/mkdocstrings-python.git - - name: Verify README - run: python ./scripts/docs.py verify-readme + - name: Verify Docs + run: python ./scripts/docs.py verify-docs - name: Export Language Codes id: show-langs run: | diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index fcac555eb..e965f4f28 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -242,7 +242,7 @@ markdown_extensions: format: !!python/name:pymdownx.superfences.fence_code_format '' pymdownx.tabbed: alternate_style: true - pymdownx.tilde: + pymdownx.tilde: null attr_list: null md_in_html: null extra: @@ -267,6 +267,8 @@ extra: alternate: - link: / name: en - English + - link: /bn/ + name: bn - বাংলা - link: /de/ name: de - Deutsch - link: /es/ @@ -304,7 +306,7 @@ extra: - link: /zh/ name: zh - 汉语 - link: /zh-hant/ - name: zh - 繁體中文 + name: zh-hant - 繁體中文 - link: /em/ name: 😉 extra_css: diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index ebf864afa..7aa0a9a47 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -4,5 +4,5 @@ set -e set -x # Check README.md is up to date -python ./scripts/docs.py verify-readme +python ./scripts/docs.py verify-docs python ./scripts/docs.py build-all diff --git a/scripts/docs.py b/scripts/docs.py index 0643e414f..59578a820 100644 --- a/scripts/docs.py +++ b/scripts/docs.py @@ -266,7 +266,7 @@ def live( mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") -def update_config() -> None: +def get_updated_config_content() -> Dict[str, Any]: config = get_en_config() languages = [{"en": "/"}] new_alternate: List[Dict[str, str]] = [] @@ -294,12 +294,42 @@ def update_config() -> None: new_alternate.append({"link": url, "name": use_name}) new_alternate.append({"link": "/em/", "name": "😉"}) config["extra"]["alternate"] = new_alternate + return config + + +def update_config() -> None: + config = get_updated_config_content() en_config_path.write_text( yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), encoding="utf-8", ) +@app.command() +def verify_config() -> None: + """ + Verify main mkdocs.yml content to make sure it uses the latest language names. + """ + typer.echo("Verifying mkdocs.yml") + config = get_en_config() + updated_config = get_updated_config_content() + if config != updated_config: + typer.secho( + "docs/en/mkdocs.yml outdated from docs/language_names.yml, " + "update language_names.yml and run " + "python ./scripts/docs.py update-languages", + color=typer.colors.RED, + ) + raise typer.Abort() + typer.echo("Valid mkdocs.yml ✅") + + +@app.command() +def verify_docs(): + verify_readme() + verify_config() + + @app.command() def langs_json(): langs = [] From 896f171aa2836765f359418742a416086021afc0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:26:37 +0000 Subject: [PATCH 1699/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 270d5e46a..133ca03b3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -72,6 +72,7 @@ hide: ### Internal +* 🔨 Verify `mkdocs.yml` languages in CI, update `docs.py`. PR [#11009](https://github.com/tiangolo/fastapi/pull/11009) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update config in `label-approved.yml` to accept translations with 1 reviewer. PR [#11007](https://github.com/tiangolo/fastapi/pull/11007) by [@alejsdev](https://github.com/alejsdev). * 👷 Add changes-requested handling in GitHub Action issue manager. PR [#10971](https://github.com/tiangolo/fastapi/pull/10971) by [@tiangolo](https://github.com/tiangolo). * 🔧 Group dependencies on dependabot updates. PR [#10952](https://github.com/tiangolo/fastapi/pull/10952) by [@Kludex](https://github.com/Kludex). From 01d774d38cb653504e6b1a9b942c9d2dc7238e1d Mon Sep 17 00:00:00 2001 From: Spike Ho Yeol Lee <rurouni24@gmail.com> Date: Tue, 23 Jan 2024 04:31:27 +0900 Subject: [PATCH 1700/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/body-nested-models.md`=20(#?= =?UTF-8?q?2506)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/body-nested-models.md | 243 ++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/ko/docs/tutorial/body-nested-models.md diff --git a/docs/ko/docs/tutorial/body-nested-models.md b/docs/ko/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..7b41aa35b --- /dev/null +++ b/docs/ko/docs/tutorial/body-nested-models.md @@ -0,0 +1,243 @@ +# 본문 - 중첩 모델 + +**FastAPI**를 이용하면 (Pydantic 덕분에) 단독으로 깊이 중첩된 모델을 정의, 검증, 문서화하며 사용할 수 있습니다. +## 리스트 필드 + +어트리뷰트를 서브타입으로 정의할 수 있습니다. 예를 들어 파이썬 `list`는: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial001.py!} +``` + +이는 `tags`를 항목 리스트로 만듭니다. 각 항목의 타입을 선언하지 않더라도요. + +## 타입 매개변수가 있는 리스트 필드 + +하지만 파이썬은 내부의 타입이나 "타입 매개변수"를 선언할 수 있는 특정 방법이 있습니다: + +### typing의 `List` 임포트 + +먼저, 파이썬 표준 `typing` 모듈에서 `List`를 임포트합니다: + +```Python hl_lines="1" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### 타입 매개변수로 `List` 선언 + +`list`, `dict`, `tuple`과 같은 타입 매개변수(내부 타입)를 갖는 타입을 선언하려면: + +* `typing` 모듈에서 임포트 +* 대괄호를 사용하여 "타입 매개변수"로 내부 타입 전달: `[` 및 `]` + +```Python +from typing import List + +my_list: List[str] +``` + +이 모든 것은 타입 선언을 위한 표준 파이썬 문법입니다. + +내부 타입을 갖는 모델 어트리뷰트에 대해 동일한 표준 문법을 사용하세요. + +마찬가지로 예제에서 `tags`를 구체적으로 "문자열의 리스트"로 만들 수 있습니다: + +```Python hl_lines="14" +{!../../../docs_src/body_nested_models/tutorial002.py!} +``` + +## 집합 타입 + +그런데 생각해보니 태그는 반복되면 안 돼고, 고유한(Unique) 문자열이어야 할 것 같습니다. + +그리고 파이썬은 집합을 위한 특별한 데이터 타입 `set`이 있습니다. + +그렇다면 `Set`을 임포트 하고 `tags`를 `str`의 `set`으로 선언할 수 있습니다: + +```Python hl_lines="1 14" +{!../../../docs_src/body_nested_models/tutorial003.py!} +``` + +덕분에 중복 데이터가 있는 요청을 수신하더라도 고유한 항목들의 집합으로 변환됩니다. + +그리고 해당 데이터를 출력 할 때마다 소스에 중복이 있더라도 고유한 항목들의 집합으로 출력됩니다. + +또한 그에 따라 주석이 생기고 문서화됩니다. + +## 중첩 모델 + +Pydantic 모델의 각 어트리뷰트는 타입을 갖습니다. + +그런데 해당 타입 자체로 또다른 Pydantic 모델의 타입이 될 수 있습니다. + +그러므로 특정한 어트리뷰트의 이름, 타입, 검증을 사용하여 깊게 중첩된 JSON "객체"를 선언할 수 있습니다. + +모든 것이 단독으로 중첩됩니다. + +### 서브모델 정의 + +예를 들어, `Image` 모델을 선언할 수 있습니다: + +```Python hl_lines="9-11" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +### 서브모듈을 타입으로 사용 + +그리고 어트리뷰트의 타입으로 사용할 수 있습니다: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial004.py!} +``` + +이는 **FastAPI**가 다음과 유사한 본문을 기대한다는 것을 의미합니다: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +다시 한번, **FastAPI**를 사용하여 해당 선언을 함으로써 얻는 것은: + +* 중첩 모델도 편집기 지원(자동완성 등) +* 데이터 변환 +* 데이터 검증 +* 자동 문서화 + +## 특별한 타입과 검증 + +`str`, `int`, `float` 등과 같은 단일 타입과는 별개로, `str`을 상속하는 더 복잡한 단일 타입을 사용할 수 있습니다. + +모든 옵션을 보려면, <a href="https://pydantic-docs.helpmanual.io/usage/types/" class="external-link" target="_blank">Pydantic's exotic types</a> 문서를 확인하세요. 다음 장에서 몇가지 예제를 볼 수 있습니다. + +예를 들어 `Image` 모델 안에 `url` 필드를 `str` 대신 Pydantic의 `HttpUrl`로 선언할 수 있습니다: + +```Python hl_lines="4 10" +{!../../../docs_src/body_nested_models/tutorial005.py!} +``` + +이 문자열이 유효한 URL인지 검사하고 JSON 스키마/OpenAPI로 문서화 됩니다. + +## 서브모델 리스트를 갖는 어트리뷰트 + +`list`, `set` 등의 서브타입으로 Pydantic 모델을 사용할 수도 있습니다: + +```Python hl_lines="20" +{!../../../docs_src/body_nested_models/tutorial006.py!} +``` + +아래와 같은 JSON 본문으로 예상(변환, 검증, 문서화 등을)합니다: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info "정보" + `images` 키가 어떻게 이미지 객체 리스트를 갖는지 주목하세요. + +## 깊게 중첩된 모델 + +단독으로 깊게 중첩된 모델을 정의할 수 있습니다: + +```Python hl_lines="9 14 20 23 27" +{!../../../docs_src/body_nested_models/tutorial007.py!} +``` + +!!! info "정보" + `Offer`가 선택사항 `Image` 리스트를 차례로 갖는 `Item` 리스트를 어떻게 가지고 있는지 주목하세요 + +## 순수 리스트의 본문 + +예상되는 JSON 본문의 최상위 값이 JSON `array`(파이썬 `list`)면, Pydantic 모델에서와 마찬가지로 함수의 매개변수에서 타입을 선언할 수 있습니다: + +```Python +images: List[Image] +``` + +이를 아래처럼: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial008.py!} +``` + +## 어디서나 편집기 지원 + +그리고 어디서나 편집기 지원을 받을수 있습니다. + +리스트 내부 항목의 경우에도: + +<img src="/img/tutorial/body-nested-models/image01.png"> + +Pydantic 모델 대신에 `dict`를 직접 사용하여 작업할 경우, 이러한 편집기 지원을 받을수 없습니다. + +하지만 수신한 딕셔너리가 자동으로 변환되고 출력도 자동으로 JSON으로 변환되므로 걱정할 필요는 없습니다. + +## 단독 `dict`의 본문 + +일부 타입의 키와 다른 타입의 값을 사용하여 `dict`로 본문을 선언할 수 있습니다. + +(Pydantic을 사용한 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 알 필요가 없습니다. + +아직 모르는 키를 받으려는 경우 유용합니다. + +--- + +다른 유용한 경우는 다른 타입의 키를 가질 때입니다. 예. `int`. + +여기서 그 경우를 볼 것입니다. + +이 경우, `float` 값을 가진 `int` 키가 있는 모든 `dict`를 받아들입니다: + +```Python hl_lines="15" +{!../../../docs_src/body_nested_models/tutorial009.py!} +``` + +!!! tip "팁" + JSON은 오직 `str`형 키만 지원한다는 것을 염두에 두세요. + + 하지만 Pydantic은 자동 데이터 변환이 있습니다. + + 즉, API 클라이언트가 문자열을 키로 보내더라도 해당 문자열이 순수한 정수를 포함하는한 Pydantic은 이를 변환하고 검증합니다. + + 그러므로 `weights`로 받은 `dict`는 실제로 `int` 키와 `float` 값을 가집니다. + +## 요약 + +**FastAPI**를 사용하면 Pydantic 모델이 제공하는 최대 유연성을 확보하면서 코드를 간단하고 짧게, 그리고 우아하게 유지할 수 있습니다. + +물론 아래의 이점도 있습니다: + +* 편집기 지원 (자동완성이 어디서나!) +* 데이터 변환 (일명 파싱/직렬화) +* 데이터 검증 +* 스키마 문서화 +* 자동 문서 From 5fb87313e20c68146c653cdb249e28fead4c7dd4 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:31:48 +0000 Subject: [PATCH 1701/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 133ca03b3..0e4da3b9f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-nested-models.md`. PR [#2506](https://github.com/tiangolo/fastapi/pull/2506) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/learn/index.md`. PR [#10977](https://github.com/tiangolo/fastapi/pull/10977) by [@KaniKim](https://github.com/KaniKim). * 🌐 Initialize translations for Traditional Chinese. PR [#10505](https://github.com/tiangolo/fastapi/pull/10505) by [@hsuanchi](https://github.com/hsuanchi). * ✏️ Tweak the german translation of `docs/de/docs/tutorial/index.md`. PR [#10962](https://github.com/tiangolo/fastapi/pull/10962) by [@nilslindemann](https://github.com/nilslindemann). From 2a8f8d1ac0edd0f021094d1afd82cbd690565d4c Mon Sep 17 00:00:00 2001 From: JRIM <jrim.choi@gmail.com> Date: Tue, 23 Jan 2024 04:34:47 +0900 Subject: [PATCH 1702/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/python-types.md`=20(#2267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/python-types.md | 315 +++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 docs/ko/docs/python-types.md diff --git a/docs/ko/docs/python-types.md b/docs/ko/docs/python-types.md new file mode 100644 index 000000000..16b93a7a9 --- /dev/null +++ b/docs/ko/docs/python-types.md @@ -0,0 +1,315 @@ +# 파이썬 타입 소개 + +파이썬은 선택적으로 "타입 힌트(type hints)"를 지원합니다. + +이러한 **타입 힌트**들은 변수의 <abbr title="예를 들면: str, int, float, bool">타입</abbr>을 선언할 수 있게 해주는 특수한 구문입니다. + +변수의 타입을 지정하면 에디터와 툴이 더 많은 도움을 줄 수 있게 됩니다. + +이 문서는 파이썬 타입 힌트에 대한 **빠른 자습서 / 내용환기** 수준의 문서입니다. 여기서는 **FastAPI**를 쓰기 위한 최소한의 내용만을 다룹니다. + +**FastAPI**는 타입 힌트에 기반을 두고 있으며, 이는 많은 장점과 이익이 있습니다. + +비록 **FastAPI**를 쓰지 않는다고 하더라도, 조금이라도 알아두면 도움이 될 것입니다. + +!!! note "참고" + 파이썬에 능숙하셔서 타입 힌트에 대해 모두 아신다면, 다음 챕터로 건너뛰세요. + +## 동기 부여 + +간단한 예제부터 시작해봅시다: + +```Python +{!../../../docs_src/python_types/tutorial001.py!} +``` + +이 프로그램을 실행한 결과값: + +``` +John Doe +``` + +함수는 아래와 같이 실행됩니다: + +* `first_name`과 `last_name`를 받습니다. +* `title()`로 각 첫 문자를 대문자로 변환시킵니다. +* 두 단어를 중간에 공백을 두고 <abbr title="두 개를 하나로 차례차례 이어지게 하다">연결</abbr>합니다. + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial001.py!} +``` + +### 코드 수정 + +이건 매우 간단한 프로그램입니다. + +그런데 처음부터 작성한다고 생각을 해봅시다. + +여러분은 매개변수를 준비했고, 함수를 정의하기 시작했을 겁니다. + +이때 "첫 글자를 대문자로 바꾸는 함수"를 호출해야 합니다. + +`upper`였나? 아니면 `uppercase`? `first_uppercase`? `capitalize`? + +그때 개발자들의 오랜 친구, 에디터 자동완성을 시도해봅니다. + +당신은 `first_name`를 입력한 뒤 점(`.`)을 입력하고 자동완성을 켜기 위해서 `Ctrl+Space`를 눌렀습니다. + +하지만 슬프게도 아무런 도움이 되지 않습니다: + +<img src="/img/python-types/image01.png"> + +### 타입 추가하기 + +이전 버전에서 한 줄만 수정해봅시다. + +저희는 이 함수의 매개변수 부분: + +```Python + first_name, last_name +``` + +을 아래와 같이 바꿀 겁니다: + +```Python + first_name: str, last_name: str +``` + +이게 다입니다. + +이게 "타입 힌트"입니다: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial002.py!} +``` + +타입힌트는 다음과 같이 기본 값을 선언하는 것과는 다릅니다: + +```Python + first_name="john", last_name="doe" +``` + +이는 다른 것입니다. + +등호(`=`) 대신 콜론(`:`)을 쓰고 있습니다. + +일반적으로 타입힌트를 추가한다고 해서 특별하게 어떤 일이 일어나지도 않습니다. + +그렇지만 이제, 다시 함수를 만드는 도중이라고 생각해봅시다. 다만 이번엔 타입 힌트가 있습니다. + +같은 상황에서 `Ctrl+Space`로 자동완성을 작동시키면, + +<img src="/img/python-types/image02.png"> + +아래와 같이 "그렇지!"하는 옵션이 나올때까지 스크롤을 내려서 볼 수 있습니다: + +<img src="/img/python-types/image03.png"> + +## 더 큰 동기부여 + +아래 함수를 보면, 이미 타입 힌트가 적용되어 있는 걸 볼 수 있습니다: + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial003.py!} +``` + +편집기가 변수의 타입을 알고 있기 때문에, 자동완성 뿐 아니라 에러도 확인할 수 있습니다: + +<img src="/img/python-types/image04.png"> + +이제 고쳐야하는 걸 알기 때문에, `age`를 `str(age)`과 같이 문자열로 바꾸게 됩니다: + +```Python hl_lines="2" +{!../../../docs_src/python_types/tutorial004.py!} +``` + +## 타입 선언 + +방금 함수의 매개변수로써 타입 힌트를 선언하는 주요 장소를 보았습니다. + +이 위치는 여러분이 **FastAPI**와 함께 이를 사용하는 주요 장소입니다. + +### Simple 타입 + +`str`뿐 아니라 모든 파이썬 표준 타입을 선언할 수 있습니다. + +예를 들면: + +* `int` +* `float` +* `bool` +* `bytes` + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial005.py!} +``` + +### 타입 매개변수를 활용한 Generic(제네릭) 타입 + +`dict`, `list`, `set`, `tuple`과 같은 값을 저장할 수 있는 데이터 구조가 있고, 내부의 값은 각자의 타입을 가질 수도 있습니다. + +타입과 내부 타입을 선언하기 위해서는 파이썬 표준 모듈인 `typing`을 이용해야 합니다. + +구체적으로는 아래 타입 힌트를 지원합니다. + +#### `List` + +예를 들면, `str`의 `list`인 변수를 정의해봅시다. + +`typing`에서 `List`(대문자 `L`)를 import 합니다. + +```Python hl_lines="1" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +콜론(`:`) 문법을 이용하여 변수를 선언합니다. + +타입으로는 `List`를 넣어줍니다. + +이때 배열은 내부 타입을 포함하는 타입이기 때문에 대괄호 안에 넣어줍니다. + +```Python hl_lines="4" +{!../../../docs_src/python_types/tutorial006.py!} +``` + +!!! tip "팁" + 대괄호 안의 내부 타입은 "타입 매개변수(type paramters)"라고 합니다. + + 이번 예제에서는 `str`이 `List`에 들어간 타입 매개변수 입니다. + +이는 "`items`은 `list`인데, 배열에 들어있는 아이템 각각은 `str`이다"라는 뜻입니다. + +이렇게 함으로써, 에디터는 배열에 들어있는 아이템을 처리할때도 도움을 줄 수 있게 됩니다: + +<img src="/img/python-types/image05.png"> + +타입이 없으면 이건 거의 불가능이나 다름 없습니다. + +변수 `item`은 `items`의 개별 요소라는 사실을 알아두세요. + +그리고 에디터는 계속 `str`라는 사실을 알고 도와줍니다. + +#### `Tuple`과 `Set` + +`tuple`과 `set`도 동일하게 선언할 수 있습니다. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial007.py!} +``` + +이 뜻은 아래와 같습니다: + +* 변수 `items_t`는, 차례대로 `int`, `int`, `str`인 `tuple`이다. +* 변수 `items_s`는, 각 아이템이 `bytes`인 `set`이다. + +#### `Dict` + +`dict`를 선언하려면 컴마로 구분된 2개의 파라미터가 필요합니다. + +첫 번째 매개변수는 `dict`의 키(key)이고, + +두 번째 매개변수는 `dict`의 값(value)입니다. + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial008.py!} +``` + +이 뜻은 아래와 같습니다: + +* 변수 `prices`는 `dict`이다: + * `dict`의 키(key)는 `str`타입이다. (각 아이템의 이름(name)) + * `dict`의 값(value)는 `float`타입이다. (각 아이템의 가격(price)) + +#### `Optional` + +`str`과 같이 타입을 선언할 때 `Optional`을 쓸 수도 있는데, "선택적(Optional)"이기때문에 `None`도 될 수 있습니다: + +```Python hl_lines="1 4" +{!../../../docs_src/python_types/tutorial009.py!} +``` + +`Optional[str]`을 `str` 대신 쓰게 되면, 특정 값이 실제로는 `None`이 될 수도 있는데 항상 `str`이라고 가정하는 상황에서 에디터가 에러를 찾게 도와줄 수 있습니다. + +#### Generic(제네릭) 타입 + +이 타입은 대괄호 안에 매개변수를 가지며, 종류는: + +* `List` +* `Tuple` +* `Set` +* `Dict` +* `Optional` +* ...등등 + +위와 같은 타입은 **Generic(제네릭) 타입** 혹은 **Generics(제네릭스)**라고 불립니다. + +### 타입으로서의 클래스 + +변수의 타입으로 클래스를 선언할 수도 있습니다. + +이름(name)을 가진 `Person` 클래스가 있다고 해봅시다. + +```Python hl_lines="1-3" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +그렇게 하면 변수를 `Person`이라고 선언할 수 있게 됩니다. + +```Python hl_lines="6" +{!../../../docs_src/python_types/tutorial010.py!} +``` + +그리고 역시나 모든 에디터 도움을 받게 되겠죠. + +<img src="/img/python-types/image06.png"> + +## Pydantic 모델 + +<a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>은 데이터 검증(Validation)을 위한 파이썬 라이브러리입니다. + +당신은 속성들을 포함한 클래스 형태로 "모양(shape)"을 선언할 수 있습니다. + +그리고 각 속성은 타입을 가지고 있습니다. + +이 클래스를 활용하여서 값을 가지고 있는 인스턴스를 만들게 되면, 필요한 경우에는 적당한 타입으로 변환까지 시키기도 하여 데이터가 포함된 객체를 반환합니다. + +그리고 결과 객체에 대해서는 에디터의 도움을 받을 수 있게 됩니다. + +Pydantic 공식 문서 예시: + +```Python +{!../../../docs_src/python_types/tutorial011.py!} +``` + +!!! info "정보" + Pydantic<에 대해 더 배우고 싶다면 <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">공식 문서</a>를 참고하세요.</a> + + +**FastAPI**는 모두 Pydantic을 기반으로 되어 있습니다. + +이 모든 것이 실제로 어떻게 사용되는지에 대해서는 [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank} 에서 더 많이 확인하실 수 있습니다. + +## **FastAPI**에서의 타입 힌트 + +**FastAPI**는 여러 부분에서 타입 힌트의 장점을 취하고 있습니다. + +**FastAPI**에서 타입 힌트와 함께 매개변수를 선언하면 장점은: + +* **에디터 도움**. +* **타입 확인**. + +...그리고 **FastAPI**는 같은 정의를 아래에도 적용합니다: + +* **요구사항 정의**: 요청 경로 매개변수, 쿼리 매개변수, 헤더, 바디, 의존성 등. +* **데이터 변환**: 요청에서 요구한 타입으로. +* **데이터 검증**: 각 요청마다: + * 데이터가 유효하지 않은 경우에는 **자동으로 에러**를 발생합니다. +* OpenAPI를 활용한 **API 문서화**: + * 자동으로 상호작용하는 유저 인터페이스에 쓰이게 됩니다. + +위 내용이 다소 추상적일 수도 있지만, 걱정마세요. [자습서 - 사용자 안내서](tutorial/index.md){.internal-link target=_blank}에서 전부 확인 가능합니다. + +가장 중요한 건, 표준 파이썬 타입을 한 곳에서(클래스를 더하거나, 데코레이터 사용하는 대신) 사용함으로써 **FastAPI**가 당신을 위해 많은 일을 해준다는 사실이죠. + +!!! info "정보" + 만약 모든 자습서를 다 보았음에도 타입에 대해서 더 보고자 방문한 경우에는 <a href="https://mypy.readthedocs.io/en/latest/cheat_sheet_py3.html" class="external-link" target="_blank">`mypy`에서 제공하는 "cheat sheet"</a>이 좋은 자료가 될 겁니다. From eea7635713bdda4a2ad9393efe5a720d15a22122 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:36:25 +0000 Subject: [PATCH 1703/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0e4da3b9f..2eb764e1e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/python-types.md`. PR [#2267](https://github.com/tiangolo/fastapi/pull/2267) by [@jrim](https://github.com/jrim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-nested-models.md`. PR [#2506](https://github.com/tiangolo/fastapi/pull/2506) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/learn/index.md`. PR [#10977](https://github.com/tiangolo/fastapi/pull/10977) by [@KaniKim](https://github.com/KaniKim). * 🌐 Initialize translations for Traditional Chinese. PR [#10505](https://github.com/tiangolo/fastapi/pull/10505) by [@hsuanchi](https://github.com/hsuanchi). From 87a4c9ef01afe9066678d655866c5f5cd8bc26c2 Mon Sep 17 00:00:00 2001 From: Spike Ho Yeol Lee <rurouni24@gmail.com> Date: Tue, 23 Jan 2024 04:37:01 +0900 Subject: [PATCH 1704/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/query-params-str-validation?= =?UTF-8?q?s.md`=20(#2415)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/query-params-str-validations.md | 303 ++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 docs/ko/docs/tutorial/query-params-str-validations.md diff --git a/docs/ko/docs/tutorial/query-params-str-validations.md b/docs/ko/docs/tutorial/query-params-str-validations.md new file mode 100644 index 000000000..7ae100dcc --- /dev/null +++ b/docs/ko/docs/tutorial/query-params-str-validations.md @@ -0,0 +1,303 @@ +# 쿼리 매개변수와 문자열 검증 + +**FastAPI**를 사용하면 매개변수에 대한 추가 정보 및 검증을 선언할 수 있습니다. + +이 응용 프로그램을 예로 들어보겠습니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial001.py!} +``` + +쿼리 매개변수 `q`는 `Optional[str]` 자료형입니다. 즉, `str` 자료형이지만 `None` 역시 될 수 있음을 뜻하고, 실제로 기본값은 `None`이기 때문에 FastAPI는 이 매개변수가 필수가 아니라는 것을 압니다. + +!!! note "참고" + FastAPI는 `q`의 기본값이 `= None`이기 때문에 필수가 아님을 압니다. + + `Optional[str]`에 있는 `Optional`은 FastAPI가 사용하는게 아니지만, 편집기에게 더 나은 지원과 오류 탐지를 제공하게 해줍니다. + +## 추가 검증 + +`q`가 선택적이지만 값이 주어질 때마다 **값이 50 글자를 초과하지 않게** 강제하려 합니다. + +### `Query` 임포트 + +이를 위해 먼저 `fastapi`에서 `Query`를 임포트합니다: + +```Python hl_lines="3" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +## 기본값으로 `Query` 사용 + +이제 `Query`를 매개변수의 기본값으로 사용하여 `max_length` 매개변수를 50으로 설정합니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial002.py!} +``` + +기본값 `None`을 `Query(None)`으로 바꿔야 하므로, `Query`의 첫 번째 매개변수는 기본값을 정의하는 것과 같은 목적으로 사용됩니다. + +그러므로: + +```Python +q: Optional[str] = Query(None) +``` + +...위 코드는 아래와 동일하게 매개변수를 선택적으로 만듭니다: + +```Python +q: Optional[str] = None +``` + +하지만 명시적으로 쿼리 매개변수를 선언합니다. + +!!! info "정보" + FastAPI는 다음 부분에 관심이 있습니다: + + ```Python + = None + ``` + + 또는: + + ```Python + = Query(None) + ``` + + 그리고 `None`을 사용하여 쿼라 매개변수가 필수적이지 않다는 것을 파악합니다. + + `Optional` 부분은 편집기에게 더 나은 지원을 제공하기 위해서만 사용됩니다. + +또한 `Query`로 더 많은 매개변수를 전달할 수 있습니다. 지금의 경우 문자열에 적용되는 `max_length` 매개변수입니다: + +```Python +q: str = Query(None, max_length=50) +``` + +이는 데이터를 검증할 것이고, 데이터가 유효하지 않다면 명백한 오류를 보여주며, OpenAPI 스키마 *경로 동작*에 매개변수를 문서화 합니다. + +## 검증 추가 + +매개변수 `min_length` 또한 추가할 수 있습니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial003.py!} +``` + +## 정규식 추가 + +매개변수와 일치해야 하는 <abbr title="정규표현식(regular expression), regex 또는 regexp는 문자열 조회 패턴을 정의하는 문자들의 순열입니다">정규표현식</abbr>을 정의할 수 있습니다: + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial004.py!} +``` + +이 특정 정규표현식은 전달 받은 매개변수 값을 검사합니다: + +* `^`: 이전에 문자가 없고 뒤따르는 문자로 시작합니다. +* `fixedquery`: 정확히 `fixedquery` 값을 갖습니다. +* `$`: 여기서 끝나고 `fixedquery` 이후로 아무 문자도 갖지 않습니다. + +**"정규표현식"** 개념에 대해 상실감을 느꼈다면 걱정하지 않아도 됩니다. 많은 사람에게 어려운 주제입니다. 아직은 정규표현식 없이도 많은 작업들을 할 수 있습니다. + +하지만 언제든지 가서 배울수 있고, **FastAPI**에서 직접 사용할 수 있다는 사실을 알고 있어야 합니다. + +## 기본값 + +기본값으로 사용하는 첫 번째 인자로 `None`을 전달하듯이, 다른 값을 전달할 수 있습니다. + +`min_length`가 `3`이고, 기본값이 `"fixedquery"`인 쿼리 매개변수 `q`를 선언해봅시다: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial005.py!} +``` + +!!! note "참고" + 기본값을 갖는 것만으로 매개변수는 선택적이 됩니다. + +## 필수로 만들기 + +더 많은 검증이나 메타데이터를 선언할 필요가 없는 경우, 다음과 같이 기본값을 선언하지 않고 쿼리 매개변수 `q`를 필수로 만들 수 있습니다: + +```Python +q: str +``` + +아래 대신: + +```Python +q: Optional[str] = None +``` + +그러나 이제 다음과 같이 `Query`로 선언합니다: + +```Python +q: Optional[str] = Query(None, min_length=3) +``` + +그래서 `Query`를 필수값으로 만들어야 할 때면, 첫 번째 인자로 `...`를 사용할 수 있습니다: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial006.py!} +``` + +!!! info "정보" + 이전에 `...`를 본적이 없다면: 특별한 단일값으로, <a href="https://docs.python.org/3/library/constants.html#Ellipsis" class="external-link" target="_blank">파이썬의 일부이며 "Ellipsis"라 부릅니다</a>. + +이렇게 하면 **FastAPI**가 이 매개변수는 필수임을 알 수 있습니다. + +## 쿼리 매개변수 리스트 / 다중값 + +쿼리 매개변수를 `Query`와 함께 명시적으로 선언할 때, 값들의 리스트나 다른 방법으로 여러 값을 받도록 선언 할 수도 있습니다. + +예를 들어, URL에서 여러번 나오는 `q` 쿼리 매개변수를 선언하려면 다음과 같이 작성할 수 있습니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial011.py!} +``` + +아래와 같은 URL을 사용합니다: + +``` +http://localhost:8000/items/?q=foo&q=bar +``` + +여러 `q` *쿼리 매개변수* 값들을 (`foo` 및 `bar`) 파이썬 `list`로 *경로 작동 함수* 내 *함수 매개변수* `q`로 전달 받습니다. + +따라서 해당 URL에 대한 응답은 다음과 같습니다: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +!!! tip "팁" + 위의 예와 같이 `list` 자료형으로 쿼리 매개변수를 선언하려면 `Query`를 명시적으로 사용해야 합니다. 그렇지 않으면 요청 본문으로 해석됩니다. + +대화형 API 문서는 여러 값을 허용하도록 수정 됩니다: + +<img src="/img/tutorial/query-params-str-validations/image02.png"> + +### 쿼리 매개변수 리스트 / 기본값을 사용하는 다중값 + +그리고 제공된 값이 없으면 기본 `list` 값을 정의할 수도 있습니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial012.py!} +``` + +아래로 이동한다면: + +``` +http://localhost:8000/items/ +``` + +`q`의 기본값은: `["foo", "bar"]`이며 응답은 다음이 됩니다: + +```JSON +{ + "q": [ + "foo", + "bar" + ] +} +``` + +#### `list` 사용하기 + +`List[str]` 대신 `list`를 직접 사용할 수도 있습니다: + +```Python hl_lines="7" +{!../../../docs_src/query_params_str_validations/tutorial013.py!} +``` + +!!! note "참고" + 이 경우 FastAPI는 리스트의 내용을 검사하지 않음을 명심하기 바랍니다. + + 예를 들어, `List[int]`는 리스트 내용이 정수인지 검사(및 문서화)합니다. 하지만 `list` 단독일 경우는 아닙니다. + +## 더 많은 메타데이터 선언 + +매개변수에 대한 정보를 추가할 수 있습니다. + +해당 정보는 생성된 OpenAPI에 포함되고 문서 사용자 인터페이스 및 외부 도구에서 사용됩니다. + +!!! note "참고" + 도구에 따라 OpenAPI 지원 수준이 다를 수 있음을 명심하기 바랍니다. + + 일부는 아직 선언된 추가 정보를 모두 표시하지 않을 수 있지만, 대부분의 경우 누락된 기능은 이미 개발 계획이 있습니다. + +`title`을 추가할 수 있습니다: + +```Python hl_lines="10" +{!../../../docs_src/query_params_str_validations/tutorial007.py!} +``` + +그리고 `description`도 추가할 수 있습니다: + +```Python hl_lines="13" +{!../../../docs_src/query_params_str_validations/tutorial008.py!} +``` + +## 별칭 매개변수 + +매개변수가 `item-query`이길 원한다고 가정해 봅시다. + +마치 다음과 같습니다: + +``` +http://127.0.0.1:8000/items/?item-query=foobaritems +``` + +그러나 `item-query`은 유효한 파이썬 변수 이름이 아닙니다. + +가장 가까운 것은 `item_query`일 겁니다. + +하지만 정확히`item-query`이길 원합니다... + +이럴 경우 `alias`를 선언할 수 있으며, 해당 별칭은 매개변수 값을 찾는 데 사용됩니다: + +```Python hl_lines="9" +{!../../../docs_src/query_params_str_validations/tutorial009.py!} +``` + +## 매개변수 사용하지 않게 하기 + +이제는 더이상 이 매개변수를 마음에 들어하지 않는다고 가정해 봅시다. + +이 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, <abbr title="구식이며, 사용하지 않는 것을 추천">사용되지 않는다(deprecated)</abbr>고 확실하게 문서에서 보여주고 싶습니다. + +그렇다면 `deprecated=True` 매개변수를 `Query`로 전달합니다: + +```Python hl_lines="18" +{!../../../docs_src/query_params_str_validations/tutorial010.py!} +``` + +문서가 아래와 같이 보일겁니다: + +<img src="/img/tutorial/query-params-str-validations/image01.png"> + +## 요약 + +매개변수에 검증과 메타데이터를 추가 선언할 수 있습니다. + +제네릭 검증과 메타데이터: + +* `alias` +* `title` +* `description` +* `deprecated` + +특정 문자열 검증: + +* `min_length` +* `max_length` +* `regex` + +예제에서 `str` 값의 검증을 어떻게 추가하는지 살펴보았습니다. + +숫자와 같은 다른 자료형에 대한 검증을 어떻게 선언하는지 확인하려면 다음 장을 확인하기 바랍니다. From 83944b9e260b865ed587e5dbe6c5203bfb003eb2 Mon Sep 17 00:00:00 2001 From: Dahun Jeong <gnsgnsek@gmail.com> Date: Tue, 23 Jan 2024 04:37:52 +0900 Subject: [PATCH 1705/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/body-multiple-params.md`=20?= =?UTF-8?q?(#2461)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/body-multiple-params.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/ko/docs/tutorial/body-multiple-params.md diff --git a/docs/ko/docs/tutorial/body-multiple-params.md b/docs/ko/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..034c2e84c --- /dev/null +++ b/docs/ko/docs/tutorial/body-multiple-params.md @@ -0,0 +1,170 @@ +# 본문 - 다중 매개변수 + +지금부터 `Path`와 `Query`를 어떻게 사용하는지 확인하겠습니다. + +요청 본문 선언에 대한 심화 사용법을 알아보겠습니다. + +## `Path`, `Query` 및 본문 매개변수 혼합 + +당연하게 `Path`, `Query` 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용할 수 있고, **FastAPI**는 어떤 동작을 할지 압니다. + +또한, 기본 값을 `None`으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있습니다. + +```Python hl_lines="19-21" +{!../../../docs_src/body_multiple_params/tutorial001.py!} +``` + +!!! note "참고" + 이 경우에는 본문으로 부터 가져온 ` item`은 기본값이 `None`이기 때문에, 선택사항이라는 점을 유의해야 합니다. + +## 다중 본문 매개변수 + +이전 예제에서 보듯이, *경로 동작*은 아래와 같이 `Item` 속성을 가진 JSON 본문을 예상합니다: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +하지만, 다중 본문 매개변수 역시 선언할 수 있습니다. 예. `item`과 `user`: + +```Python hl_lines="22" +{!../../../docs_src/body_multiple_params/tutorial002.py!} +``` + +이 경우에, **FastAPI**는 이 함수 안에 한 개 이상의 본문 매개변수(Pydantic 모델인 두 매개변수)가 있다고 알 것입니다. + +그래서, 본문의 매개변수 이름을 키(필드 명)로 사용할 수 있고, 다음과 같은 본문을 예측합니다: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note "참고" + 이전과 같이 `item`이 선언 되었더라도, 본문 내의 `item` 키가 있을 것이라고 예측합니다. + +FastAPI는 요청을 자동으로 변환해, 매개변수의 `item`과 `user`를 특별한 내용으로 받도록 할 것입니다. + +복합 데이터의 검증을 수행하고 OpenAPI 스키마 및 자동 문서를 문서화합니다. + +## 본문 내의 단일 값 + +쿼리 및 경로 매개변수에 대한 추가 데이터를 정의하는 `Query`와 `Path`와 같이, **FastAPI**는 동등한 `Body`를 제공합니다. + +예를 들어 이전의 모델을 확장하면, `item`과 `user`와 동일한 본문에 또 다른 `importance`라는 키를 갖도록 할 수있습니다. + +단일 값을 그대로 선언한다면, **FastAPI**는 쿼리 매개변수로 가정할 것입니다. + +하지만, **FastAPI**의 `Body`를 사용해 다른 본문 키로 처리하도록 제어할 수 있습니다: + + +```Python hl_lines="23" +{!../../../docs_src/body_multiple_params/tutorial003.py!} +``` + +이 경우에는 **FastAPI**는 본문을 이와 같이 예측할 것입니다: + + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +다시 말해, 데이터 타입, 검증, 문서 등을 변환합니다. + +## 다중 본문 매개변수와 쿼리 + +당연히, 필요할 때마다 추가적인 쿼리 매개변수를 선언할 수 있고, 이는 본문 매개변수에 추가됩니다. + +기본적으로 단일 값은 쿼리 매개변수로 해석되므로, 명시적으로 `Query`를 추가할 필요가 없고, 아래처럼 할 수 있습니다: + +```Python hl_lines="27" +{!../../../docs_src/body_multiple_params/tutorial004.py!} +``` + +이렇게: + +```Python +q: Optional[str] = None +``` + +!!! info "정보" + `Body` 또한 `Query`, `Path` 그리고 이후에 볼 다른 것들처럼 동일한 추가 검증과 메타데이터 매개변수를 갖고 있습니다. + +## 단일 본문 매개변수 삽입하기 + +Pydantic 모델 `Item`의 `item`을 본문 매개변수로 오직 한개만 갖고있다고 하겠습니다. + +기본적으로 **FastAPI**는 직접 본문으로 예측할 것입니다. + +하지만, 만약 모델 내용에 `item `키를 가진 JSON으로 예측하길 원한다면, 추가적인 본문 매개변수를 선언한 것처럼 `Body`의 특별한 매개변수인 `embed`를 사용할 수 있습니다: + +```Python hl_lines="17" +{!../../../docs_src/body_multiple_params/tutorial005.py!} +``` + +아래 처럼: + +```Python +item: Item = Body(..., embed=True) +``` + +이 경우에 **FastAPI**는 본문을 아래 대신에: + +```JSON hl_lines="2" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +아래 처럼 예측할 것 입니다: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +## 정리 + +요청이 단 한개의 본문을 가지고 있더라도, *경로 동작 함수*로 다중 본문 매개변수를 추가할 수 있습니다. + +하지만, **FastAPI**는 이를 처리하고, 함수에 올바른 데이터를 제공하며, *경로 동작*으로 올바른 스키마를 검증하고 문서화 합니다. + +또한, 단일 값을 본문의 일부로 받도록 선언할 수 있습니다. + +그리고 **FastAPI**는 단 한개의 매개변수가 선언 되더라도, 본문 내의 키로 삽입 시킬 수 있습니다. From adf61e567548183f03aecf36e42b8fca593081dc Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:39:08 +0000 Subject: [PATCH 1706/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2eb764e1e..634967faf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/query-params-str-validations.md`. PR [#2415](https://github.com/tiangolo/fastapi/pull/2415) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/python-types.md`. PR [#2267](https://github.com/tiangolo/fastapi/pull/2267) by [@jrim](https://github.com/jrim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-nested-models.md`. PR [#2506](https://github.com/tiangolo/fastapi/pull/2506) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/learn/index.md`. PR [#10977](https://github.com/tiangolo/fastapi/pull/10977) by [@KaniKim](https://github.com/KaniKim). From ef1ccb563d3acbdad86c00c2c89bfe356aca79b9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:39:50 +0000 Subject: [PATCH 1707/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 634967faf..96c920d85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-multiple-params.md`. PR [#2461](https://github.com/tiangolo/fastapi/pull/2461) by [@PandaHun](https://github.com/PandaHun). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/query-params-str-validations.md`. PR [#2415](https://github.com/tiangolo/fastapi/pull/2415) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/python-types.md`. PR [#2267](https://github.com/tiangolo/fastapi/pull/2267) by [@jrim](https://github.com/jrim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-nested-models.md`. PR [#2506](https://github.com/tiangolo/fastapi/pull/2506) by [@hard-coders](https://github.com/hard-coders). From 8ec9e30010313fd883aaa54ab8b4b14b88483907 Mon Sep 17 00:00:00 2001 From: Spike Ho Yeol Lee <rurouni24@gmail.com> Date: Tue, 23 Jan 2024 04:41:09 +0900 Subject: [PATCH 1708/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/response-model.md`=20(#2766?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/response-model.md | 210 ++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/ko/docs/tutorial/response-model.md diff --git a/docs/ko/docs/tutorial/response-model.md b/docs/ko/docs/tutorial/response-model.md new file mode 100644 index 000000000..fa90c10ae --- /dev/null +++ b/docs/ko/docs/tutorial/response-model.md @@ -0,0 +1,210 @@ +# 응답 모델 + +어떤 *경로 동작*이든 매개변수 `response_model`를 사용하여 응답을 위한 모델을 선언할 수 있습니다: + +* `@app.get()` +* `@app.post()` +* `@app.put()` +* `@app.delete()` +* 기타. + +```Python hl_lines="17" +{!../../../docs_src/response_model/tutorial001.py!} +``` + +!!! note "참고" + `response_model`은 "데코레이터" 메소드(`get`, `post`, 등)의 매개변수입니다. 모든 매개변수들과 본문(body)처럼 *경로 동작 함수*가 아닙니다. + +Pydantic 모델 어트리뷰트를 선언한 것과 동일한 타입을 수신하므로 Pydantic 모델이 될 수 있지만, `List[Item]`과 같이 Pydantic 모델들의 `list`일 수도 있습니다. + +FastAPI는 이 `response_model`를 사용하여: + +* 출력 데이터를 타입 선언으로 변환. +* 데이터 검증. +* OpenAPI *경로 동작*의 응답에 JSON 스키마 추가. +* 자동 생성 문서 시스템에 사용. + +하지만 가장 중요한 것은: + +* 해당 모델의 출력 데이터 제한. 이것이 얼마나 중요한지 아래에서 볼 것입니다. + +!!! note "기술 세부사항" + 응답 모델은 함수의 타입 어노테이션 대신 이 매개변수로 선언하는데, 경로 함수가 실제 응답 모델을 반환하지 않고 `dict`, 데이터베이스 객체나 기타 다른 모델을 `response_model`을 사용하여 필드 제한과 직렬화를 수행하고 반환할 수 있기 때문입니다 + +## 동일한 입력 데이터 반환 + +여기서 우리는 평문 비밀번호를 포함하는 `UserIn` 모델을 선언합니다: + +```Python hl_lines="9 11" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +그리고 이 모델을 사용하여 입력을 선언하고 같은 모델로 출력을 선언합니다: + +```Python hl_lines="17-18" +{!../../../docs_src/response_model/tutorial002.py!} +``` + +이제 브라우저가 비밀번호로 사용자를 만들 때마다 API는 응답으로 동일한 비밀번호를 반환합니다. + +이 경우, 사용자가 스스로 비밀번호를 발신했기 때문에 문제가 되지 않을 수 있습니다. + +그러나 동일한 모델을 다른 *경로 동작*에서 사용할 경우, 모든 클라이언트에게 사용자의 비밀번호를 발신할 수 있습니다. + +!!! danger "위험" + 절대로 사용자의 평문 비밀번호를 저장하거나 응답으로 발신하지 마십시오. + +## 출력 모델 추가 + +대신 평문 비밀번호로 입력 모델을 만들고 해당 비밀번호 없이 출력 모델을 만들 수 있습니다: + +```Python hl_lines="9 11 16" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +여기서 *경로 동작 함수*가 비밀번호를 포함하는 동일한 입력 사용자를 반환할지라도: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +...`response_model`을 `UserOut` 모델로 선언했기 때문에 비밀번호를 포함하지 않습니다: + +```Python hl_lines="22" +{!../../../docs_src/response_model/tutorial003.py!} +``` + +따라서 **FastAPI**는 출력 모델에서 선언하지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다. + +## 문서에서 보기 + +자동 생성 문서를 보면 입력 모델과 출력 모델이 각자의 JSON 스키마를 가지고 있음을 확인할 수 있습니다: + +<img src="/img/tutorial/response-model/image01.png"> + +그리고 두 모델 모두 대화형 API 문서에 사용됩니다: + +<img src="/img/tutorial/response-model/image02.png"> + +## 응답 모델 인코딩 매개변수 + +응답 모델은 아래와 같이 기본값을 가질 수 있습니다: + +```Python hl_lines="11 13-14" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +* `description: Optional[str] = None`은 기본값으로 `None`을 갖습니다. +* `tax: float = 10.5`는 기본값으로 `10.5`를 갖습니다. +* `tags: List[str] = []` 빈 리스트의 기본값으로: `[]`. + +그러나 실제로 저장되지 않았을 경우 결과에서 값을 생략하고 싶을 수 있습니다. + +예를 들어, NoSQL 데이터베이스에 많은 선택적 속성이 있는 모델이 있지만, 기본값으로 가득 찬 매우 긴 JSON 응답을 보내고 싶지 않습니다. + +### `response_model_exclude_unset` 매개변수 사용 + +*경로 동작 데코레이터* 매개변수를 `response_model_exclude_unset=True`로 설정 할 수 있습니다: + +```Python hl_lines="24" +{!../../../docs_src/response_model/tutorial004.py!} +``` + +이러한 기본값은 응답에 포함되지 않고 실제로 설정된 값만 포함됩니다. + +따라서 해당 *경로 동작*에 ID가 `foo`인 항목(items)을 요청으로 보내면 (기본값을 제외한) 응답은 다음과 같습니다: + +```JSON +{ + "name": "Foo", + "price": 50.2 +} +``` + +!!! info "정보" + FastAPI는 이를 위해 Pydantic 모델의 `.dict()`의 <a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" class="external-link" target="_blank"> `exclude_unset` 매개변수</a>를 사용합니다. + +!!! info "정보" + 아래 또한 사용할 수 있습니다: + + * `response_model_exclude_defaults=True` + * `response_model_exclude_none=True` + + <a href="https://pydantic-docs.helpmanual.io/usage/exporting_models/#modeldict" class="external-link" target="_blank">Pydantic 문서</a>에서 `exclude_defaults` 및 `exclude_none`에 대해 설명한 대로 사용할 수 있습니다. + +#### 기본값이 있는 필드를 갖는 값의 데이터 + +하지만 모델의 필드가 기본값이 있어도 ID가 `bar`인 항목(items)처럼 데이터가 값을 갖는다면: + +```Python hl_lines="3 5" +{ + "name": "Bar", + "description": "The bartenders", + "price": 62, + "tax": 20.2 +} +``` + +응답에 해당 값들이 포함됩니다. + +#### 기본값과 동일한 값을 갖는 데이터 + +If the data has the same values as the default ones, like the item with ID `baz`: +ID가 `baz`인 항목(items)처럼 기본값과 동일한 값을 갖는다면: + +```Python hl_lines="3 5-6" +{ + "name": "Baz", + "description": None, + "price": 50.2, + "tax": 10.5, + "tags": [] +} +``` + +`description`, `tax` 그리고 `tags`가 기본값과 같더라도 (기본값에서 가져오는 대신) 값들이 명시적으로 설정되었다는 것을 인지할 정도로 FastAPI는 충분히 똑똑합니다(사실, Pydantic이 충분히 똑똑합니다). + +따라서 JSON 스키마에 포함됩니다. + +!!! tip "팁" + `None` 뿐만 아니라 다른 어떤 것도 기본값이 될 수 있습니다. + + 리스트(`[]`), `float`인 `10.5` 등이 될 수 있습니다. + +### `response_model_include` 및 `response_model_exclude` + +*경로 동작 데코레이터* 매개변수 `response_model_include` 및 `response_model_exclude`를 사용할 수 있습니다. + +이들은 포함(나머지 생략)하거나 제외(나머지 포함) 할 어트리뷰트의 이름과 `str`의 `set`을 받습니다. + +Pydantic 모델이 하나만 있고 출력에서 ​​일부 데이터를 제거하려는 경우 빠른 지름길로 사용할 수 있습니다. + +!!! tip "팁" + 하지만 이러한 매개변수 대신 여러 클래스를 사용하여 위 아이디어를 사용하는 것을 추천합니다. + + 이는 일부 어트리뷰트를 생략하기 위해 `response_model_include` 또는 `response_model_exclude`를 사용하더라도 앱의 OpenAPI(및 문서)가 생성한 JSON 스키마가 여전히 전체 모델에 대한 스키마이기 때문입니다. + + 비슷하게 작동하는 `response_model_by_alias` 역시 마찬가지로 적용됩니다. + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial005.py!} +``` + +!!! tip "팁" + 문법 `{"name", "description"}`은 두 값을 갖는 `set`을 만듭니다. + + 이는 `set(["name", "description"])`과 동일합니다. + +#### `set` 대신 `list` 사용하기 + +`list` 또는 `tuple` 대신 `set`을 사용하는 법을 잊었더라도, FastAPI는 `set`으로 변환하고 정상적으로 작동합니다: + +```Python hl_lines="31 37" +{!../../../docs_src/response_model/tutorial006.py!} +``` + +## 요약 + +응답 모델을 정의하고 개인정보가 필터되는 것을 보장하기 위해 *경로 동작 데코레이터*의 매개변수 `response_model`을 사용하세요. + +명시적으로 설정된 값만 반환하려면 `response_model_exclude_unset`을 사용하세요. From ea6e0ffdc01464873452af46b36883b7a21e8fec Mon Sep 17 00:00:00 2001 From: Jeesang Kim <jeenowden@gmail.com> Date: Tue, 23 Jan 2024 04:42:37 +0900 Subject: [PATCH 1709/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/static-files.md`=20(#2957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/static-files.md | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/ko/docs/tutorial/static-files.md diff --git a/docs/ko/docs/tutorial/static-files.md b/docs/ko/docs/tutorial/static-files.md new file mode 100644 index 000000000..fe1aa4e5e --- /dev/null +++ b/docs/ko/docs/tutorial/static-files.md @@ -0,0 +1,40 @@ +# 정적 파일 + +'StaticFiles'를 사용하여 디렉토리에서 정적 파일을 자동으로 제공할 수 있습니다. + +## `StaticFiles` 사용 + +* `StaticFiles` 임포트합니다. +* 특정 경로에 `StaticFiles()` 인스턴스를 "마운트" 합니다. + +```Python hl_lines="2 6" +{!../../../docs_src/static_files/tutorial001.py!} +``` + +!!! note "기술적 세부사항" + `from starlette.staticfiles import StaticFiles` 를 사용할 수도 있습니다. + + **FastAPI**는 단지 개발자인, 당신에게 편의를 제공하기 위해 `fastapi.static files` 와 동일한 `starlett.static files`를 제공합니다. 하지만 사실 이것은 Starlett에서 직접 온 것입니다. + +### "마운팅" 이란 + +"마운팅"은 특정 경로에 완전히 "독립적인" 애플리케이션을 추가하는 것을 의미하는데, 그 후 모든 하위 경로에 대해서도 적용됩니다. + +마운트된 응용 프로그램은 완전히 독립적이기 때문에 `APIRouter`를 사용하는 것과는 다릅니다. OpenAPI 및 응용 프로그램의 문서는 마운트된 응용 프로그램 등에서 어떤 것도 포함하지 않습니다. + +자세한 내용은 **숙련된 사용자 안내서**에서 확인할 수 있습니다. + +## 세부사항 + +첫 번째 `"/static"`은 이 "하위 응용 프로그램"이 "마운트"될 하위 경로를 가리킵니다. 따라서 `"/static"`으로 시작하는 모든 경로는 `"/static"`으로 처리됩니다. + +`'directory="static"`은 정적 파일이 들어 있는 디렉토리의 이름을 나타냅니다. + +`name="static"`은 **FastAPI**에서 내부적으로 사용할 수 있는 이름을 제공합니다. + +이 모든 매개변수는 "`static`"과 다를 수 있으며, 사용자 응용 프로그램의 요구 사항 및 구체적인 세부 정보에 따라 매개변수를 조정할 수 있습니다. + + +## 추가 정보 + +자세한 내용과 선택 사항을 보려면 <a href="https://www.starlette.io/staticfiles/" class="external-link" target="_blank">Starlette의 정적 파일에 관한 문서</a>를 확인하십시오. From 792ba017459189ebfe99019d2b0e070b2a38a1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E9=9D=88=E8=AA=9E?= <i@qingly.me> Date: Tue, 23 Jan 2024 03:42:53 +0800 Subject: [PATCH 1710/1881] =?UTF-8?q?=F0=9F=8C=90=20Modify=20the=20descrip?= =?UTF-8?q?tion=20of=20`zh`=20-=20Traditional=20Chinese=20(#10889)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- docs/en/mkdocs.yml | 2 +- docs/language_names.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index e965f4f28..d34e919bd 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -304,7 +304,7 @@ extra: - link: /yo/ name: yo - Yorùbá - link: /zh/ - name: zh - 汉语 + name: zh - 简体中文 - link: /zh-hant/ name: zh-hant - 繁體中文 - link: /em/ diff --git a/docs/language_names.yml b/docs/language_names.yml index 7c37ff2b1..c5a15ddd9 100644 --- a/docs/language_names.yml +++ b/docs/language_names.yml @@ -178,6 +178,6 @@ xh: isiXhosa yi: ייִדיש yo: Yorùbá za: Saɯ cueŋƅ -zh: 汉语 +zh: 简体中文 zh-hant: 繁體中文 zu: isiZulu From 66ef70a2ba2806ad5a2dae9dcc7566609e5ed172 Mon Sep 17 00:00:00 2001 From: "jungsu.kwon" <jungsu.kwon@seoulrobotics.org> Date: Tue, 23 Jan 2024 04:43:22 +0900 Subject: [PATCH 1711/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/path-operation-configuratio?= =?UTF-8?q?n.md`=20(#3639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/path-operation-configuration.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/ko/docs/tutorial/path-operation-configuration.md diff --git a/docs/ko/docs/tutorial/path-operation-configuration.md b/docs/ko/docs/tutorial/path-operation-configuration.md new file mode 100644 index 000000000..22aad0421 --- /dev/null +++ b/docs/ko/docs/tutorial/path-operation-configuration.md @@ -0,0 +1,97 @@ +# 경로 동작 설정 + +*경로 작동 데코레이터*를 설정하기 위해서 전달할수 있는 몇 가지 매개변수가 있습니다. + +!!! warning "경고" + 아래 매개변수들은 *경로 작동 함수*가 아닌 *경로 작동 데코레이터*에 직접 전달된다는 사실을 기억하십시오. + +## 응답 상태 코드 + +*경로 작동*의 응답에 사용될 (HTTP) `status_code`를 정의할수 있습니다. + +`404`와 같은 `int`형 코드를 직접 전달할수 있습니다. + +하지만 각 코드의 의미를 모른다면, `status`에 있는 단축 상수들을 사용할수 있습니다: + +```Python hl_lines="3 17" +{!../../../docs_src/path_operation_configuration/tutorial001.py!} +``` + +각 상태 코드들은 응답에 사용되며, OpenAPI 스키마에 추가됩니다. + +!!! note "기술적 세부사항" + 다음과 같이 임포트하셔도 좋습니다. `from starlette import status`. + + **FastAPI**는 개발자 여러분의 편의를 위해서 `starlette.status`와 동일한 `fastapi.status`를 제공합니다. 하지만 Starlette에서 직접 온 것입니다. + +## 태그 + +(보통 단일 `str`인) `str`로 구성된 `list`와 함께 매개변수 `tags`를 전달하여, `경로 작동`에 태그를 추가할 수 있습니다: + +```Python hl_lines="17 22 27" +{!../../../docs_src/path_operation_configuration/tutorial002.py!} +``` + +전달된 태그들은 OpenAPI의 스키마에 추가되며, 자동 문서 인터페이스에서 사용됩니다: + +<img src="/img/tutorial/path-operation-configuration/image01.png"> + +## 요약과 기술 + +`summary`와 `description`을 추가할 수 있습니다: + +```Python hl_lines="20-21" +{!../../../docs_src/path_operation_configuration/tutorial003.py!} +``` + +## 독스트링으로 만든 기술 + +설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, *경로 작동* 기술을 함수 <abbr title="함수안에 있는 첫번째 표현식으로, 문서로 사용될 여러 줄에 걸친 (변수에 할당되지 않은) 문자열"> 독스트링</abbr> 에 선언할 수 있습니다, 이를 **FastAPI**가 독스트링으로부터 읽습니다. + +<a href="https://ko.wikipedia.org/wiki/%EB%A7%88%ED%81%AC%EB%8B%A4%EC%9A%B4" class="external-link" target="_blank">마크다운</a> 문법으로 독스트링을 작성할 수 있습니다, 작성된 마크다운 형식의 독스트링은 (마크다운의 들여쓰기를 고려하여) 올바르게 화면에 출력됩니다. + +```Python hl_lines="19-27" +{!../../../docs_src/path_operation_configuration/tutorial004.py!} +``` + +이는 대화형 문서에서 사용됩니다: + +<img src="/img/tutorial/path-operation-configuration/image02.png"> + +## 응답 기술 + +`response_description` 매개변수로 응답에 관한 설명을 명시할 수 있습니다: + +```Python hl_lines="21" +{!../../../docs_src/path_operation_configuration/tutorial005.py!} +``` + +!!! info "정보" + `response_description`은 구체적으로 응답을 지칭하며, `description`은 일반적인 *경로 작동*을 지칭합니다. + +!!! check "확인" + OpenAPI는 각 *경로 작동*이 응답에 관한 설명을 요구할 것을 명시합니다. + + 따라서, 응답에 관한 설명이 없을경우, **FastAPI**가 자동으로 "성공 응답" 중 하나를 생성합니다. + +<img src="/img/tutorial/path-operation-configuration/image03.png"> + +## 단일 *경로 작동* 지원중단 + +단일 *경로 작동*을 없애지 않고 <abbr title="구식, 사용하지 않는것이 권장됨">지원중단</abbr>을 해야한다면, `deprecated` 매개변수를 전달하면 됩니다. + +```Python hl_lines="16" +{!../../../docs_src/path_operation_configuration/tutorial006.py!} +``` + +대화형 문서에 지원중단이라고 표시됩니다. + +<img src="/img/tutorial/path-operation-configuration/image04.png"> + +지원중단된 경우와 지원중단 되지 않은 경우에 대한 *경로 작동*이 어떻게 보이는 지 확인하십시오. + +<img src="/img/tutorial/path-operation-configuration/image05.png"> + +## 정리 + +*경로 작동 데코레이터*에 매개변수(들)를 전달함으로 *경로 작동*을 설정하고 메타데이터를 추가할수 있습니다. From 77fe266a690d85aaad3da67cae4951bebcb8c0f5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:44:45 +0000 Subject: [PATCH 1712/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 96c920d85..d264b1c76 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/response-model.md`. PR [#2766](https://github.com/tiangolo/fastapi/pull/2766) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-multiple-params.md`. PR [#2461](https://github.com/tiangolo/fastapi/pull/2461) by [@PandaHun](https://github.com/PandaHun). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/query-params-str-validations.md`. PR [#2415](https://github.com/tiangolo/fastapi/pull/2415) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/python-types.md`. PR [#2267](https://github.com/tiangolo/fastapi/pull/2267) by [@jrim](https://github.com/jrim). From d532602eed903483da3ab815994cea104111ac09 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:46:50 +0000 Subject: [PATCH 1713/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d264b1c76..7895e7e85 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/static-files.md`. PR [#2957](https://github.com/tiangolo/fastapi/pull/2957) by [@jeesang7](https://github.com/jeesang7). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/response-model.md`. PR [#2766](https://github.com/tiangolo/fastapi/pull/2766) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-multiple-params.md`. PR [#2461](https://github.com/tiangolo/fastapi/pull/2461) by [@PandaHun](https://github.com/PandaHun). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/query-params-str-validations.md`. PR [#2415](https://github.com/tiangolo/fastapi/pull/2415) by [@hard-coders](https://github.com/hard-coders). From 167d2524b4dc945fe0b271a5a06de6f227b2a35c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:47:31 +0000 Subject: [PATCH 1714/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7895e7e85..d90e5966e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Modify the description of `zh` - Traditional Chinese. PR [#10889](https://github.com/tiangolo/fastapi/pull/10889) by [@cherinyy](https://github.com/cherinyy). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/static-files.md`. PR [#2957](https://github.com/tiangolo/fastapi/pull/2957) by [@jeesang7](https://github.com/jeesang7). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/response-model.md`. PR [#2766](https://github.com/tiangolo/fastapi/pull/2766) by [@hard-coders](https://github.com/hard-coders). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/body-multiple-params.md`. PR [#2461](https://github.com/tiangolo/fastapi/pull/2461) by [@PandaHun](https://github.com/PandaHun). From 3f95f6fe4113a739cd03f46b99175793cb98e305 Mon Sep 17 00:00:00 2001 From: gyudoza <jujumilk3@gmail.com> Date: Tue, 23 Jan 2024 04:47:57 +0900 Subject: [PATCH 1715/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/deployment/index.md`=20(#4561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/deployment/index.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 docs/ko/docs/deployment/index.md diff --git a/docs/ko/docs/deployment/index.md b/docs/ko/docs/deployment/index.md new file mode 100644 index 000000000..87b05b68f --- /dev/null +++ b/docs/ko/docs/deployment/index.md @@ -0,0 +1,21 @@ +# 배포하기 - 들어가면서 + +**FastAPI**을 배포하는 것은 비교적 쉽습니다. + +## 배포의 의미 + +**배포**란 애플리케이션을 **사용자가 사용**할 수 있도록 하는 데 필요한 단계를 수행하는 것을 의미합니다. + +**웹 API**의 경우, 일반적으로 **사용자**가 중단이나 오류 없이 애플리케이션에 효율적으로 **접근**할 수 있도록 좋은 성능, 안정성 등을 제공하는 **서버 프로그램과** 함께 **원격 시스템**에 이를 설치하는 작업을 의미합니다. + +이는 지속적으로 코드를 변경하고, 지우고, 수정하고, 개발 서버를 중지했다가 다시 시작하는 등의 **개발** 단계와 대조됩니다. + +## 배포 전략 + +사용하는 도구나 특정 사례에 따라 여러 가지 방법이 있습니다. + +배포도구들을 사용하여 직접 **서버에 배포**하거나, 배포작업의 일부를 수행하는 **클라우드 서비스** 또는 다른 방법을 사용할 수도 있습니다. + +**FastAPI** 애플리케이션을 배포할 때 선택할 수 있는 몇 가지 주요 방법을 보여 드리겠습니다 (대부분 다른 유형의 웹 애플리케이션에도 적용됩니다). + +다음 차례에 자세한 내용과 이를 위한 몇 가지 기술을 볼 수 있습니다. ✨ From 79ab317cbdad7a24dcf4b8f17492f54ba2f8a130 Mon Sep 17 00:00:00 2001 From: gyudoza <jujumilk3@gmail.com> Date: Tue, 23 Jan 2024 04:49:13 +0900 Subject: [PATCH 1716/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/deployment/server-workers.md`=20(#49?= =?UTF-8?q?35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/deployment/server-workers.md | 180 ++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/ko/docs/deployment/server-workers.md diff --git a/docs/ko/docs/deployment/server-workers.md b/docs/ko/docs/deployment/server-workers.md new file mode 100644 index 000000000..5653c55e3 --- /dev/null +++ b/docs/ko/docs/deployment/server-workers.md @@ -0,0 +1,180 @@ +# 서버 워커 - 구니콘과 유비콘 + +전단계에서의 배포 개념들을 다시 확인해보겠습니다: + +* 보안 - HTTPS +* 서버 시작과 동시에 실행하기 +* 재시작 +* **복제본 (실행 중인 프로세스의 숫자)** +* 메모리 +* 시작하기 전의 여러 단계들 + +지금까지 문서의 모든 튜토리얼을 참고하여 **단일 프로세스**로 Uvicorn과 같은 **서버 프로그램**을 실행했을 것입니다. + +애플리케이션을 배포할 때 **다중 코어**를 활용하고 더 많은 요청을 처리할 수 있도록 **프로세스 복제본**이 필요합니다. + +전 과정이었던 [배포 개념들](./concepts.md){.internal-link target=_blank}에서 본 것처럼 여러가지 방법이 존재합니다. + +지금부터 <a href="https://gunicorn.org/" class="external-link" target="_blank">**구니콘**</a>을 **유비콘 워커 프로세스**와 함께 사용하는 방법을 알려드리겠습니다. + +!!! 정보 + 만약 도커와 쿠버네티스 같은 컨테이너를 사용하고 있다면 다음 챕터 [FastAPI와 컨테이너 - 도커](./docker.md){.internal-link target=_blank}에서 더 많은 정보를 얻을 수 있습니다. + + 특히, 쿠버네티스에서 실행할 때는 구니콘을 사용하지 않고 대신 컨테이너당 하나의 유비콘 프로세스를 실행하는 것이 좋습니다. 이 장의 뒷부분에서 설명하겠습니다. + +## 구니콘과 유비콘 워커 + +**Gunicorn**은 **WSGI 표준**을 주로 사용하는 애플리케이션 서버입니다. 이것은 구니콘이 플라스크와 쟝고와 같은 애플리케이션을 제공할 수 있다는 것을 의미합니다. 구니콘 자체는 최신 **<a href="https://asgi.readthedocs.io/en/latest/" class="external-link" target="_blank">ASGI 표준</a>**을 사용하기 때문에 FastAPI와 호환되지 않습니다. + +하지만 구니콘은 **프로세스 관리자**역할을 하고 사용자에게 특정 **워커 프로세스 클래스**를 알려줍니다. 그런 다음 구니콘은 해당 클래스를 사용하여 하나 이상의 **워커 프로세스**를 시작합니다. + +그리고 **유비콘**은 **구니콘과 호환되는 워커 클래스**가 있습니다. + +이 조합을 사용하여 구니콘은 **프로세스 관리자** 역할을 하며 **포트**와 **IP**를 관찰하고, **유비콘 클래스**를 실행하는 워커 프로세스로 통신 정보를 **전송**합니다. + +그리고 나서 구니콘과 호환되는 **유비콘 워커** 클래스는 구니콘이 보낸 데이터를 FastAPI에서 사용하기 위한 ASGI 표준으로 변환하는 일을 담당합니다. + +## 구니콘과 유비콘 설치하기 + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" gunicorn + +---> 100% +``` + +</div> + +이 명령어는 유비콘 `standard` 추가 패키지(좋은 성능을 위한)와 구니콘을 설치할 것입니다. + +## 구니콘을 유비콘 워커와 함께 실행하기 + +설치 후 구니콘 실행하기: + +<div class="termy"> + +```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. +``` + +</div> + +각 옵션이 무엇을 의미하는지 살펴봅시다: + +* 이것은 유비콘과 똑같은 문법입니다. `main`은 파이썬 모듈 네임 "`main`"을 의미하므로 `main.py`파일을 뜻합니다. 그리고 `app`은 **FastAPI** 어플리케이션이 들어 있는 변수의 이름입니다. + * `main:app`이 파이썬의 `import` 문법과 흡사한 면이 있다는 걸 알 수 있습니다: + + ```Python + from main import app + ``` + + * 곧, `main:app`안에 있는 콜론의 의미는 파이썬에서 `from main import app`에서의 `import`와 같습니다. +* `--workers`: 사용할 워커 프로세스의 개수이며 숫자만큼의 유비콘 워커를 실행합니다. 이 예제에서는 4개의 워커를 실행합니다. +* `--worker-class`: 워커 프로세스에서 사용하기 위한 구니콘과 호환되는 워커클래스. + * 이런식으로 구니콘이 import하여 사용할 수 있는 클래스를 전달해줍니다: + + ```Python + import uvicorn.workers.UvicornWorker + ``` + +* `--bind`: 구니콘이 관찰할 IP와 포트를 의미합니다. 콜론 (`:`)을 사용하여 IP와 포트를 구분합니다. + * 만약에 `--bind 0.0.0.0:80` (구니콘 옵션) 대신 유비콘을 직접 실행하고 싶다면 `--host 0.0.0.0`과 `--port 80`을 사용해야 합니다. + +출력에서 각 프로세스에 대한 **PID** (process ID)를 확인할 수 있습니다. (단순한 숫자입니다) + +출력 내용: + +* 구니콘 **프로세스 매니저**는 PID `19499`로 실행됩니다. (직접 실행할 경우 숫자가 다를 수 있습니다) +* 다음으로 `Listening at: http://0.0.0.0:80`을 시작합니다. +* 그런 다음 사용해야할 `uvicorn.workers.UvicornWorker`의 워커클래스를 탐지합니다. +* 그리고 PID `19511`, `19513`, `19514`, 그리고 `19515`를 가진 **4개의 워커**를 실행합니다. + + +또한 구니콘은 워커의 수를 유지하기 위해 **죽은 프로세스**를 관리하고 **재시작**하는 작업을 책임집니다. 이것은 이번 장 상단 목록의 **재시작** 개념을 부분적으로 도와주는 것입니다. + +그럼에도 불구하고 필요할 경우 외부에서 **구니콘을 재시작**하고, 혹은 **서버를 시작할 때 실행**할 수 있도록 하고 싶어할 것입니다. + +## 유비콘과 워커 + +유비콘은 몇 개의 **워커 프로세스**와 함께 실행할 수 있는 선택지가 있습니다. + +그럼에도 불구하고, 유비콘은 워커 프로세스를 다루는 데에 있어서 구니콘보다 더 제한적입니다. 따라서 이 수준(파이썬 수준)의 프로세스 관리자를 사용하려면 구니콘을 프로세스 관리자로 사용하는 것이 좋습니다. + +보통 이렇게 실행할 수 있습니다: + +<div class="termy"> + +```console +$ uvicorn main:app --host 0.0.0.0 --port 8080 --workers 4 +<font color="#A6E22E">INFO</font>: Uvicorn running on <b>http://0.0.0.0:8080</b> (Press CTRL+C to quit) +<font color="#A6E22E">INFO</font>: Started parent process [<font color="#A1EFE4"><b>27365</b></font>] +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27368</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27369</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27370</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +<font color="#A6E22E">INFO</font>: Started server process [<font color="#A1EFE4">27367</font>] +<font color="#A6E22E">INFO</font>: Waiting for application startup. +<font color="#A6E22E">INFO</font>: Application startup complete. +``` + +</div> + +새로운 옵션인 `--workers`은 유비콘에게 4개의 워커 프로세스를 사용한다고 알려줍니다. + +각 프로세스의 **PID**를 확인할 수 있습니다. `27365`는 상위 프로세스(**프로세스 매니저**), 그리고 각각의 워커프로세스는 `27368`, `27369`, `27370`, 그리고 `27367`입니다. + +## 배포 개념들 + +여기에서는 **유비콘 워커 프로세스**를 관리하는 **구니콘**(또는 유비콘)을 사용하여 애플리케이션을 **병렬화**하고, CPU **멀티 코어**의 장점을 활용하고, **더 많은 요청**을 처리할 수 있는 방법을 살펴보았습니다. + +워커를 사용하는 것은 배포 개념 목록에서 주로 **복제본** 부분과 **재시작**에 약간 도움이 되지만 다른 배포 개념들도 다루어야 합니다: + +* **보안 - HTTPS** +* **서버 시작과 동시에 실행하기** +* ***재시작*** +* 복제본 (실행 중인 프로세스의 숫자) +* **메모리** +* **시작하기 전의 여러 단계들** + + +## 컨테이너와 도커 + +다음 장인 [FastAPI와 컨테이너 - 도커](./docker.md){.internal-link target=_blank}에서 다른 **배포 개념들**을 다루는 전략들을 알려드리겠습니다. + +또한 간단한 케이스에서 사용할 수 있는, **구니콘과 유비콘 워커**가 포함돼 있는 **공식 도커 이미지**와 함께 몇 가지 기본 구성을 보여드리겠습니다. + +그리고 단일 유비콘 프로세스(구니콘 없이)를 실행할 수 있도록 **사용자 자신의 이미지를 처음부터 구축**하는 방법도 보여드리겠습니다. 이는 간단한 과정이며, **쿠버네티스**와 같은 분산 컨테이너 관리 시스템을 사용할 때 수행할 작업입니다. + +## 요약 + +당신은 **구니콘**(또는 유비콘)을 유비콘 워커와 함께 프로세스 관리자로 사용하여 **멀티-코어 CPU**를 활용하는 **멀티 프로세스를 병렬로 실행**할 수 있습니다. + +다른 배포 개념을 직접 다루면서 **자신만의 배포 시스템**을 구성하는 경우 이러한 도구와 개념들을 활용할 수 있습니다. + +다음 장에서 컨테이너(예: 도커 및 쿠버네티스)와 함께하는 **FastAPI**에 대해 배워보세요. 이러한 툴에는 다른 **배포 개념**들을 간단히 해결할 수 있는 방법이 있습니다. ✨ From 6f4223430124a30648726e02db429cd13f9e44dd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:49:43 +0000 Subject: [PATCH 1717/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d90e5966e..7f88bc379 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/path-operation-configuration.md`. PR [#3639](https://github.com/tiangolo/fastapi/pull/3639) by [@jungsu-kwon](https://github.com/jungsu-kwon). * 🌐 Modify the description of `zh` - Traditional Chinese. PR [#10889](https://github.com/tiangolo/fastapi/pull/10889) by [@cherinyy](https://github.com/cherinyy). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/static-files.md`. PR [#2957](https://github.com/tiangolo/fastapi/pull/2957) by [@jeesang7](https://github.com/jeesang7). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/response-model.md`. PR [#2766](https://github.com/tiangolo/fastapi/pull/2766) by [@hard-coders](https://github.com/hard-coders). From 6c3d8eb2d9318e4e2a5d3e697b18e1bcdacb290e Mon Sep 17 00:00:00 2001 From: HyeonJeong Yeo <84669195+nearnear@users.noreply.github.com> Date: Tue, 23 Jan 2024 04:50:44 +0900 Subject: [PATCH 1718/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/deployment/docker.md`=20(#5657)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/deployment/docker.md | 698 ++++++++++++++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 docs/ko/docs/deployment/docker.md diff --git a/docs/ko/docs/deployment/docker.md b/docs/ko/docs/deployment/docker.md new file mode 100644 index 000000000..587b445fc --- /dev/null +++ b/docs/ko/docs/deployment/docker.md @@ -0,0 +1,698 @@ +# 컨테이너의 FastAPI - 도커 + +FastAPI 어플리케이션을 배포할 때 일반적인 접근 방법은 **리눅스 컨테이너 이미지**를 생성하는 것입니다. 이 방법은 주로 <a href="https://www.docker.com/" class="external-link" target="_blank">**도커**</a>를 사용해 이루어집니다. 그런 다음 해당 컨테이너 이미지를 몇가지 방법으로 배포할 수 있습니다. + +리눅스 컨테이너를 사용하는 데에는 **보안**, **반복 가능성**, **단순함** 등의 장점이 있습니다. + +!!! 팁 + 시간에 쫓기고 있고 이미 이런것들을 알고 있다면 [`Dockerfile`👇](#build-a-docker-image-for-fastapi)로 점프할 수 있습니다. + +<details> +<summary>도커파일 미리보기 👀</summary> + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +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"] +``` + +</details> + +## 컨테이너란 + +컨테이너(주로 리눅스 컨테이너)는 어플리케이션의 의존성과 필요한 파일들을 모두 패키징하는 매우 **가벼운** 방법입니다. 컨테이너는 같은 시스템에 있는 다른 컨테이너(다른 어플리케이션이나 요소들)와 독립적으로 유지됩니다. + +리눅스 컨테이너는 호스트(머신, 가상 머신, 클라우드 서버 등)와 같은 리눅스 커널을 사용해 실행됩니다. 이말은 리눅스 컨테이너가 (전체 운영체제를 모방하는 다른 가상 머신과 비교했을 때) 매우 가볍다는 것을 의미합니다. + +이 방법을 통해, 컨테이너는 직접 프로세스를 실행하는 것과 비슷한 정도의 **적은 자원**을 소비합니다 (가상 머신은 훨씬 많은 자원을 소비할 것입니다). + +컨테이너는 또한 그들만의 **독립된** 실행 프로세스 (일반적으로 하나의 프로세스로 충분합니다), 파일 시스템, 그리고 네트워크를 가지므로 배포, 보안, 개발 및 기타 과정을 단순화 합니다. + +## 컨테이너 이미지란 + +**컨테이너**는 **컨테이너 이미지**를 실행한 것 입니다. + +컨테이너 이미지란 컨테이너에 필요한 모든 파일, 환경 변수 그리고 디폴트 명령/프로그램의 **정적** 버전입니다. 여기서 **정적**이란 말은 컨테이너 **이미지**가 작동되거나 실행되지 않으며, 단지 패키지 파일과 메타 데이터라는 것을 의미합니다. + +저장된 정적 컨텐츠인 **컨테이너 이미지**와 대조되게, **컨테이너**란 보통 실행될 수 있는 작동 인스턴스를 의미합니다. + +**컨테이너**가 (**컨테이너 이미지**로 부터) 시작되고 실행되면, 컨테이너는 파일이나 환경 변수를 생성하거나 변경할 수 있습니다. 이러한 변화는 오직 컨테이너에서만 존재하며, 그 기반이 되는 컨테이너 이미지에는 지속되지 않습니다 (즉 디스크에는 저장되지 않습니다). + +컨테이너 이미지는 **프로그램** 파일과 컨텐츠, 즉 `python`과 어떤 파일 `main.py`에 비교할 수 있습니다. + +그리고 (**컨테이너 이미지**와 대비해서) **컨테이너**는 이미지의 실제 실행 인스턴스로 **프로세스**에 비교할 수 있습니다. 사실, 컨테이너는 **프로세스 러닝**이 있을 때만 실행됩니다 (그리고 보통 하나의 프로세스 입니다). 컨테이너는 내부에서 실행되는 프로세스가 없으면 종료됩니다. + +## 컨테이너 이미지 + +도커는 **컨테이너 이미지**와 **컨테이너**를 생성하고 관리하는데 주요 도구 중 하나가 되어왔습니다. + +그리고 <a href="https://hub.docker.com/" class="external-link" target="_blank">도커 허브</a>에 다양한 도구, 환경, 데이터베이스, 그리고 어플리케이션에 대해 미리 만들어진 **공식 컨테이너 이미지**가 공개되어 있습니다. + +예를 들어, 공식 <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">파이썬 이미지</a>가 있습니다. + +또한 다른 대상, 예를 들면 데이터베이스를 위한 이미지들도 있습니다: + +* <a href="https://hub.docker.com/_/postgres" class="external-link" target="_blank">PostgreSQL</a> +* <a href="https://hub.docker.com/_/mysql" class="external-link" target="_blank">MySQL</a> +* <a href="https://hub.docker.com/_/mongo" class="external-link" target="_blank">MongoDB</a> +* <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a> 등 + +미리 만들어진 컨테이너 이미지를 사용하면 서로 다른 도구들을 **결합**하기 쉽습니다. 대부분의 경우에, **공식 이미지들**을 사용하고 환경 변수를 통해 설정할 수 있습니다. + +이런 방법으로 대부분의 경우에 컨테이너와 도커에 대해 배울 수 있으며 다양한 도구와 요소들에 대한 지식을 재사용할 수 있습니다. + +따라서, 서로 다른 **다중 컨테이너**를 생성한 다음 이들을 연결할 수 있습니다. 예를 들어 데이터베이스, 파이썬 어플리케이션, 리액트 프론트엔드 어플리케이션을 사용하는 웹 서버에 대한 컨테이너를 만들어 이들의 내부 네트워크로 각 컨테이너를 연결할 수 있습니다. + +모든 컨테이너 관리 시스템(도커나 쿠버네티스)은 이러한 네트워킹 특성을 포함하고 있습니다. + +## 컨테이너와 프로세스 + +**컨테이너 이미지**는 보통 **컨테이너**를 시작하기 위해 필요한 메타데이터와 디폴트 커맨드/프로그램과 그 프로그램에 전달하기 위한 파라미터들을 포함합니다. 이는 커맨드 라인에서 프로그램을 실행할 때 필요한 값들과 유사합니다. + +**컨테이너**가 시작되면, 해당 커맨드/프로그램이 실행됩니다 (그러나 다른 커맨드/프로그램을 실행하도록 오버라이드 할 수 있습니다). + +컨테이너는 **메인 프로세스**(커맨드 또는 프로그램)이 실행되는 동안 실행됩니다. + +컨테이너는 일반적으로 **단일 프로세스**를 가지고 있지만, 메인 프로세스의 서브 프로세스를 시작하는 것도 가능하며, 이 방법으로 하나의 컨테이너에 **다중 프로세스**를 가질 수 있습니다. + +그러나 **최소한 하나의 실행중인 프로세스**를 가지지 않고서는 실행중인 컨테이너를 가질 수 없습니다. 만약 메인 프로세스가 중단되면, 컨테이너도 중단됩니다. + +## FastAPI를 위한 도커 이미지 빌드하기 + +이제 무언가를 만들어 봅시다! 🚀 + +**공식 파이썬** 이미지에 기반하여, FastAPI를 위한 **도커 이미지**를 **맨 처음부터** 생성하는 방법을 보이겠습니다. + +**대부분의 경우**에 다음과 같은 것들을 하게 됩니다. 예를 들면: + +* **쿠버네티스** 또는 유사한 도구 사용하기 +* **라즈베리 파이**로 실행하기 +* 컨테이너 이미지를 실행할 클라우드 서비스 사용하기 등 + +### 요구 패키지 + +일반적으로는 어플리케이션의 특정 파일을 위한 **패키지 요구 조건**이 있을 것입니다. + +그 요구 조건을 **설치**하는 방법은 여러분이 사용하는 도구에 따라 다를 것입니다. + +가장 일반적인 방법은 패키지 이름과 버전이 줄 별로 기록된 `requirements.txt` 파일을 만드는 것입니다. + +버전의 범위를 설정하기 위해서는 [FastAPI 버전들에 대하여](./versions.md){.internal-link target=_blank}에 쓰여진 것과 같은 아이디어를 사용합니다. + +예를 들어, `requirements.txt` 파일은 다음과 같을 수 있습니다: + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +그리고 일반적으로 패키지 종속성은 `pip`로 설치합니다. 예를 들어: + +<div class="termy"> + +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +</div> + +!!! 정보 + 패키지 종속성을 정의하고 설치하기 위한 방법과 도구는 다양합니다. + + 나중에 아래 세션에서 Poetry를 사용한 예시를 보이겠습니다. 👇 + +### **FastAPI** 코드 생성하기 + +* `app` 디렉터리를 생성하고 이동합니다. +* 빈 파일 `__init__.py`을 생성합니다. +* 다음과 같은 `main.py`을 생성합니다: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### 도커파일 + +이제 같은 프로젝트 디렉터리에 다음과 같은 파일 `Dockerfile`을 생성합니다: + +```{ .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. 공식 파이썬 베이스 이미지에서 시작합니다. + +2. 현재 워킹 디렉터리를 `/code`로 설정합니다. + + 여기에 `requirements.txt` 파일과 `app` 디렉터리를 위치시킬 것입니다. + +3. 요구 조건과 파일을 `/code` 디렉터리로 복사합니다. + + 처음에는 **오직** 요구 조건이 필요한 파일만 복사하고, 이외의 코드는 그대로 둡니다. + + 이 파일이 **자주 바뀌지 않기 때문에**, 도커는 파일을 탐지하여 이 단계의 **캐시**를 사용하여 다음 단계에서도 캐시를 사용할 수 있도록 합니다. + +4. 요구 조건 파일에 있는 패키지 종속성을 설치합니다. + + `--no-cache-dir` 옵션은 `pip`에게 다운로드한 패키지들을 로컬 환경에 저장하지 않도록 전달합니다. 이는 마치 같은 패키지를 설치하기 위해 오직 `pip`만 다시 실행하면 될 것 같지만, 컨테이너로 작업하는 경우 그렇지는 않습니다. + + !!! 노트 + `--no-cache-dir` 는 오직 `pip`와 관련되어 있으며, 도커나 컨테이너와는 무관합니다. + + `--upgrade` 옵션은 `pip`에게 설치된 패키지들을 업데이트하도록 합니다. + + 이전 단계에서 파일을 복사한 것이 **도커 캐시**에 의해 탐지되기 때문에, 이 단계에서도 가능한 한 **도커 캐시**를 사용하게 됩니다. + + 이 단계에서 캐시를 사용하면 **매번** 모든 종속성을 다운로드 받고 설치할 필요가 없어, 개발 과정에서 이미지를 지속적으로 생성하는 데에 드는 **시간**을 많이 **절약**할 수 있습니다. + +5. `/code` 디렉터리에 `./app` 디렉터리를 복사합니다. + + **자주 변경되는** 모든 코드를 포함하고 있기 때문에, 도커 **캐시**는 이 단계나 **이후의 단계에서** 잘 사용되지 않습니다. + + 그러므로 컨테이너 이미지 빌드 시간을 최적화하기 위해 `Dockerfile`의 **거의 끝 부분**에 입력하는 것이 중요합니다. + +6. `uvicorn` 서버를 실행하기 위해 **커맨드**를 설정합니다. + + `CMD`는 문자열 리스트를 입력받고, 각 문자열은 커맨드 라인의 각 줄에 입력할 문자열입니다. + + 이 커맨드는 **현재 워킹 디렉터리**에서 실행되며, 이는 위에서 `WORKDIR /code`로 설정한 `/code` 디렉터리와 같습니다. + + 프로그램이 `/code`에서 시작하고 그 속에 `./app` 디렉터리가 여러분의 코드와 함께 들어있기 때문에, **Uvicorn**은 이를 보고 `app`을 `app.main`으로부터 **불러 올** 것입니다. + +!!! 팁 + 각 코드 라인을 코드의 숫자 버블을 클릭하여 리뷰할 수 있습니다. 👆 + +이제 여러분은 다음과 같은 디렉터리 구조를 가지고 있을 것입니다: + +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + +#### TLS 종료 프록시의 배후 + +만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, `--proxy-headers` 옵션을 더하는 것이 좋습니다. 이 옵션은 Uvicorn에게 어플리케이션이 HTTPS 등의 뒤에서 실행되고 있으므로 프록시에서 전송된 헤더를 신뢰할 수 있다고 알립니다. + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### 도커 캐시 + +이 `Dockerfile`에는 중요한 트릭이 있는데, 처음에는 **의존성이 있는 파일만** 복사하고, 나머지 코드는 그대로 둡니다. 왜 이런 방법을 써야하는지 설명하겠습니다. + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +도커와 다른 도구들은 컨테이너 이미지를 **증가하는 방식으로 빌드**합니다. `Dockerfile`의 맨 윗 부분부터 시작해, 레이어 위에 새로운 레이어를 더하는 방식으로, `Dockerfile`의 각 지시 사항으로 부터 생성된 어떤 파일이든 더해갑니다. + +도커 그리고 이와 유사한 도구들은 이미지 생성 시에 **내부 캐시**를 사용합니다. 만약 어떤 파일이 마지막으로 컨테이너 이미지를 빌드한 때로부터 바뀌지 않았다면, 파일을 다시 복사하여 새로운 레이어를 처음부터 생성하는 것이 아니라, 마지막에 생성했던 **같은 레이어를 재사용**합니다. + +단지 파일 복사를 지양하는 것으로 효율이 많이 향상되는 것은 아니지만, 그 단계에서 캐시를 사용했기 때문에, **다음 단계에서도 마찬가지로 캐시를 사용**할 수 있습니다. 예를 들어, 다음과 같은 의존성을 설치하는 지시 사항을 위한 캐시를 사용할 수 있습니다: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + +패키지를 포함하는 파일은 **자주 변경되지 않습니다**. 따라서 해당 파일만 복사하므로서, 도커는 그 단계의 **캐시를 사용**할 수 있습니다. + +그 다음으로, 도커는 **다음 단계에서** 의존성을 다운로드하고 설치하는 **캐시를 사용**할 수 있게 됩니다. 바로 이 과정에서 우리는 **많은 시간을 절약**하게 됩니다. ✨ ...그리고 기다리는 지루함도 피할 수 있습니다. 😪😆 + +패키지 의존성을 다운로드 받고 설치하는 데이는 **수 분이 걸릴 수 있지만**, **캐시**를 사용하면 최대 **수 초만에** 끝낼 수 있습니다. + +또한 여러분이 개발 과정에서 코드의 변경 사항이 반영되었는지 확인하기 위해 컨테이너 이미지를 계속해서 빌드하면, 절약된 시간은 축적되어 더욱 커질 것입니다. + +그리고 나서 `Dockerfile`의 거의 끝 부분에서, 모든 코드를 복사합니다. 이것이 **가장 빈번하게 변경**되는 부분이며, 대부분의 경우에 이 다음 단계에서는 캐시를 사용할 수 없기 때문에 가장 마지막에 둡니다. + +```Dockerfile +COPY ./app /code/app +``` + +### 도커 이미지 생성하기 + +이제 모든 파일이 제자리에 있으니, 컨테이너 이미지를 빌드합니다. + +* (여러분의 `Dockerfile`과 `app` 디렉터리가 위치한) 프로젝트 디렉터리로 이동합니다. +* FastAPI 이미지를 빌드합니다: + +<div class="termy"> + +```console +$ docker build -t myimage . + +---> 100% +``` + +</div> + +!!! 팁 + 맨 끝에 있는 `.` 에 주목합시다. 이는 `./`와 동등하며, 도커에게 컨테이너 이미지를 빌드하기 위한 디렉터리를 알려줍니다. + + 이 경우에는 현재 디렉터리(`.`)와 같습니다. + +### 도커 컨테이너 시작하기 + +* 여러분의 이미지에 기반하여 컨테이너를 실행합니다: + +<div class="termy"> + +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +</div> + +## 체크하기 + +여러분의 도커 컨테이너 URL에서 실행 사항을 체크할 수 있습니다. 예를 들어: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> 또는 <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (또는 동일하게, 여러분의 도커 호스트를 이용해서 체크할 수도 있습니다). + +아래와 비슷한 것을 보게 될 것입니다: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## 인터랙티브 API 문서 + +이제 여러분은 <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> 또는 <a href="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a>로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). + +여러분은 자동으로 생성된 인터랙티브 API(<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>에서 제공된)를 볼 수 있습니다: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## 대안 API 문서 + +또한 여러분은 <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> 또는 <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a>으로 이동할 수 있습니다(또는, 여러분의 도커 호스트를 이용할 수 있습니다). + +여러분은 자동으로 생성된 대안 문서(<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>에서 제공된)를 볼 수 있습니다: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 단일 파일 FastAPI로 도커 이미지 생성하기 + +만약 여러분의 FastAPI가 하나의 파일이라면, 예를 들어 `./app` 디렉터리 없이 `main.py` 파일만으로 이루어져 있다면, 파일 구조는 다음과 유사할 것입니다: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +그러면 여러분들은 `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. `main.py` 파일을 `/code` 디렉터리로 곧바로 복사합니다(`./app` 디렉터리는 고려하지 않습니다). + +2. Uvicorn을 실행해 `app` 객체를 (`app.main` 대신) `main`으로 부터 불러오도록 합니다. + +그 다음 Uvicorn 커맨드를 조정해서 FastAPI 객체를 불러오는데 `app.main` 대신에 새로운 모듈 `main`을 사용하도록 합니다. + +## 배포 개념 + +이제 컨테이너의 측면에서 [배포 개념](./concepts.md){.internal-link target=_blank}에서 다루었던 것과 같은 배포 개념에 대해 이야기해 보겠습니다. + +컨테이너는 주로 어플리케이션을 빌드하고 배포하기 위한 과정을 단순화하는 도구이지만, **배포 개념**에 대한 특정한 접근법을 강요하지 않기 때문에 가능한 배포 전략에는 여러가지가 있습니다. + +**좋은 소식**은 서로 다른 전략들을 포괄하는 배포 개념이 있다는 점입니다. 🎉 + +컨테이너 측면에서 **배포 개념**을 리뷰해 보겠습니다: + +* HTTPS +* 구동하기 +* 재시작 +* 복제 (실행 중인 프로세스 개수) +* 메모리 +* 시작하기 전 단계들 + +## HTTPS + +만약 우리가 FastAPI 어플리케이션을 위한 **컨테이너 이미지**에만 집중한다면 (그리고 나중에 실행될 **컨테이너**에), HTTPS는 일반적으로 다른 도구에 의해 **외부적으로** 다루어질 것 입니다. + +**HTTPS**와 **인증서**의 **자동** 취득을 다루는 것은 다른 컨테이너가 될 수 있는데, 예를 들어 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>을 사용하는 것입니다. + +!!! 팁 + Traefik은 도커, 쿠버네티스, 그리고 다른 도구와 통합되어 있어 여러분의 컨테이너를 포함하는 HTTPS를 셋업하고 설정하는 것이 매우 쉽습니다. + +대안적으로, HTTPS는 클라우드 제공자에 의해 서비스의 일환으로 다루어질 수도 있습니다 (이때도 어플리케이션은 여전히 컨테이너에서 실행될 것입니다). + +## 구동과 재시작 + +여러분의 컨테이너를 **시작하고 실행하는** 데에 일반적으로 사용되는 도구는 따로 있습니다. + +이는 **도커** 자체일 수도 있고, **도커 컴포즈**, **쿠버네티스**, **클라우드 서비스** 등이 될 수 있습니다. + +대부분 (또는 전체) 경우에, 컨테이너를 구동하거나 고장시에 재시작하도록 하는 간단한 옵션이 있습니다. 예를 들어, 도커에서는, 커맨드 라인 옵션 `--restart` 입니다. + +컨테이너를 사용하지 않고서는, 어플리케이션을 구동하고 재시작하는 것이 매우 번거롭고 어려울 수 있습니다. 하지만 **컨테이너를 사용한다면** 대부분의 경우에 이런 기능은 기본적으로 포함되어 있습니다. ✨ + +## 복제 - 프로세스 개수 + +만약 여러분이 **쿠버네티스**와 머신 <abbr title="A group of machines that are configured to be connected and work together in some way.">클러스터</abbr>, 도커 스왐 모드, 노마드, 또는 다른 여러 머신 위에 분산 컨테이너를 관리하는 복잡한 시스템을 다루고 있다면, 여러분은 각 컨테이너에서 (워커와 함께 사용하는 Gunicorn 같은) **프로세스 매니저** 대신 **클러스터 레벨**에서 **복제를 다루**고 싶을 것입니다. + +쿠버네티스와 같은 분산 컨테이너 관리 시스템 중 일부는 일반적으로 들어오는 요청에 대한 **로드 밸런싱**을 지원하면서 **컨테이너 복제**를 다루는 통합된 방법을 가지고 있습니다. 모두 **클러스터 레벨**에서 말이죠. + +이런 경우에, 여러분은 [위에서 묘사된 것](#dockerfile)처럼 **처음부터 도커 이미지를** 빌드해서, 의존성을 설치하고, Uvicorn 워커를 관리하는 Gunicorn 대신 **단일 Uvicorn 프로세스**를 실행하고 싶을 것입니다. + +### 로드 밸런서 + +컨테이너로 작업할 때, 여러분은 일반적으로 **메인 포트의 상황을 감지하는** 요소를 가지고 있을 것입니다. 이는 **HTTPS**를 다루는 **TLS 종료 프록시**와 같은 다른 컨테이너일 수도 있고, 유사한 다른 도구일 수도 있습니다. + +이 요소가 요청들의 **로드**를 읽어들이고 각 워커에게 (바라건대) **균형적으로** 분배한다면, 이 요소는 일반적으로 **로드 밸런서**라고 불립니다. + +!!! 팁 + HTTPS를 위해 사용된 **TLS 종료 프록시** 요소 또한 **로드 밸런서**가 될 수 있습니다. + +또한 컨테이너로 작업할 때, 컨테이너를 시작하고 관리하기 위해 사용한 것과 동일한 시스템은 이미 해당 **로드 밸런서**로 부터 여러분의 앱에 해당하는 컨테이너로 **네트워크 통신**(예를 들어, HTTP 요청)을 전송하는 내부적인 도구를 가지고 있을 것입니다 (여기서도 로드 밸런서는 **TLS 종료 프록시**일 수 있습니다). + +### 하나의 로드 밸런서 - 다중 워커 컨테이너 + +**쿠버네티스**나 또는 다른 분산 컨테이너 관리 시스템으로 작업할 때, 시스템 내부의 네트워킹 메커니즘을 이용함으로써 메인 **포트**를 감지하고 있는 단일 **로드 밸런서**는 여러분의 앱에서 실행되고 있는 **여러개의 컨테이너**에 통신(요청들)을 전송할 수 있게 됩니다. + +여러분의 앱에서 실행되고 있는 각각의 컨테이너는 일반적으로 **하나의 프로세스**만 가질 것입니다 (예를 들어, FastAPI 어플리케이션에서 실행되는 하나의 Uvicorn 프로세스처럼). 이 컨테이너들은 모두 같은 것을 실행하는 점에서 **동일한 컨테이너**이지만, 프로세스, 메모리 등은 공유하지 않습니다. 이 방식으로 여러분은 CPU의 **서로 다른 코어들** 또는 **서로 다른 머신들**을 **병렬화**하는 이점을 얻을 수 있습니다. + +또한 **로드 밸런서**가 있는 분산 컨테이너 시스템은 여러분의 앱에 있는 컨테이너 각각에 **차례대로 요청을 분산**시킬 것 입니다. 따라서 각 요청은 여러분의 앱에서 실행되는 여러개의 **복제된 컨테이너들** 중 하나에 의해 다루어질 것 입니다. + +그리고 일반적으로 **로드 밸런서**는 여러분의 클러스터에 있는 *다른* 앱으로 가는 요청들도 다룰 수 있으며 (예를 들어, 다른 도메인으로 가거나 다른 URL 경로 접두사를 가지는 경우), 이 통신들을 클러스터에 있는 *바로 그 다른* 어플리케이션으로 제대로 전송할 수 있습니다. + +### 단일 프로세스를 가지는 컨테이너 + +이 시나리오의 경우, 여러분은 이미 클러스터 레벨에서 복제를 다루고 있을 것이므로 **컨테이너 당 단일 (Uvicorn) 프로세스**를 가지고자 할 것입니다. + +따라서, 여러분은 Gunicorn 이나 Uvicorn 워커, 또는 Uvicorn 워커를 사용하는 Uvicorn 매니저와 같은 프로세스 매니저를 가지고 싶어하지 **않을** 것입니다. 여러분은 컨테이너 당 **단일 Uvicorn 프로세스**를 가지고 싶어할 것입니다 (그러나 아마도 다중 컨테이너를 가질 것입니다). + +이미 여러분이 클러스터 시스템을 관리하고 있으므로, (Uvicorn 워커를 관리하는 Gunicorn 이나 Uvicorn 처럼) 컨테이너 내에 다른 프로세스 매니저를 가지는 것은 **불필요한 복잡성**만 더하게 될 것입니다. + +### 다중 프로세스를 가지는 컨테이너와 특수한 경우들 + +당연한 말이지만, 여러분이 내부적으로 **Uvicorn 워커 프로세스들**를 시작하는 **Gunicorn 프로세스 매니저**를 가지는 단일 컨테이너를 원하는 **특수한 경우**도 있을 것입니다. + +그런 경우에, 여러분들은 **Gunicorn**을 프로세스 매니저로 포함하는 **공식 도커 이미지**를 사용할 수 있습니다. 이 프로세스 매니저는 다중 **Uvicorn 워커 프로세스들**을 실행하며, 디폴트 세팅으로 현재 CPU 코어에 기반하여 자동으로 워커 개수를 조정합니다. 이 사항에 대해서는 아래의 [Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn](#official-docker-image-with-gunicorn-uvicorn)에서 더 다루겠습니다. + +이런 경우에 해당하는 몇가지 예시가 있습니다: + +#### 단순한 앱 + +만약 여러분의 어플리케이션이 **충분히 단순**해서 (적어도 아직은) 프로세스 개수를 파인-튠 할 필요가 없거나 클러스터가 아닌 **단일 서버**에서 실행하고 있다면, 여러분은 컨테이너 내에 프로세스 매니저를 사용하거나 (공식 도커 이미지에서) 자동으로 설정되는 디폴트 값을 사용할 수 있습니다. + +#### 도커 구성 + +여러분은 **도커 컴포즈**로 (클러스터가 아닌) **단일 서버로** 배포할 수 있으며, 이 경우에 공유된 네트워크와 **로드 밸런싱**을 포함하는 (도커 컴포즈로) 컨테이너의 복제를 관리하는 단순한 방법이 없을 수도 있습니다. + +그렇다면 여러분은 **프로세스 매니저**와 함께 내부에 **몇개의 워커 프로세스들**을 시작하는 **단일 컨테이너**를 필요로 할 수 있습니다. + +#### Prometheus와 다른 이유들 + +여러분은 **단일 프로세스**를 가지는 **다중 컨테이너** 대신 **다중 프로세스**를 가지는 **단일 컨테이너**를 채택하는 **다른 이유**가 있을 수 있습니다. + +예를 들어 (여러분의 장치 설정에 따라) Prometheus 익스포터와 같이 같은 컨테이너에 들어오는 **각 요청에 대해** 접근권한을 가지는 도구를 사용할 수 있습니다. + +이 경우에 여러분이 **여러개의 컨테이너들**을 가지고 있다면, Prometheus가 **메트릭을 읽어 들일 때**, 디폴트로 **매번 하나의 컨테이너**(특정 리퀘스트를 관리하는 바로 그 컨테이너)로 부터 읽어들일 것입니다. 이는 모든 복제된 컨테이너에 대해 **축적된 메트릭들**을 읽어들이는 것과 대비됩니다. + +그렇다면 이 경우에는 **다중 프로세스**를 가지는 **하나의 컨테이너**를 두어서 같은 컨테이너에서 모든 내부 프로세스에 대한 Prometheus 메트릭을 수집하는 로컬 도구(예를 들어 Prometheus 익스포터 같은)를 두어서 이 메그릭들을 하나의 컨테이너에 내에서 공유하는 방법이 더 단순할 것입니다. + +--- + +요점은, 이 중의 **어느것도** 여러분들이 반드시 따라야하는 **확정된 사실**이 아니라는 것입니다. 여러분은 이 아이디어들을 **여러분의 고유한 이용 사례를 평가**하는데 사용하고, 여러분의 시스템에 가장 적합한 접근법이 어떤 것인지 결정하며, 다음의 개념들을 관리하는 방법을 확인할 수 있습니다: + +* 보안 - HTTPS +* 구동하기 +* 재시작 +* 복제 (실행 중인 프로세스 개수) +* 메모리 +* 시작하기 전 단계들 + +## 메모리 + +만약 여러분이 **컨테이너 당 단일 프로세스**를 실행한다면, 여러분은 각 컨테이너(복제된 경우에는 여러개의 컨테이너들)에 대해 잘 정의되고, 안정적이며, 제한된 용량의 메모리 소비량을 가지고 있을 것입니다. + +그러면 여러분의 컨테이너 관리 시스템(예를 들어 **쿠버네티스**) 설정에서 앞서 정의된 것과 같은 메모리 제한과 요구사항을 설정할 수 있습니다. 이런 방법으로 **가용 머신**이 필요로하는 메모리와 클러스터에 있는 가용 머신들을 염두에 두고 **컨테이너를 복제**할 수 있습니다. + +만약 여러분의 어플리케이션이 **단순**하다면, 이것은 **문제가 되지 않을** 것이고, 고정된 메모리 제한을 구체화할 필요도 없을 것입니다. 하지만 여러분의 어플리케이션이 (예를 들어 **머신 러닝** 모델같이) **많은 메모리를 소요한다면**, 어플리케이션이 얼마나 많은 양의 메모리를 사용하는지 확인하고 **각 머신에서** 사용하는 **컨테이너의 수**를 조정할 필요가 있습니다 (그리고 필요에 따라 여러분의 클러스터에 머신을 추가할 수 있습니다). + +만약 여러분이 **컨테이너 당 여러개의 프로세스**를 실행한다면 (예를 들어 공식 도커 이미지 처럼), 여러분은 시작된 프로세스 개수가 가용한 것 보다 **더 많은 메모리를 소비**하지 않는지 확인해야 합니다. + +## 시작하기 전 단계들과 컨테이너 + +만약 여러분이 컨테이너(예를 들어 도커, 쿠버네티스)를 사용한다면, 여러분이 접근할 수 있는 주요 방법은 크게 두가지가 있습니다. + +### 다중 컨테이너 + +만약 여러분이 **여러개의 컨테이너**를 가지고 있다면, 아마도 각각의 컨테이너는 **하나의 프로세스**를 가지고 있을 것입니다(예를 들어, **쿠버네티스** 클러스터에서). 그러면 여러분은 복제된 워커 컨테이너를 실행하기 **이전에**, 하나의 컨테이너에 있는 **이전의 단계들을** 수행하는 단일 프로세스를 가지는 **별도의 컨테이너들**을 가지고 싶을 것입니다. + +!!! 정보 + 만약 여러분이 쿠버네티스를 사용하고 있다면, 아마도 이는 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>일 것입니다. + +만약 여러분의 이용 사례에서 이전 단계들을 **병렬적으로 여러번** 수행하는데에 문제가 없다면 (예를 들어 데이터베이스 이전을 실행하지 않고 데이터베이스가 준비되었는지 확인만 하는 경우), 메인 프로세스를 시작하기 전에 이 단계들을 각 컨테이너에 넣을 수 있습니다. + +### 단일 컨테이너 + +만약 여러분의 셋업이 **다중 프로세스**(또는 하나의 프로세스)를 시작하는 **하나의 컨테이너**를 가지는 단순한 셋업이라면, 사전 단계들을 앱을 포함하는 프로세스를 시작하기 직전에 같은 컨테이너에서 실행할 수 있습니다. 공식 도커 이미지는 이를 내부적으로 지원합니다. + +## Gunicorn과 함께하는 공식 도커 이미지 - Uvicorn + +앞 챕터에서 자세하게 설명된 것 처럼, Uvicorn 워커와 같이 실행되는 Gunicorn을 포함하는 공식 도커 이미지가 있습니다: [서버 워커 - Uvicorn과 함께하는 Gunicorn](./server-workers.md){.internal-link target=_blank}. + +이 이미지는 주로 위에서 설명된 상황에서 유용할 것입니다: [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases). + +* <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. + +!!! 경고 + 여러분이 이 베이스 이미지 또는 다른 유사한 이미지를 필요로 하지 **않을** 높은 가능성이 있으며, [위에서 설명된 것처럼: FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi) 처음부터 이미지를 빌드하는 것이 더 나을 수 있습니다. + +이 이미지는 가능한 CPU 코어에 기반한 **몇개의 워커 프로세스**를 설정하는 **자동-튜닝** 메커니즘을 포함하고 있습니다. + +이 이미지는 **민감한 디폴트** 값을 가지고 있지만, 여러분들은 여전히 **환경 변수** 또는 설정 파일을 통해 설정값을 수정하고 업데이트 할 수 있습니다. + +또한 스크립트를 통해 <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#pre_start_path" class="external-link" target="_blank">**시작하기 전 사전 단계**</a>를 실행하는 것을 지원합니다. + +!!! 팁 + 모든 설정과 옵션을 보려면, 도커 이미지 페이지로 이동합니다: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. + +### 공식 도커 이미지에 있는 프로세스 개수 + +이 이미지에 있는 **프로세스 개수**는 가용한 CPU **코어들**로 부터 **자동으로 계산**됩니다. + +이것이 의미하는 바는 이미지가 CPU로부터 **최대한의 성능**을 **쥐어짜낸다**는 것입니다. + +여러분은 이 설정 값을 **환경 변수**나 기타 방법들로 조정할 수 있습니다. + +그러나 프로세스의 개수가 컨테이너가 실행되고 있는 CPU에 의존한다는 것은 또한 **소요되는 메모리의 크기** 또한 이에 의존한다는 것을 의미합니다. + +그렇기 때문에, 만약 여러분의 어플리케이션이 많은 메모리를 요구하고 (예를 들어 머신러닝 모델처럼), 여러분의 서버가 CPU 코어 수는 많지만 **적은 메모리**를 가지고 있다면, 여러분의 컨테이너는 가용한 메모리보다 많은 메모리를 사용하려고 시도할 수 있으며, 결국 퍼포먼스를 크게 떨어뜨릴 수 있습니다(심지어 고장이 날 수도 있습니다). 🚨 + +### `Dockerfile` 생성하기 + +이 이미지에 기반해 `Dockerfile`을 생성하는 방법은 다음과 같습니다: + +```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 +``` + +### 더 큰 어플리케이션 + +만약 여러분이 [다중 파일을 가지는 더 큰 어플리케이션](../tutorial/bigger-applications.md){.internal-link target=_blank}을 생성하는 섹션을 따랐다면, 여러분의 `Dockerfile`은 대신 이렇게 생겼을 것입니다: + +```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 +``` + +### 언제 사용할까 + +여러분들이 **쿠버네티스**(또는 유사한 다른 도구) 사용하거나 클러스터 레벨에서 다중 컨테이너를 이용해 이미 **사본**을 설정하고 있다면, 공식 베이스 이미지(또는 유사한 다른 이미지)를 사용하지 **않는** 것 좋습니다. 그런 경우에 여러분은 다음에 설명된 것 처럼 **처음부터 이미지를 빌드하는 것**이 더 낫습니다: [FastAPI를 위한 도커 이미지 빌드하기](#build-a-docker-image-for-fastapi). + +이 이미지는 위의 [다중 프로세스를 가지는 컨테이너와 특수한 경우들](#containers-with-multiple-processes-and-special-cases)에서 설명된 특수한 경우에 대해서만 주로 유용할 것입니다. 예를 들어, 만약 여러분의 어플리케이션이 **충분히 단순**해서 CPU에 기반한 디폴트 프로세스 개수를 설정하는 것이 잘 작동한다면, 클러스터 레벨에서 수동으로 사본을 설정할 필요가 없을 것이고, 여러분의 앱에서 하나 이상의 컨테이너를 실행하지도 않을 것입니다. 또는 만약에 여러분이 **도커 컴포즈**로 배포하거나, 단일 서버에서 실행하거나 하는 경우에도 마찬가지입니다. + +## 컨테이너 이미지 배포하기 + +컨테이너 (도커) 이미지를 완성한 뒤에 이를 배포하는 방법에는 여러가지 방법이 있습니다. + +예를 들어: + +* 단일 서버에서 **도커 컴포즈**로 배포하기 +* **쿠버네티스** 클러스터로 배포하기 +* 도커 스왐 모드 클러스터로 배포하기 +* 노마드 같은 다른 도구로 배포하기 +* 여러분의 컨테이너 이미지를 배포해주는 클라우드 서비스로 배포하기 + +## Poetry의 도커 이미지 + +만약 여러분들이 프로젝트 의존성을 관리하기 위해 <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a>를 사용한다면, 도커의 멀티-스테이지 빌딩을 사용할 수 있습니다: + +```{ .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. 첫 스테이지로, `requirements-stage`라고 이름 붙였습니다. + +2. `/tmp`를 현재의 워킹 디렉터리로 설정합니다. + + 이 위치에 우리는 `requirements.txt` 파일을 생성할 것입니다. + +3. 이 도커 스테이지에서 Poetry를 설치합니다. + +4. 파일 `pyproject.toml`와 `poetry.lock`를 `/tmp` 디렉터리로 복사합니다. + + `./poetry.lock*` (`*`로 끝나는) 파일을 사용하기 때문에, 파일이 아직 사용가능하지 않더라도 고장나지 않을 것입니다. + +5. `requirements.txt` 파일을 생성합니다. + +6. 이것이 마지막 스테이지로, 여기에 위치한 모든 것이 마지막 컨테이너 이미지에 포함될 것입니다. + +7. 현재의 워킹 디렉터리를 `/code`로 설정합니다. + +8. 파일 `requirements.txt`를 `/code` 디렉터리로 복사합니다. + + 이 파일은 오직 이전의 도커 스테이지에만 존재하며, 때문에 복사하기 위해서 `--from-requirements-stage` 옵션이 필요합니다. + +9. 생성된 `requirements.txt` 파일에 패키지 의존성을 설치합니다. + +10. `app` 디렉터리를 `/code` 디렉터리로 복사합니다. + +11. `uvicorn` 커맨드를 실행하여, `app.main`에서 불러온 `app` 객체를 사용하도록 합니다. + +!!! 팁 + 버블 숫자를 클릭해 각 줄이 하는 일을 알아볼 수 있습니다. + +**도커 스테이지**란 `Dockefile`의 일부로서 나중에 사용하기 위한 파일들을 생성하기 위한 **일시적인 컨테이너 이미지**로 작동합니다. + +첫 스테이지는 오직 **Poetry를 설치**하고 Poetry의 `pyproject.toml` 파일로부터 프로젝트 의존성을 위한 **`requirements.txt`를 생성**하기 위해 사용됩니다. + +이 `requirements.txt` 파일은 **다음 스테이지**에서 `pip`로 사용될 것입니다. + +마지막 컨테이너 이미지에는 **오직 마지막 스테이지만** 보존됩니다. 이전 스테이지(들)은 버려집니다. + +Poetry를 사용할 때 **도커 멀티-스테이지 빌드**를 사용하는 것이 좋은데, 여러분들의 프로젝트 의존성을 설치하기 위해 마지막 컨테이너 이미지에 **오직** `requirements.txt` 파일만 필요하지, Poetry와 그 의존성은 있을 필요가 없기 때문입니다. + +이 다음 (또한 마지막) 스테이지에서 여러분들은 이전에 설명된 것과 비슷한 방식으로 방식으로 이미지를 빌드할 수 있습니다. + +### TLS 종료 프록시의 배후 - Poetry + +이전에 언급한 것과 같이, 만약 여러분이 컨테이너를 Nginx 또는 Traefik과 같은 TLS 종료 프록시 (로드 밸런서) 뒤에서 실행하고 있다면, 커맨드에 `--proxy-headers` 옵션을 추가합니다: + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## 요약 + +컨테이너 시스템(예를 들어 **도커**나 **쿠버네티스**)을 사용하여 모든 **배포 개념**을 다루는 것은 꽤 간단합니다: + +* HTTPS +* 구동하기 +* 재시작 +* 복제 (실행 중인 프로세스 개수) +* 메모리 +* 시작하기 전 단계들 + +대부분의 경우에서 여러분은 어떤 베이스 이미지도 사용하지 않고 공식 파이썬 도커 이미지에 기반해 **처음부터 컨테이너 이미지를 빌드**할 것입니다. + +`Dockerfile`에 있는 지시 사항을 **순서대로** 다루고 **도커 캐시**를 사용하는 것으로 여러분은 **빌드 시간을 최소화**할 수 있으며, 이로써 생산성을 최대화할 수 있습니다 (그리고 지루함을 피할 수 있죠) 😎 + +특별한 경우에는, FastAPI를 위한 공식 도커 이미지를 사용할 수도 있습니다. 🤓 From 22c34a39561e3c95eac73847e7279bc0e97912c2 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:54:45 +0000 Subject: [PATCH 1719/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7f88bc379..7407d7c0b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/deployment/index.md`. PR [#4561](https://github.com/tiangolo/fastapi/pull/4561) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/path-operation-configuration.md`. PR [#3639](https://github.com/tiangolo/fastapi/pull/3639) by [@jungsu-kwon](https://github.com/jungsu-kwon). * 🌐 Modify the description of `zh` - Traditional Chinese. PR [#10889](https://github.com/tiangolo/fastapi/pull/10889) by [@cherinyy](https://github.com/cherinyy). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/static-files.md`. PR [#2957](https://github.com/tiangolo/fastapi/pull/2957) by [@jeesang7](https://github.com/jeesang7). From f8e77fb64c358d44b71d5a77389ffac3b7ca0509 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:55:21 +0000 Subject: [PATCH 1720/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7407d7c0b..37936f5ad 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/deployment/server-workers.md`. PR [#4935](https://github.com/tiangolo/fastapi/pull/4935) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/deployment/index.md`. PR [#4561](https://github.com/tiangolo/fastapi/pull/4561) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/path-operation-configuration.md`. PR [#3639](https://github.com/tiangolo/fastapi/pull/3639) by [@jungsu-kwon](https://github.com/jungsu-kwon). * 🌐 Modify the description of `zh` - Traditional Chinese. PR [#10889](https://github.com/tiangolo/fastapi/pull/10889) by [@cherinyy](https://github.com/cherinyy). From 0a105dc285ed5026d9fada2fe0105d005ec0db73 Mon Sep 17 00:00:00 2001 From: bilal alpaslan <47563997+BilalAlpaslan@users.noreply.github.com> Date: Mon, 22 Jan 2024 22:55:41 +0300 Subject: [PATCH 1721/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/project-generation.md`=20(#5192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/project-generation.md | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/tr/docs/project-generation.md diff --git a/docs/tr/docs/project-generation.md b/docs/tr/docs/project-generation.md new file mode 100644 index 000000000..75e3ae339 --- /dev/null +++ b/docs/tr/docs/project-generation.md @@ -0,0 +1,84 @@ +# Proje oluşturma - Şablonlar + +Başlamak için bir proje oluşturucu kullanabilirsiniz, çünkü sizin için önceden yapılmış birçok başlangıç ​​kurulumu, güvenlik, veritabanı ve temel API endpoinlerini içerir. + +Bir proje oluşturucu, her zaman kendi ihtiyaçlarınıza göre güncellemeniz ve uyarlamanız gereken esnek bir kuruluma sahip olacaktır, ancak bu, projeniz için iyi bir başlangıç ​​noktası olabilir. + +## Full Stack FastAPI PostgreSQL + +GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a> + +### Full Stack FastAPI PostgreSQL - Özellikler + +* Full **Docker** entegrasyonu (Docker based). +* Docker Swarm Mode ile deployment. +* **Docker Compose** entegrasyonu ve lokal geliştirme için optimizasyon. +* Uvicorn ve Gunicorn ile **Production ready** Python web server'ı. +* Python <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">**FastAPI**</a> backend: + * **Hızlı**: **NodeJS** ve **Go** ile eşit, çok yüksek performans (Starlette ve Pydantic'e teşekkürler). + * **Sezgisel**: Editor desteğı. <abbr title="auto-complete, IntelliSense gibi isimlerle de bilinir">Otomatik tamamlama</abbr>. Daha az debugging. + * **Kolay**: Kolay öğrenip kolay kullanmak için tasarlandı. Daha az döküman okuma daha çok iş. + * **Kısa**: Minimum kod tekrarı. Her parametre bildiriminde birden çok özellik. + * **Güçlü**: Production-ready. Otomatik interaktif dökümantasyon. + * **Standartlara dayalı**: API'ler için açık standartlara dayanır (ve tamamen uyumludur): <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> ve <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Şeması</a>. + * <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**Birçok diger özelliği**</a> dahili otomatik doğrulama, serialization, interaktif dokümantasyon, OAuth2 JWT token ile authentication, vb. +* **Güvenli şifreleme** . +* **JWT token** kimlik doğrulama. +* **SQLAlchemy** models (Flask dan bağımsızdır. Celery worker'ları ile kullanılabilir). +* Kullanıcılar için temel başlangıç ​​modeli (gerektiği gibi değiştirin ve kaldırın). +* **Alembic** migration. +* **CORS** (Cross Origin Resource Sharing). +* **Celery** worker'ları ile backend içerisinden seçilen işleri çalıştırabilirsiniz. +* **Pytest**'e dayalı, Docker ile entegre REST backend testleri ile veritabanından bağımsız olarak tam API etkileşimini test edebilirsiniz. Docker'da çalıştığı için her seferinde sıfırdan yeni bir veri deposu oluşturabilir (böylece ElasticSearch, MongoDB, CouchDB veya ne istersen kullanabilirsin ve sadece API'nin çalışıp çalışmadığını test edebilirsin). +* Atom Hydrogen veya Visual Studio Code Jupyter gibi uzantılarla uzaktan veya Docker içi geliştirme için **Jupyter Çekirdekleri** ile kolay Python entegrasyonu. +* **Vue** ile frontend: + * Vue CLI ile oluşturulmuş. + * Dahili **JWT kimlik doğrulama**. + * Dahili Login. + * Login sonrası, Kontrol paneli. + * Kullanıcı oluşturma ve düzenleme kontrol paneli + * Kendi kendine kullanıcı sürümü. + * **Vuex**. + * **Vue-router**. + * **Vuetify** güzel material design kompanentleri için. + * **TypeScript**. + * **Nginx** tabanlı Docker sunucusu (Vue-router için yapılandırılmış). + * Docker ile multi-stage yapı, böylece kodu derlemeniz, kaydetmeniz veya işlemeniz gerekmez. + * Derleme zamanında Frontend testi (devre dışı bırakılabilir). + * Mümkün olduğu kadar modüler yapılmıştır, bu nedenle kutudan çıktığı gibi çalışır, ancak Vue CLI ile yeniden oluşturabilir veya ihtiyaç duyduğunuz şekilde oluşturabilir ve istediğinizi yeniden kullanabilirsiniz. +* **PGAdmin** PostgreSQL database admin tool'u, PHPMyAdmin ve MySQL ile kolayca değiştirilebilir. +* **Flower** ile Celery job'larını monitörleme. +* **Traefik** ile backend ve frontend arasında yük dengeleme, böylece her ikisini de aynı domain altında, path ile ayrılmış, ancak farklı kapsayıcılar tarafından sunulabilirsiniz. +* Let's Encrypt **HTTPS** sertifikalarının otomatik oluşturulması dahil olmak üzere Traefik entegrasyonu. +* GitLab **CI** (sürekli entegrasyon), backend ve frontend testi dahil. + +## Full Stack FastAPI Couchbase + +GitHub: <a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a> + +⚠️ **UYARI** ⚠️ + +Sıfırdan bir projeye başlıyorsanız alternatiflerine bakın. + +Örneğin, <a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">Full Stack FastAPI PostgreSQL</a> daha iyi bir alternatif olabilir, aktif olarak geliştiriliyor ve kullanılıyor. Ve yeni özellik ve ilerlemelere sahip. + +İsterseniz Couchbase tabanlı generator'ı kullanmakta özgürsünüz, hala iyi çalışıyor olmalı ve onunla oluşturulmuş bir projeniz varsa bu da sorun değil (ve muhtemelen zaten ihtiyaçlarınıza göre güncellediniz). + +Bununla ilgili daha fazla bilgiyi repo belgelerinde okuyabilirsiniz. + +## Full Stack FastAPI MongoDB + +... müsaitliğime ve diğer faktörlere bağlı olarak daha sonra gelebilir. 😅 🎉 + +## Machine Learning modelleri, spaCy ve FastAPI + +GitHub: <a href="https://github.com/microsoft/cookiecutter-spacy-fastapi" class="external-link" target="_blank">https://github.com/microsoft/cookiecutter-spacy-fastapi</a> + +### Machine Learning modelleri, spaCy ve FastAPI - Features + +* **spaCy** NER model entegrasyonu. +* **Azure Cognitive Search** yerleşik istek biçimi. +* Uvicorn ve Gunicorn ile **Production ready** Python web server'ı. +* Dahili **Azure DevOps** Kubernetes (AKS) CI/CD deployment. +* **Multilingual**, Proje kurulumu sırasında spaCy'nin yerleşik dillerinden birini kolayca seçin. +* **Esnetilebilir** diğer frameworkler (Pytorch, Tensorflow) ile de çalışır sadece spaCy değil. From 63ffd735d1c88820807b585eacb05de04f142429 Mon Sep 17 00:00:00 2001 From: bilal alpaslan <47563997+BilalAlpaslan@users.noreply.github.com> Date: Mon, 22 Jan 2024 22:57:04 +0300 Subject: [PATCH 1722/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20`docs/tr/docs/async.md`=20(#5191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/async.md | 401 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 docs/tr/docs/async.md diff --git a/docs/tr/docs/async.md b/docs/tr/docs/async.md new file mode 100644 index 000000000..2be594343 --- /dev/null +++ b/docs/tr/docs/async.md @@ -0,0 +1,401 @@ +# Concurrency ve async / await + +*path operasyon fonksiyonu* için `async def `sözdizimi, asenkron kod, eşzamanlılık ve paralellik hakkında bazı ayrıntılar. + +## Aceleniz mi var? + +<abbr title="too long; didn't read"><strong>TL;DR:</strong></abbr> + +Eğer `await` ile çağrılması gerektiğini belirten üçüncü taraf kütüphaneleri kullanıyorsanız, örneğin: + +```Python +results = await some_library() +``` + +O zaman *path operasyon fonksiyonunu* `async def` ile tanımlayın örneğin: + +```Python hl_lines="2" +@app.get('/') +async def read_results(): + results = await some_library() + return results +``` + +!!! not + Sadece `async def` ile tanımlanan fonksiyonlar içinde `await` kullanabilirsiniz. + +--- + +Eğer bir veritabanı, bir API, dosya sistemi vb. ile iletişim kuran bir üçüncü taraf bir kütüphane kullanıyorsanız ve `await` kullanımını desteklemiyorsa, (bu şu anda çoğu veritabanı kütüphanesi için geçerli bir durumdur), o zaman *path operasyon fonksiyonunuzu* `def` kullanarak normal bir şekilde tanımlayın, örneğin: + +```Python hl_lines="2" +@app.get('/') +def results(): + results = some_library() + return results +``` + +--- + +Eğer uygulamanız (bir şekilde) başka bir şeyle iletişim kurmak ve onun cevap vermesini beklemek zorunda değilse, `async def` kullanın. + +--- + +Sadece bilmiyorsanız, normal `def` kullanın. + +--- + +**Not**: *path operasyon fonksiyonlarınızda* `def` ve `async def`'i ihtiyaç duyduğunuz gibi karıştırabilir ve her birini sizin için en iyi seçeneği kullanarak tanımlayabilirsiniz. FastAPI onlarla doğru olanı yapacaktır. + +Her neyse, yukarıdaki durumlardan herhangi birinde, FastAPI yine de asenkron olarak çalışacak ve son derece hızlı olacaktır. + +Ancak yukarıdaki adımları takip ederek, bazı performans optimizasyonları yapılabilecektir. + +## Teknik Detaylar + +Python'un modern versiyonlarında **`async` ve `await`** sözdizimi ile **"coroutines"** kullanan **"asenkron kod"** desteğine sahiptir. + +Bu ifadeyi aşağıdaki bölümlerde daha da ayrıntılı açıklayalım: + +* **Asenkron kod** +* **`async` ve `await`** +* **Coroutines** + +## Asenkron kod + +Asenkron kod programlama dilinin 💬 bilgisayara / programa 🤖 kodun bir noktasında, *başka bir kodun* bir yerde bitmesini 🤖 beklemesi gerektiğini söylemenin bir yoludur. Bu *başka koda* "slow-file" denir 📝. + +Böylece, bu süreçte bilgisayar "slow-file" 📝 tamamlanırken gidip başka işler yapabilir. + +Sonra bilgisayar / program 🤖 her fırsatı olduğunda o noktada yaptığı tüm işleri 🤖 bitirene kadar geri dönücek. Ve 🤖 yapması gerekeni yaparak, beklediği görevlerden herhangi birinin bitip bitmediğini görecek. + +Ardından, 🤖 bitirmek için ilk görevi alır ("slow-file" 📝) ve onunla ne yapması gerekiyorsa onu devam ettirir. + +Bu "başka bir şey için bekle" normalde, aşağıdakileri beklemek gibi (işlemcinin ve RAM belleğinin hızına kıyasla) nispeten "yavaş" olan <abbr title="Input ve Output (Giriş ve Çıkış)">I/O</abbr> işlemlerine atıfta bulunur: + +* istemci tarafından ağ üzerinden veri göndermek +* ağ üzerinden istemciye gönderilen veriler +* sistem tarafından okunacak ve programınıza verilecek bir dosya içeriği +* programınızın diske yazılmak üzere sisteme verdiği dosya içerikleri +* uzak bir API işlemi +* bir veritabanı bitirme işlemi +* sonuçları döndürmek için bir veritabanı sorgusu +* vb. + +Yürütme süresi çoğunlukla <abbr title="Input ve Output (Giriş ve Çıkış)">I/O</abbr> işlemleri beklenerek tüketildiğinden bunlara "I/O bağlantılı" işlemler denir. + +Buna "asenkron" denir, çünkü bilgisayar/program yavaş görevle "senkronize" olmak zorunda değildir, görevin tam olarak biteceği anı bekler, hiçbir şey yapmadan, görev sonucunu alabilmek ve çalışmaya devam edebilmek için . + +Bunun yerine, "asenkron" bir sistem olarak, bir kez bittiğinde, bilgisayarın / programın yapması gerekeni bitirmesi için biraz (birkaç mikrosaniye) sırada bekleyebilir ve ardından sonuçları almak için geri gelebilir ve onlarla çalışmaya devam edebilir. + +"Senkron" ("asenkron"un aksine) için genellikle "sıralı" terimini de kullanırlar, çünkü bilgisayar/program, bu adımlar beklemeyi içerse bile, farklı bir göreve geçmeden önce tüm adımları sırayla izler. + + +### Eşzamanlılık (Concurrency) ve Burgerler + + +Yukarıda açıklanan bu **asenkron** kod fikrine bazen **"eşzamanlılık"** da denir. **"Paralellikten"** farklıdır. + +**Eşzamanlılık** ve **paralellik**, "aynı anda az ya da çok olan farklı işler" ile ilgilidir. + +Ancak *eşzamanlılık* ve *paralellik* arasındaki ayrıntılar oldukça farklıdır. + + +Farkı görmek için burgerlerle ilgili aşağıdaki hikayeyi hayal edin: + +### Eşzamanlı Burgerler + +<!-- Cinsiyetten bağımsız olan aşçı emojisi "🧑‍🍳" tarayıcılarda yeterince iyi görüntülenmiyor. Bu yüzden erken "👨‍🍳" ve kadın "👩‍🍳" aşçıları karışık bir şekilde kullanıcağım. --> + +Aşkınla beraber 😍 dışarı hamburger yemeye çıktınız 🍔, kasiyer 💁 öndeki insanlardan sipariş alırken siz sıraya girdiniz. + +Sıra sizde ve sen aşkın 😍 ve kendin için 2 çılgın hamburger 🍔 söylüyorsun. + +Ödemeyi yaptın 💸. + +Kasiyer 💁 mutfakdaki aşçıya 👨‍🍳 hamburgerleri 🍔 hazırlaması gerektiğini söyler ve aşçı bunu bilir (o an önceki müşterilerin siparişlerini hazırlıyor olsa bile). + +Kasiyer 💁 size bir sıra numarası verir. + +Beklerken askınla 😍 bir masaya oturur ve uzun bir süre konuşursunuz(Burgerleriniz çok çılgın olduğundan ve hazırlanması biraz zaman alıyor ✨🍔✨). + +Hamburgeri beklerkenki zamanı 🍔, aşkının ne kadar zeki ve tatlı olduğuna hayran kalarak harcayabilirsin ✨😍✨. + +Aşkınla 😍 konuşurken arada sıranın size gelip gelmediğini kontrol ediyorsun. + +Nihayet sıra size geldi. Tezgaha gidip hamburgerleri 🍔kapıp masaya geri dönüyorsun. + +Aşkınla hamburgerlerinizi yiyor 🍔 ve iyi vakit geçiriyorsunuz ✨. + +--- + +Bu hikayedeki bilgisayar / program 🤖 olduğunuzu hayal edin. + +Sırada beklerken boştasın 😴, sıranı beklerken herhangi bir "üretim" yapmıyorsun. Ama bu sıra hızlı çünkü kasiyer sadece siparişleri alıyor (onları hazırlamıyor), burada bir sıknıtı yok. + +Sonra sıra size geldiğinde gerçekten "üretken" işler yapabilirsiniz 🤓, menüyü oku, ne istediğine larar ver, aşkının seçimini al 😍, öde 💸, doğru kartı çıkart, ödemeyi kontrol et, faturayı kontrol et, siparişin doğru olup olmadığını kontrol et, vb. + +Ama hamburgerler 🍔 hazır olmamasına rağmen Kasiyer 💁 ile işiniz "duraklıyor" ⏸, çünkü hamburgerlerin hazır olmasını bekliyoruz 🕙. + +Ama tezgahtan uzaklaşıp sıranız gelene kadarmasanıza dönebilir 🔀 ve dikkatinizi aşkınıza 😍 verebilirsiniz vr bunun üzerine "çalışabilirsiniz" ⏯ 🤓. Artık "üretken" birşey yapıyorsunuz 🤓, sevgilinle 😍 flört eder gibi. + +Kasiyer 💁 "Hamburgerler hazır !" 🍔 dediğinde ve görüntülenen numara sizin numaranız olduğunda hemen koşup hamburgerlerinizi almaya çalışmıyorsunuz. Biliyorsunuzki kimse sizin hamburgerlerinizi 🍔 çalmayacak çünkü sıra sizin. + +Yani Aşkınızın😍 hikayeyi bitirmesini bekliyorsunuz (çalışmayı bitir ⏯ / görev işleniyor.. 🤓), nazikçe gülümseyin ve hamburger yemeye gittiğinizi söyleyin ⏸. + +Ardından tezgaha 🔀, şimdi biten ilk göreve ⏯ gidin, Hamburgerleri 🍔 alın, teşekkür edin ve masaya götürün. sayacın bu adımı tamamlanır ⏹. Bu da yeni bir görev olan "hamburgerleri ye" 🔀 ⏯ görevini başlatırken "hamburgerleri al" ⏹ görevini bitirir. + +### Parallel Hamburgerler + +Şimdi bunların "Eşzamanlı Hamburger" değil, "Paralel Hamburger" olduğunu düşünelim. + +Hamburger 🍔 almak için 😍 aşkınla Paralel fast food'a gidiyorsun. + +Birden fazla kasiyer varken (varsayalım 8) sıraya girdiniz👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 ve sıranız gelene kadar bekliyorsunuz. + +Sizden önceki herkez ayrılmadan önce hamburgerlerinin 🍔 hazır olmasını bekliyor 🕙. Çünkü kasiyerlerin her biri bir hamburger hazırlanmadan önce bir sonraki siparişe geçmiiyor. + +Sonunda senin sıran, aşkın 😍 ve kendin için 2 hamburger 🍔 siparişi verdiniz. + +Ödemeyi yaptınız 💸. + +Kasiyer mutfağa gider 👨‍🍳. + +Sırada bekliyorsunuz 🕙, kimse sizin burgerinizi 🍔 almaya çalışmıyor çünkü sıra sizin. + +Sen ve aşkın 😍 sıranızı korumak ve hamburgerleri almakla o kadar meşgulsünüz ki birbirinize vakit 🕙 ayıramıyorsunuz 😞. + +İşte bu "senkron" çalışmadır. Kasiyer/aşçı 👨‍🍳ile senkron hareket ediyorsunuz. Bu yüzden beklemek 🕙 ve kasiyer/aşçı burgeri 🍔bitirip size getirdiğinde orda olmak zorundasınız yoksa başka biri alabilir. + +Sonra kasiyeri/aşçı 👨‍🍳 nihayet hamburgerlerinizle 🍔, uzun bir süre sonra 🕙 tezgaha geri geliyor. + +Burgerlerinizi 🍔 al ve aşkınla masanıza doğru ilerle 😍. + +Sadece burgerini yiyorsun 🍔 ve bitti ⏹. + +Bekleyerek çok fazla zaman geçtiğinden 🕙 konuşmaya çok fazla vakit kalmadı 😞. + +--- + +Paralel burger senaryosunda ise, siz iki işlemcili birer robotsunuz 🤖 (sen ve sevgilin 😍), Beklıyorsunuz 🕙 hem konuşarak güzel vakit geçirirken ⏯ hem de sıranızı bekliyorsunuz 🕙. + +Mağazada ise 8 işlemci bulunuyor (Kasiyer/aşçı) 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳. Eşzamanlı burgerde yalnızca 2 kişi olabiliyordu (bir kasiyer ve bir aşçı) 💁 👨‍🍳. + +Ama yine de bu en iyisi değil 😞. + +--- + +Bu hikaye burgerler 🍔 için paralel. + +Bir gerçek hayat örneği verelim. Bir banka hayal edin. + +Bankaların çoğunda birkaç kasiyer 👨‍💼👨‍💼👨‍💼👨‍💼 ve uzun bir sıra var 🕙🕙🕙🕙🕙🕙🕙🕙. + +Tüm işi sırayla bir müşteri ile yapan tüm kasiyerler 👨‍💼⏯. + +Ve uzun süre kuyrukta beklemek 🕙 zorundasın yoksa sıranı kaybedersin. + +Muhtemelen ayak işlerı yaparken sevgilini 😍 bankaya 🏦 getirmezsin. + +### Burger Sonucu + +Bu "aşkınla fast food burgerleri" senaryosunda, çok fazla bekleme olduğu için 🕙, eşzamanlı bir sisteme sahip olmak çok daha mantıklı ⏸🔀⏯. + +Web uygulamalarının çoğu için durum böyledir. + +Pek çok kullanıcı var, ama sunucunuz pek de iyi olmayan bir bağlantı ile istek atmalarını bekliyor. + +Ve sonra yanıtların geri gelmesi için tekrar 🕙 bekliyor + +Bu "bekleme" 🕙 mikrosaniye cinsinden ölçülür, yine de, hepsini toplarsak çok fazla bekleme var. + +Bu nedenle, web API'leri için asenkron ⏸🔀⏯ kod kullanmak çok daha mantıklı. + +Mevcut popüler Python frameworklerinin çoğu (Flask ve Django gibi), Python'daki yeni asenkron özellikler mevcut olmadan önce yazıldı. Bu nedenle, dağıtılma biçimleri paralel yürütmeyi ve yenisi kadar güçlü olmayan eski bir eşzamansız yürütme biçimini destekler. + +Asenkron web (ASGI) özelliği, WebSockets için destek eklemek için Django'ya eklenmiş olsa da. + +Asenkron çalışabilme NodeJS in popüler olmasının sebebi (paralel olamasa bile) ve Go dilini güçlü yapan özelliktir. + +Ve bu **FastAPI** ile elde ettiğiniz performans düzeyiyle aynıdır. + +Aynı anda paralellik ve asenkronluğa sahip olabildiğiniz için, test edilen NodeJS çerçevelerinin çoğundan daha yüksek performans elde edersiniz ve C'ye daha yakın derlenmiş bir dil olan Go ile eşit bir performans elde edersiniz <a href="https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=query&l=zijmkf-1" class="external-link" target="_blank">(bütün teşekkürler Starlette'e )</a>. + +### Eşzamanlılık paralellikten daha mı iyi? + +Hayır! Hikayenin ahlakı bu değil. + +Eşzamanlılık paralellikten farklıdır. Ve çok fazla bekleme içeren **belirli** senaryolarda daha iyidir. Bu nedenle, genellikle web uygulamaları için paralellikten çok daha iyidir. Ama her şey için değil. + +Yanı, bunu aklınızda oturtmak için aşağıdaki kısa hikayeyi hayal edin: + +> Büyük, kirli bir evi temizlemelisin. + +*Evet, tüm hikaye bu*. + +--- + +Beklemek yok 🕙. Hiçbir yerde. Sadece evin birden fazla yerinde yapılacak fazlasıyla iş var. + +You could have turns as in the burgers example, first the living room, then the kitchen, but as you are not waiting 🕙 for anything, just cleaning and cleaning, the turns wouldn't affect anything. +Hamburger örneğindeki gibi dönüşleriniz olabilir, önce oturma odası, sonra mutfak, ama hiçbir şey için 🕙 beklemediğinizden, sadece temizlik, temizlik ve temizlik, dönüşler hiçbir şeyi etkilemez. + +Sıralı veya sırasız (eşzamanlılık) bitirmek aynı zaman alır ve aynı miktarda işi yaparsınız. + +Ama bu durumda, 8 eski kasiyer/aşçı - yeni temizlikçiyi getirebilseydiniz 👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳👩‍🍳👨‍🍳 ve her birini (artı siz) evin bir bölgesini temizlemek için görevlendirseydiniz, ekstra yardımla tüm işleri **paralel** olarak yapabilir ve çok daha erken bitirebilirdiniz. + +Bu senaryoda, temizlikçilerin her biri (siz dahil) birer işlemci olacak ve üzerine düşeni yapacaktır. + +Yürütme süresinin çoğu (beklemek yerine) iş yapıldığından ve bilgisayardaki iş bir <abbr title="Central Processing Unit">CPU</abbr> tarafından yapıldığından, bu sorunlara "CPU bound" diyorlar". + +--- + +CPU'ya bağlı işlemlerin yaygın örnekleri, karmaşık matematik işlemleri gerektiren işlerdir. + +Örneğin: + +* **Ses** veya **görüntü işleme**. +* **Bilgisayar görüsü**: bir görüntü milyonlarca pikselden oluşur, her pikselin 3 değeri / rengi vardır, bu pikseller üzerinde aynı anda bir şeyler hesaplamayı gerektiren işleme. +* **Makine Öğrenimi**: Çok sayıda "matris" ve "vektör" çarpımı gerektirir. Sayıları olan ve hepsini aynı anda çarpan büyük bir elektronik tablo düşünün. +* **Derin Öğrenme**: Bu, Makine Öğreniminin bir alt alanıdır, dolayısıyla aynısı geçerlidir. Sadece çarpılacak tek bir sayı tablosu değil, büyük bir sayı kümesi vardır ve çoğu durumda bu modelleri oluşturmak ve/veya kullanmak için özel işlemciler kullanırsınız. + +### Eşzamanlılık + Paralellik: Web + Makine Öğrenimi + +**FastAPI** ile web geliştirme için çok yaygın olan eşzamanlılıktan yararlanabilirsiniz (NodeJS'in aynı çekiciliği). + +Ancak, Makine Öğrenimi sistemlerindekile gibi **CPU'ya bağlı** iş yükleri için paralellik ve çoklu işlemenin (birden çok işlemin paralel olarak çalışması) avantajlarından da yararlanabilirsiniz. + +Buna ek olarak Python'un **Veri Bilimi**, Makine Öğrenimi ve özellikle Derin Öğrenme için ana dil olduğu gerçeği, FastAPI'yi Veri Bilimi / Makine Öğrenimi web API'leri ve uygulamaları için çok iyi bir seçenek haline getirir. + +Production'da nasıl oldugunu görmek için şu bölüme bakın [Deployment](deployment/index.md){.internal-link target=_blank}. + +## `async` ve `await` + +Python'un modern sürümleri, asenkron kodu tanımlamanın çok sezgisel bir yoluna sahiptir. Bu, normal "sequentıal" (sıralı) kod gibi görünmesini ve doğru anlarda sizin için "awaıt" ile bekleme yapmasını sağlar. + +Sonuçları vermeden önce beklemeyi gerektirecek ve yeni Python özelliklerini destekleyen bir işlem olduğunda aşağıdaki gibi kodlayabilirsiniz: + +```Python +burgers = await get_burgers(2) +``` + +Buradaki `await` anahtari Python'a, sonuçları `burgers` degiskenine atamadan önce `get_burgers(2)` kodunun işini bitirmesini 🕙 beklemesi gerektiğini söyler. Bununla Python, bu ara zamanda başka bir şey 🔀 ⏯ yapabileceğini bilecektir (başka bir istek almak gibi). + + `await`kodunun çalışması için, eşzamansızlığı destekleyen bir fonksiyonun içinde olması gerekir. Bunu da yapmak için fonksiyonu `async def` ile tanımlamamız yeterlidir: + +```Python hl_lines="1" +async def get_burgers(number: int): + # burgerleri oluşturmak için asenkron birkaç iş + return burgers +``` + +...`def` yerine: + +```Python hl_lines="2" +# bu kod asenkron değil +def get_sequential_burgers(number: int): + # burgerleri oluşturmak için senkron bırkaç iş + return burgers +``` + +`async def` ile Python, bu fonksıyonun içinde, `await` ifadelerinin farkında olması gerektiğini ve çalışma zamanı gelmeden önce bu işlevin yürütülmesini "duraklatabileceğini" ve başka bir şey yapabileceğini 🔀 bilir. + +`async def` fonksiyonunu çağırmak istediğinizde, onu "awaıt" ıle kullanmanız gerekir. Yani, bu işe yaramaz: + +```Python +# Bu işe yaramaz, çünkü get_burgers, şu şekilde tanımlandı: async def +burgers = get_burgers(2) +``` + +--- + +Bu nedenle, size onu `await` ile çağırabileceğinizi söyleyen bir kitaplık kullanıyorsanız, onu `async def` ile tanımlanan *path fonksiyonu* içerisinde kullanmanız gerekir, örneğin: + +```Python hl_lines="2-3" +@app.get('/burgers') +async def read_burgers(): + burgers = await get_burgers(2) + return burgers +``` + +### Daha fazla teknik detay + +`await` in yalnızca `async def` ile tanımlanan fonksıyonların içinde kullanılabileceğini fark etmişsinizdir. + +Ama aynı zamanda, `async def` ile tanımlanan fonksiyonların "await" ile beklenmesi gerekir. Bu nedenle, "`async def` içeren fonksiyonlar yalnızca "`async def` ile tanımlanan fonksiyonların içinde çağrılabilir. + + +Yani yumurta mı tavukdan, tavuk mu yumurtadan gibi ilk `async` fonksiyonu nasıl çağırılır? + +**FastAPI** ile çalışıyorsanız bunun için endişelenmenize gerek yok, çünkü bu "ilk" fonksiyon sizin *path fonksiyonunuz* olacak ve FastAPI doğru olanı nasıl yapacağını bilecek. + +Ancak FastAPI olmadan `async` / `await` kullanmak istiyorsanız, <a href="https://docs.python.org/3/library/asyncio-task.html#coroutine" class="external-link" target="_blank">resmi Python belgelerini kontrol edin</a>. + +### Asenkron kodun diğer biçimleri + +Bu `async` ve `await` kullanimi oldukça yenidir. + +Ancak asenkron kodla çalışmayı çok daha kolay hale getirir. + +Aynı sözdizimi (hemen hemen aynı) son zamanlarda JavaScript'in modern sürümlerine de dahil edildi (Tarayıcı ve NodeJS'de). + +Ancak bundan önce, asenkron kodu işlemek oldukça karmaşık ve zordu. + +Python'un önceki sürümlerinde, threadlerı veya <a href="https://www.gevent.org/" class="external-link" target="_blank">Gevent</a> kullanıyor olabilirdin. Ancak kodu anlamak, hata ayıklamak ve düşünmek çok daha karmaşık olurdu. + +NodeJS / Browser JavaScript'in önceki sürümlerinde, "callback" kullanırdınız. Bu da <a href="http://callbackhell.com/" class="external-link" target="_blank">callbacks cehennemine</a> yol açar. + +## Coroutine'ler + +**Coroutine**, bir `async def` fonksiyonu tarafından döndürülen değer için çok süslü bir terimdir. Python bunun bir fonksiyon gibi bir noktada başlayıp biteceğini bilir, ancak içinde bir `await` olduğunda dahili olarak da duraklatılabilir ⏸. + +Ancak, `async` ve `await` ile asenkron kod kullanmanın tüm bu işlevselliği, çoğu zaman "Coroutine" kullanmak olarak adlandırılır. Go'nun ana özelliği olan "Goroutines" ile karşılaştırılabilir. + +## Sonuç + +Aynı ifadeyi yukarıdan görelim: + +> Python'ın modern sürümleri, **"async" ve "await"** sözdizimi ile birlikte **"coroutines"** adlı bir özelliği kullanan **"asenkron kod"** desteğine sahiptir. + +Şimdi daha mantıklı gelmeli. ✨ + +FastAPI'ye (Starlette aracılığıyla) güç veren ve bu kadar etkileyici bir performansa sahip olmasını sağlayan şey budur. + +## Çok Teknik Detaylar + +!!! warning + Muhtemelen burayı atlayabilirsiniz. + + Bunlar, **FastAPI**'nin altta nasıl çalıştığına dair çok teknik ayrıntılardır. + + Biraz teknik bilginiz varsa (co-routines, threads, blocking, vb)ve FastAPI'nin "async def" ile normal "def" arasındaki farkı nasıl işlediğini merak ediyorsanız, devam edin. + +### Path fonksiyonu + +"async def" yerine normal "def" ile bir *yol işlem işlevi* bildirdiğinizde, doğrudan çağrılmak yerine (sunucuyu bloke edeceğinden) daha sonra beklenen harici bir iş parçacığı havuzunda çalıştırılır. + +Yukarıda açıklanan şekilde çalışmayan başka bir asenkron framework'den geliyorsanız ve küçük bir performans kazancı (yaklaşık 100 nanosaniye) için "def" ile *path fonksiyonu* tanımlamaya alışkınsanız, **FastAPI**'de tam tersi olacağını unutmayın. Bu durumlarda, *path fonksiyonu* <abbr title="Input/Output: disk okuma veya yazma, ağ iletişimleri.">G/Ç</abbr> engelleyen durum oluşturmadıkça "async def" kullanmak daha iyidir. + +Yine de, her iki durumda da, **FastAPI**'nin önceki frameworkden [hala daha hızlı](/#performance){.internal-link target=_blank} (veya en azından karşılaştırılabilir) olma olasılığı vardır. + +### Bagımlılıklar + +Aynısı bağımlılıklar için de geçerlidir. Bir bağımlılık, "async def" yerine standart bir "def" işleviyse, harici iş parçacığı havuzunda çalıştırılır. + +### Alt-bağımlıklar + +Birbirini gerektiren (fonksiyonlarin parametreleri olarak) birden fazla bağımlılık ve alt bağımlılıklarınız olabilir, bazıları 'async def' ve bazıları normal 'def' ile oluşturulabilir. Yine de normal 'def' ile oluşturulanlar, "await" kulanilmadan harici bir iş parçacığında (iş parçacığı havuzundan) çağrılır. + +### Diğer yardımcı fonksiyonlar + +Doğrudan çağırdığınız diğer herhangi bir yardımcı fonksiyonu, normal "def" veya "async def" ile tanimlayabilirsiniz. FastAPI onu çağırma şeklinizi etkilemez. + +Bu, FastAPI'nin sizin için çağırdığı fonksiyonlarin tam tersidir: *path fonksiyonu* ve bağımlılıklar. + +Yardımcı program fonksiyonunuz 'def' ile normal bir işlevse, bir iş parçacığı havuzunda değil doğrudan (kodunuzda yazdığınız gibi) çağrılır, işlev 'async def' ile oluşturulmuşsa çağırıldığı yerde 'await' ile beklemelisiniz. + +--- + +Yeniden, bunlar, onları aramaya geldiğinizde muhtemelen işinize yarayacak çok teknik ayrıntılardır. + +Aksi takdirde, yukarıdaki bölümdeki yönergeleri iyi bilmelisiniz: <a href="#in-a-hurry">Aceleniz mi var?</a>. From 29c8b19af878c0f4b215da4a719969040fe7b5bc Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 19:58:14 +0000 Subject: [PATCH 1723/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 37936f5ad..10e0c24b1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/deployment/docker.md`. PR [#5657](https://github.com/tiangolo/fastapi/pull/5657) by [@nearnear](https://github.com/nearnear). * 🌐 Add Korean translation for `docs/ko/docs/deployment/server-workers.md`. PR [#4935](https://github.com/tiangolo/fastapi/pull/4935) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/deployment/index.md`. PR [#4561](https://github.com/tiangolo/fastapi/pull/4561) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/path-operation-configuration.md`. PR [#3639](https://github.com/tiangolo/fastapi/pull/3639) by [@jungsu-kwon](https://github.com/jungsu-kwon). From 1ac6b761e12c0cf40dc012396be5bbe12f47b8e3 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 23 Jan 2024 05:01:49 +0900 Subject: [PATCH 1724/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/extra-data-types.md`=20(#?= =?UTF-8?q?1932)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ja/docs/tutorial/extra-data-types.md | 66 +++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/ja/docs/tutorial/extra-data-types.md diff --git a/docs/ja/docs/tutorial/extra-data-types.md b/docs/ja/docs/tutorial/extra-data-types.md new file mode 100644 index 000000000..a152e0322 --- /dev/null +++ b/docs/ja/docs/tutorial/extra-data-types.md @@ -0,0 +1,66 @@ +# 追加データ型 + +今までは、以下のような一般的なデータ型を使用してきました: + +* `int` +* `float` +* `str` +* `bool` + +しかし、より複雑なデータ型を使用することもできます。 + +そして、今まで見てきたのと同じ機能を持つことになります: + +* 素晴らしいエディタのサポート +* 受信したリクエストからのデータ変換 +* レスポンスデータのデータ変換 +* データの検証 +* 自動注釈と文書化 + +## 他のデータ型 + +ここでは、使用できる追加のデータ型のいくつかを紹介します: + +* `UUID`: + * 多くのデータベースやシステムで共通のIDとして使用される、標準的な「ユニバーサルにユニークな識別子」です。 + * リクエストとレスポンスでは`str`として表現されます。 +* `datetime.datetime`: + * Pythonの`datetime.datetime`です。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15T15:53:00+05:00` +* `datetime.date`: + * Pythonの`datetime.date`です。 + * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `2008-09-15` +* `datetime.time`: + * Pythonの`datetime.time`. + * リクエストとレスポンスはISO 8601形式の`str`で表現されます: `14:23:55.003` +* `datetime.timedelta`: + * Pythonの`datetime.timedelta`です。 + * リクエストとレスポンスでは合計秒数の`float`で表現されます。 + * Pydanticでは「ISO 8601 time diff encoding」として表現することも可能です。<a href="https://pydantic-docs.helpmanual.io/#json-serialisation" class="external-link" target="_blank">詳細はドキュメントを参照してください</a>。 +* `frozenset`: + * リクエストとレスポンスでは`set`と同じように扱われます: + * リクエストでは、リストが読み込まれ、重複を排除して`set`に変換されます。 + * レスポンスでは`set`が`list`に変換されます。 + * 生成されたスキーマは`set`の値が一意であることを指定します(JSON Schemaの`uniqueItems`を使用します)。 +* `bytes`: + * Pythonの標準的な`bytes`です。 + * リクエストとレスポンスでは`str`として扱われます。 + * 生成されたスキーマは`str`で`binary`の「フォーマット」持つことを指定します。 +* `Decimal`: + * Pythonの標準的な`Decimal`です。 + * リクエストやレスポンスでは`float`と同じように扱います。 + +* Pydanticの全ての有効な型はこちらで確認できます: <a href="https://pydantic-docs.helpmanual.io/usage/types" class="external-link" target="_blank">Pydantic data types</a>。 +## 例 + +ここでは、上記の型のいくつかを使用したパラメータを持つ*path operation*の例を示します。 + +```Python hl_lines="1 2 12-16" +{!../../../docs_src/extra_data_types/tutorial001.py!} +``` + +関数内のパラメータは自然なデータ型を持っていることに注意してください。そして、以下のように通常の日付操作を行うことができます: + +```Python hl_lines="18 19" +{!../../../docs_src/extra_data_types/tutorial001.py!} +``` From 7514aab30bd8355aec58a77ab18c6b22a26a00ec Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 20:02:56 +0000 Subject: [PATCH 1725/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 10e0c24b1..17202d8bf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/project-generation.md`. PR [#5192](https://github.com/tiangolo/fastapi/pull/5192) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Korean translation for `docs/ko/docs/deployment/docker.md`. PR [#5657](https://github.com/tiangolo/fastapi/pull/5657) by [@nearnear](https://github.com/nearnear). * 🌐 Add Korean translation for `docs/ko/docs/deployment/server-workers.md`. PR [#4935](https://github.com/tiangolo/fastapi/pull/4935) by [@jujumilk3](https://github.com/jujumilk3). * 🌐 Add Korean translation for `docs/ko/docs/deployment/index.md`. PR [#4561](https://github.com/tiangolo/fastapi/pull/4561) by [@jujumilk3](https://github.com/jujumilk3). From f772868a569651020d72f64fa2e66c3f4b3535f7 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 20:06:11 +0000 Subject: [PATCH 1726/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 17202d8bf..f915e3d26 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/async.md`. PR [#5191](https://github.com/tiangolo/fastapi/pull/5191) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Turkish translation for `docs/tr/docs/project-generation.md`. PR [#5192](https://github.com/tiangolo/fastapi/pull/5192) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Korean translation for `docs/ko/docs/deployment/docker.md`. PR [#5657](https://github.com/tiangolo/fastapi/pull/5657) by [@nearnear](https://github.com/nearnear). * 🌐 Add Korean translation for `docs/ko/docs/deployment/server-workers.md`. PR [#4935](https://github.com/tiangolo/fastapi/pull/4935) by [@jujumilk3](https://github.com/jujumilk3). From 851daec7542bc5e565579bd2d491310d2d6b7445 Mon Sep 17 00:00:00 2001 From: SwftAlpc <alpaca.swift@gmail.com> Date: Tue, 23 Jan 2024 05:09:02 +0900 Subject: [PATCH 1727/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/encoder.md`=20(#1955)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ja/docs/tutorial/encoder.md | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/ja/docs/tutorial/encoder.md diff --git a/docs/ja/docs/tutorial/encoder.md b/docs/ja/docs/tutorial/encoder.md new file mode 100644 index 000000000..305867ab7 --- /dev/null +++ b/docs/ja/docs/tutorial/encoder.md @@ -0,0 +1,34 @@ +# JSON互換エンコーダ + +データ型(Pydanticモデルのような)をJSONと互換性のあるもの(`dict`や`list`など)に変更する必要がある場合があります。 + +例えば、データベースに保存する必要がある場合です。 + +そのために、**FastAPI** は`jsonable_encoder()`関数を提供しています。 + +## `jsonable_encoder`の使用 + +JSON互換のデータのみを受信するデータベース`fase_db`があるとしましょう。 + +例えば、`datetime`オブジェクトはJSONと互換性がないので、このデーターベースには受け取られません。 + +そのため、`datetime`オブジェクトは<a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">ISO形式</a>のデータを含む`str`に変換されなければなりません。 + +同様に、このデータベースはPydanticモデル(属性を持つオブジェクト)を受け取らず、`dict`だけを受け取ります。 + +そのために`jsonable_encoder`を使用することができます。 + +Pydanticモデルのようなオブジェクトを受け取り、JSON互換版を返します: + +```Python hl_lines="5 22" +{!../../../docs_src/encoder/tutorial001.py!} +``` + +この例では、Pydanticモデルを`dict`に、`datetime`を`str`に変換します。 + +呼び出した結果は、Pythonの標準の<a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>でエンコードできるものです。 + +これはJSON形式のデータを含む大きな`str`を(文字列として)返しません。JSONと互換性のある値とサブの値を持つPython標準のデータ構造(例:`dict`)を返します。 + +!!! note "備考" + `jsonable_encoder`は実際には **FastAPI** が内部的にデータを変換するために使用します。しかしこれは他の多くのシナリオで有用です。 From c945d686bbd1c56e4e5c0ef876fe3c45e8d5bb2d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 20:12:06 +0000 Subject: [PATCH 1728/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f915e3d26..601d83856 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-data-types.md`. PR [#1932](https://github.com/tiangolo/fastapi/pull/1932) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Turkish translation for `docs/tr/docs/async.md`. PR [#5191](https://github.com/tiangolo/fastapi/pull/5191) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Turkish translation for `docs/tr/docs/project-generation.md`. PR [#5192](https://github.com/tiangolo/fastapi/pull/5192) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Korean translation for `docs/ko/docs/deployment/docker.md`. PR [#5657](https://github.com/tiangolo/fastapi/pull/5657) by [@nearnear](https://github.com/nearnear). From d7c588d6930e0985253aafa396091beb76340fbd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 22 Jan 2024 20:18:27 +0000 Subject: [PATCH 1729/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 601d83856..49c1cab09 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/encoder.md`. PR [#1955](https://github.com/tiangolo/fastapi/pull/1955) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-data-types.md`. PR [#1932](https://github.com/tiangolo/fastapi/pull/1932) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Turkish translation for `docs/tr/docs/async.md`. PR [#5191](https://github.com/tiangolo/fastapi/pull/5191) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). * 🌐 Add Turkish translation for `docs/tr/docs/project-generation.md`. PR [#5192](https://github.com/tiangolo/fastapi/pull/5192) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). From 9a33950344c96445063e8e2b33993cea3b0d55d8 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 12:22:17 +0100 Subject: [PATCH 1730/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20introduction=20documents=20(#10497)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/about/index.md | 3 +++ docs/de/docs/help/index.md | 3 +++ docs/de/docs/learn/index.md | 5 +++++ docs/de/docs/reference/index.md | 8 ++++++++ docs/de/docs/resources/index.md | 3 +++ 5 files changed, 22 insertions(+) create mode 100644 docs/de/docs/about/index.md create mode 100644 docs/de/docs/help/index.md create mode 100644 docs/de/docs/learn/index.md create mode 100644 docs/de/docs/reference/index.md create mode 100644 docs/de/docs/resources/index.md diff --git a/docs/de/docs/about/index.md b/docs/de/docs/about/index.md new file mode 100644 index 000000000..4c309e02a --- /dev/null +++ b/docs/de/docs/about/index.md @@ -0,0 +1,3 @@ +# Über + +Über FastAPI, sein Design, seine Inspiration und mehr. 🤓 diff --git a/docs/de/docs/help/index.md b/docs/de/docs/help/index.md new file mode 100644 index 000000000..8fdc4a049 --- /dev/null +++ b/docs/de/docs/help/index.md @@ -0,0 +1,3 @@ +# Hilfe + +Helfen und Hilfe erhalten, beitragen, mitmachen. 🤝 diff --git a/docs/de/docs/learn/index.md b/docs/de/docs/learn/index.md new file mode 100644 index 000000000..b5582f55b --- /dev/null +++ b/docs/de/docs/learn/index.md @@ -0,0 +1,5 @@ +# Lernen + +Hier finden Sie die einführenden Kapitel und Tutorials zum Erlernen von **FastAPI**. + +Sie könnten dies als **Buch**, als **Kurs**, als **offizielle** und empfohlene Methode zum Erlernen von FastAPI betrachten. 😎 diff --git a/docs/de/docs/reference/index.md b/docs/de/docs/reference/index.md new file mode 100644 index 000000000..e9362b962 --- /dev/null +++ b/docs/de/docs/reference/index.md @@ -0,0 +1,8 @@ +# Referenz – Code-API + +Hier ist die Referenz oder Code-API, die Klassen, Funktionen, Parameter, Attribute und alle FastAPI-Teile, die Sie in Ihren Anwendungen verwenden können. + +Wenn Sie **FastAPI** lernen möchten, ist es viel besser, das [FastAPI-Tutorial](https://fastapi.tiangolo.com/tutorial/) zu lesen. + +!!! note "Hinweis Deutsche Übersetzung" + Die nachfolgende API wird aus der Quelltext-Dokumentation erstellt, daher sind nur die Einleitungen auf Deutsch. diff --git a/docs/de/docs/resources/index.md b/docs/de/docs/resources/index.md new file mode 100644 index 000000000..abf270d9f --- /dev/null +++ b/docs/de/docs/resources/index.md @@ -0,0 +1,3 @@ +# Ressourcen + +Zusätzliche Ressourcen, externe Links, Artikel und mehr. ✈️ From d2d5a5290ccd0598fdefabe74c28904406d8132f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 11:22:48 +0000 Subject: [PATCH 1731/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 49c1cab09..7bbee3c44 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for introduction documents. PR [#10497](https://github.com/tiangolo/fastapi/pull/10497) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/encoder.md`. PR [#1955](https://github.com/tiangolo/fastapi/pull/1955) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-data-types.md`. PR [#1932](https://github.com/tiangolo/fastapi/pull/1932) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Turkish translation for `docs/tr/docs/async.md`. PR [#5191](https://github.com/tiangolo/fastapi/pull/5191) by [@BilalAlpaslan](https://github.com/BilalAlpaslan). From 2f6fdf62b95befaa854eb4637ed8e928514ebaa1 Mon Sep 17 00:00:00 2001 From: Johannes Jungbluth <johannesjungbluth@gmail.com> Date: Tue, 23 Jan 2024 12:26:59 +0100 Subject: [PATCH 1732/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/middleware.md`=20(#10391)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/middleware.md | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/de/docs/tutorial/middleware.md diff --git a/docs/de/docs/tutorial/middleware.md b/docs/de/docs/tutorial/middleware.md new file mode 100644 index 000000000..7d6e6b71a --- /dev/null +++ b/docs/de/docs/tutorial/middleware.md @@ -0,0 +1,61 @@ +# Middleware + +Sie können Middleware zu **FastAPI**-Anwendungen hinzufügen. + +Eine „Middleware“ ist eine Funktion, die mit jedem **Request** arbeitet, bevor er von einer bestimmten *Pfadoperation* verarbeitet wird. Und auch mit jeder **Response**, bevor sie zurückgegeben wird. + +* Sie nimmt jeden **Request** entgegen, der an Ihre Anwendung gesendet wird. +* Sie kann dann etwas mit diesem **Request** tun oder beliebigen Code ausführen. +* Dann gibt sie den **Request** zur Verarbeitung durch den Rest der Anwendung weiter (durch eine bestimmte *Pfadoperation*). +* Sie nimmt dann die **Response** entgegen, die von der Anwendung generiert wurde (durch eine bestimmte *Pfadoperation*). +* Sie kann etwas mit dieser **Response** tun oder beliebigen Code ausführen. +* Dann gibt sie die **Response** zurück. + +!!! note "Technische Details" + Wenn Sie Abhängigkeiten mit `yield` haben, wird der Exit-Code *nach* der Middleware ausgeführt. + + Wenn es Hintergrundaufgaben gab (später dokumentiert), werden sie *nach* allen Middlewares ausgeführt. + +## Erstellung einer Middleware + +Um eine Middleware zu erstellen, verwenden Sie den Dekorator `@app.middleware("http")` über einer Funktion. + +Die Middleware-Funktion erhält: + +* Den `request`. +* Eine Funktion `call_next`, die den `request` als Parameter erhält. + * Diese Funktion gibt den `request` an die entsprechende *Pfadoperation* weiter. + * Dann gibt es die von der entsprechenden *Pfadoperation* generierte `response` zurück. +* Sie können die `response` dann weiter modifizieren, bevor Sie sie zurückgeben. + +```Python hl_lines="8-9 11 14" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +!!! tip "Tipp" + Beachten Sie, dass benutzerdefinierte proprietäre Header hinzugefügt werden können. <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">Verwenden Sie dafür das Präfix 'X-'</a>. + + Wenn Sie jedoch benutzerdefinierte Header haben, die ein Client in einem Browser sehen soll, müssen Sie sie zu Ihrer CORS-Konfigurationen ([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank}) hinzufügen, indem Sie den Parameter `expose_headers` verwenden, der in der <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette-CORS-Dokumentation</a> dokumentiert ist. + +!!! note "Technische Details" + Sie könnten auch `from starlette.requests import Request` verwenden. + + **FastAPI** bietet es als Komfort für Sie, den Entwickler, an. Aber es stammt direkt von Starlette. + +### Vor und nach der `response` + +Sie können Code hinzufügen, der mit dem `request` ausgeführt wird, bevor dieser von einer beliebigen *Pfadoperation* empfangen wird. + +Und auch nachdem die `response` generiert wurde, bevor sie zurückgegeben wird. + +Sie könnten beispielsweise einen benutzerdefinierten Header `X-Process-Time` hinzufügen, der die Zeit in Sekunden enthält, die benötigt wurde, um den Request zu verarbeiten und eine Response zu generieren: + +```Python hl_lines="10 12-13" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +## Andere Middlewares + +Sie können später mehr über andere Middlewares in [Handbuch für fortgeschrittene Benutzer: Fortgeschrittene Middleware](../advanced/middleware.md){.internal-link target=_blank} lesen. + +In der nächsten Sektion erfahren Sie, wie Sie <abbr title="Cross-Origin Resource Sharing">CORS</abbr> mit einer Middleware behandeln können. From cedea4d7b5b62c8985ca806816a188a9451d1225 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 11:27:20 +0000 Subject: [PATCH 1733/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7bbee3c44..69954afc1 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/middleware.md`. PR [#10391](https://github.com/tiangolo/fastapi/pull/10391) by [@JohannesJungbluth](https://github.com/JohannesJungbluth). * 🌐 Add German translation for introduction documents. PR [#10497](https://github.com/tiangolo/fastapi/pull/10497) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/encoder.md`. PR [#1955](https://github.com/tiangolo/fastapi/pull/1955) by [@SwftAlpc](https://github.com/SwftAlpc). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/extra-data-types.md`. PR [#1932](https://github.com/tiangolo/fastapi/pull/1932) by [@SwftAlpc](https://github.com/SwftAlpc). From 690edc03853e1982eaa92e67976591c7625f4aa3 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 14:04:57 +0100 Subject: [PATCH 1734/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/advanced/additional-status-codes.md`?= =?UTF-8?q?=20(#10617)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/advanced/additional-status-codes.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/de/docs/advanced/additional-status-codes.md diff --git a/docs/de/docs/advanced/additional-status-codes.md b/docs/de/docs/advanced/additional-status-codes.md new file mode 100644 index 000000000..e9de267cf --- /dev/null +++ b/docs/de/docs/advanced/additional-status-codes.md @@ -0,0 +1,69 @@ +# Zusätzliche Statuscodes + +Standardmäßig liefert **FastAPI** die Rückgabewerte (Responses) als `JSONResponse` zurück und fügt den Inhalt der jeweiligen *Pfadoperation* in das `JSONResponse` Objekt ein. + +Es wird der Default-Statuscode oder derjenige verwendet, den Sie in Ihrer *Pfadoperation* festgelegt haben. + +## Zusätzliche Statuscodes + +Wenn Sie neben dem Hauptstatuscode weitere Statuscodes zurückgeben möchten, können Sie dies tun, indem Sie direkt eine `Response` zurückgeben, wie etwa eine `JSONResponse`, und den zusätzlichen Statuscode direkt festlegen. + +Angenommen, Sie möchten eine *Pfadoperation* haben, die das Aktualisieren von Artikeln ermöglicht und bei Erfolg den HTTP-Statuscode 200 „OK“ zurückgibt. + +Sie möchten aber auch, dass sie neue Artikel akzeptiert. Und wenn die Elemente vorher nicht vorhanden waren, werden diese Elemente erstellt und der HTTP-Statuscode 201 „Created“ zurückgegeben. + +Um dies zu erreichen, importieren Sie `JSONResponse`, und geben Sie Ihren Inhalt direkt zurück, indem Sie den gewünschten `status_code` setzen: + +=== "Python 3.10+" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 26" + {!> ../../../docs_src/additional_status_codes/tutorial001_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="2 23" + {!> ../../../docs_src/additional_status_codes/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="4 25" + {!> ../../../docs_src/additional_status_codes/tutorial001.py!} + ``` + +!!! warning "Achtung" + Wenn Sie eine `Response` direkt zurückgeben, wie im obigen Beispiel, wird sie direkt zurückgegeben. + + Sie wird nicht mit einem Modell usw. serialisiert. + + Stellen Sie sicher, dass sie die gewünschten Daten enthält und dass die Werte gültiges JSON sind (wenn Sie `JSONResponse` verwenden). + +!!! note "Technische Details" + Sie können auch `from starlette.responses import JSONResponse` verwenden. + + **FastAPI** bietet dieselben `starlette.responses` auch via `fastapi.responses` an, als Annehmlichkeit für Sie, den Entwickler. Die meisten verfügbaren Responses kommen aber direkt von Starlette. Das Gleiche gilt für `status`. + +## OpenAPI- und API-Dokumentation + +Wenn Sie zusätzliche Statuscodes und Responses direkt zurückgeben, werden diese nicht in das OpenAPI-Schema (die API-Dokumentation) aufgenommen, da FastAPI keine Möglichkeit hat, im Voraus zu wissen, was Sie zurückgeben werden. + +Sie können das jedoch in Ihrem Code dokumentieren, indem Sie Folgendes verwenden: [Zusätzliche Responses](additional-responses.md){.internal-link target=_blank}. From c3914a19a7375e0b52e8e7db77cc23290a90a0c8 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 14:05:12 +0100 Subject: [PATCH 1735/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/advanced/custom-response.md`=20(#106?= =?UTF-8?q?24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/advanced/custom-response.md | 300 +++++++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 docs/de/docs/advanced/custom-response.md diff --git a/docs/de/docs/advanced/custom-response.md b/docs/de/docs/advanced/custom-response.md new file mode 100644 index 000000000..68c037ad7 --- /dev/null +++ b/docs/de/docs/advanced/custom-response.md @@ -0,0 +1,300 @@ +# Benutzerdefinierte Response – HTML, Stream, Datei, andere + +Standardmäßig gibt **FastAPI** die Responses mittels `JSONResponse` zurück. + +Sie können das überschreiben, indem Sie direkt eine `Response` zurückgeben, wie in [Eine Response direkt zurückgeben](response-directly.md){.internal-link target=_blank} gezeigt. + +Wenn Sie jedoch direkt eine `Response` zurückgeben, werden die Daten nicht automatisch konvertiert und die Dokumentation wird nicht automatisch generiert (zum Beispiel wird der spezifische „Medientyp“, der im HTTP-Header `Content-Type` angegeben ist, nicht Teil der generierten OpenAPI). + +Sie können aber auch die `Response`, die Sie verwenden möchten, im *Pfadoperation-Dekorator* deklarieren. + +Der Inhalt, den Sie von Ihrer *Pfadoperation-Funktion* zurückgeben, wird in diese `Response` eingefügt. + +Und wenn diese `Response` einen JSON-Medientyp (`application/json`) hat, wie es bei `JSONResponse` und `UJSONResponse` der Fall ist, werden die von Ihnen zurückgegebenen Daten automatisch mit jedem Pydantic `response_model` konvertiert (und gefiltert), das Sie im *Pfadoperation-Dekorator* deklariert haben. + +!!! note "Hinweis" + Wenn Sie eine Response-Klasse ohne Medientyp verwenden, erwartet FastAPI, dass Ihre Response keinen Inhalt hat, und dokumentiert daher das Format der Response nicht in deren generierter OpenAPI-Dokumentation. + +## `ORJSONResponse` verwenden + +Um beispielsweise noch etwas Leistung herauszuholen, können Sie <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> installieren und verwenden, und die Response als `ORJSONResponse` deklarieren. + +Importieren Sie die `Response`-Klasse (-Unterklasse), die Sie verwenden möchten, und deklarieren Sie sie im *Pfadoperation-Dekorator*. + +Bei umfangreichen Responses ist die direkte Rückgabe einer `Response` viel schneller als ein Dictionary zurückzugeben. + +Das liegt daran, dass FastAPI standardmäßig jedes enthaltene Element überprüft und sicherstellt, dass es als JSON serialisierbar ist, und zwar unter Verwendung desselben [JSON-kompatiblen Encoders](../tutorial/encoder.md){.internal-link target=_blank}, der im Tutorial erläutert wurde. Dadurch können Sie **beliebige Objekte** zurückgeben, zum Beispiel Datenbankmodelle. + +Wenn Sie jedoch sicher sind, dass der von Ihnen zurückgegebene Inhalt **mit JSON serialisierbar** ist, können Sie ihn direkt an die Response-Klasse übergeben und die zusätzliche Arbeit vermeiden, die FastAPI hätte, indem es Ihren zurückgegebenen Inhalt durch den `jsonable_encoder` leitet, bevor es ihn an die Response-Klasse übergibt. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001b.py!} +``` + +!!! info + Der Parameter `response_class` wird auch verwendet, um den „Medientyp“ der Response zu definieren. + + In diesem Fall wird der HTTP-Header `Content-Type` auf `application/json` gesetzt. + + Und er wird als solcher in OpenAPI dokumentiert. + +!!! tip "Tipp" + Die `ORJSONResponse` ist derzeit nur in FastAPI verfügbar, nicht in Starlette. + +## HTML-Response + +Um eine Response mit HTML direkt von **FastAPI** zurückzugeben, verwenden Sie `HTMLResponse`. + +* Importieren Sie `HTMLResponse`. +* Übergeben Sie `HTMLResponse` als den Parameter `response_class` Ihres *Pfadoperation-Dekorators*. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial002.py!} +``` + +!!! info + Der Parameter `response_class` wird auch verwendet, um den „Medientyp“ der Response zu definieren. + + In diesem Fall wird der HTTP-Header `Content-Type` auf `text/html` gesetzt. + + Und er wird als solcher in OpenAPI dokumentiert. + +### Eine `Response` zurückgeben + +Wie in [Eine Response direkt zurückgeben](response-directly.md){.internal-link target=_blank} gezeigt, können Sie die Response auch direkt in Ihrer *Pfadoperation* überschreiben, indem Sie diese zurückgeben. + +Das gleiche Beispiel von oben, das eine `HTMLResponse` zurückgibt, könnte so aussehen: + +```Python hl_lines="2 7 19" +{!../../../docs_src/custom_response/tutorial003.py!} +``` + +!!! warning "Achtung" + Eine `Response`, die direkt von Ihrer *Pfadoperation-Funktion* zurückgegeben wird, wird in OpenAPI nicht dokumentiert (zum Beispiel wird der `Content-Type` nicht dokumentiert) und ist in der automatischen interaktiven Dokumentation nicht sichtbar. + +!!! info + Natürlich stammen der eigentliche `Content-Type`-Header, der Statuscode, usw., aus dem `Response`-Objekt, das Sie zurückgegeben haben. + +### In OpenAPI dokumentieren und `Response` überschreiben + +Wenn Sie die Response innerhalb der Funktion überschreiben und gleichzeitig den „Medientyp“ in OpenAPI dokumentieren möchten, können Sie den `response_class`-Parameter verwenden UND ein `Response`-Objekt zurückgeben. + +Die `response_class` wird dann nur zur Dokumentation der OpenAPI-Pfadoperation* verwendet, Ihre `Response` wird jedoch unverändert verwendet. + +#### Eine `HTMLResponse` direkt zurückgeben + +Es könnte zum Beispiel so etwas sein: + +```Python hl_lines="7 21 23" +{!../../../docs_src/custom_response/tutorial004.py!} +``` + +In diesem Beispiel generiert die Funktion `generate_html_response()` bereits eine `Response` und gibt sie zurück, anstatt das HTML in einem `str` zurückzugeben. + +Indem Sie das Ergebnis des Aufrufs von `generate_html_response()` zurückgeben, geben Sie bereits eine `Response` zurück, die das Standardverhalten von **FastAPI** überschreibt. + +Aber da Sie die `HTMLResponse` auch in der `response_class` übergeben haben, weiß **FastAPI**, dass sie in OpenAPI und der interaktiven Dokumentation als HTML mit `text/html` zu dokumentieren ist: + +<img src="/img/tutorial/custom-response/image01.png"> + +## Verfügbare Responses + +Hier sind einige der verfügbaren Responses. + +Bedenken Sie, dass Sie `Response` verwenden können, um alles andere zurückzugeben, oder sogar eine benutzerdefinierte Unterklasse zu erstellen. + +!!! note "Technische Details" + Sie können auch `from starlette.responses import HTMLResponse` verwenden. + + **FastAPI** bietet dieselben `starlette.responses` auch via `fastapi.responses` an, als Annehmlichkeit für Sie, den Entwickler. Die meisten verfügbaren Responses kommen aber direkt von Starlette. + +### `Response` + +Die Hauptklasse `Response`, alle anderen Responses erben von ihr. + +Sie können sie direkt zurückgeben. + +Sie akzeptiert die folgenden Parameter: + +* `content` – Ein `str` oder `bytes`. +* `status_code` – Ein `int`-HTTP-Statuscode. +* `headers` – Ein `dict` von Strings. +* `media_type` – Ein `str`, der den Medientyp angibt. Z. B. `"text/html"`. + +FastAPI (eigentlich Starlette) fügt automatisch einen Content-Length-Header ein. Außerdem wird es einen Content-Type-Header einfügen, der auf dem media_type basiert, und für Texttypen einen Zeichensatz (charset) anfügen. + +```Python hl_lines="1 18" +{!../../../docs_src/response_directly/tutorial002.py!} +``` + +### `HTMLResponse` + +Nimmt Text oder Bytes entgegen und gibt eine HTML-Response zurück, wie Sie oben gelesen haben. + +### `PlainTextResponse` + +Nimmt Text oder Bytes entgegen und gibt eine Plain-Text-Response zurück. + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial005.py!} +``` + +### `JSONResponse` + +Nimmt einige Daten entgegen und gibt eine `application/json`-codierte Response zurück. + +Dies ist die Standard-Response, die in **FastAPI** verwendet wird, wie Sie oben gelesen haben. + +### `ORJSONResponse` + +Eine schnelle alternative JSON-Response mit <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a>, wie Sie oben gelesen haben. + +### `UJSONResponse` + +Eine alternative JSON-Response mit <a href="https://github.com/ultrajson/ultrajson" class="external-link" target="_blank">`ujson`</a>. + +!!! warning "Achtung" + `ujson` ist bei der Behandlung einiger Sonderfälle weniger sorgfältig als Pythons eingebaute Implementierung. + +```Python hl_lines="2 7" +{!../../../docs_src/custom_response/tutorial001.py!} +``` + +!!! tip "Tipp" + Möglicherweise ist `ORJSONResponse` eine schnellere Alternative. + +### `RedirectResponse` + +Gibt eine HTTP-Weiterleitung (HTTP-Redirect) zurück. Verwendet standardmäßig den Statuscode 307 – Temporäre Weiterleitung (Temporary Redirect). + +Sie können eine `RedirectResponse` direkt zurückgeben: + +```Python hl_lines="2 9" +{!../../../docs_src/custom_response/tutorial006.py!} +``` + +--- + +Oder Sie können sie im Parameter `response_class` verwenden: + + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006b.py!} +``` + +Wenn Sie das tun, können Sie die URL direkt von Ihrer *Pfadoperation*-Funktion zurückgeben. + +In diesem Fall ist der verwendete `status_code` der Standardcode für die `RedirectResponse`, also `307`. + +--- + +Sie können den Parameter `status_code` auch in Kombination mit dem Parameter `response_class` verwenden: + +```Python hl_lines="2 7 9" +{!../../../docs_src/custom_response/tutorial006c.py!} +``` + +### `StreamingResponse` + +Nimmt einen asynchronen Generator oder einen normalen Generator/Iterator und streamt den Responsebody. + +```Python hl_lines="2 14" +{!../../../docs_src/custom_response/tutorial007.py!} +``` + +#### Verwendung von `StreamingResponse` mit dateiähnlichen Objekten + +Wenn Sie ein dateiähnliches (file-like) Objekt haben (z. B. das von `open()` zurückgegebene Objekt), können Sie eine Generatorfunktion erstellen, um über dieses dateiähnliche Objekt zu iterieren. + +Auf diese Weise müssen Sie nicht alles zuerst in den Arbeitsspeicher lesen und können diese Generatorfunktion an `StreamingResponse` übergeben und zurückgeben. + +Das umfasst viele Bibliotheken zur Interaktion mit Cloud-Speicher, Videoverarbeitung und anderen. + +```{ .python .annotate hl_lines="2 10-12 14" } +{!../../../docs_src/custom_response/tutorial008.py!} +``` + +1. Das ist die Generatorfunktion. Es handelt sich um eine „Generatorfunktion“, da sie `yield`-Anweisungen enthält. +2. Durch die Verwendung eines `with`-Blocks stellen wir sicher, dass das dateiähnliche Objekt geschlossen wird, nachdem die Generatorfunktion fertig ist. Also, nachdem sie mit dem Senden der Response fertig ist. +3. Dieses `yield from` weist die Funktion an, über das Ding namens `file_like` zu iterieren. Und dann für jeden iterierten Teil, diesen Teil so zurückzugeben, als wenn er aus dieser Generatorfunktion (`iterfile`) stammen würde. + + Es handelt sich also hier um eine Generatorfunktion, die die „generierende“ Arbeit intern auf etwas anderes überträgt. + + Auf diese Weise können wir das Ganze in einen `with`-Block einfügen und so sicherstellen, dass das dateiartige Objekt nach Abschluss geschlossen wird. + +!!! tip "Tipp" + Beachten Sie, dass wir, da wir Standard-`open()` verwenden, welches `async` und `await` nicht unterstützt, hier die Pfadoperation mit normalen `def` deklarieren. + +### `FileResponse` + +Streamt eine Datei asynchron als Response. + +Nimmt zur Instanziierung einen anderen Satz von Argumenten entgegen als die anderen Response-Typen: + +* `path` – Der Dateipfad zur Datei, die gestreamt werden soll. +* `headers` – Alle benutzerdefinierten Header, die inkludiert werden sollen, als Dictionary. +* `media_type` – Ein String, der den Medientyp angibt. Wenn nicht gesetzt, wird der Dateiname oder Pfad verwendet, um auf einen Medientyp zu schließen. +* `filename` – Wenn gesetzt, wird das in der `Content-Disposition` der Response eingefügt. + +Datei-Responses enthalten die entsprechenden `Content-Length`-, `Last-Modified`- und `ETag`-Header. + +```Python hl_lines="2 10" +{!../../../docs_src/custom_response/tutorial009.py!} +``` + +Sie können auch den Parameter `response_class` verwenden: + +```Python hl_lines="2 8 10" +{!../../../docs_src/custom_response/tutorial009b.py!} +``` + +In diesem Fall können Sie den Dateipfad direkt von Ihrer *Pfadoperation*-Funktion zurückgeben. + +## Benutzerdefinierte Response-Klasse + +Sie können Ihre eigene benutzerdefinierte Response-Klasse erstellen, die von `Response` erbt und diese verwendet. + +Nehmen wir zum Beispiel an, dass Sie <a href="https://github.com/ijl/orjson" class="external-link" target="_blank">`orjson`</a> verwenden möchten, aber mit einigen benutzerdefinierten Einstellungen, die in der enthaltenen `ORJSONResponse`-Klasse nicht verwendet werden. + +Sie möchten etwa, dass Ihre Response eingerücktes und formatiertes JSON zurückgibt. Dafür möchten Sie die orjson-Option `orjson.OPT_INDENT_2` verwenden. + +Sie könnten eine `CustomORJSONResponse` erstellen. Das Wichtigste, was Sie tun müssen, ist, eine `Response.render(content)`-Methode zu erstellen, die den Inhalt als `bytes` zurückgibt: + +```Python hl_lines="9-14 17" +{!../../../docs_src/custom_response/tutorial009c.py!} +``` + +Statt: + +```json +{"message": "Hello World"} +``` + +... wird die Response jetzt Folgendes zurückgeben: + +```json +{ + "message": "Hello World" +} +``` + +Natürlich werden Sie wahrscheinlich viel bessere Möglichkeiten finden, Vorteil daraus zu ziehen, als JSON zu formatieren. 😉 + +## Standard-Response-Klasse + +Beim Erstellen einer **FastAPI**-Klasseninstanz oder eines `APIRouter`s können Sie angeben, welche Response-Klasse standardmäßig verwendet werden soll. + +Der Parameter, der das definiert, ist `default_response_class`. + +Im folgenden Beispiel verwendet **FastAPI** standardmäßig `ORJSONResponse` in allen *Pfadoperationen*, anstelle von `JSONResponse`. + +```Python hl_lines="2 4" +{!../../../docs_src/custom_response/tutorial010.py!} +``` + +!!! tip "Tipp" + Sie können dennoch weiterhin `response_class` in *Pfadoperationen* überschreiben, wie bisher. + +## Zusätzliche Dokumentation + +Sie können auch den Medientyp und viele andere Details in OpenAPI mit `responses` deklarieren: [Zusätzliche Responses in OpenAPI](additional-responses.md){.internal-link target=_blank}. From 149fa96dc73225b8f6817b829eee9870cb787c60 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:05:21 +0000 Subject: [PATCH 1736/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 69954afc1..41ade632c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/advanced/additional-status-codes.md`. PR [#10617](https://github.com/tiangolo/fastapi/pull/10617) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/middleware.md`. PR [#10391](https://github.com/tiangolo/fastapi/pull/10391) by [@JohannesJungbluth](https://github.com/JohannesJungbluth). * 🌐 Add German translation for introduction documents. PR [#10497](https://github.com/tiangolo/fastapi/pull/10497) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/encoder.md`. PR [#1955](https://github.com/tiangolo/fastapi/pull/1955) by [@SwftAlpc](https://github.com/SwftAlpc). From 2c1dd4a92be3dda8af3835783e85980958ab8228 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:05:40 +0000 Subject: [PATCH 1737/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 41ade632c..9b091c494 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/advanced/custom-response.md`. PR [#10624](https://github.com/tiangolo/fastapi/pull/10624) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/additional-status-codes.md`. PR [#10617](https://github.com/tiangolo/fastapi/pull/10617) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/middleware.md`. PR [#10391](https://github.com/tiangolo/fastapi/pull/10391) by [@JohannesJungbluth](https://github.com/JohannesJungbluth). * 🌐 Add German translation for introduction documents. PR [#10497](https://github.com/tiangolo/fastapi/pull/10497) by [@nilslindemann](https://github.com/nilslindemann). From 43a7ff782bc7e136c579e9b704916e6e8bea8dc5 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 14:06:03 +0100 Subject: [PATCH 1738/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/advanced/openapi-webhooks.md`=20(#10?= =?UTF-8?q?712)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/advanced/openapi-webhooks.md | 51 +++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/de/docs/advanced/openapi-webhooks.md diff --git a/docs/de/docs/advanced/openapi-webhooks.md b/docs/de/docs/advanced/openapi-webhooks.md new file mode 100644 index 000000000..339218080 --- /dev/null +++ b/docs/de/docs/advanced/openapi-webhooks.md @@ -0,0 +1,51 @@ +# OpenAPI-Webhooks + +Es gibt Fälle, in denen Sie Ihren API-Benutzern mitteilen möchten, dass Ihre Anwendung mit einigen Daten *deren* Anwendung aufrufen (ein Request senden) könnte, normalerweise um über ein bestimmtes **Event** zu **benachrichtigen**. + +Das bedeutet, dass anstelle des normalen Prozesses, bei dem Benutzer Requests an Ihre API senden, **Ihre API** (oder Ihre Anwendung) **Requests an deren System** (an deren API, deren Anwendung) senden könnte. + +Das wird normalerweise als **Webhook** bezeichnet. + +## Webhooks-Schritte + +Der Prozess besteht normalerweise darin, dass **Sie in Ihrem Code definieren**, welche Nachricht Sie senden möchten, den **Body des Requests**. + +Sie definieren auch auf irgendeine Weise, zu welchen **Momenten** Ihre Anwendung diese Requests oder Events sendet. + +Und **Ihre Benutzer** definieren auf irgendeine Weise (zum Beispiel irgendwo in einem Web-Dashboard) die **URL**, an die Ihre Anwendung diese Requests senden soll. + +Die gesamte **Logik** zur Registrierung der URLs für Webhooks und der Code zum tatsächlichen Senden dieser Requests liegt bei Ihnen. Sie schreiben es so, wie Sie möchten, in **Ihrem eigenen Code**. + +## Webhooks mit **FastAPI** und OpenAPI dokumentieren + +Mit **FastAPI** können Sie mithilfe von OpenAPI die Namen dieser Webhooks, die Arten von HTTP-Operationen, die Ihre Anwendung senden kann (z. B. `POST`, `PUT`, usw.) und die Request**bodys** definieren, die Ihre Anwendung senden würde. + +Dies kann es Ihren Benutzern viel einfacher machen, **deren APIs zu implementieren**, um Ihre **Webhook**-Requests zu empfangen. Möglicherweise können diese sogar einen Teil des eigenem API-Codes automatisch generieren. + +!!! info + Webhooks sind in OpenAPI 3.1.0 und höher verfügbar und werden von FastAPI `0.99.0` und höher unterstützt. + +## Eine Anwendung mit Webhooks + +Wenn Sie eine **FastAPI**-Anwendung erstellen, gibt es ein `webhooks`-Attribut, mit dem Sie *Webhooks* definieren können, genauso wie Sie *Pfadoperationen* definieren würden, zum Beispiel mit `@app.webhooks.post()`. + +```Python hl_lines="9-13 36-53" +{!../../../docs_src/openapi_webhooks/tutorial001.py!} +``` + +Die von Ihnen definierten Webhooks landen im **OpenAPI**-Schema und der automatischen **Dokumentations-Oberfläche**. + +!!! info + Das `app.webhooks`-Objekt ist eigentlich nur ein `APIRouter`, derselbe Typ, den Sie verwenden würden, wenn Sie Ihre Anwendung mit mehreren Dateien strukturieren. + +Beachten Sie, dass Sie bei Webhooks tatsächlich keinen *Pfad* (wie `/items/`) deklarieren, sondern dass der Text, den Sie dort übergeben, lediglich eine **Kennzeichnung** des Webhooks (der Name des Events) ist. Zum Beispiel ist in `@app.webhooks.post("new-subscription")` der Webhook-Name `new-subscription`. + +Das liegt daran, dass erwartet wird, dass **Ihre Benutzer** den tatsächlichen **URL-Pfad**, an dem diese den Webhook-Request empfangen möchten, auf andere Weise definieren (z. B. über ein Web-Dashboard). + +### Es in der Dokumentation ansehen + +Jetzt können Sie Ihre Anwendung mit Uvicorn starten und auf <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> gehen. + +Sie werden sehen, dass Ihre Dokumentation die normalen *Pfadoperationen* und jetzt auch einige **Webhooks** enthält: + +<img src="/img/tutorial/openapi-webhooks/image01.png"> From 74cf1c97025b3afe2c77baeba18620ac2b496d03 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 14:07:40 +0100 Subject: [PATCH 1739/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/advanced/generate-clients.md`=20(#10?= =?UTF-8?q?725)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/advanced/generate-clients.md | 286 ++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 docs/de/docs/advanced/generate-clients.md diff --git a/docs/de/docs/advanced/generate-clients.md b/docs/de/docs/advanced/generate-clients.md new file mode 100644 index 000000000..2fcba5956 --- /dev/null +++ b/docs/de/docs/advanced/generate-clients.md @@ -0,0 +1,286 @@ +# Clients generieren + +Da **FastAPI** auf der OpenAPI-Spezifikation basiert, erhalten Sie automatische Kompatibilität mit vielen Tools, einschließlich der automatischen API-Dokumentation (bereitgestellt von Swagger UI). + +Ein besonderer Vorteil, der nicht unbedingt offensichtlich ist, besteht darin, dass Sie für Ihre API **Clients generieren** können (manchmal auch <abbr title="Software Development Kits">**SDKs**</abbr> genannt), für viele verschiedene **Programmiersprachen**. + +## OpenAPI-Client-Generatoren + +Es gibt viele Tools zum Generieren von Clients aus **OpenAPI**. + +Ein gängiges Tool ist <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>. + +Wenn Sie ein **Frontend** erstellen, ist <a href="https://github.com/ferdikoomen/openapi-typescript-codegen" class="external-link" target="_blank">openapi-typescript-codegen</a> eine sehr interessante Alternative. + +## Client- und SDK-Generatoren – Sponsor + +Es gibt auch einige **vom Unternehmen entwickelte** Client- und SDK-Generatoren, die auf OpenAPI (FastAPI) basieren. In einigen Fällen können diese Ihnen **weitere Funktionalität** zusätzlich zu qualitativ hochwertigen generierten SDKs/Clients bieten. + +Einige von diesen ✨ [**sponsern FastAPI**](../help-fastapi.md#den-autor-sponsern){.internal-link target=_blank} ✨, das gewährleistet die kontinuierliche und gesunde **Entwicklung** von FastAPI und seinem **Ökosystem**. + +Und es zeigt deren wahres Engagement für FastAPI und seine **Community** (Sie), da diese Ihnen nicht nur einen **guten Service** bieten möchten, sondern auch sicherstellen möchten, dass Sie über ein **gutes und gesundes Framework** verfügen, FastAPI. 🙇 + +Beispielsweise könnten Sie <a href="https://speakeasyapi.dev/?utm_source=fastapi+repo&utm_medium=github+sponsorship" class="external-link" target="_blank">Speakeasy</a> ausprobieren. + +Es gibt auch mehrere andere Unternehmen, welche ähnliche Dienste anbieten und die Sie online suchen und finden können. 🤓 + +## Einen TypeScript-Frontend-Client generieren + +Beginnen wir mit einer einfachen FastAPI-Anwendung: + +=== "Python 3.9+" + + ```Python hl_lines="7-9 12-13 16-17 21" + {!> ../../../docs_src/generate_clients/tutorial001_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11 14-15 18 19 23" + {!> ../../../docs_src/generate_clients/tutorial001.py!} + ``` + +Beachten Sie, dass die *Pfadoperationen* die Modelle definieren, welche diese für die Request- und Response-<abbr title="Die eigentlichen Nutzdaten, abzüglich der Metadaten">Payload</abbr> verwenden, indem sie die Modelle `Item` und `ResponseMessage` verwenden. + +### API-Dokumentation + +Wenn Sie zur API-Dokumentation gehen, werden Sie sehen, dass diese die **Schemas** für die Daten enthält, welche in Requests gesendet und in Responses empfangen werden: + +<img src="/img/tutorial/generate-clients/image01.png"> + +Sie können diese Schemas sehen, da sie mit den Modellen in der Anwendung deklariert wurden. + +Diese Informationen sind im **OpenAPI-Schema** der Anwendung verfügbar und werden dann in der API-Dokumentation angezeigt (von Swagger UI). + +Und dieselben Informationen aus den Modellen, die in OpenAPI enthalten sind, können zum **Generieren des Client-Codes** verwendet werden. + +### Einen TypeScript-Client generieren + +Nachdem wir nun die Anwendung mit den Modellen haben, können wir den Client-Code für das Frontend generieren. + +#### `openapi-typescript-codegen` installieren + +Sie können `openapi-typescript-codegen` in Ihrem Frontend-Code installieren mit: + +<div class="termy"> + +```console +$ npm install openapi-typescript-codegen --save-dev + +---> 100% +``` + +</div> + +#### Client-Code generieren + +Um den Client-Code zu generieren, können Sie das Kommandozeilentool `openapi` verwenden, das soeben installiert wurde. + +Da es im lokalen Projekt installiert ist, könnten Sie diesen Befehl wahrscheinlich nicht direkt aufrufen, sondern würden ihn in Ihre Datei `package.json` einfügen. + +Diese könnte so aussehen: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +Nachdem Sie das NPM-Skript `generate-client` dort stehen haben, können Sie es ausführen mit: + +<div class="termy"> + +```console +$ npm run generate-client + +frontend-app@1.0.0 generate-client /home/user/code/frontend-app +> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios --useOptions --useUnionTypes +``` + +</div> + +Dieser Befehl generiert Code in `./src/client` und verwendet intern `axios` (die Frontend-HTTP-Bibliothek). + +### Den Client-Code ausprobieren + +Jetzt können Sie den Client-Code importieren und verwenden. Er könnte wie folgt aussehen, beachten Sie, dass Sie automatische Codevervollständigung für die Methoden erhalten: + +<img src="/img/tutorial/generate-clients/image02.png"> + +Sie erhalten außerdem automatische Vervollständigung für die zu sendende Payload: + +<img src="/img/tutorial/generate-clients/image03.png"> + +!!! tip "Tipp" + Beachten Sie die automatische Vervollständigung für `name` und `price`, welche in der FastAPI-Anwendung im `Item`-Modell definiert wurden. + +Sie erhalten Inline-Fehlerberichte für die von Ihnen gesendeten Daten: + +<img src="/img/tutorial/generate-clients/image04.png"> + +Das Response-Objekt hat auch automatische Vervollständigung: + +<img src="/img/tutorial/generate-clients/image05.png"> + +## FastAPI-Anwendung mit Tags + +In vielen Fällen wird Ihre FastAPI-Anwendung größer sein und Sie werden wahrscheinlich Tags verwenden, um verschiedene Gruppen von *Pfadoperationen* zu separieren. + +Beispielsweise könnten Sie einen Abschnitt für **Items (Artikel)** und einen weiteren Abschnitt für **Users (Benutzer)** haben, und diese könnten durch Tags getrennt sein: + +=== "Python 3.9+" + + ```Python hl_lines="21 26 34" + {!> ../../../docs_src/generate_clients/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="23 28 36" + {!> ../../../docs_src/generate_clients/tutorial002.py!} + ``` + +### Einen TypeScript-Client mit Tags generieren + +Wenn Sie unter Verwendung von Tags einen Client für eine FastAPI-Anwendung generieren, wird normalerweise auch der Client-Code anhand der Tags getrennt. + +Auf diese Weise können Sie die Dinge für den Client-Code richtig ordnen und gruppieren: + +<img src="/img/tutorial/generate-clients/image06.png"> + +In diesem Fall haben Sie: + +* `ItemsService` +* `UsersService` + +### Client-Methodennamen + +Im Moment sehen die generierten Methodennamen wie `createItemItemsPost` nicht sehr sauber aus: + +```TypeScript +ItemsService.createItemItemsPost({name: "Plumbus", price: 5}) +``` + +... das liegt daran, dass der Client-Generator für jede *Pfadoperation* die OpenAPI-interne **Operation-ID** verwendet. + +OpenAPI erfordert, dass jede Operation-ID innerhalb aller *Pfadoperationen* eindeutig ist. Daher verwendet FastAPI den **Funktionsnamen**, den **Pfad** und die **HTTP-Methode/-Operation**, um diese Operation-ID zu generieren. Denn so kann sichergestellt werden, dass die Operation-IDs eindeutig sind. + +Aber ich zeige Ihnen als nächstes, wie Sie das verbessern können. 🤓 + +## Benutzerdefinierte Operation-IDs und bessere Methodennamen + +Sie können die Art und Weise, wie diese Operation-IDs **generiert** werden, **ändern**, um sie einfacher zu machen und **einfachere Methodennamen** in den Clients zu haben. + +In diesem Fall müssen Sie auf andere Weise sicherstellen, dass jede Operation-ID **eindeutig** ist. + +Sie könnten beispielsweise sicherstellen, dass jede *Pfadoperation* einen Tag hat, und dann die Operation-ID basierend auf dem **Tag** und dem **Namen** der *Pfadoperation* (dem Funktionsnamen) generieren. + +### Funktion zum Generieren einer eindeutigen ID erstellen + +FastAPI verwendet eine **eindeutige ID** für jede *Pfadoperation*, diese wird für die **Operation-ID** und auch für die Namen aller benötigten benutzerdefinierten Modelle für Requests oder Responses verwendet. + +Sie können diese Funktion anpassen. Sie nimmt eine `APIRoute` und gibt einen String zurück. + +Hier verwendet sie beispielsweise den ersten Tag (Sie werden wahrscheinlich nur einen Tag haben) und den Namen der *Pfadoperation* (den Funktionsnamen). + +Anschließend können Sie diese benutzerdefinierte Funktion als Parameter `generate_unique_id_function` an **FastAPI** übergeben: + +=== "Python 3.9+" + + ```Python hl_lines="6-7 10" + {!> ../../../docs_src/generate_clients/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8-9 12" + {!> ../../../docs_src/generate_clients/tutorial003.py!} + ``` + +### Einen TypeScript-Client mit benutzerdefinierten Operation-IDs generieren + +Wenn Sie nun den Client erneut generieren, werden Sie feststellen, dass er über die verbesserten Methodennamen verfügt: + +<img src="/img/tutorial/generate-clients/image07.png"> + +Wie Sie sehen, haben die Methodennamen jetzt den Tag und dann den Funktionsnamen, aber keine Informationen aus dem URL-Pfad und der HTTP-Operation. + +### Vorab-Modifikation der OpenAPI-Spezifikation für den Client-Generator + +Der generierte Code enthält immer noch etwas **verdoppelte Information**. + +Wir wissen bereits, dass diese Methode mit den **Items** zusammenhängt, da sich dieses Wort in `ItemsService` befindet (vom Tag übernommen), aber wir haben auch immer noch den Tagnamen im Methodennamen vorangestellt. 😕 + +Wir werden das wahrscheinlich weiterhin für OpenAPI im Allgemeinen beibehalten wollen, da dadurch sichergestellt wird, dass die Operation-IDs **eindeutig** sind. + +Aber für den generierten Client könnten wir die OpenAPI-Operation-IDs direkt vor der Generierung der Clients **modifizieren**, um diese Methodennamen schöner und **sauberer** zu machen. + +Wir könnten das OpenAPI-JSON in eine Datei `openapi.json` herunterladen und dann mit einem Skript wie dem folgenden **den vorangestellten Tag entfernen**: + +=== "Python" + + ```Python + {!> ../../../docs_src/generate_clients/tutorial004.py!} + ``` + +=== "Node.js" + + ```Javascript + {!> ../../../docs_src/generate_clients/tutorial004.js!} + ``` + +Damit würden die Operation-IDs von Dingen wie `items-get_items` in `get_items` umbenannt, sodass der Client-Generator einfachere Methodennamen generieren kann. + +### Einen TypeScript-Client mit der modifizierten OpenAPI generieren + +Da das Endergebnis nun in einer Datei `openapi.json` vorliegt, würden Sie die `package.json` ändern, um diese lokale Datei zu verwenden, zum Beispiel: + +```JSON hl_lines="7" +{ + "name": "frontend-app", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios --useOptions --useUnionTypes" + }, + "author": "", + "license": "", + "devDependencies": { + "openapi-typescript-codegen": "^0.20.1", + "typescript": "^4.6.2" + } +} +``` + +Nach der Generierung des neuen Clients hätten Sie nun **saubere Methodennamen** mit allen **Autovervollständigungen**, **Inline-Fehlerberichten**, usw.: + +<img src="/img/tutorial/generate-clients/image08.png"> + +## Vorteile + +Wenn Sie die automatisch generierten Clients verwenden, erhalten Sie **automatische Codevervollständigung** für: + +* Methoden. +* Request-Payloads im Body, Query-Parameter, usw. +* Response-Payloads. + +Außerdem erhalten Sie für alles **Inline-Fehlerberichte**. + +Und wann immer Sie den Backend-Code aktualisieren und das Frontend **neu generieren**, stehen alle neuen *Pfadoperationen* als Methoden zur Verfügung, die alten werden entfernt und alle anderen Änderungen werden im generierten Code reflektiert. 🤓 + +Das bedeutet auch, dass, wenn sich etwas ändert, dies automatisch im Client-Code **reflektiert** wird. Und wenn Sie den Client **erstellen**, kommt es zu einer Fehlermeldung, wenn die verwendeten Daten **nicht übereinstimmen**. + +Sie würden also sehr früh im Entwicklungszyklus **viele Fehler erkennen**, anstatt darauf warten zu müssen, dass die Fehler Ihren Endbenutzern in der Produktion angezeigt werden, und dann zu versuchen, zu debuggen, wo das Problem liegt. ✨ From 5ca3d175879166c4956b92d4bb7745086fe3b21d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:08:30 +0000 Subject: [PATCH 1740/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9b091c494..d7a8e0c60 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/advanced/openapi-webhooks.md`. PR [#10712](https://github.com/tiangolo/fastapi/pull/10712) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/custom-response.md`. PR [#10624](https://github.com/tiangolo/fastapi/pull/10624) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/additional-status-codes.md`. PR [#10617](https://github.com/tiangolo/fastapi/pull/10617) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/middleware.md`. PR [#10391](https://github.com/tiangolo/fastapi/pull/10391) by [@JohannesJungbluth](https://github.com/JohannesJungbluth). From 13b908df68e48416d968980d9b3f6f00f8ff54ad Mon Sep 17 00:00:00 2001 From: 3w36zj6 <52315048+3w36zj6@users.noreply.github.com> Date: Tue, 23 Jan 2024 22:10:49 +0900 Subject: [PATCH 1741/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/security/index.md`=20(#57?= =?UTF-8?q?98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ja/docs/tutorial/security/index.md | 101 ++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/ja/docs/tutorial/security/index.md diff --git a/docs/ja/docs/tutorial/security/index.md b/docs/ja/docs/tutorial/security/index.md new file mode 100644 index 000000000..390f21047 --- /dev/null +++ b/docs/ja/docs/tutorial/security/index.md @@ -0,0 +1,101 @@ +# セキュリティ入門 + +セキュリティ、認証、認可を扱うには多くの方法があります。 + +そして、通常、それは複雑で「難しい」トピックです。 + +多くのフレームワークやシステムでは、セキュリティと認証を処理するだけで、膨大な労力とコードが必要になります(多くの場合、書かれた全コードの50%以上を占めることがあります)。 + +**FastAPI** は、セキュリティの仕様をすべて勉強して学ぶことなく、標準的な方法で簡単に、迅速に**セキュリティ**を扱うためのツールをいくつか提供します。 + +しかし、その前に、いくつかの小さな概念を確認しましょう。 + +## お急ぎですか? + +もし、これらの用語に興味がなく、ユーザー名とパスワードに基づく認証でセキュリティを**今すぐ**確保する必要がある場合は、次の章に進んでください。 + +## OAuth2 + +OAuth2は、認証と認可を処理するためのいくつかの方法を定義した仕様です。 + +かなり広範囲な仕様で、いくつかの複雑なユースケースをカバーしています。 + +これには「サードパーティ」を使用して認証する方法が含まれています。 + +これが、「Facebook、Google、Twitter、GitHubを使ってログイン」を使用したすべてのシステムの背後で使われている仕組みです。 + +### OAuth 1 + +OAuth 1というものもありましたが、これはOAuth2とは全く異なり、通信をどのように暗号化するかという仕様が直接的に含まれており、より複雑なものとなっています。 + +現在ではあまり普及していませんし、使われてもいません。 + +OAuth2は、通信を暗号化する方法を指定せず、アプリケーションがHTTPSで提供されることを想定しています。 + +!!! tip "豆知識" + **デプロイ**のセクションでは、TraefikとLet's Encryptを使用して、無料でHTTPSを設定する方法が紹介されています。 + + +## OpenID Connect + +OpenID Connectは、**OAuth2**をベースにした別の仕様です。 + +これはOAuth2を拡張したもので、OAuth2ではやや曖昧だった部分を明確にし、より相互運用性を高めようとしたものです。 + +例として、GoogleのログインはOpenID Connectを使用しています(これはOAuth2がベースになっています)。 + +しかし、FacebookのログインはOpenID Connectをサポートしていません。OAuth2を独自にアレンジしています。 + +### OpenID (「OpenID Connect」ではない) + +また、「OpenID」という仕様もありました。それは、**OpenID Connect**と同じことを解決しようとしたものですが、OAuth2に基づいているわけではありませんでした。 + +つまり、完全な追加システムだったのです。 + +現在ではあまり普及していませんし、使われてもいません。 + +## OpenAPI + +OpenAPI(以前はSwaggerとして知られていました)は、APIを構築するためのオープンな仕様です(現在はLinux Foundationの一部になっています)。 + +**FastAPI**は、**OpenAPI**をベースにしています。 + +それが、複数の自動対話型ドキュメント・インターフェースやコード生成などを可能にしているのです。 + +OpenAPIには、複数のセキュリティ「スキーム」を定義する方法があります。 + +それらを使用することで、これらの対話型ドキュメントシステムを含む、標準ベースのツールをすべて活用できます。 + +OpenAPIでは、以下のセキュリティスキームを定義しています: + +* `apiKey`: アプリケーション固有のキーで、これらのものから取得できます。 + * クエリパラメータ + * ヘッダー + * クッキー +* `http`: 標準的なHTTP認証システムで、これらのものを含みます。 + * `bearer`: ヘッダ `Authorization` の値が `Bearer ` で、トークンが含まれます。これはOAuth2から継承しています。 + * HTTP Basic認証 + * HTTP ダイジェスト認証など +* `oauth2`: OAuth2のセキュリティ処理方法(「フロー」と呼ばれます)のすべて。 + * これらのフローのいくつかは、OAuth 2.0認証プロバイダ(Google、Facebook、Twitter、GitHubなど)を構築するのに適しています。 + * `implicit` + * `clientCredentials` + * `authorizationCode` + * しかし、同じアプリケーション内で認証を直接処理するために完全に機能する特定の「フロー」があります。 + * `password`: 次のいくつかの章では、その例を紹介します。 +* `openIdConnect`: OAuth2認証データを自動的に発見する方法を定義できます。 + * この自動検出メカニズムは、OpenID Connectの仕様で定義されているものです。 + + +!!! tip "豆知識" + Google、Facebook、Twitter、GitHubなど、他の認証/認可プロバイダを統合することも可能で、比較的簡単です。 + + 最も複雑な問題は、それらのような認証/認可プロバイダを構築することですが、**FastAPI**は、あなたのために重い仕事をこなしながら、それを簡単に行うためのツールを提供します。 + +## **FastAPI** ユーティリティ + +FastAPIは `fastapi.security` モジュールの中で、これらのセキュリティスキームごとにいくつかのツールを提供し、これらのセキュリティメカニズムを簡単に使用できるようにします。 + +次の章では、**FastAPI** が提供するこれらのツールを使って、あなたのAPIにセキュリティを追加する方法について見ていきます。 + +また、それがどのようにインタラクティブなドキュメントシステムに自動的に統合されるかも見ていきます。 From 7c9cb476a48d1de73557bcee22c62323d7b19971 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:11:16 +0000 Subject: [PATCH 1742/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d7a8e0c60..ffbeaff15 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/advanced/generate-clients.md`. PR [#10725](https://github.com/tiangolo/fastapi/pull/10725) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/openapi-webhooks.md`. PR [#10712](https://github.com/tiangolo/fastapi/pull/10712) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/custom-response.md`. PR [#10624](https://github.com/tiangolo/fastapi/pull/10624) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/additional-status-codes.md`. PR [#10617](https://github.com/tiangolo/fastapi/pull/10617) by [@nilslindemann](https://github.com/nilslindemann). From 280f49ea835eb6f2d900aee2d9bb2f8425ab4dde Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:15:22 +0000 Subject: [PATCH 1743/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ffbeaff15..68f6b0762 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/index.md`. PR [#5798](https://github.com/tiangolo/fastapi/pull/5798) by [@3w36zj6](https://github.com/3w36zj6). * 🌐 Add German translation for `docs/de/docs/advanced/generate-clients.md`. PR [#10725](https://github.com/tiangolo/fastapi/pull/10725) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/openapi-webhooks.md`. PR [#10712](https://github.com/tiangolo/fastapi/pull/10712) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/custom-response.md`. PR [#10624](https://github.com/tiangolo/fastapi/pull/10624) by [@nilslindemann](https://github.com/nilslindemann). From 51329762539c3ed1ed72e4a4860ccf5711aa7301 Mon Sep 17 00:00:00 2001 From: Nikita <78080842+NiKuma0@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:54:17 +0300 Subject: [PATCH 1744/1881] =?UTF-8?q?=F0=9F=8C=90=20Russian=20translation:?= =?UTF-8?q?=20updated=20`fastapi-people.md`.=20(#10255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/fastapi-people.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ru/docs/fastapi-people.md b/docs/ru/docs/fastapi-people.md index 64ae66a03..6778cceab 100644 --- a/docs/ru/docs/fastapi-people.md +++ b/docs/ru/docs/fastapi-people.md @@ -5,7 +5,7 @@ ## Создатель и хранитель -Ку! 👋 +Хай! 👋 Это я: From 315d8184e7e393106cb985ac3aff10d22b4d4080 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:54:45 +0000 Subject: [PATCH 1745/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 68f6b0762..a1ee0f0cf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Russian translation: updated `fastapi-people.md`.. PR [#10255](https://github.com/tiangolo/fastapi/pull/10255) by [@NiKuma0](https://github.com/NiKuma0). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/index.md`. PR [#5798](https://github.com/tiangolo/fastapi/pull/5798) by [@3w36zj6](https://github.com/3w36zj6). * 🌐 Add German translation for `docs/de/docs/advanced/generate-clients.md`. PR [#10725](https://github.com/tiangolo/fastapi/pull/10725) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/advanced/openapi-webhooks.md`. PR [#10712](https://github.com/tiangolo/fastapi/pull/10712) by [@nilslindemann](https://github.com/nilslindemann). From 95d5902af17cf1e260d2ff6e9b5afb22c5a2bb4e Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 23 Jan 2024 16:55:32 +0300 Subject: [PATCH 1746/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/body-updates.md`=20(#10373?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/body-updates.md | 153 ++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 docs/ru/docs/tutorial/body-updates.md diff --git a/docs/ru/docs/tutorial/body-updates.md b/docs/ru/docs/tutorial/body-updates.md new file mode 100644 index 000000000..4998ab31a --- /dev/null +++ b/docs/ru/docs/tutorial/body-updates.md @@ -0,0 +1,153 @@ +# Body - Обновления + +## Полное обновление с помощью `PUT` + +Для полного обновления элемента можно воспользоваться операцией <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT" class="external-link" target="_blank">HTTP `PUT`</a>. + +Вы можете использовать функцию `jsonable_encoder` для преобразования входных данных в JSON, так как нередки случаи, когда работать можно только с простыми типами данных (например, для хранения в NoSQL-базе данных). + +=== "Python 3.10+" + + ```Python hl_lines="28-33" + {!> ../../../docs_src/body_updates/tutorial001_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="30-35" + {!> ../../../docs_src/body_updates/tutorial001.py!} + ``` + +`PUT` используется для получения данных, которые должны полностью заменить существующие данные. + +### Предупреждение о замене + +Это означает, что если вы хотите обновить элемент `bar`, используя `PUT` с телом, содержащим: + +```Python +{ + "name": "Barz", + "price": 3, + "description": None, +} +``` + +поскольку оно не включает уже сохраненный атрибут `"tax": 20.2`, входная модель примет значение по умолчанию `"tax": 10.5`. + +И данные будут сохранены с этим "новым" `tax`, равным `10,5`. + +## Частичное обновление с помощью `PATCH` + +Также можно использовать <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH" class="external-link" target="_blank">HTTP `PATCH`</a> операцию для *частичного* обновления данных. + +Это означает, что можно передавать только те данные, которые необходимо обновить, оставляя остальные нетронутыми. + +!!! note "Технические детали" + `PATCH` менее распространен и известен, чем `PUT`. + + А многие команды используют только `PUT`, даже для частичного обновления. + + Вы можете **свободно** использовать их как угодно, **FastAPI** не накладывает никаких ограничений. + + Но в данном руководстве более или менее понятно, как они должны использоваться. + +### Использование параметра `exclude_unset` в Pydantic + +Если необходимо выполнить частичное обновление, то очень полезно использовать параметр `exclude_unset` в методе `.dict()` модели Pydantic. + +Например, `item.dict(exclude_unset=True)`. + +В результате будет сгенерирован словарь, содержащий только те данные, которые были заданы при создании модели `item`, без учета значений по умолчанию. Затем вы можете использовать это для создания словаря только с теми данными, которые были установлены (отправлены в запросе), опуская значения по умолчанию: + +=== "Python 3.10+" + + ```Python hl_lines="32" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="34" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +### Использование параметра `update` в Pydantic + +Теперь можно создать копию существующей модели, используя `.copy()`, и передать параметр `update` с `dict`, содержащим данные для обновления. + +Например, `stored_item_model.copy(update=update_data)`: + +=== "Python 3.10+" + + ```Python hl_lines="33" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="35" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +### Кратко о частичном обновлении + +В целом, для применения частичных обновлений необходимо: + +* (Опционально) использовать `PATCH` вместо `PUT`. +* Извлечь сохранённые данные. +* Поместить эти данные в Pydantic модель. +* Сгенерировать `dict` без значений по умолчанию из входной модели (с использованием `exclude_unset`). + * Таким образом, можно обновлять только те значения, которые действительно установлены пользователем, вместо того чтобы переопределять значения, уже сохраненные в модели по умолчанию. +* Создать копию хранимой модели, обновив ее атрибуты полученными частичными обновлениями (с помощью параметра `update`). +* Преобразовать скопированную модель в то, что может быть сохранено в вашей БД (например, с помощью `jsonable_encoder`). + * Это сравнимо с повторным использованием метода модели `.dict()`, но при этом происходит проверка (и преобразование) значений в типы данных, которые могут быть преобразованы в JSON, например, `datetime` в `str`. +* Сохранить данные в своей БД. +* Вернуть обновленную модель. + +=== "Python 3.10+" + + ```Python hl_lines="28-35" + {!> ../../../docs_src/body_updates/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002_py39.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="30-37" + {!> ../../../docs_src/body_updates/tutorial002.py!} + ``` + +!!! tip "Подсказка" + Эту же технику можно использовать и для операции HTTP `PUT`. + + Но в приведенном примере используется `PATCH`, поскольку он был создан именно для таких случаев использования. + +!!! note "Технические детали" + Обратите внимание, что входная модель по-прежнему валидируется. + + Таким образом, если вы хотите получать частичные обновления, в которых могут быть опущены все атрибуты, вам необходимо иметь модель, в которой все атрибуты помечены как необязательные (со значениями по умолчанию или `None`). + + Чтобы отличить модели со всеми необязательными значениями для **обновления** от моделей с обязательными значениями для **создания**, можно воспользоваться идеями, описанными в [Дополнительные модели](extra-models.md){.internal-link target=_blank}. From 672b501b98c5d3b2589b6092080e4da8fc9ba835 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 23 Jan 2024 16:56:12 +0300 Subject: [PATCH 1747/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/encoder.md`=20(#10374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/encoder.md | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/ru/docs/tutorial/encoder.md diff --git a/docs/ru/docs/tutorial/encoder.md b/docs/ru/docs/tutorial/encoder.md new file mode 100644 index 000000000..c26b2c941 --- /dev/null +++ b/docs/ru/docs/tutorial/encoder.md @@ -0,0 +1,42 @@ +# JSON кодировщик + +В некоторых случаях может потребоваться преобразование типа данных (например, Pydantic-модели) в тип, совместимый с JSON (например, `dict`, `list` и т.д.). + +Например, если необходимо хранить его в базе данных. + +Для этого **FastAPI** предоставляет функцию `jsonable_encoder()`. + +## Использование `jsonable_encoder` + +Представим, что у вас есть база данных `fake_db`, которая принимает только JSON-совместимые данные. + +Например, он не принимает объекты `datetime`, так как они не совместимы с JSON. + +В таком случае объект `datetime` следует преобразовать в строку соответствующую <a href="https://en.wikipedia.org/wiki/ISO_8601" class="external-link" target="_blank">формату ISO</a>. + +Точно так же эта база данных не может принять Pydantic модель (объект с атрибутами), а только `dict`. + +Для этого можно использовать функцию `jsonable_encoder`. + +Она принимает объект, например, модель Pydantic, и возвращает его версию, совместимую с JSON: + +=== "Python 3.10+" + + ```Python hl_lines="4 21" + {!> ../../../docs_src/encoder/tutorial001_py310.py!} + ``` + +=== "Python 3.6+" + + ```Python hl_lines="5 22" + {!> ../../../docs_src/encoder/tutorial001.py!} + ``` + +В данном примере она преобразует Pydantic модель в `dict`, а `datetime` - в `str`. + +Результатом её вызова является объект, который может быть закодирован с помощью функции из стандартной библиотеки Python – <a href="https://docs.python.org/3/library/json.html#json.dumps" class="external-link" target="_blank">`json.dumps()`</a>. + +Функция не возвращает большой `str`, содержащий данные в формате JSON (в виде строки). Она возвращает стандартную структуру данных Python (например, `dict`) со значениями и подзначениями, которые совместимы с JSON. + +!!! note "Технические детали" + `jsonable_encoder` фактически используется **FastAPI** внутри системы для преобразования данных. Однако он полезен и во многих других сценариях. From ac5e73b19dd09a60e49097de1ce0068b4f5464e1 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 23 Jan 2024 16:56:29 +0300 Subject: [PATCH 1748/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/handling-errors.md`=20(#10?= =?UTF-8?q?375)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/handling-errors.md | 261 +++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 docs/ru/docs/tutorial/handling-errors.md diff --git a/docs/ru/docs/tutorial/handling-errors.md b/docs/ru/docs/tutorial/handling-errors.md new file mode 100644 index 000000000..f578cf198 --- /dev/null +++ b/docs/ru/docs/tutorial/handling-errors.md @@ -0,0 +1,261 @@ +# Обработка ошибок + +Существует множество ситуаций, когда необходимо сообщить об ошибке клиенту, использующему ваш API. + +Таким клиентом может быть браузер с фронтендом, чужой код, IoT-устройство и т.д. + +Возможно, вам придется сообщить клиенту о следующем: + +* Клиент не имеет достаточных привилегий для выполнения данной операции. +* Клиент не имеет доступа к данному ресурсу. +* Элемент, к которому клиент пытался получить доступ, не существует. +* и т.д. + +В таких случаях обычно возвращается **HTTP-код статуса ответа** в диапазоне **400** (от 400 до 499). + +Они похожи на двухсотые HTTP статус-коды (от 200 до 299), которые означают, что запрос обработан успешно. + +Четырёхсотые статус-коды означают, что ошибка произошла по вине клиента. + +Помните ли ошибки **"404 Not Found "** (и шутки) ? + +## Использование `HTTPException` + +Для возврата клиенту HTTP-ответов с ошибками используется `HTTPException`. + +### Импортируйте `HTTPException` + +```Python hl_lines="1" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### Вызовите `HTTPException` в своем коде + +`HTTPException` - это обычное исключение Python с дополнительными данными, актуальными для API. + +Поскольку это исключение Python, то его не `возвращают`, а `вызывают`. + +Это также означает, что если вы находитесь внутри функции, которая вызывается внутри вашей *функции операции пути*, и вы поднимаете `HTTPException` внутри этой функции, то она не будет выполнять остальной код в *функции операции пути*, а сразу завершит запрос и отправит HTTP-ошибку из `HTTPException` клиенту. + +О том, насколько выгоднее `вызывать` исключение, чем `возвращать` значение, будет рассказано в разделе, посвященном зависимостям и безопасности. + +В данном примере, когда клиент запрашивает элемент по несуществующему ID, возникает исключение со статус-кодом `404`: + +```Python hl_lines="11" +{!../../../docs_src/handling_errors/tutorial001.py!} +``` + +### Возвращаемый ответ + +Если клиент запросит `http://example.com/items/foo` (`item_id` `"foo"`), то он получит статус-код 200 и ответ в формате JSON: + +```JSON +{ + "item": "The Foo Wrestlers" +} +``` + +Но если клиент запросит `http://example.com/items/bar` (несуществующий `item_id` `"bar"`), то он получит статус-код 404 (ошибка "не найдено") и JSON-ответ в виде: + +```JSON +{ + "detail": "Item not found" +} +``` + +!!! tip "Подсказка" + При вызове `HTTPException` в качестве параметра `detail` можно передавать любое значение, которое может быть преобразовано в JSON, а не только `str`. + + Вы можете передать `dict`, `list` и т.д. + + Они автоматически обрабатываются **FastAPI** и преобразуются в JSON. + +## Добавление пользовательских заголовков + +В некоторых ситуациях полезно иметь возможность добавлять пользовательские заголовки к ошибке HTTP. Например, для некоторых типов безопасности. + +Скорее всего, вам не потребуется использовать его непосредственно в коде. + +Но в случае, если это необходимо для продвинутого сценария, можно добавить пользовательские заголовки: + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial002.py!} +``` + +## Установка пользовательских обработчиков исключений + +Вы можете добавить пользовательские обработчики исключений с помощью <a href="https://www.starlette.io/exceptions/" class="external-link" target="_blank">то же самое исключение - утилиты от Starlette</a>. + +Допустим, у вас есть пользовательское исключение `UnicornException`, которое вы (или используемая вами библиотека) можете `вызвать`. + +И вы хотите обрабатывать это исключение глобально с помощью FastAPI. + +Можно добавить собственный обработчик исключений с помощью `@app.exception_handler()`: + +```Python hl_lines="5-7 13-18 24" +{!../../../docs_src/handling_errors/tutorial003.py!} +``` + +Здесь, если запросить `/unicorns/yolo`, то *операция пути* вызовет `UnicornException`. + +Но оно будет обработано `unicorn_exception_handler`. + +Таким образом, вы получите чистую ошибку с кодом состояния HTTP `418` и содержимым JSON: + +```JSON +{"message": "Oops! yolo did something. There goes a rainbow..."} +``` + +!!! note "Технические детали" + Также можно использовать `from starlette.requests import Request` и `from starlette.responses import JSONResponse`. + + **FastAPI** предоставляет тот же `starlette.responses`, что и `fastapi.responses`, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette. То же самое касается и `Request`. + +## Переопределение стандартных обработчиков исключений + +**FastAPI** имеет некоторые обработчики исключений по умолчанию. + +Эти обработчики отвечают за возврат стандартных JSON-ответов при `вызове` `HTTPException` и при наличии в запросе недопустимых данных. + +Вы можете переопределить эти обработчики исключений на свои собственные. + +### Переопределение исключений проверки запроса + +Когда запрос содержит недопустимые данные, **FastAPI** внутренне вызывает ошибку `RequestValidationError`. + +А также включает в себя обработчик исключений по умолчанию. + +Чтобы переопределить его, импортируйте `RequestValidationError` и используйте его с `@app.exception_handler(RequestValidationError)` для создания обработчика исключений. + +Обработчик исключения получит объект `Request` и исключение. + +```Python hl_lines="2 14-16" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +Теперь, если перейти к `/items/foo`, то вместо стандартной JSON-ошибки с: + +```JSON +{ + "detail": [ + { + "loc": [ + "path", + "item_id" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ] +} +``` + +вы получите текстовую версию: + +``` +1 validation error +path -> item_id + value is not a valid integer (type=type_error.integer) +``` + +#### `RequestValidationError` или `ValidationError` + +!!! warning "Внимание" + Это технические детали, которые можно пропустить, если они не важны для вас сейчас. + +`RequestValidationError` является подклассом Pydantic <a href="https://pydantic-docs.helpmanual.io/usage/models/#error-handling" class="external-link" target="_blank">`ValidationError`</a>. + +**FastAPI** использует его для того, чтобы, если вы используете Pydantic-модель в `response_model`, и ваши данные содержат ошибку, вы увидели ошибку в журнале. + +Но клиент/пользователь этого не увидит. Вместо этого клиент получит сообщение "Internal Server Error" с кодом состояния HTTP `500`. + +Так и должно быть, потому что если в вашем *ответе* или где-либо в вашем коде (не в *запросе* клиента) возникает Pydantic `ValidationError`, то это действительно ошибка в вашем коде. + +И пока вы не устраните ошибку, ваши клиенты/пользователи не должны иметь доступа к внутренней информации о ней, так как это может привести к уязвимости в системе безопасности. + +### Переопределите обработчик ошибок `HTTPException` + +Аналогичным образом можно переопределить обработчик `HTTPException`. + +Например, для этих ошибок можно вернуть обычный текстовый ответ вместо JSON: + +```Python hl_lines="3-4 9-11 22" +{!../../../docs_src/handling_errors/tutorial004.py!} +``` + +!!! note "Технические детали" + Можно также использовать `from starlette.responses import PlainTextResponse`. + + **FastAPI** предоставляет тот же `starlette.responses`, что и `fastapi.responses`, просто для удобства разработчика. Однако большинство доступных ответов поступает непосредственно из Starlette. + +### Используйте тело `RequestValidationError` + +Ошибка `RequestValidationError` содержит полученное `тело` с недопустимыми данными. + +Вы можете использовать его при разработке приложения для регистрации тела и его отладки, возврата пользователю и т.д. + +```Python hl_lines="14" +{!../../../docs_src/handling_errors/tutorial005.py!} +``` + +Теперь попробуйте отправить недействительный элемент, например: + +```JSON +{ + "title": "towel", + "size": "XL" +} +``` + +Вы получите ответ о том, что данные недействительны, содержащий следующее тело: + +```JSON hl_lines="12-15" +{ + "detail": [ + { + "loc": [ + "body", + "size" + ], + "msg": "value is not a valid integer", + "type": "type_error.integer" + } + ], + "body": { + "title": "towel", + "size": "XL" + } +} +``` + +#### `HTTPException` в FastAPI или в Starlette + +**FastAPI** имеет собственный `HTTPException`. + +Класс ошибок **FastAPI** `HTTPException` наследует от класса ошибок Starlette `HTTPException`. + +Единственное отличие заключается в том, что `HTTPException` от **FastAPI** позволяет добавлять заголовки, которые будут включены в ответ. + +Он необходим/используется внутри системы для OAuth 2.0 и некоторых утилит безопасности. + +Таким образом, вы можете продолжать вызывать `HTTPException` от **FastAPI** как обычно в своем коде. + +Но когда вы регистрируете обработчик исключений, вы должны зарегистрировать его для `HTTPException` от Starlette. + +Таким образом, если какая-либо часть внутреннего кода Starlette, расширение или плагин Starlette вызовет исключение Starlette `HTTPException`, ваш обработчик сможет перехватить и обработать его. + +В данном примере, чтобы иметь возможность использовать оба `HTTPException` в одном коде, исключения Starlette переименованы в `StarletteHTTPException`: + +```Python +from starlette.exceptions import HTTPException as StarletteHTTPException +``` + +### Переиспользование обработчиков исключений **FastAPI** + +Если вы хотите использовать исключение вместе с теми же обработчиками исключений по умолчанию из **FastAPI**, вы можете импортировать и повторно использовать обработчики исключений по умолчанию из `fastapi.exception_handlers`: + +```Python hl_lines="2-5 15 21" +{!../../../docs_src/handling_errors/tutorial006.py!} +``` + +В этом примере вы просто `выводите в терминал` ошибку с очень выразительным сообщением, но идея вам понятна. Вы можете использовать исключение, а затем просто повторно использовать стандартные обработчики исключений. From ccdc96293683a2e44237a500f4cb4740784a8f17 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:56:42 +0000 Subject: [PATCH 1749/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a1ee0f0cf..6a261712f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-updates.md`. PR [#10373](https://github.com/tiangolo/fastapi/pull/10373) by [@AlertRED](https://github.com/AlertRED). * 🌐 Russian translation: updated `fastapi-people.md`.. PR [#10255](https://github.com/tiangolo/fastapi/pull/10255) by [@NiKuma0](https://github.com/NiKuma0). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/index.md`. PR [#5798](https://github.com/tiangolo/fastapi/pull/5798) by [@3w36zj6](https://github.com/3w36zj6). * 🌐 Add German translation for `docs/de/docs/advanced/generate-clients.md`. PR [#10725](https://github.com/tiangolo/fastapi/pull/10725) by [@nilslindemann](https://github.com/nilslindemann). From cc9c448ed45b544b32bb5e59efc5091f0b52db3b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:57:19 +0000 Subject: [PATCH 1750/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6a261712f..a5f76de0b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/encoder.md`. PR [#10374](https://github.com/tiangolo/fastapi/pull/10374) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-updates.md`. PR [#10373](https://github.com/tiangolo/fastapi/pull/10373) by [@AlertRED](https://github.com/AlertRED). * 🌐 Russian translation: updated `fastapi-people.md`.. PR [#10255](https://github.com/tiangolo/fastapi/pull/10255) by [@NiKuma0](https://github.com/NiKuma0). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/index.md`. PR [#5798](https://github.com/tiangolo/fastapi/pull/5798) by [@3w36zj6](https://github.com/3w36zj6). From 0fb326fc6ed21d94647822a9dee949abcb538ec6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 13:58:04 +0000 Subject: [PATCH 1751/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a5f76de0b..4ccedda1d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/handling-errors.md`. PR [#10375](https://github.com/tiangolo/fastapi/pull/10375) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/encoder.md`. PR [#10374](https://github.com/tiangolo/fastapi/pull/10374) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-updates.md`. PR [#10373](https://github.com/tiangolo/fastapi/pull/10373) by [@AlertRED](https://github.com/AlertRED). * 🌐 Russian translation: updated `fastapi-people.md`.. PR [#10255](https://github.com/tiangolo/fastapi/pull/10255) by [@NiKuma0](https://github.com/NiKuma0). From 9060c427a6deb883bc07df16ae9b573d2e6585d3 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrukhov <drammtv@gmail.com> Date: Tue, 23 Jan 2024 17:00:11 +0300 Subject: [PATCH 1752/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Russian=20transl?= =?UTF-8?q?ation=20for=20`docs/ru/docs/tutorial/security/first-steps.md`?= =?UTF-8?q?=20(#10541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ru/docs/tutorial/security/first-steps.md | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 docs/ru/docs/tutorial/security/first-steps.md diff --git a/docs/ru/docs/tutorial/security/first-steps.md b/docs/ru/docs/tutorial/security/first-steps.md new file mode 100644 index 000000000..b70a60a38 --- /dev/null +++ b/docs/ru/docs/tutorial/security/first-steps.md @@ -0,0 +1,232 @@ +# Безопасность - первые шаги + +Представим, что у вас есть свой **бэкенд** API на некотором домене. + +И у вас есть **фронтенд** на другом домене или на другом пути того же домена (или в мобильном приложении). + +И вы хотите иметь возможность аутентификации фронтенда с бэкендом, используя **имя пользователя** и **пароль**. + +Мы можем использовать **OAuth2** для создания такой системы с помощью **FastAPI**. + +Но давайте избавим вас от необходимости читать всю длинную спецификацию, чтобы найти те небольшие кусочки информации, которые вам нужны. + +Для работы с безопасностью воспользуемся средствами, предоставленными **FastAPI**. + +## Как это выглядит + +Давайте сначала просто воспользуемся кодом и посмотрим, как он работает, а затем детально разберём, что происходит. + +## Создание `main.py` + +Скопируйте пример в файл `main.py`: + +=== "Python 3.9+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python + {!> ../../../docs_src/security/tutorial001.py!} + ``` + + +## Запуск + +!!! info "Дополнительная информация" + Вначале, установите библиотеку <a href="https://andrew-d.github.io/python-multipart/" class="external-link" target="_blank">`python-multipart`</a>. + + А именно: `pip install python-multipart`. + + Это связано с тем, что **OAuth2** использует "данные формы" для передачи `имени пользователя` и `пароля`. + +Запустите ваш сервер: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +## Проверка + +Перейдите к интерактивной документации по адресу: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +Вы увидите примерно следующее: + +<img src="/img/tutorial/security/image01.png"> + +!!! check "Кнопка авторизации!" + У вас уже появилась новая кнопка "Authorize". + + А у *операции пути* теперь появился маленький замочек в правом верхнем углу, на который можно нажать. + +При нажатии на нее появляется небольшая форма авторизации, в которую нужно ввести `имя пользователя` и `пароль` (и другие необязательные поля): + +<img src="/img/tutorial/security/image02.png"> + +!!! note "Технические детали" + Неважно, что вы введете в форму, она пока не будет работать. Но мы к этому еще придем. + +Конечно, это не фронтенд для конечных пользователей, но это отличный автоматический инструмент для интерактивного документирования всех ваших API. + +Он может использоваться командой фронтенда (которой можете быть и вы сами). + +Он может быть использован сторонними приложениями и системами. + +Кроме того, его можно использовать самостоятельно для отладки, проверки и тестирования одного и того же приложения. + +## Аутентификация по паролю + +Теперь давайте вернемся немного назад и разберемся, что же это такое. + +Аутентификация по паролю является одним из способов, определенных в OAuth2, для обеспечения безопасности и аутентификации. + +OAuth2 был разработан для того, чтобы бэкэнд или API были независимы от сервера, который аутентифицирует пользователя. + +Но в нашем случае одно и то же приложение **FastAPI** будет работать с API и аутентификацией. + +Итак, рассмотрим его с этой упрощенной точки зрения: + +* Пользователь вводит на фронтенде `имя пользователя` и `пароль` и нажимает `Enter`. +* Фронтенд (работающий в браузере пользователя) отправляет эти `имя пользователя` и `пароль` на определенный URL в нашем API (объявленный с помощью параметра `tokenUrl="token"`). +* API проверяет эти `имя пользователя` и `пароль` и выдает в ответ "токен" (мы еще не реализовали ничего из этого). + * "Токен" - это просто строка с некоторым содержимым, которое мы можем использовать позже для верификации пользователя. + * Обычно срок действия токена истекает через некоторое время. + * Таким образом, пользователю придется снова войти в систему в какой-то момент времени. + * И если токен будет украден, то риск будет меньше, так как он не похож на постоянный ключ, который будет работать вечно (в большинстве случаев). +* Фронтенд временно хранит этот токен в каком-то месте. +* Пользователь щелкает мышью на фронтенде, чтобы перейти в другой раздел на фронтенде. +* Фронтенду необходимо получить дополнительные данные из API. + * Но для этого необходима аутентификация для конкретной конечной точки. + * Поэтому для аутентификации в нашем API он посылает заголовок `Authorization` со значением `Bearer` плюс сам токен. + * Если токен содержит `foobar`, то содержание заголовка `Authorization` будет таким: `Bearer foobar`. + +## Класс `OAuth2PasswordBearer` в **FastAPI** + +**FastAPI** предоставляет несколько средств на разных уровнях абстракции для реализации этих функций безопасности. + +В данном примере мы будем использовать **OAuth2**, с аутентификацией по паролю, используя токен **Bearer**. Для этого мы используем класс `OAuth2PasswordBearer`. + +!!! info "Дополнительная информация" + Токен "bearer" - не единственный вариант, но для нашего случая он является наилучшим. + + И это может быть лучшим вариантом для большинства случаев использования, если только вы не являетесь экспертом в области OAuth2 и точно знаете, почему вам лучше подходит какой-то другой вариант. + + В этом случае **FastAPI** также предоставляет инструменты для его реализации. + +При создании экземпляра класса `OAuth2PasswordBearer` мы передаем в него параметр `tokenUrl`. Этот параметр содержит URL, который клиент (фронтенд, работающий в браузере пользователя) будет использовать для отправки `имени пользователя` и `пароля` с целью получения токена. + +=== "Python 3.9+" + + ```Python hl_lines="8" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="6" + {!> ../../../docs_src/security/tutorial001.py!} + ``` + +!!! tip "Подсказка" + Здесь `tokenUrl="token"` ссылается на относительный URL `token`, который мы еще не создали. Поскольку это относительный URL, он эквивалентен `./token`. + + Поскольку мы используем относительный URL, если ваш API расположен по адресу `https://example.com/`, то он будет ссылаться на `https://example.com/token`. Если же ваш API расположен по адресу `https://example.com/api/v1/`, то он будет ссылаться на `https://example.com/api/v1/token`. + + Использование относительного URL важно для того, чтобы ваше приложение продолжало работать даже в таких сложных случаях, как оно находится [за прокси-сервером](../../advanced/behind-a-proxy.md){.internal-link target=_blank}. + +Этот параметр не создает конечную точку / *операцию пути*, а объявляет, что URL `/token` будет таким, который клиент должен использовать для получения токена. Эта информация используется в OpenAPI, а затем в интерактивных системах документации API. + +Вскоре мы создадим и саму операцию пути. + +!!! info "Дополнительная информация" + Если вы очень строгий "питонист", то вам может не понравиться стиль названия параметра `tokenUrl` вместо `token_url`. + + Это связано с тем, что тут используется то же имя, что и в спецификации OpenAPI. Таким образом, если вам необходимо более подробно изучить какую-либо из этих схем безопасности, вы можете просто использовать копирование/вставку, чтобы найти дополнительную информацию о ней. + +Переменная `oauth2_scheme` является экземпляром `OAuth2PasswordBearer`, но она также является "вызываемой". + +Ее можно вызвать следующим образом: + +```Python +oauth2_scheme(some, parameters) +``` + +Поэтому ее можно использовать вместе с `Depends`. + +### Использование + +Теперь вы можете передать ваш `oauth2_scheme` в зависимость с помощью `Depends`. + +=== "Python 3.9+" + + ```Python hl_lines="12" + {!> ../../../docs_src/security/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="11" + {!> ../../../docs_src/security/tutorial001_an.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="10" + {!> ../../../docs_src/security/tutorial001.py!} + ``` + +Эта зависимость будет предоставлять `строку`, которая присваивается параметру `token` в *функции операции пути*. + +**FastAPI** будет знать, что он может использовать эту зависимость для определения "схемы безопасности" в схеме OpenAPI (и автоматической документации по API). + +!!! info "Технические детали" + **FastAPI** будет знать, что он может использовать класс `OAuth2PasswordBearer` (объявленный в зависимости) для определения схемы безопасности в OpenAPI, поскольку он наследуется от `fastapi.security.oauth2.OAuth2`, который, в свою очередь, наследуется от `fastapi.security.base.SecurityBase`. + + Все утилиты безопасности, интегрируемые в OpenAPI (и автоматическая документация по API), наследуются от `SecurityBase`, поэтому **FastAPI** может знать, как интегрировать их в OpenAPI. + +## Что он делает + +Он будет искать в запросе заголовок `Authorization` и проверять, содержит ли он значение `Bearer` с некоторым токеном, и возвращать токен в виде `строки`. + +Если он не видит заголовка `Authorization` или значение не имеет токена `Bearer`, то в ответ будет выдана ошибка с кодом состояния 401 (`UNAUTHORIZED`). + +Для возврата ошибки даже не нужно проверять, существует ли токен. Вы можете быть уверены, что если ваша функция будет выполнилась, то в этом токене есть `строка`. + +Проверить это можно уже сейчас в интерактивной документации: + +<img src="/img/tutorial/security/image03.png"> + +Мы пока не проверяем валидность токена, но для начала неплохо. + +## Резюме + +Таким образом, всего за 3-4 дополнительные строки вы получаете некую примитивную форму защиты. From a56d32c3a463eb83173d01bb42eac46511e98952 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:01:38 +0000 Subject: [PATCH 1753/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 4ccedda1d..1a0f15d30 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#10541](https://github.com/tiangolo/fastapi/pull/10541) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/handling-errors.md`. PR [#10375](https://github.com/tiangolo/fastapi/pull/10375) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/encoder.md`. PR [#10374](https://github.com/tiangolo/fastapi/pull/10374) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/body-updates.md`. PR [#10373](https://github.com/tiangolo/fastapi/pull/10373) by [@AlertRED](https://github.com/AlertRED). From 0ec0df50906a2fcb30e6c96c7206b185e61a3de4 Mon Sep 17 00:00:00 2001 From: DoHyun Kim <tnghwk0661@gmail.com> Date: Tue, 23 Jan 2024 23:02:49 +0900 Subject: [PATCH 1754/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/security/get-current-user.m?= =?UTF-8?q?d`=20(#5737)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial/security/get-current-user.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 docs/ko/docs/tutorial/security/get-current-user.md diff --git a/docs/ko/docs/tutorial/security/get-current-user.md b/docs/ko/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..ce944b16d --- /dev/null +++ b/docs/ko/docs/tutorial/security/get-current-user.md @@ -0,0 +1,151 @@ +# 현재 사용자 가져오기 + +이전 장에서 (의존성 주입 시스템을 기반으로 한)보안 시스템은 *경로 작동 함수*에서 `str`로 `token`을 제공했습니다: + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +그러나 아직도 유용하지 않습니다. + +현재 사용자를 제공하도록 합시다. + +## 유저 모델 생성하기 + +먼저 Pydantic 유저 모델을 만들어 보겠습니다. + +Pydantic을 사용하여 본문을 선언하는 것과 같은 방식으로 다른 곳에서 사용할 수 있습니다. + +=== "파이썬 3.7 이상" + + ```Python hl_lines="5 12-16" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="3 10-14" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## `get_current_user` 의존성 생성하기 + +의존성 `get_current_user`를 만들어 봅시다. + +의존성이 하위 의존성을 가질 수 있다는 것을 기억하십니까? + +`get_current_user`는 이전에 생성한 것과 동일한 `oauth2_scheme`과 종속성을 갖게 됩니다. + +이전에 *경로 동작*에서 직접 수행했던 것과 동일하게 새 종속성 `get_current_user`는 하위 종속성 `oauth2_scheme`에서 `str`로 `token`을 수신합니다. + +=== "파이썬 3.7 이상" + + ```Python hl_lines="25" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="23" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 유저 가져오기 + +`get_current_user`는 토큰을 `str`로 취하고 Pydantic `User` 모델을 반환하는 우리가 만든 (가짜) 유틸리티 함수를 사용합니다. + +=== "파이썬 3.7 이상" + + ```Python hl_lines="19-22 26-27" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="17-20 24-25" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 현재 유저 주입하기 + +이제 *경로 작동*에서 `get_current_user`와 동일한 `Depends`를 사용할 수 있습니다. + +=== "파이썬 3.7 이상" + + ```Python hl_lines="31" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="29" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +Pydantic 모델인 `User`로 `current_user`의 타입을 선언하는 것을 알아야 합니다. + +이것은 모든 완료 및 타입 검사를 통해 함수 내부에서 우리를 도울 것입니다. + +!!! 팁 + 요청 본문도 Pydantic 모델로 선언된다는 것을 기억할 것입니다. + + 여기서 **FastAPI**는 `Depends`를 사용하고 있기 때문에 혼동되지 않습니다. + +!!! 확인 + 이 의존성 시스템이 설계된 방식은 모두 `User` 모델을 반환하는 다양한 의존성(다른 "의존적인")을 가질 수 있도록 합니다. + + 해당 타입의 데이터를 반환할 수 있는 의존성이 하나만 있는 것으로 제한되지 않습니다. + +## 다른 모델 + +이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있으며 `Depends`를 사용하여 **의존성 주입** 수준에서 보안 메커니즘을 처리할 수 있습니다. + +그리고 보안 요구 사항에 대한 모든 모델 또는 데이터를 사용할 수 있습니다(이 경우 Pydantic 모델 `User`). + +그러나 일부 특정 데이터 모델, 클래스 또는 타입을 사용하도록 제한되지 않습니다. + +모델에 `id`와 `email`이 있고 `username`이 없길 원하십니까? 맞습니다. 이들은 동일한 도구를 사용할 수 있습니다. + +`str`만 갖고 싶습니까? 아니면 그냥 `dict`를 갖고 싶습니까? 아니면 데이터베이스 클래스 모델 인스턴스를 직접 갖고 싶습니까? 그들은 모두 같은 방식으로 작동합니다. + +실제로 애플리케이션에 로그인하는 사용자가 없지만 액세스 토큰만 있는 로봇, 봇 또는 기타 시스템이 있습니까? 다시 말하지만 모두 동일하게 작동합니다. + +애플리케이션에 필요한 모든 종류의 모델, 모든 종류의 클래스, 모든 종류의 데이터베이스를 사용하십시오. **FastAPI**는 의존성 주입 시스템을 다루었습니다. + +## 코드 사이즈 + +이 예는 장황해 보일 수 있습니다. 동일한 파일에서 보안, 데이터 모델, 유틸리티 기능 및 *경로 작동*을 혼합하고 있음을 염두에 두십시오. + +그러나 이게 키포인트입니다. + +보안과 종속성 주입 항목을 한 번만 작성하면 됩니다. + +그리고 원하는 만큼 복잡하게 만들 수 있습니다. 그래도 유연성과 함께 한 곳에 한 번에 작성할 수 있습니다. + +그러나 동일한 보안 시스템을 사용하여 수천 개의 엔드포인트(*경로 작동*)를 가질 수 있습니다. + +그리고 그들 모두(또는 원하는 부분)는 이러한 의존성 또는 생성한 다른 의존성을 재사용하는 이점을 얻을 수 있습니다. + +그리고 이 수천 개의 *경로 작동*은 모두 3줄 정도로 줄일 수 있습니다. + +=== "파이썬 3.7 이상" + + ```Python hl_lines="30-32" + {!> ../../../docs_src/security/tutorial002.py!} + ``` + +=== "파이썬 3.10 이상" + + ```Python hl_lines="28-30" + {!> ../../../docs_src/security/tutorial002_py310.py!} + ``` + +## 요약 + +이제 *경로 작동 함수*에서 현재 사용자를 직접 가져올 수 있습니다. + +우리는 이미 이들 사이에 있습니다. + +사용자/클라이언트가 실제로 `username`과 `password`를 보내려면 *경로 작동*을 추가하기만 하면 됩니다. + +다음 장을 확인해 봅시다. From 058044fdb148d6af6a90fbd13001c6423209569e Mon Sep 17 00:00:00 2001 From: Kani Kim <kkh5428@gmail.com> Date: Tue, 23 Jan 2024 23:04:27 +0900 Subject: [PATCH 1755/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/features.md`=20(#10976)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/features.md | 203 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 docs/ko/docs/features.md diff --git a/docs/ko/docs/features.md b/docs/ko/docs/features.md new file mode 100644 index 000000000..42a3ff172 --- /dev/null +++ b/docs/ko/docs/features.md @@ -0,0 +1,203 @@ +--- +hide: + - navigation +--- + +# 기능 + +## FastAPI의 기능 + +**FastAPI**는 다음과 같은 기능을 제공합니다: + +### 개방형 표준을 기반으로 + +* <abbr title="엔드포인트, 라우트로도 알려져 있습니다">경로</abbr><abbr title="POST, GET, PUT, DELETE와 같은 HTTP 메소드로 알려져 있습니다">작동</abbr>, 매개변수, 본문 요청, 보안 그 외의 선언을 포함한 API 생성을 위한 <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank"><strong>OpenAPI</strong></a> +* <a href="https://json-schema.org/" class="external-link" target="_blank"><strong>JSON Schema</strong></a> (OpenAPI 자체가 JSON Schema를 기반으로 하고 있습니다)를 사용한 자동 데이터 모델 문서화. +* 단순히 떠올려서 덧붙인 기능이 아닙니다. 세심한 검토를 거친 후, 이러한 표준을 기반으로 설계되었습니다. +* 이는 또한 다양한 언어로 자동적인 **클라이언트 코드 생성**을 사용할 수 있게 지원합니다. + +### 문서 자동화 + +대화형 API 문서와 웹 탐색 유저 인터페이스를 제공합니다. 프레임워크가 OpenAPI를 기반으로 하기에, 2가지 옵션이 기본적으로 들어간 여러 옵션이 존재합니다. + +* 대화형 탐색 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank"><strong>Swagger UI</strong></a>를 이용해, 브라우저에서 바로 여러분의 API를 호출하거나 테스트할 수 있습니다. + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank"><strong>ReDoc</strong></a>을 이용해 API 문서화를 대체할 수 있습니다. + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### 그저 현대 파이썬 + +(Pydantic 덕분에) FastAPI는 표준 **파이썬 3.6 타입** 선언에 기반하고 있습니다. 새로 배울 문법이 없습니다. 그저 표준적인 현대 파이썬입니다. + +만약 여러분이 파이썬 타입을 어떻게 사용하는지에 대한 2분 정도의 복습이 필요하다면 (비록 여러분이 FastAPI를 사용하지 않는다 하더라도), 다음의 짧은 자습서를 확인하세요: [파이썬 타입](python-types.md){.internal-link target=\_blank}. + +여러분은 타입을 이용한 표준 파이썬을 다음과 같이 적을 수 있습니다: + +```Python +from datetime import date + +from pydantic import BaseModel + +# 변수를 str로 선언 +# 그 후 함수 안에서 편집기 지원을 받으세요 +def main(user_id: str): + return user_id + + +# Pydantic 모델 +class User(BaseModel): + id: int + name: str + joined: date +``` + +위의 코드는 다음과 같이 사용될 수 있습니다: + +```Python +my_user: User = User(id=3, name="John Doe", joined="2018-07-19") + +second_user_data = { + "id": 4, + "name": "Mary", + "joined": "2018-11-30", +} + +my_second_user: User = User(**second_user_data) +``` + +!!! 정보 + `**second_user_data`가 뜻하는 것: + + `second_user_data` 딕셔너리의 키와 값을 키-값 인자로서 바로 넘겨줍니다. 다음과 동일합니다: `User(id=4, name="Mary", joined="2018-11-30")` + +### 편집기 지원 + +모든 프레임워크는 사용하기 쉽고 직관적으로 설계되었으며, 좋은 개발 경험을 보장하기 위해 개발을 시작하기도 전에 모든 결정들은 여러 편집기에서 테스트됩니다. + +최근 파이썬 개발자 설문조사에서 <a href="https://www.jetbrains.com/research/python-developers-survey-2017/#tools-and-features" class="external-link" target="_blank">"자동 완성"이 가장 많이 사용되는 기능</a>이라는 것이 밝혀졌습니다. + +**FastAPI** 프레임워크의 모든 부분은 이를 충족하기 위해 설계되었습니다. 자동완성은 어느 곳에서나 작동합니다. + +여러분은 문서로 다시 돌아올 일이 거의 없을 겁니다. + +다음은 편집기가 어떻게 여러분을 도와주는지 보여줍니다: + +* <a href="https://code.visualstudio.com/" class="external-link" target="_blank">Visual Studio Code</a>에서: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +* <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>에서: + +![editor support](https://fastapi.tiangolo.com/img/pycharm-completion.png) + +여러분이 이전에 불가능하다고 고려했던 코드도 완성할 수 있을 겁니다. 예를 들어, 요청에서 전달되는 (중첩될 수도 있는)JSON 본문 내부에 있는 `price` 키입니다. + +잘못된 키 이름을 적을 일도, 문서를 왔다 갔다할 일도 없으며, 혹은 마지막으로 `username` 또는 `user_name`을 사용했는지 찾기 위해 위 아래로 스크롤할 일도 없습니다. + +### 토막 정보 + +어느 곳에서나 선택적 구성이 가능한 모든 것에 합리적인 기본값이 설정되어 있습니다. 모든 매개변수는 여러분이 필요하거나, 원하는 API를 정의하기 위해 미세하게 조정할 수 있습니다. + +하지만 기본적으로 모든 것이 "그냥 작동합니다". + +### 검증 + +* 다음을 포함한, 대부분의 (혹은 모든?) 파이썬 **데이터 타입** 검증할 수 있습니다: + * JSON 객체 (`dict`). + * 아이템 타입을 정의하는 JSON 배열 (`list`). + * 최소 길이와 최대 길이를 정의하는 문자열 (`str`) 필드. + * 최솟값과 최댓값을 가지는 숫자 (`int`, `float`), 그 외. + +* 다음과 같이 더욱 이색적인 타입에 대해 검증할 수 있습니다: + * URL. + * 이메일. + * UUID. + * ...다른 것들. + +모든 검증은 견고하면서 잘 확립된 **Pydantic**에 의해 처리됩니다. + +### 보안과 인증 + +보안과 인증이 통합되어 있습니다. 데이터베이스나 데이터 모델과의 타협없이 사용할 수 있습니다. + +다음을 포함하는, 모든 보안 스키마가 OpenAPI에 정의되어 있습니다. + +* HTTP Basic. +* **OAuth2** (**JWT tokens** 또한 포함). [OAuth2 with JWT](tutorial/security/oauth2-jwt.md){.internal-link target=\_blank}에 있는 자습서를 확인해 보세요. +* 다음에 들어 있는 API 키: + * 헤더. + * 매개변수. + * 쿠키 및 그 외. + +추가적으로 (**세션 쿠키**를 포함한) 모든 보안 기능은 Starlette에 있습니다. + +모두 재사용할 수 있는 도구와 컴포넌트로 만들어져 있어 여러분의 시스템, 데이터 저장소, 관계형 및 NoSQL 데이터베이스 등과 쉽게 통합할 수 있습니다. + +### 의존성 주입 + +FastAPI는 사용하기 매우 간편하지만, 엄청난 <abbr title='"컴포넌트", "자원", "서비스", "제공자"로도 알려진'><strong>의존성 주입</strong></abbr>시스템을 포함하고 있습니다. + +* 의존성은 의존성을 가질수도 있어, 이를 통해 의존성의 계층이나 **의존성의 "그래프"**를 형성합니다. +* 모든 것이 프레임워크에 의해 **자동적으로 처리됩니다**. +* 모든 의존성은 요청에서 데이터를 요구하여 자동 문서화와 **경로 작동 제약을 강화할 수 있습니다**. +* 의존성에서 정의된 _경로 작동_ 매개변수에 대해서도 **자동 검증**이 이루어 집니다. +* 복잡한 사용자의 인증 시스템, **데이터베이스 연결**, 등등을 지원합니다. +* 데이터베이스, 프론트엔드 등과 관련되어 **타협하지 않아도 됩니다**. 하지만 그 모든 것과 쉽게 통합이 가능합니다. + +### 제한 없는 "플러그인" + +또는 다른 방법으로, 그것들을 사용할 필요 없이 필요한 코드만 임포트할 수 있습니다. + +어느 통합도 (의존성과 함께) 사용하기 쉽게 설계되어 있어, *경로 작동*에 사용된 것과 동일한 구조와 문법을 사용하여 2줄의 코드로 여러분의 어플리케이션에 사용할 "플러그인"을 만들 수 있습니다. + +### 테스트 결과 + +* 100% <abbr title="자동적으로 테스트된 코드의 양">테스트 범위</abbr>. +* 100% <abbr title="파이썬의 타입 어노테이션, 이를 통해 여러분의 편집기와 외부 도구는 여러분에게 더 나은 지원을 할 수 있습니다">타입이 명시된</abbr> 코드 베이스. +* 상용 어플리케이션에서의 사용. + +## Starlette 기능 + +**FastAPI**는 <a href="https://www.starlette.io/" class="external-link" target="_blank"><strong>Starlette</strong></a>를 기반으로 구축되었으며, 이와 완전히 호환됩니다. 따라서, 여러분이 보유하고 있는 어떤 추가적인 Starlette 코드도 작동할 것입니다. + +`FastAPI`는 실제로 `Starlette`의 하위 클래스입니다. 그래서, 여러분이 이미 Starlette을 알고 있거나 사용하고 있으면, 대부분의 기능이 같은 방식으로 작동할 것입니다. + +**FastAPI**를 사용하면 여러분은 **Starlette**의 기능 대부분을 얻게 될 것입니다(FastAPI가 단순히 Starlette를 강화했기 때문입니다): + +* 아주 인상적인 성능. 이는 <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank">**NodeJS**와 **Go**와 동등하게 사용 가능한 가장 빠른 파이썬 프레임워크 중 하나입니다</a>. +* **WebSocket** 지원. +* 프로세스 내의 백그라운드 작업. +* 시작과 종료 이벤트. +* HTTPX 기반 테스트 클라이언트. +* **CORS**, GZip, 정적 파일, 스트리밍 응답. +* **세션과 쿠키** 지원. +* 100% 테스트 범위. +* 100% 타입이 명시된 코드 베이스. + +## Pydantic 기능 + +**FastAPI**는 <a href="https://pydantic-docs.helpmanual.io" class="external-link" target="_blank"><strong>Pydantic</strong></a>을 기반으로 하며 Pydantic과 완벽하게 호환됩니다. 그래서 어느 추가적인 Pydantic 코드를 여러분이 가지고 있든 작동할 것입니다. + +Pydantic을 기반으로 하는, 데이터베이스를 위한 <abbr title="Object-Relational Mapper">ORM</abbr>, <abbr title="Object-Document Mapper">ODM</abbr>을 포함한 외부 라이브러리를 포함합니다. + +이는 모든 것이 자동으로 검증되기 때문에, 많은 경우에서 요청을 통해 얻은 동일한 객체를, **직접 데이터베이스로** 넘겨줄 수 있습니다. + +반대로도 마찬가지이며, 많은 경우에서 여러분은 **직접 클라이언트로** 그저 객체를 넘겨줄 수 있습니다. + +**FastAPI**를 사용하면 (모든 데이터 처리를 위해 FastAPI가 Pydantic을 기반으로 하기 있기에) **Pydantic**의 모든 기능을 얻게 됩니다: + +* **어렵지 않은 언어**: + * 새로운 스키마 정의 마이크로 언어를 배우지 않아도 됩니다. + * 여러분이 파이썬 타입을 안다면, 여러분은 Pydantic을 어떻게 사용하는지 아는 겁니다. +* 여러분의 **<abbr title="통합 개발 환경, 코드 편집기와 비슷합니다">IDE</abbr>/<abbr title="코드 에러를 확인하는 프로그램">린터</abbr>/뇌**와 잘 어울립니다: + * Pydantic 데이터 구조는 단순 여러분이 정의한 클래스의 인스턴스이기 때문에, 자동 완성, 린팅, mypy 그리고 여러분의 직관까지 여러분의 검증된 데이터와 올바르게 작동합니다. +* **복잡한 구조**를 검증합니다: + * 계층적인 Pydantic 모델, 파이썬 `typing`의 `List`와 `Dict`, 그 외를 사용합니다. + * 그리고 검증자는 복잡한 데이터 스키마를 명확하고 쉽게 정의 및 확인하며 JSON 스키마로 문서화합니다. + * 여러분은 깊게 **중첩된 JSON** 객체를 가질 수 있으며, 이 객체 모두 검증하고 설명을 붙일 수 있습니다. +* **확장 가능성**: + * Pydantic은 사용자 정의 데이터 타입을 정의할 수 있게 하거나, 검증자 데코레이터가 붙은 모델의 메소드를 사용하여 검증을 확장할 수 있습니다. +* 100% 테스트 범위. From 3351674918894ac32cf041972b76e841c77efb1a Mon Sep 17 00:00:00 2001 From: Kani Kim <kkh5428@gmail.com> Date: Tue, 23 Jan 2024 23:05:09 +0900 Subject: [PATCH 1756/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/help/index.md`=20(#10983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/help/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/ko/docs/help/index.md diff --git a/docs/ko/docs/help/index.md b/docs/ko/docs/help/index.md new file mode 100644 index 000000000..fc023071a --- /dev/null +++ b/docs/ko/docs/help/index.md @@ -0,0 +1,3 @@ +# 도움 + +도움을 주고 받고, 기여하고, 참여합니다. 🤝 From aa3ed353b3219b32095eeeceb77dfc5d4fd7c582 Mon Sep 17 00:00:00 2001 From: Matteo <spartacus990@gmail.com> Date: Tue, 23 Jan 2024 15:06:33 +0100 Subject: [PATCH 1757/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Italian=20transl?= =?UTF-8?q?ation=20for=20`docs/it/docs/index.md`=20(#5233)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/it/docs/index.md | 464 ++++++++++++++++++++++++++++++++++++++++++ docs/it/mkdocs.yml | 1 + 2 files changed, 465 insertions(+) create mode 100644 docs/it/docs/index.md create mode 100644 docs/it/mkdocs.yml diff --git a/docs/it/docs/index.md b/docs/it/docs/index.md new file mode 100644 index 000000000..6190eb6aa --- /dev/null +++ b/docs/it/docs/index.md @@ -0,0 +1,464 @@ + +{!../../../docs/missing-translation.md!} + + +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>FastAPI framework, alte prestazioni, facile da imparare, rapido da implementare, pronto per il rilascio in produzione</em> +</p> +<p align="center"> +<a href="https://travis-ci.com/tiangolo/fastapi" target="_blank"> + <img src="https://travis-ci.com/tiangolo/fastapi.svg?branch=master" alt="Build Status"> +</a> +<a href="https://codecov.io/gh/tiangolo/fastapi" target="_blank"> + <img src="https://img.shields.io/codecov/c/github/tiangolo/fastapi" alt="Coverage"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://badge.fury.io/py/fastapi.svg" alt="Package version"> +</a> +</p> + +--- + +**Documentazione**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**Codice Sorgente**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- + +FastAPI è un web framework moderno e veloce (a prestazioni elevate) che serve a creare API con Python 3.6+ basato sulle annotazioni di tipo di Python. + +Le sue caratteristiche principali sono: + +* **Velocità**: Prestazioni molto elevate, alla pari di **NodeJS** e **Go** (grazie a Starlette e Pydantic). [Uno dei framework Python più veloci in circolazione](#performance). +* **Veloce da programmare**: Velocizza il lavoro consentendo il rilascio di nuove funzionalità tra il 200% e il 300% più rapidamente. * +* **Meno bug**: Riduce di circa il 40% gli errori che commettono gli sviluppatori durante la scrittura del codice. * +* **Intuitivo**: Grande supporto per gli editor di testo con <abbr title="anche conosciuto come auto-completamento, autocompletion, IntelliSense">autocompletamento</abbr> in ogni dove. In questo modo si può dedicare meno tempo al debugging. +* **Facile**: Progettato per essere facile da usare e imparare. Si riduce il tempo da dedicare alla lettura della documentazione. +* **Sintentico**: Minimizza la duplicazione di codice. Molteplici funzionalità, ognuna con la propria dichiarazione dei parametri. Meno errori. +* **Robusto**: Crea codice pronto per la produzione con documentazione automatica interattiva. +* **Basato sugli standard**: Basato su (e completamente compatibile con) gli open standard per le API: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (precedentemente Swagger) e <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. + +<small>* Stima basata sull'esito di test eseguiti su codice sorgente di applicazioni rilasciate in produzione da un team interno di sviluppatori.</small> + +## Sponsor + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%} +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/fastapi-people/#sponsors" class="external-link" target="_blank">Altri sponsor</a> + +## Recensioni + +"_[...] I'm using **FastAPI** a ton these days. [...] I'm actually planning to use it for all of my team's **ML services at Microsoft**. Some of them are getting integrated into the core **Windows** product and some **Office** products._" + +<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_We adopted the **FastAPI** library to spawn a **REST** server that can be queried to obtain **predictions**. [for Ludwig]_" + +<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, e Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** is pleased to announce the open-source release of our **crisis management** orchestration framework: **Dispatch**! [built with **FastAPI**]_" + +<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_I’m over the moon excited about **FastAPI**. It’s so fun!_" + +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Honestly, what you've built looks super solid and polished. In many ways, it's what I wanted **Hug** to be - it's really inspiring to see someone build that._" + +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"_If you're looking to learn one **modern framework** for building REST APIs, check out **FastAPI** [...] It's fast, easy to use and easy to learn [...]_" + +"_We've switched over to **FastAPI** for our **APIs** [...] I think you'll like it [...]_" + +<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**, la FastAPI delle CLI + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +Se stai sviluppando un'app <abbr title="Command Line Interface (interfaccia della riga di comando)">CLI</abbr> da usare nel terminale invece che una web API, ti consigliamo <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>. + +**Typer** è il fratello minore di FastAPI. Ed è stato ideato per essere la **FastAPI delle CLI**. ⌨️ 🚀 + +## Requisiti + +Python 3.6+ + +FastAPI è basata su importanti librerie: + +* <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> per le parti web. +* <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> per le parti dei dati. + +## Installazione + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +Per il rilascio in produzione, sarà necessario un server ASGI come <a href="http://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> oppure <a href="https://gitlab.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a>. + +<div class="termy"> + +```console +$ pip install uvicorn[standard] + +---> 100% +``` + +</div> + +## Esempio + +### Crea un file + +* Crea un file `main.py` con: + +```Python +from fastapi import FastAPI +from typing import Optional + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: str = Optional[None]): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>Oppure usa <code>async def</code>...</summary> + +Se il tuo codice usa `async` / `await`, allora usa `async def`: + +```Python hl_lines="7 12" +from fastapi import FastAPI +from typing import Optional + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Optional[str] = None): + return {"item_id": item_id, "q": q} +``` + +**Nota**: + +e vuoi approfondire, consulta la sezione _"In a hurry?"_ su <a href="https://fastapi.tiangolo.com/async/#in-a-hurry" target="_blank">`async` e `await` nella documentazione</a>. + +</details> + +### Esegui il server + +Puoi far partire il server così: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary>Informazioni sul comando <code>uvicorn main:app --reload</code>...</summary> + +Vediamo il comando `uvicorn main:app` in dettaglio: + +* `main`: il file `main.py` (il "modulo" Python). +* `app`: l'oggetto creato dentro `main.py` con la riga di codice `app = FastAPI()`. +* `--reload`: ricarica il server se vengono rilevati cambiamenti del codice. Usalo solo durante la fase di sviluppo. + +</details> + +### Testa l'API + +Apri il browser all'indirizzo <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. + +Vedrai la seguente risposta JSON: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Hai appena creato un'API che: + +* Riceve richieste HTTP sui _paths_ `/` and `/items/{item_id}`. +* Entrambi i _paths_ accettano`GET` <em>operations</em> (conosciuti anche come <abbr title="metodi HTTP">HTTP _methods_</abbr>). +* Il _path_ `/items/{item_id}` ha un _path parameter_ `item_id` che deve essere un `int`. +* Il _path_ `/items/{item_id}` ha una `str` _query parameter_ `q`. + +### Documentazione interattiva dell'API + +Adesso vai all'indirizzo <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +Vedrai la documentazione interattiva dell'API (offerta da <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a>): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Documentazione interattiva alternativa + +Adesso accedi all'url <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. + +Vedrai la documentazione interattiva dell'API (offerta da <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a>): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Esempio più avanzato + +Adesso modifica il file `main.py` per ricevere un _body_ da una richiesta `PUT`. + +Dichiara il _body_ usando le annotazioni di tipo standard di Python, grazie a Pydantic. + +```Python hl_lines="2 7-10 23-25" +from fastapi import FastAPI +from pydantic import BaseModel +from typing import Optional + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: bool = Optional[None] + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Optional[str] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` + +Il server dovrebbe ricaricarsi in automatico (perché hai specificato `--reload` al comando `uvicorn` lanciato precedentemente). + +### Aggiornamento della documentazione interattiva + +Adesso vai su <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>. + +* La documentazione interattiva dell'API verrà automaticamente aggiornata, includendo il nuovo _body_: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* Fai click sul pulsante "Try it out", che ti permette di inserire i parametri per interagire direttamente con l'API: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Successivamente, premi sul pulsante "Execute". L'interfaccia utente comunicherà con la tua API, invierà i parametri, riceverà i risultati della richiesta, e li mostrerà sullo schermo: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Aggiornamento della documentazione alternativa + +Ora vai su <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a>. + +* Anche la documentazione alternativa dell'API mostrerà il nuovo parametro della query e il _body_: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Riepilogo + +Ricapitolando, è sufficiente dichiarare **una sola volta** i tipi dei parametri, del body, ecc. come parametri di funzioni. + +Questo con le annotazioni per i tipi standard di Python. + +Non c'è bisogno di imparare una nuova sintassi, metodi o classi specifici a una libreria, ecc. + +È normalissimo **Python 3.6+**. + +Per esempio, per un `int`: + +```Python +item_id: int +``` + +o per un modello `Item` più complesso: + +```Python +item: Item +``` + +...e con quella singola dichiarazione hai in cambio: + +* Supporto per gli editor di testo, incluso: + * Autocompletamento. + * Controllo sulle annotazioni di tipo. +* Validazione dei dati: + * Errori chiari e automatici quando i dati sono invalidi. + * Validazione anche per gli oggetti JSON più complessi. +* <abbr title="anche noto come: serializzazione, parsing, marshalling">Conversione</abbr> dei dati di input: da risorse esterne a dati e tipi di Python. È possibile leggere da: + * JSON. + * Path parameters. + * Query parameters. + * Cookies. + * Headers. + * Form. + * File. +* <abbr title="detta anche: serialization, parsing, marshalling">Conversione</abbr> dei dati di output: converte dati e tipi di Python a dati per la rete (come JSON): + * Converte i tipi di Python (`str`, `int`, `float`, `bool`, `list`, ecc). + * Oggetti `datetime`. + * Oggetti `UUID`. + * Modelli del database. + * ...e molto di più. +* Generazione di una documentazione dell'API interattiva, con scelta dell'interfaccia grafica: + * Swagger UI. + * ReDoc. + +--- + +Tornando al precedente esempio, **FastAPI**: + +* Validerà che esiste un `item_id` nel percorso delle richieste `GET` e `PUT`. +* Validerà che `item_id` sia di tipo `int` per le richieste `GET` e `PUT`. + * Se non lo è, il client vedrà un errore chiaro e utile. +* Controllerà se ci sia un parametro opzionale chiamato `q` (per esempio `http://127.0.0.1:8000/items/foo?q=somequery`) per le richieste `GET`. + * Siccome il parametro `q` è dichiarato con `= None`, è opzionale. + * Senza il `None` sarebbe stato obbligatorio (come per il body della richiesta `PUT`). +* Per le richieste `PUT` su `/items/{item_id}`, leggerà il body come JSON, questo comprende: + * verifica che la richiesta abbia un attributo obbligatorio `name` e che sia di tipo `str`. + * verifica che la richiesta abbia un attributo obbligatorio `price` e che sia di tipo `float`. + * verifica che la richiesta abbia un attributo opzionale `is_offer` e che sia di tipo `bool`, se presente. + * Tutto questo funzionerebbe anche con oggetti JSON più complessi. +* Convertirà *da* e *a* JSON automaticamente. +* Documenterà tutto con OpenAPI, che può essere usato per: + * Sistemi di documentazione interattivi. + * Sistemi di generazione di codice dal lato client, per molti linguaggi. +* Fornirà 2 interfacce di documentazione dell'API interattive. + +--- + +Questa è solo la punta dell'iceberg, ma dovresti avere già un'idea di come il tutto funzioni. + +Prova a cambiare questa riga di codice: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...da: + +```Python + ... "item_name": item.name ... +``` + +...a: + +```Python + ... "item_price": item.price ... +``` + +...e osserva come il tuo editor di testo autocompleterà gli attributi e sarà in grado di riconoscere i loro tipi: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Per un esempio più completo che mostra più funzionalità del framework, consulta <a href="https://fastapi.tiangolo.com/tutorial/">Tutorial - Guida Utente</a>. + +**Spoiler alert**: il tutorial - Guida Utente include: + +* Dichiarazione di **parameters** da altri posti diversi come: **headers**, **cookies**, **form fields** e **files**. +* Come stabilire **vincoli di validazione** come `maximum_length` o `regex`. +* Un sistema di **<abbr title="detto anche components, resources, providers, services, injectables">Dependency Injection</abbr>** facile da usare e molto potente. +e potente. +* Sicurezza e autenticazione, incluso il supporto per **OAuth2** con **token JWT** e autenticazione **HTTP Basic**. +* Tecniche più avanzate (ma ugualmente semplici) per dichiarare **modelli JSON altamente nidificati** (grazie a Pydantic). +* E altre funzionalità (grazie a Starlette) come: + * **WebSockets** + * **GraphQL** + * test molto facili basati su `requests` e `pytest` + * **CORS** + * **Cookie Sessions** + * ...e altro ancora. + +## Prestazioni + +Benchmark indipendenti di TechEmpower mostrano che **FastAPI** basato su Uvicorn è <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">uno dei framework Python più veloci in circolazione</a>, solamente dietro a Starlette e Uvicorn (usate internamente da FastAPI). (*) + +Per approfondire, consulta la sezione <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">Benchmarks</a>. + +## Dipendenze opzionali + +Usate da Pydantic: + +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - per un <abbr title="convertire la stringa che proviene da una richiesta HTTP in dati Python">"parsing"</abbr> di JSON più veloce. +* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - per la validazione di email. + +Usate da Starlette: + +* <a href="http://docs.python-requests.org" target="_blank"><code>requests</code></a> - Richiesto se vuoi usare il `TestClient`. +* <a href="https://github.com/Tinche/aiofiles" target="_blank"><code>aiofiles</code></a> - Richiesto se vuoi usare `FileResponse` o `StaticFiles`. +* <a href="http://jinja.pocoo.org" target="_blank"><code>jinja2</code></a> - Richiesto se vuoi usare la configurazione template di default. +* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - Richiesto se vuoi supportare il <abbr title="convertire la stringa che proviene da una richiesta HTTP in dati Python">"parsing"</abbr> con `request.form()`. +* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - Richiesto per usare `SessionMiddleware`. +* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - Richiesto per il supporto dello `SchemaGenerator` di Starlette (probabilmente non ti serve con FastAPI). +* <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - Richiesto per il supporto di `GraphQLApp`. +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - Richiesto se vuoi usare `UJSONResponse`. + +Usate da FastAPI / Starlette: + +* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - per il server che carica e serve la tua applicazione. +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - ichiesto se vuoi usare `ORJSONResponse`. + +Puoi installarle tutte con `pip install fastapi[all]`. + +## Licenza + +Questo progetto è concesso in licenza in base ai termini della licenza MIT. diff --git a/docs/it/mkdocs.yml b/docs/it/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/it/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml From f021ccb9058429641aa6400db83eb37717d5827f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:09:28 +0000 Subject: [PATCH 1758/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1a0f15d30..66faea43b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/security/get-current-user.md`. PR [#5737](https://github.com/tiangolo/fastapi/pull/5737) by [@KdHyeon0661](https://github.com/KdHyeon0661). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#10541](https://github.com/tiangolo/fastapi/pull/10541) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/handling-errors.md`. PR [#10375](https://github.com/tiangolo/fastapi/pull/10375) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/encoder.md`. PR [#10374](https://github.com/tiangolo/fastapi/pull/10374) by [@AlertRED](https://github.com/AlertRED). From 6d46b60cb3a84a1746e7aec36cf22bec5f495860 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:09:56 +0000 Subject: [PATCH 1759/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 66faea43b..94e1b062e 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/features.md`. PR [#10976](https://github.com/tiangolo/fastapi/pull/10976) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/security/get-current-user.md`. PR [#5737](https://github.com/tiangolo/fastapi/pull/5737) by [@KdHyeon0661](https://github.com/KdHyeon0661). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#10541](https://github.com/tiangolo/fastapi/pull/10541) by [@AlertRED](https://github.com/AlertRED). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/handling-errors.md`. PR [#10375](https://github.com/tiangolo/fastapi/pull/10375) by [@AlertRED](https://github.com/AlertRED). From 189f679f9babaa1c31e6661d861f82601fc98173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:10:30 +0300 Subject: [PATCH 1760/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Turkish=20tra?= =?UTF-8?q?nslation=20for=20`docs/tr/docs/benchmarks.md`=20(#11005)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/benchmarks.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/tr/docs/benchmarks.md b/docs/tr/docs/benchmarks.md index 1ce3c758f..eb5472869 100644 --- a/docs/tr/docs/benchmarks.md +++ b/docs/tr/docs/benchmarks.md @@ -1,34 +1,34 @@ # Kıyaslamalar -Bağımsız TechEmpower kıyaslamaları gösteriyor ki Uvicorn'la beraber çalışan **FastAPI** uygulamaları <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">Python'un en hızlı frameworklerinden birisi </a>, sadece Starlette ve Uvicorn'dan daha düşük sıralamada (FastAPI bu frameworklerin üzerine kurulu). (*) +Bağımsız TechEmpower kıyaslamaları gösteriyor ki <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">en hızlı Python frameworklerinden birisi</a> olan Uvicorn ile çalıştırılan **FastAPI** uygulamaları, sadece Starlette ve Uvicorn'dan daha düşük sıralamada (FastAPI bu frameworklerin üzerine kurulu) yer alıyor. (*) Fakat kıyaslamaları ve karşılaştırmaları incelerken şunları aklınızda bulundurmalısınız. -## Kıyaslamalar ve hız +## Kıyaslamalar ve Hız -Kıyaslamaları incelediğinizde, farklı özelliklere sahip birçok araçların eşdeğer olarak karşılaştırıldığını görmek yaygındır. +Kıyaslamaları incelediğinizde, farklı özelliklere sahip araçların eşdeğer olarak karşılaştırıldığını yaygın bir şekilde görebilirsiniz. -Özellikle, Uvicorn, Starlette ve FastAPI'ın birlikte karşılaştırıldığını görmek için (diğer birçok araç arasında). +Özellikle, (diğer birçok araç arasında) Uvicorn, Starlette ve FastAPI'ın birlikte karşılaştırıldığını görebilirsiniz. -Araç tarafından çözülen sorun ne kadar basitse, o kadar iyi performans alacaktır. Ve kıyaslamaların çoğu, araç tarafından sağlanan ek özellikleri test etmez. +Aracın çözdüğü problem ne kadar basitse, performansı o kadar iyi olacaktır. Ancak kıyaslamaların çoğu, aracın sağladığı ek özellikleri test etmez. Hiyerarşi şöyledir: * **Uvicorn**: bir ASGI sunucusu - * **Starlette**: (Uvicorn'u kullanır) bir web microframeworkü - * **FastAPI**: (Starlette'i kullanır) data validation vb. ile API'lar oluşturmak için çeşitli ek özelliklere sahip bir API frameworkü + * **Starlette**: (Uvicorn'u kullanır) bir web mikroframeworkü + * **FastAPI**: (Starlette'i kullanır) veri doğrulama vb. çeşitli ek özelliklere sahip, API oluşturmak için kullanılan bir API mikroframeworkü * **Uvicorn**: - * Sunucunun kendisi dışında ekstra bir kod içermediği için en iyi performansa sahip olacaktır - * Direkt olarak Uvicorn'da bir uygulama yazmazsınız. Bu, en azından Starlette tarafından sağlanan tüm kodu (veya **FastAPI**) az çok içermesi gerektiği anlamına gelir. Ve eğer bunu yaptıysanız, son uygulamanız bir framework kullanmak ve uygulama kodlarını ve bugları en aza indirmekle aynı ek yüke sahip olacaktır. + * Sunucunun kendisi dışında ekstra bir kod içermediği için en iyi performansa sahip olacaktır. + * Doğrudan Uvicorn ile bir uygulama yazmazsınız. Bu, yazdığınız kodun en azından Starlette tarafından sağlanan tüm kodu (veya **FastAPI**) az çok içermesi gerektiği anlamına gelir. Eğer bunu yaptıysanız, son uygulamanız bir framework kullanmak ve uygulama kodlarını ve hataları en aza indirmekle aynı ek yüke sahip olacaktır. * Eğer Uvicorn'u karşılaştırıyorsanız, Daphne, Hypercorn, uWSGI, vb. uygulama sunucuları ile karşılaştırın. * **Starlette**: - * Uvicorn'dan sonraki en iyi performansa sahip olacak. Aslında, Starlette çalışmak için Uvicorn'u kullanıyor. Dolayısıyla, muhtemelen daha fazla kod çalıştırmak zorunda kaldığında Uvicorn'dan sadece "daha yavaş" olabilir. - * Ancak routing based on paths ile vb. basit web uygulamaları oluşturmak için araçlar sağlar. - * Eğer Starlette'i karşılaştırıyorsanız, Sanic, Flask, Django, vb. frameworkler (veya microframeworkler) ile karşılaştırın. + * Uvicorn'dan sonraki en iyi performansa sahip olacaktır. İşin aslı, Starlette çalışmak için Uvicorn'u kullanıyor. Dolayısıyla, daha fazla kod çalıştırmaası gerektiğinden muhtemelen Uvicorn'dan sadece "daha yavaş" olabilir. + * Ancak yol bazlı yönlendirme vb. basit web uygulamaları oluşturmak için araçlar sağlar. + * Eğer Starlette'i karşılaştırıyorsanız, Sanic, Flask, Django, vb. frameworkler (veya mikroframeworkler) ile karşılaştırın. * **FastAPI**: - * Starlette'in Uvicorn'u kullandığı ve ondan daha hızlı olamayacağı gibi, **FastAPI** da Starlette'i kullanır, bu yüzden ondan daha hızlı olamaz. - * FastAPI, Starlette'e ek olarak daha fazla özellik sunar. Data validation ve serialization gibi API'lar oluştururken neredeyse ve her zaman ihtiyaç duyduğunuz özellikler. Ve bunu kullanarak, ücretsiz olarak otomatik dokümantasyon elde edersiniz (otomatik dokümantasyon çalışan uygulamalara ek yük getirmez, başlangıçta oluşturulur). - * FastAPI'ı kullanmadıysanız ve Starlette'i doğrudan kullandıysanız (veya başka bir araç, Sanic, Flask, Responder, vb.) tüm data validation'ı ve serialization'ı kendiniz sağlamanız gerekir. Dolayısıyla, son uygulamanız FastAPI kullanılarak oluşturulmuş gibi hâlâ aynı ek yüke sahip olacaktır. Çoğu durumda, uygulamalarda yazılan kodun büyük çoğunluğunu data validation ve serialization oluşturur. - * Dolayısıyla, FastAPI'ı kullanarak geliştirme süresinden, buglardan, kod satırlarından tasarruf edersiniz ve muhtemelen kullanmasaydınız aynı performansı (veya daha iyisini) elde edersiniz. (hepsini kodunuza uygulamak zorunda kalacağınız gibi) - * Eğer FastAPI'ı karşılaştırıyorsanız, Flask-apispec, NestJS, Molten, vb. gibi data validation, serialization ve dokümantasyon sağlayan bir web uygulaması frameworkü ile (veya araç setiyle) karşılaştırın. Entegre otomatik data validation, serialization ve dokümantasyon içeren frameworkler. + * Starlette'in Uvicorn'u kullandığı ve ondan daha hızlı olamayacağı gibi, **FastAPI**'da Starlette'i kullanır, dolayısıyla ondan daha hızlı olamaz. + * FastAPI, Starlette'e ek olarak daha fazla özellik sunar. Bunlar veri doğrulama ve <abbr title="Dönüşüm: serialization, parsing, marshalling olarak da biliniyor">dönüşümü</abbr> gibi API'lar oluştururken neredeyse ve her zaman ihtiyaç duyduğunuz özelliklerdir. Ve bunu kullanarak, ücretsiz olarak otomatik dokümantasyon elde edersiniz (otomatik dokümantasyon çalışan uygulamalara ek yük getirmez, başlangıçta oluşturulur). + * FastAPI'ı kullanmadıysanız ve Starlette'i doğrudan kullandıysanız (veya başka bir araç, Sanic, Flask, Responder, vb.) tüm veri doğrulama ve dönüştürme araçlarını kendiniz geliştirmeniz gerekir. Dolayısıyla, son uygulamanız FastAPI kullanılarak oluşturulmuş gibi hâlâ aynı ek yüke sahip olacaktır. Çoğu durumda, uygulamalarda yazılan kodun büyük bir kısmını veri doğrulama ve dönüştürme kodları oluşturur. + * Dolayısıyla, FastAPI'ı kullanarak geliştirme süresinden, hatalardan, kod satırlarından tasarruf edersiniz ve kullanmadığınız durumda (birçok özelliği geliştirmek zorunda kalmakla birlikte) muhtemelen aynı performansı (veya daha iyisini) elde ederdiniz. + * Eğer FastAPI'ı karşılaştırıyorsanız, Flask-apispec, NestJS, Molten, vb. gibi veri doğrulama, dönüştürme ve dokümantasyon sağlayan bir web uygulaması frameworkü ile (veya araç setiyle) karşılaştırın. From 9e06513033d881001b04616084348ee6300e98e6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:10:41 +0000 Subject: [PATCH 1761/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 94e1b062e..f8c4589c6 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/help/index.md`. PR [#10983](https://github.com/tiangolo/fastapi/pull/10983) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/features.md`. PR [#10976](https://github.com/tiangolo/fastapi/pull/10976) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/security/get-current-user.md`. PR [#5737](https://github.com/tiangolo/fastapi/pull/5737) by [@KdHyeon0661](https://github.com/KdHyeon0661). * 🌐 Add Russian translation for `docs/ru/docs/tutorial/security/first-steps.md`. PR [#10541](https://github.com/tiangolo/fastapi/pull/10541) by [@AlertRED](https://github.com/AlertRED). From 7586688cc961791afbfc3c568aba2b3fecea27c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:11:15 +0300 Subject: [PATCH 1762/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/about/index.md`=20(#11006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/about/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/tr/docs/about/index.md diff --git a/docs/tr/docs/about/index.md b/docs/tr/docs/about/index.md new file mode 100644 index 000000000..e9dee5217 --- /dev/null +++ b/docs/tr/docs/about/index.md @@ -0,0 +1,3 @@ +# Hakkında + +FastAPI, tasarımı, ilham kaynağı ve daha fazlası hakkında. 🤓 From 39cff8d7d6dea15c4b6e767dec5317b949f4e645 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:12:29 +0000 Subject: [PATCH 1763/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f8c4589c6..baae05a03 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Italian translation for `docs/it/docs/index.md`. PR [#5233](https://github.com/tiangolo/fastapi/pull/5233) by [@matteospanio](https://github.com/matteospanio). * 🌐 Add Korean translation for `docs/ko/docs/help/index.md`. PR [#10983](https://github.com/tiangolo/fastapi/pull/10983) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/features.md`. PR [#10976](https://github.com/tiangolo/fastapi/pull/10976) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/security/get-current-user.md`. PR [#5737](https://github.com/tiangolo/fastapi/pull/5737) by [@KdHyeon0661](https://github.com/KdHyeon0661). From 754ea10fcc745975473c10745e543b162a015961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 23 Jan 2024 17:13:01 +0300 Subject: [PATCH 1764/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/help/index.md`=20(#11013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/help/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/tr/docs/help/index.md diff --git a/docs/tr/docs/help/index.md b/docs/tr/docs/help/index.md new file mode 100644 index 000000000..cef0914ce --- /dev/null +++ b/docs/tr/docs/help/index.md @@ -0,0 +1,3 @@ +# Yardım + +Yardım alın, yardım edin, katkıda bulunun, dahil olun. 🤝 From 2341f7210137f4b85b361fd2dfd21a6261d62208 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:15:50 +0000 Subject: [PATCH 1765/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 baae05a03..bff875447 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Update Turkish translation for `docs/tr/docs/benchmarks.md`. PR [#11005](https://github.com/tiangolo/fastapi/pull/11005) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Italian translation for `docs/it/docs/index.md`. PR [#5233](https://github.com/tiangolo/fastapi/pull/5233) by [@matteospanio](https://github.com/matteospanio). * 🌐 Add Korean translation for `docs/ko/docs/help/index.md`. PR [#10983](https://github.com/tiangolo/fastapi/pull/10983) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `docs/ko/docs/features.md`. PR [#10976](https://github.com/tiangolo/fastapi/pull/10976) by [@KaniKim](https://github.com/KaniKim). From a12c5db74c6f12d90761356250ff3e2d72d678d4 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:16:59 +0000 Subject: [PATCH 1766/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 bff875447..09f599436 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/about/index.md`. PR [#11006](https://github.com/tiangolo/fastapi/pull/11006) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Update Turkish translation for `docs/tr/docs/benchmarks.md`. PR [#11005](https://github.com/tiangolo/fastapi/pull/11005) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Italian translation for `docs/it/docs/index.md`. PR [#5233](https://github.com/tiangolo/fastapi/pull/5233) by [@matteospanio](https://github.com/matteospanio). * 🌐 Add Korean translation for `docs/ko/docs/help/index.md`. PR [#10983](https://github.com/tiangolo/fastapi/pull/10983) by [@KaniKim](https://github.com/KaniKim). From 30f1a1c4efbcfb4025090a06b4ac94d29fc0b9b7 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 14:19:05 +0000 Subject: [PATCH 1767/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 09f599436..0379bd921 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/help/index.md`. PR [#11013](https://github.com/tiangolo/fastapi/pull/11013) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/about/index.md`. PR [#11006](https://github.com/tiangolo/fastapi/pull/11006) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Update Turkish translation for `docs/tr/docs/benchmarks.md`. PR [#11005](https://github.com/tiangolo/fastapi/pull/11005) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Italian translation for `docs/it/docs/index.md`. PR [#5233](https://github.com/tiangolo/fastapi/pull/5233) by [@matteospanio](https://github.com/matteospanio). From 30f31540fc3bfe8725cc1ebee0a440c4812539c9 Mon Sep 17 00:00:00 2001 From: mojtaba <121169359+mojtabapaso@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:36:11 +0330 Subject: [PATCH 1768/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20transl?= =?UTF-8?q?ation=20for=20`docs/fa/docs/tutorial/security/index.md`=20(#994?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fa/docs/tutorial/security/index.md | 100 ++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/fa/docs/tutorial/security/index.md diff --git a/docs/fa/docs/tutorial/security/index.md b/docs/fa/docs/tutorial/security/index.md new file mode 100644 index 000000000..4e68ba961 --- /dev/null +++ b/docs/fa/docs/tutorial/security/index.md @@ -0,0 +1,100 @@ +# امنیت + +روش‌های مختلفی برای مدیریت امنیت، تأیید هویت و اعتبارسنجی وجود دارد. + +عموماً این یک موضوع پیچیده و "سخت" است. + +در بسیاری از فریم ورک ها و سیستم‌ها، فقط مدیریت امنیت و تأیید هویت نیاز به تلاش و کد نویسی زیادی دارد (در بسیاری از موارد می‌تواند 50% یا بیشتر کل کد نوشته شده باشد). + + +فریم ورک **FastAPI** ابزارهای متعددی را در اختیار شما قرار می دهد تا به راحتی، با سرعت، به صورت استاندارد و بدون نیاز به مطالعه و یادگیری همه جزئیات امنیت، در مدیریت **امنیت** به شما کمک کند. + +اما قبل از آن، بیایید برخی از مفاهیم کوچک را بررسی کنیم. + +## عجله دارید؟ + +اگر به هیچ یک از این اصطلاحات اهمیت نمی دهید و فقط نیاز به افزودن امنیت با تأیید هویت بر اساس نام کاربری و رمز عبور دارید، *همین الان* به فصل های بعدی بروید. + +## پروتکل استاندارد OAuth2 + +پروتکل استاندارد OAuth2 یک مشخصه است که چندین روش برای مدیریت تأیید هویت و اعتبار سنجی تعریف می کند. + +این مشخصه بسیار گسترده است و چندین حالت استفاده پیچیده را پوشش می دهد. + +در آن روش هایی برای تأیید هویت با استفاده از "برنامه های شخص ثالث" وجود دارد. + +این همان چیزی است که تمامی سیستم های با "ورود با فیسبوک، گوگل، توییتر، گیت هاب" در پایین آن را استفاده می کنند. + +### پروتکل استاندارد OAuth 1 + +پروتکل استاندارد OAuth1 نیز وجود داشت که با OAuth2 خیلی متفاوت است و پیچیدگی بیشتری داشت، زیرا شامل مشخصات مستقیم در مورد رمزگذاری ارتباط بود. + +در حال حاضر OAuth1 بسیار محبوب یا استفاده شده نیست. + +پروتکل استاندارد OAuth2 روش رمزگذاری ارتباط را مشخص نمی کند، بلکه انتظار دارد که برنامه شما با HTTPS سرویس دهی شود. + +!!! نکته + در بخش در مورد **استقرار** ، شما یاد خواهید گرفت که چگونه با استفاده از Traefik و Let's Encrypt رایگان HTTPS را راه اندازی کنید. + +## استاندارد OpenID Connect + +استاندارد OpenID Connect، مشخصه‌ای دیگر است که بر پایه **OAuth2** ساخته شده است. + +این مشخصه، به گسترش OAuth2 می‌پردازد و برخی مواردی که در OAuth2 نسبتاً تردید برانگیز هستند را مشخص می‌کند تا سعی شود آن را با سایر سیستم‌ها قابل ارتباط کند. + +به عنوان مثال، ورود به سیستم گوگل از OpenID Connect استفاده می‌کند (که در زیر از OAuth2 استفاده می‌کند). + +اما ورود به سیستم فیسبوک، از OpenID Connect پشتیبانی نمی‌کند. به جای آن، نسخه خودش از OAuth2 را دارد. + +### استاندارد OpenID (نه "OpenID Connect" ) + +همچنین مشخصه "OpenID" نیز وجود داشت که سعی در حل مسائل مشابه OpenID Connect داشت، اما بر پایه OAuth2 ساخته نشده بود. + +بنابراین، یک سیستم جداگانه بود. + +اکنون این مشخصه کمتر استفاده می‌شود و محبوبیت زیادی ندارد. + +## استاندارد OpenAPI + +استاندارد OpenAPI (قبلاً با نام Swagger شناخته می‌شد) یک open specification برای ساخت APIs (که در حال حاضر جزئی از بنیاد لینوکس میباشد) است. + +فریم ورک **FastAPI** بر اساس **OpenAPI** است. + +این خاصیت، امکان دارد تا چندین رابط مستندات تعاملی خودکار(automatic interactive documentation interfaces)، تولید کد و غیره وجود داشته باشد. + +مشخصه OpenAPI روشی برای تعریف چندین "schemes" دارد. + +با استفاده از آن‌ها، شما می‌توانید از همه این ابزارهای مبتنی بر استاندارد استفاده کنید، از جمله این سیستم‌های مستندات تعاملی(interactive documentation systems). + +استاندارد OpenAPI شیوه‌های امنیتی زیر را تعریف می‌کند: + +* شیوه `apiKey`: یک کلید اختصاصی برای برنامه که می‌تواند از موارد زیر استفاده شود: + * پارامتر جستجو. + * هدر. + * کوکی. +* شیوه `http`: سیستم‌های استاندارد احراز هویت HTTP، از جمله: + * مقدار `bearer`: یک هدر `Authorization` با مقدار `Bearer` به همراه یک توکن. این از OAuth2 به ارث برده شده است. + * احراز هویت پایه HTTP. + * ویژگی HTTP Digest و غیره. +* شیوه `oauth2`: تمام روش‌های OAuth2 برای مدیریت امنیت (به نام "flows"). + * چندین از این flows برای ساخت یک ارائه‌دهنده احراز هویت OAuth 2.0 مناسب هستند (مانند گوگل، فیسبوک، توییتر، گیت‌هاب و غیره): + * ویژگی `implicit` + * ویژگی `clientCredentials` + * ویژگی `authorizationCode` + * اما یک "flow" خاص وجود دارد که می‌تواند به طور کامل برای مدیریت احراز هویت در همان برنامه به کار رود: + * بررسی `password`: چند فصل بعدی به مثال‌های این مورد خواهیم پرداخت. +* شیوه `openIdConnect`: یک روش برای تعریف نحوه کشف داده‌های احراز هویت OAuth2 به صورت خودکار. + * کشف خودکار این موضوع را که در مشخصه OpenID Connect تعریف شده است، مشخص می‌کند. + +!!! نکته + ادغام سایر ارائه‌دهندگان احراز هویت/اجازه‌دهی مانند گوگل، فیسبوک، توییتر، گیت‌هاب و غیره نیز امکان‌پذیر و نسبتاً آسان است. + + مشکل پیچیده‌ترین مسئله، ساخت یک ارائه‌دهنده احراز هویت/اجازه‌دهی مانند آن‌ها است، اما **FastAPI** ابزارهای لازم برای انجام این کار را با سهولت به شما می‌دهد و همه کارهای سنگین را برای شما انجام می‌دهد. + +## ابزارهای **FastAPI** + +فریم ورک FastAPI ابزارهایی برای هر یک از این شیوه‌های امنیتی در ماژول`fastapi.security` فراهم می‌کند که استفاده از این مکانیزم‌های امنیتی را ساده‌تر می‌کند. + +در فصل‌های بعدی، شما یاد خواهید گرفت که چگونه با استفاده از این ابزارهای ارائه شده توسط **FastAPI**، امنیت را به API خود اضافه کنید. + +همچنین، خواهید دید که چگونه به صورت خودکار در سیستم مستندات تعاملی ادغام می‌شود. From 9a5181abfcea5cfa6cbaf3439304ec676272ed95 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 15:06:34 +0000 Subject: [PATCH 1769/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0379bd921..0755b6826 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Persian translation for `docs/fa/docs/tutorial/security/index.md`. PR [#9945](https://github.com/tiangolo/fastapi/pull/9945) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Turkish translation for `docs/tr/docs/help/index.md`. PR [#11013](https://github.com/tiangolo/fastapi/pull/11013) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/about/index.md`. PR [#11006](https://github.com/tiangolo/fastapi/pull/11006) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Update Turkish translation for `docs/tr/docs/benchmarks.md`. PR [#11005](https://github.com/tiangolo/fastapi/pull/11005) by [@hasansezertasan](https://github.com/hasansezertasan). From aae29cac5c1b25722bd5d7ce1796d4930998ca85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:02:27 +0300 Subject: [PATCH 1770/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/learn/index.md`=20(#11014)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/learn/index.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/tr/docs/learn/index.md diff --git a/docs/tr/docs/learn/index.md b/docs/tr/docs/learn/index.md new file mode 100644 index 000000000..52e3aa54d --- /dev/null +++ b/docs/tr/docs/learn/index.md @@ -0,0 +1,5 @@ +# Öğren + +**FastAPI** öğrenmek için giriş bölümleri ve öğreticiler burada yer alıyor. + +Burayı, bir **kitap**, bir **kurs**, ve FastAPI öğrenmenin **resmi** ve önerilen yolu olarak düşünülebilirsiniz. 😎 From 6aa521aa03772cea0e9eb0b31f36fb41f8f15991 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 16:02:56 +0000 Subject: [PATCH 1771/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0755b6826..7d2dc438f 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/learn/index.md`. PR [#11014](https://github.com/tiangolo/fastapi/pull/11014) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/security/index.md`. PR [#9945](https://github.com/tiangolo/fastapi/pull/9945) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Turkish translation for `docs/tr/docs/help/index.md`. PR [#11013](https://github.com/tiangolo/fastapi/pull/11013) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/about/index.md`. PR [#11006](https://github.com/tiangolo/fastapi/pull/11006) by [@hasansezertasan](https://github.com/hasansezertasan). From dcf8b24ece34a7aa8f3c28d1e962733aebe97a05 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Tue, 23 Jan 2024 17:04:13 +0100 Subject: [PATCH 1772/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/benchmarks.md`=20(#10866)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/benchmarks.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/de/docs/benchmarks.md diff --git a/docs/de/docs/benchmarks.md b/docs/de/docs/benchmarks.md new file mode 100644 index 000000000..6efd56e83 --- /dev/null +++ b/docs/de/docs/benchmarks.md @@ -0,0 +1,34 @@ +# Benchmarks + +Unabhängige TechEmpower-Benchmarks zeigen, **FastAPI**-Anwendungen, die unter Uvicorn ausgeführt werden, gehören zu <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">den schnellsten existierenden Python-Frameworks</a>, nur Starlette und Uvicorn selbst (intern von FastAPI verwendet) sind schneller. + +Beim Ansehen von Benchmarks und Vergleichen sollten Sie jedoch Folgende Punkte beachten. + +## Benchmarks und Geschwindigkeit + +Wenn Sie sich die Benchmarks ansehen, werden häufig mehrere Tools mit unterschiedlichen Eigenschaften als gleichwertig verglichen. + +Konkret geht es darum, Uvicorn, Starlette und FastAPI miteinander zu vergleichen (neben vielen anderen Tools). + +Je einfacher das Problem, welches durch das Tool gelöst wird, desto besser ist die Performanz. Und die meisten Benchmarks testen nicht die zusätzlichen Funktionen, welche das Tool bietet. + +Die Hierarchie ist wie folgt: + +* **Uvicorn**: ein ASGI-Server + * **Starlette**: (verwendet Uvicorn) ein Web-Mikroframework + * **FastAPI**: (verwendet Starlette) ein API-Mikroframework mit mehreren zusätzlichen Funktionen zum Erstellen von APIs, mit Datenvalidierung, usw. + +* **Uvicorn**: + * Bietet die beste Leistung, da außer dem Server selbst nicht viel zusätzlicher Code vorhanden ist. + * Sie würden eine Anwendung nicht direkt in Uvicorn schreiben. Das würde bedeuten, dass Ihr Code zumindest mehr oder weniger den gesamten von Starlette (oder **FastAPI**) bereitgestellten Code enthalten müsste. Und wenn Sie das täten, hätte Ihre endgültige Anwendung den gleichen Overhead wie die Verwendung eines Frameworks nebst Minimierung Ihres Anwendungscodes und der Fehler. + * Wenn Sie Uvicorn vergleichen, vergleichen Sie es mit Anwendungsservern wie Daphne, Hypercorn, uWSGI, usw. +* **Starlette**: + * Wird nach Uvicorn die nächstbeste Performanz erbringen. Tatsächlich nutzt Starlette intern Uvicorn. Daher kann es wahrscheinlich nur „langsamer“ als Uvicorn sein, weil mehr Code ausgeführt wird. + * Aber es bietet Ihnen die Tools zum Erstellen einfacher Webanwendungen, mit Routing basierend auf Pfaden, usw. + * Wenn Sie Starlette vergleichen, vergleichen Sie es mit Webframeworks (oder Mikroframeworks) wie Sanic, Flask, Django, usw. +* **FastAPI**: + * So wie Starlette Uvicorn verwendet und nicht schneller als dieses sein kann, verwendet **FastAPI** Starlette, sodass es nicht schneller als dieses sein kann. + * FastAPI bietet zusätzlich zu Starlette weitere Funktionen. Funktionen, die Sie beim Erstellen von APIs fast immer benötigen, wie Datenvalidierung und Serialisierung. Und wenn Sie es verwenden, erhalten Sie kostenlos automatische Dokumentation (die automatische Dokumentation verursacht nicht einmal zusätzlichen Aufwand für laufende Anwendungen, sie wird beim Start generiert). + * Wenn Sie FastAPI nicht, und direkt Starlette (oder ein anderes Tool wie Sanic, Flask, Responder, usw.) verwenden würden, müssten Sie die gesamte Datenvalidierung und Serialisierung selbst implementieren. Ihre finale Anwendung hätte also immer noch den gleichen Overhead, als ob sie mit FastAPI erstellt worden wäre. Und in vielen Fällen ist diese Datenvalidierung und Serialisierung der größte Teil des in Anwendungen geschriebenen Codes. + * Durch die Verwendung von FastAPI sparen Sie also Entwicklungszeit, Fehler und Codezeilen und würden wahrscheinlich die gleiche Leistung (oder eine bessere) erzielen, die Sie hätten, wenn Sie es nicht verwenden würden (da Sie alles in Ihrem Code implementieren müssten). + * Wenn Sie FastAPI vergleichen, vergleichen Sie es mit einem Webanwendung-Framework (oder einer Reihe von Tools), welche Datenvalidierung, Serialisierung und Dokumentation bereitstellen, wie Flask-apispec, NestJS, Molten, usw. – Frameworks mit integrierter automatischer Datenvalidierung, Serialisierung und Dokumentation. From 4c077492aec56aba2f0bf6a8b16c7f7212a0382f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 16:04:37 +0000 Subject: [PATCH 1773/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7d2dc438f..0a9b35362 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -45,6 +45,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/benchmarks.md`. PR [#10866](https://github.com/tiangolo/fastapi/pull/10866) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Turkish translation for `docs/tr/docs/learn/index.md`. PR [#11014](https://github.com/tiangolo/fastapi/pull/11014) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/security/index.md`. PR [#9945](https://github.com/tiangolo/fastapi/pull/9945) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Add Turkish translation for `docs/tr/docs/help/index.md`. PR [#11013](https://github.com/tiangolo/fastapi/pull/11013) by [@hasansezertasan](https://github.com/hasansezertasan). From 8e9af7932c3ee53a7263044c53b88341f0b87745 Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:31:56 -0500 Subject: [PATCH 1774/1881] =?UTF-8?q?=F0=9F=94=A7=20Add=20Italian=20to=20`?= =?UTF-8?q?mkdocs.yml`=20(#11016)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index d34e919bd..2b843e026 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -283,6 +283,8 @@ extra: name: hu - magyar - link: /id/ name: id - Bahasa Indonesia + - link: /it/ + name: it - italiano - link: /ja/ name: ja - 日本語 - link: /ko/ From e96e74ad36c77273018e87932ad1de7a19aeb67b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 23 Jan 2024 17:32:19 +0000 Subject: [PATCH 1775/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0a9b35362..9e28af40b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -109,6 +109,7 @@ hide: ### Internal +* 🔧 Add Italian to `mkdocs.yml`. PR [#11016](https://github.com/tiangolo/fastapi/pull/11016) by [@alejsdev](https://github.com/alejsdev). * 🔨 Verify `mkdocs.yml` languages in CI, update `docs.py`. PR [#11009](https://github.com/tiangolo/fastapi/pull/11009) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update config in `label-approved.yml` to accept translations with 1 reviewer. PR [#11007](https://github.com/tiangolo/fastapi/pull/11007) by [@alejsdev](https://github.com/alejsdev). * 👷 Add changes-requested handling in GitHub Action issue manager. PR [#10971](https://github.com/tiangolo/fastapi/pull/10971) by [@tiangolo](https://github.com/tiangolo). From 9ee70f82e7691246d78446c0ff04c1e4496bf383 Mon Sep 17 00:00:00 2001 From: Jessica Temporal <jtemporal@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:53:05 +0900 Subject: [PATCH 1776/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=20Tips=20on=20migrating=20from=20Flask=20to=20FastAPI=20and=20?= =?UTF-8?q?vice-versa=20(#11029)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 00d6f696d..44b064fe9 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Jessica Temporal + author_link: https://jtemporal.com/socials + link: https://jtemporal.com/tips-on-migrating-from-flask-to-fastapi-and-vice-versa/ + title: Tips on migrating from Flask to FastAPI and vice-versa - author: Ankit Anchlia author_link: https://linkedin.com/in/aanchlia21 link: https://hackernoon.com/explore-how-to-effectively-use-jwt-with-fastapi @@ -302,6 +306,11 @@ Articles: author_link: https://qiita.com/mtitg link: https://qiita.com/mtitg/items/47770e9a562dd150631d title: FastAPI|DB接続してCRUDするPython製APIサーバーを構築 + Portuguese: + - author: Jessica Temporal + author_link: https://jtemporal.com/socials + link: https://jtemporal.com/dicas-para-migrar-de-flask-para-fastapi-e-vice-versa/ + title: Dicas para migrar uma aplicação de Flask para FastAPI e vice-versa Russian: - author: Troy Köhler author_link: https://www.linkedin.com/in/trkohler/ From 9af7f2a5d5e6ce37ecfe2449f645f69881abc953 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 14:53:25 +0000 Subject: [PATCH 1777/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9e28af40b..5d31e5261 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Add External Link: Tips on migrating from Flask to FastAPI and vice-versa. PR [#11029](https://github.com/tiangolo/fastapi/pull/11029) by [@jtemporal](https://github.com/jtemporal). * 📝 Deprecate old tutorials: Peewee, Couchbase, encode/databases. PR [#10979](https://github.com/tiangolo/fastapi/pull/10979) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/security/oauth2.py`. PR [#10972](https://github.com/tiangolo/fastapi/pull/10972) by [@RafalSkolasinski](https://github.com/RafalSkolasinski). * 📝 Update `HTTPException` details in `docs/en/docs/tutorial/handling-errors.md`. PR [#5418](https://github.com/tiangolo/fastapi/pull/5418) by [@papb](https://github.com/papb). From e55c7ccbcb723b5d290d29c20df530cfc7df4d72 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Thu, 25 Jan 2024 15:53:41 +0100 Subject: [PATCH 1778/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/query-params.md`=20(#10293)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/query-params.md | 226 ++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/de/docs/tutorial/query-params.md diff --git a/docs/de/docs/tutorial/query-params.md b/docs/de/docs/tutorial/query-params.md new file mode 100644 index 000000000..1b9b56bea --- /dev/null +++ b/docs/de/docs/tutorial/query-params.md @@ -0,0 +1,226 @@ +# Query-Parameter + +Wenn Sie in ihrer Funktion Parameter deklarieren, die nicht Teil der Pfad-Parameter sind, dann werden diese automatisch als „Query“-Parameter interpretiert. + +```Python hl_lines="9" +{!../../../docs_src/query_params/tutorial001.py!} +``` + +Query-Parameter (Deutsch: Abfrage-Parameter) sind die Schlüssel-Wert-Paare, die nach dem `?` in einer URL aufgelistet sind, getrennt durch `&`-Zeichen. + +Zum Beispiel sind in der URL: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +... die Query-Parameter: + +* `skip`: mit dem Wert `0` +* `limit`: mit dem Wert `10` + +Da sie Teil der URL sind, sind sie „naturgemäß“ Strings. + +Aber wenn Sie sie mit Python-Typen deklarieren (im obigen Beispiel als `int`), werden sie zu diesem Typ konvertiert, und gegen diesen validiert. + +Die gleichen Prozesse, die für Pfad-Parameter stattfinden, werden auch auf Query-Parameter angewendet: + +* Editor Unterstützung (natürlich) +* <abbr title="Konvertieren des Strings, der von einer HTTP-Anfrage kommt, in Python-Daten">„Parsen“</abbr> der Daten +* Datenvalidierung +* Automatische Dokumentation + +## Defaultwerte + +Da Query-Parameter nicht ein festgelegter Teil des Pfades sind, können sie optional sein und Defaultwerte haben. + +Im obigen Beispiel haben sie die Defaultwerte `skip=0` und `limit=10`. + +Wenn Sie also zur URL: + +``` +http://127.0.0.1:8000/items/ +``` + +gehen, so ist das das gleiche wie die URL: + +``` +http://127.0.0.1:8000/items/?skip=0&limit=10 +``` + +Aber wenn Sie zum Beispiel zu: + +``` +http://127.0.0.1:8000/items/?skip=20 +``` + +gehen, werden die Parameter-Werte Ihrer Funktion sein: + +* `skip=20`: da Sie das in der URL gesetzt haben +* `limit=10`: weil das der Defaultwert ist + +## Optionale Parameter + +Auf die gleiche Weise können Sie optionale Query-Parameter deklarieren, indem Sie deren Defaultwert auf `None` setzen: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial002.py!} + ``` + +In diesem Fall wird der Funktionsparameter `q` optional, und standardmäßig `None` sein. + +!!! check + Beachten Sie auch, dass **FastAPI** intelligent genug ist, um zu erkennen, dass `item_id` ein Pfad-Parameter ist und `q` keiner, daher muss letzteres ein Query-Parameter sein. + +## Query-Parameter Typkonvertierung + +Sie können auch `bool`-Typen deklarieren und sie werden konvertiert: + +=== "Python 3.10+" + + ```Python hl_lines="7" + {!> ../../../docs_src/query_params/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/query_params/tutorial003.py!} + ``` + +Wenn Sie nun zu: + +``` +http://127.0.0.1:8000/items/foo?short=1 +``` + +oder + +``` +http://127.0.0.1:8000/items/foo?short=True +``` + +oder + +``` +http://127.0.0.1:8000/items/foo?short=true +``` + +oder + +``` +http://127.0.0.1:8000/items/foo?short=on +``` + +oder + +``` +http://127.0.0.1:8000/items/foo?short=yes +``` + +gehen, oder zu irgendeiner anderen Variante der Groß-/Kleinschreibung (Alles groß, Anfangsbuchstabe groß, usw.), dann wird Ihre Funktion den Parameter `short` mit dem `bool`-Wert `True` sehen, ansonsten mit dem Wert `False`. + +## Mehrere Pfad- und Query-Parameter + +Sie können mehrere Pfad-Parameter und Query-Parameter gleichzeitig deklarieren, **FastAPI** weiß, was welches ist. + +Und Sie müssen sie auch nicht in einer spezifischen Reihenfolge deklarieren. + +Parameter werden anhand ihres Namens erkannt: + +=== "Python 3.10+" + + ```Python hl_lines="6 8" + {!> ../../../docs_src/query_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="8 10" + {!> ../../../docs_src/query_params/tutorial004.py!} + ``` + +## Erforderliche Query-Parameter + +Wenn Sie einen Defaultwert für Nicht-Pfad-Parameter deklarieren (Bis jetzt haben wir nur Query-Parameter gesehen), dann ist der Parameter nicht erforderlich. + +Wenn Sie keinen spezifischen Wert haben wollen, sondern der Parameter einfach optional sein soll, dann setzen Sie den Defaultwert auf `None`. + +Aber wenn Sie wollen, dass ein Query-Parameter erforderlich ist, vergeben Sie einfach keinen Defaultwert: + +```Python hl_lines="6-7" +{!../../../docs_src/query_params/tutorial005.py!} +``` + +Hier ist `needy` ein erforderlicher Query-Parameter vom Typ `str`. + +Wenn Sie in Ihrem Browser eine URL wie: + +``` +http://127.0.0.1:8000/items/foo-item +``` + +... öffnen, ohne den benötigten Parameter `needy`, dann erhalten Sie einen Fehler wie den folgenden: + +```JSON +{ + "detail": [ + { + "type": "missing", + "loc": [ + "query", + "needy" + ], + "msg": "Field required", + "input": null, + "url": "https://errors.pydantic.dev/2.1/v/missing" + } + ] +} +``` + +Da `needy` ein erforderlicher Parameter ist, müssen Sie ihn in der URL setzen: + +``` +http://127.0.0.1:8000/items/foo-item?needy=sooooneedy +``` + +... Das funktioniert: + +```JSON +{ + "item_id": "foo-item", + "needy": "sooooneedy" +} +``` + +Und natürlich können Sie einige Parameter als erforderlich, einige mit Defaultwert, und einige als vollständig optional definieren: + +=== "Python 3.10+" + + ```Python hl_lines="8" + {!> ../../../docs_src/query_params/tutorial006_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="10" + {!> ../../../docs_src/query_params/tutorial006.py!} + ``` + +In diesem Fall gibt es drei Query-Parameter: + +* `needy`, ein erforderlicher `str`. +* `skip`, ein `int` mit einem Defaultwert `0`. +* `limit`, ein optionales `int`. + +!!! tip "Tipp" + Sie können auch `Enum`s verwenden, auf die gleiche Weise wie mit [Pfad-Parametern](path-params.md#vordefinierte-parameterwerte){.internal-link target=_blank}. From 28e679d6dccabce4336901f840f125237f6bed12 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 14:55:49 +0000 Subject: [PATCH 1779/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 5d31e5261..6434871d8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/query-params.md`. PR [#10293](https://github.com/tiangolo/fastapi/pull/10293) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/benchmarks.md`. PR [#10866](https://github.com/tiangolo/fastapi/pull/10866) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Turkish translation for `docs/tr/docs/learn/index.md`. PR [#11014](https://github.com/tiangolo/fastapi/pull/11014) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/security/index.md`. PR [#9945](https://github.com/tiangolo/fastapi/pull/9945) by [@mojtabapaso](https://github.com/mojtabapaso). From ecee093e340de9221558e06fe34c9d80f2609819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:56:05 +0300 Subject: [PATCH 1780/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/how-to/index.md`=20(#11021)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/how-to/index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docs/tr/docs/how-to/index.md diff --git a/docs/tr/docs/how-to/index.md b/docs/tr/docs/how-to/index.md new file mode 100644 index 000000000..8ece29515 --- /dev/null +++ b/docs/tr/docs/how-to/index.md @@ -0,0 +1,11 @@ +# Nasıl Yapılır - Tarifler + +Burada çeşitli konular hakkında farklı tarifler veya "nasıl yapılır" kılavuzları yer alıyor. + +Bu fikirlerin büyük bir kısmı aşağı yukarı **bağımsız** olacaktır, çoğu durumda bunları sadece **projenize** hitap ediyorsa incelemelisiniz. + +Projeniz için ilginç ve yararlı görünen bir şey varsa devam edin ve inceleyin, aksi halde bunları atlayabilirsiniz. + +!!! tip "İpucu" + + **FastAPI**'ı düzgün (ve önerilen) şekilde öğrenmek istiyorsanız [Öğretici - Kullanıcı Rehberi](../tutorial/index.md){.internal-link target=_blank}'ni bölüm bölüm okuyun. From 3e98fb9c8390f068bb6b40bbe12c6abbddaadaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:57:16 +0300 Subject: [PATCH 1781/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/resources/index.md`=20(#11020)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/resources/index.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 docs/tr/docs/resources/index.md diff --git a/docs/tr/docs/resources/index.md b/docs/tr/docs/resources/index.md new file mode 100644 index 000000000..fc71a9ca1 --- /dev/null +++ b/docs/tr/docs/resources/index.md @@ -0,0 +1,3 @@ +# Kaynaklar + +Ek kaynaklar, dış bağlantılar, makaleler ve daha fazlası. ✈️ From 01c56c059e71ff1c902bc1b4dcf672a0f6ca7e58 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 14:58:23 +0000 Subject: [PATCH 1782/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6434871d8..7e8e8487d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/how-to/index.md`. PR [#11021](https://github.com/tiangolo/fastapi/pull/11021) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add German translation for `docs/de/docs/tutorial/query-params.md`. PR [#10293](https://github.com/tiangolo/fastapi/pull/10293) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/benchmarks.md`. PR [#10866](https://github.com/tiangolo/fastapi/pull/10866) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Turkish translation for `docs/tr/docs/learn/index.md`. PR [#11014](https://github.com/tiangolo/fastapi/pull/11014) by [@hasansezertasan](https://github.com/hasansezertasan). From 06bdf03bcee34761ca94b97c2aa93b4dd01f667c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 14:59:03 +0000 Subject: [PATCH 1783/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7e8e8487d..cf9a031ac 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/resources/index.md`. PR [#11020](https://github.com/tiangolo/fastapi/pull/11020) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/how-to/index.md`. PR [#11021](https://github.com/tiangolo/fastapi/pull/11021) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add German translation for `docs/de/docs/tutorial/query-params.md`. PR [#10293](https://github.com/tiangolo/fastapi/pull/10293) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/benchmarks.md`. PR [#10866](https://github.com/tiangolo/fastapi/pull/10866) by [@nilslindemann](https://github.com/nilslindemann). From 5d74e58e952e9286d8352fb9f2e904fad8746f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Sezer=20Ta=C5=9Fan?= <13135006+hasansezertasan@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:59:43 +0300 Subject: [PATCH 1784/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Turkish=20transl?= =?UTF-8?q?ation=20for=20`docs/tr/docs/history-design-future.md`=20(#11012?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tr/docs/history-design-future.md | 79 +++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/tr/docs/history-design-future.md diff --git a/docs/tr/docs/history-design-future.md b/docs/tr/docs/history-design-future.md new file mode 100644 index 000000000..950fcf37d --- /dev/null +++ b/docs/tr/docs/history-design-future.md @@ -0,0 +1,79 @@ +# Geçmişi, Tasarımı ve Geleceği + +Bir süre önce, <a href="https://github.com/tiangolo/fastapi/issues/3#issuecomment-454956920" class="external-link" target="_blank">bir **FastAPI** kullanıcısı sordu</a>: + +> Bu projenin geçmişi nedir? Birkaç hafta içinde hiçbir yerden harika bir şeye dönüşmüş gibi görünüyor [...] + +İşte o geçmişin bir kısmı. + +## Alternatifler + +Bir süredir karmaşık gereksinimlere sahip API'lar oluşturuyor (Makine Öğrenimi, dağıtık sistemler, asenkron işler, NoSQL veritabanları vb.) ve farklı geliştirici ekiplerini yönetiyorum. + +Bu süreçte birçok alternatifi araştırmak, test etmek ve kullanmak zorunda kaldım. + +**FastAPI**'ın geçmişi, büyük ölçüde önceden geliştirilen araçların geçmişini kapsıyor. + +[Alternatifler](alternatives.md){.internal-link target=_blank} bölümünde belirtildiği gibi: + +<blockquote markdown="1"> + +Başkalarının daha önceki çalışmaları olmasaydı, **FastAPI** var olmazdı. + +Geçmişte oluşturulan pek çok araç **FastAPI**'a ilham kaynağı olmuştur. + +Yıllardır yeni bir framework oluşturmaktan kaçınıyordum. Başlangıçta **FastAPI**'ın çözdüğü sorunları çözebilmek için pek çok farklı framework, <abbr title="Eklenti: Plug-In">eklenti</abbr> ve araç kullanmayı denedim. + +Ancak bir noktada, geçmişteki diğer araçlardan en iyi fikirleri alarak bütün bu çözümleri kapsayan, ayrıca bütün bunları Python'ın daha önce mevcut olmayan özelliklerini (Python 3.6+ ile gelen <abbr title="Tip belirteçleri: Type Hints">tip belirteçleri</abbr>) kullanarak yapan bir şey üretmekten başka bir seçenek kalmamıştı. + +</blockquote> + +## Araştırma + +Önceki alternatifleri kullanarak hepsinden bir şeyler öğrenip, fikirler alıp, bunları kendim ve çalıştığım geliştirici ekipler için en iyi şekilde birleştirebilme şansım oldu. + +Mesela, ideal olarak standart Python tip belirteçlerine dayanması gerektiği açıktı. + +Ayrıca, en iyi yaklaşım zaten mevcut olan standartları kullanmaktı. + +Sonuç olarak, **FastAPI**'ı kodlamaya başlamadan önce, birkaç ay boyunca OpenAPI, JSON Schema, OAuth2 ve benzerlerinin tanımlamalarını inceledim. İlişkilerini, örtüştükleri noktaları ve farklılıklarını anlamaya çalıştım. + +## Tasarım + +Sonrasında, (**FastAPI** kullanan bir geliştirici olarak) sahip olmak istediğim "API"ı tasarlamak için biraz zaman harcadım. + +Çeşitli fikirleri en popüler Python editörlerinde test ettim: PyCharm, VS Code, Jedi tabanlı editörler. + +Bu test, en son <a href="https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools" class="external-link" target="_blank">Python Developer Survey</a>'ine göre, kullanıcıların yaklaşık %80'inin kullandığı editörleri kapsıyor. + +Bu da demek oluyor ki **FastAPI**, Python geliştiricilerinin %80'inin kullandığı editörlerle test edildi. Ve diğer editörlerin çoğu benzer şekilde çalıştığından, avantajları neredeyse tüm editörlerde çalışacaktır. + +Bu şekilde, kod tekrarını mümkün olduğunca azaltmak, her yerde <abbr title="Otomatik Tamamlama: auto-complete, autocompletion, IntelliSense">otomatik tamamlama</abbr>, tip ve hata kontrollerine sahip olmak için en iyi yolları bulabildim. + +Hepsi, tüm geliştiriciler için en iyi geliştirme deneyimini sağlayacak şekilde. + +## Gereksinimler + +Çeşitli alternatifleri test ettikten sonra, avantajlarından dolayı <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">**Pydantic**</a>'i kullanmaya karar verdim. + +Sonra, JSON Schema ile tamamen uyumlu olmasını sağlamak, kısıtlama bildirimlerini tanımlamanın farklı yollarını desteklemek ve birkaç editördeki testlere dayanarak editör desteğini (tip kontrolleri, otomatik tamamlama) geliştirmek için katkıda bulundum. + +Geliştirme sırasında, diğer ana gereksinim olan <a href="https://www.starlette.io/" class="external-link" target="_blank">**Starlette**</a>'e de katkıda bulundum. + +## Geliştirme + +**FastAPI**'ı oluşturmaya başladığımda, parçaların çoğu zaten yerindeydi, tasarım tanımlanmıştı, gereksinimler ve araçlar hazırdı, standartlar ve tanımlamalar hakkındaki bilgi net ve tazeydi. + +## Gelecek + +Şimdiye kadar, **FastAPI**'ın fikirleriyle birçok kişiye faydalı olduğu apaçık ortada. + +Birçok kullanım durumuna daha iyi uyduğu için, önceki alternatiflerin yerine seçiliyor. + +Ben ve ekibim dahil, birçok geliştirici ve ekip projelerinde **FastAPI**'ya bağlı. + +Tabi, geliştirilecek birçok özellik ve iyileştirme mevcut. + +**FastAPI**'ın önünde harika bir gelecek var. + +[Yardımlarınız](help-fastapi.md){.internal-link target=_blank} çok değerlidir. From 1b01cbe0927d3dc7ababf4d5dd52cc0c4874e296 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 15:03:50 +0000 Subject: [PATCH 1785/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 cf9a031ac..b6ac779a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Add Turkish translation for `docs/tr/docs/history-design-future.md`. PR [#11012](https://github.com/tiangolo/fastapi/pull/11012) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/resources/index.md`. PR [#11020](https://github.com/tiangolo/fastapi/pull/11020) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/how-to/index.md`. PR [#11021](https://github.com/tiangolo/fastapi/pull/11021) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add German translation for `docs/de/docs/tutorial/query-params.md`. PR [#10293](https://github.com/tiangolo/fastapi/pull/10293) by [@nilslindemann](https://github.com/nilslindemann). From d693d0a980eb7aaa3b98ed7731057543493c014c Mon Sep 17 00:00:00 2001 From: Luccas Mateus <Luccasmmg@gmail.com> Date: Thu, 25 Jan 2024 12:05:24 -0300 Subject: [PATCH 1786/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Portuguese=20tra?= =?UTF-8?q?nslation=20for=20`docs/pt/docs/tutorial/schema-extra-example.md?= =?UTF-8?q?`=20(#4065)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/pt/docs/tutorial/schema-extra-example.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/pt/docs/tutorial/schema-extra-example.md diff --git a/docs/pt/docs/tutorial/schema-extra-example.md b/docs/pt/docs/tutorial/schema-extra-example.md new file mode 100644 index 000000000..0355450fa --- /dev/null +++ b/docs/pt/docs/tutorial/schema-extra-example.md @@ -0,0 +1,109 @@ +# Declare um exemplo dos dados da requisição + +Você pode declarar exemplos dos dados que a sua aplicação pode receber. + +Aqui estão várias formas de se fazer isso. + +## `schema_extra` do Pydantic + +Você pode declarar um `example` para um modelo Pydantic usando `Config` e `schema_extra`, conforme descrito em <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Documentação do Pydantic: Schema customization</a>: + +```Python hl_lines="15-23" +{!../../../docs_src/schema_extra_example/tutorial001.py!} +``` + +Essas informações extras serão adicionadas como se encontram no **JSON Schema** de resposta desse modelo e serão usadas na documentação da API. + +!!! tip "Dica" + Você pode usar a mesma técnica para estender o JSON Schema e adicionar suas próprias informações extras de forma personalizada. + + Por exemplo, você pode usar isso para adicionar metadados para uma interface de usuário de front-end, etc. + +## `Field` de argumentos adicionais + +Ao usar `Field ()` com modelos Pydantic, você também pode declarar informações extras para o **JSON Schema** passando quaisquer outros argumentos arbitrários para a função. + +Você pode usar isso para adicionar um `example` para cada campo: + +```Python hl_lines="4 10-13" +{!../../../docs_src/schema_extra_example/tutorial002.py!} +``` + +!!! warning "Atenção" + Lembre-se de que esses argumentos extras passados ​​não adicionarão nenhuma validação, apenas informações extras, para fins de documentação. + +## `example` e `examples` no OpenAPI + +Ao usar quaisquer dos: + +* `Path()` +* `Query()` +* `Header()` +* `Cookie()` +* `Body()` +* `Form()` +* `File()` + +você também pode declarar um dado `example` ou um grupo de `examples` com informações adicionais que serão adicionadas ao **OpenAPI**. + +### `Body` com `example` + +Aqui nós passamos um `example` dos dados esperados por `Body()`: + +```Python hl_lines="21-26" +{!../../../docs_src/schema_extra_example/tutorial003.py!} +``` + +### Exemplo na UI da documentação + +Com qualquer um dos métodos acima, os `/docs` vão ficar assim: + +<img src="/img/tutorial/body-fields/image01.png"> + +### `Body` com vários `examples` + +Alternativamente ao único `example`, você pode passar `examples` usando um `dict` com **vários examples**, cada um com informações extras que serão adicionadas no **OpenAPI** também. + +As chaves do `dict` identificam cada exemplo, e cada valor é outro `dict`. + +Cada `dict` de exemplo específico em `examples` pode conter: + +* `summary`: Pequena descrição do exemplo. +* `description`: Uma descrição longa que pode conter texto em Markdown. +* `value`: O próprio exemplo mostrado, ex: um `dict`. +* `externalValue`: alternativa ao `value`, uma URL apontando para o exemplo. Embora isso possa não ser suportado por tantas ferramentas quanto `value`. + +```Python hl_lines="22-48" +{!../../../docs_src/schema_extra_example/tutorial004.py!} +``` + +### Exemplos na UI da documentação + +Com `examples` adicionado a `Body()`, os `/docs` vão ficar assim: + +<img src="/img/tutorial/body-fields/image02.png"> + +## Detalhes técnicos + +!!! warning "Atenção" + Esses são detalhes muito técnicos sobre os padrões **JSON Schema** e **OpenAPI**. + + Se as ideias explicadas acima já funcionam para você, isso pode ser o suficiente, e você provavelmente não precisa desses detalhes, fique à vontade para pular. + +Quando você adiciona um exemplo dentro de um modelo Pydantic, usando `schema_extra` ou` Field(example="something") `esse exemplo é adicionado ao **JSON Schema** para esse modelo Pydantic. + +E esse **JSON Schema** do modelo Pydantic está incluído no **OpenAPI** da sua API e, em seguida, é usado na UI da documentação. + +O **JSON Schema** na verdade não tem um campo `example` nos padrões. Versões recentes do JSON Schema definem um campo <a href="https://json-schema.org/draft/2019-09/json-schema-validation.html#rfc.section.9.5" class="external-link" target="_blank">`examples`</a>, mas o OpenAPI 3.0.3 é baseado numa versão mais antiga do JSON Schema que não tinha `examples`. + +Por isso, o OpenAPI 3.0.3 definiu o seu próprio <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-20" class="external-link" target="_blank">`example`</a> para a versão modificada do **JSON Schema** que é usada, para o mesmo próposito (mas é apenas `example` no singular, não `examples`), e é isso que é usado pela UI da documentação da API(usando o Swagger UI). + +Portanto, embora `example` não seja parte do JSON Schema, é parte da versão customizada do JSON Schema usada pelo OpenAPI, e é isso que vai ser usado dentro da UI de documentação. + +Mas quando você usa `example` ou `examples` com qualquer um dos outros utilitários (`Query()`, `Body()`, etc.) esses exemplos não são adicionados ao JSON Schema que descreve esses dados (nem mesmo para versão própria do OpenAPI do JSON Schema), eles são adicionados diretamente à declaração da *operação de rota* no OpenAPI (fora das partes do OpenAPI que usam o JSON Schema). + +Para `Path()`, `Query()`, `Header()`, e `Cookie()`, o `example` e `examples` são adicionados a <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object" class="external-link" target="_blank">definição do OpenAPI, dentro do `Parameter Object` (na especificação)</a>. + +E para `Body()`, `File()`, e `Form()`, o `example` e `examples` são de maneira equivalente adicionados para a <a href="https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject" class="external-link" target="_blank">definição do OpenAPI, dentro do `Request Body Object`, no campo `content`, no `Media Type Object` (na especificação)</a>. + +Por outro lado, há uma versão mais recente do OpenAPI: **3.1.0**, lançada recentemente. Baseado no JSON Schema mais recente e a maioria das modificações da versão customizada do OpenAPI do JSON Schema são removidas, em troca dos recursos das versões recentes do JSON Schema, portanto, todas essas pequenas diferenças são reduzidas. No entanto, a UI do Swagger atualmente não oferece suporte a OpenAPI 3.1.0, então, por enquanto, é melhor continuar usando as opções acima. From 92feb735317996ef81763da370efa92c61a6d925 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 25 Jan 2024 15:09:59 +0000 Subject: [PATCH 1787/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b6ac779a8..a2b2199a8 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -46,6 +46,7 @@ hide: ### Translations +* 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/schema-extra-example.md`. PR [#4065](https://github.com/tiangolo/fastapi/pull/4065) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Turkish translation for `docs/tr/docs/history-design-future.md`. PR [#11012](https://github.com/tiangolo/fastapi/pull/11012) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/resources/index.md`. PR [#11020](https://github.com/tiangolo/fastapi/pull/11020) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/how-to/index.md`. PR [#11021](https://github.com/tiangolo/fastapi/pull/11021) by [@hasansezertasan](https://github.com/hasansezertasan). From 7ee93035515abbbb32c0dd14e6b9e9464a8fe50b Mon Sep 17 00:00:00 2001 From: Donny Peeters <46660228+Donnype@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:08:54 +0100 Subject: [PATCH 1788/1881] =?UTF-8?q?=F0=9F=93=9D=20Add=20External=20Link:?= =?UTF-8?q?=2010=20Tips=20for=20adding=20SQLAlchemy=20to=20FastAPI=20(#110?= =?UTF-8?q?36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/external_links.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/data/external_links.yml b/docs/en/data/external_links.yml index 44b064fe9..58e7acefe 100644 --- a/docs/en/data/external_links.yml +++ b/docs/en/data/external_links.yml @@ -1,5 +1,9 @@ Articles: English: + - author: Donny Peeters + author_link: https://github.com/Donnype + link: https://bitestreams.com/blog/fastapi-sqlalchemy/ + title: 10 Tips for adding SQLAlchemy to FastAPI - author: Jessica Temporal author_link: https://jtemporal.com/socials link: https://jtemporal.com/tips-on-migrating-from-flask-to-fastapi-and-vice-versa/ From e196abad3ed64d0b25054e1d7a9ed558cb9b3294 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:09:13 +0000 Subject: [PATCH 1789/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a2b2199a8..a778d7fbf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Add External Link: 10 Tips for adding SQLAlchemy to FastAPI. PR [#11036](https://github.com/tiangolo/fastapi/pull/11036) by [@Donnype](https://github.com/Donnype). * 📝 Add External Link: Tips on migrating from Flask to FastAPI and vice-versa. PR [#11029](https://github.com/tiangolo/fastapi/pull/11029) by [@jtemporal](https://github.com/jtemporal). * 📝 Deprecate old tutorials: Peewee, Couchbase, encode/databases. PR [#10979](https://github.com/tiangolo/fastapi/pull/10979) by [@tiangolo](https://github.com/tiangolo). * ✏️ Fix typo in `fastapi/security/oauth2.py`. PR [#10972](https://github.com/tiangolo/fastapi/pull/10972) by [@RafalSkolasinski](https://github.com/RafalSkolasinski). From 2378cfd56ab87738edd16e97e11716c8d9a80b9b Mon Sep 17 00:00:00 2001 From: Kani Kim <kkh5428@gmail.com> Date: Sat, 27 Jan 2024 18:11:46 +0900 Subject: [PATCH 1790/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`/docs/ko/docs/tutorial/body.md`=20(#11000)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/body.md | 213 ++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/ko/docs/tutorial/body.md diff --git a/docs/ko/docs/tutorial/body.md b/docs/ko/docs/tutorial/body.md new file mode 100644 index 000000000..931728572 --- /dev/null +++ b/docs/ko/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# 요청 본문 + +클라이언트(브라우저라고 해봅시다)로부터 여러분의 API로 데이터를 보내야 할 때, **요청 본문**으로 보냅니다. + +**요청** 본문은 클라이언트에서 API로 보내지는 데이터입니다. **응답** 본문은 API가 클라이언트로 보내는 데이터입니다. + +여러분의 API는 대부분의 경우 **응답** 본문을 보내야 합니다. 하지만 클라이언트는 **요청** 본문을 매 번 보낼 필요가 없습니다. + +**요청** 본문을 선언하기 위해서 모든 강력함과 이점을 갖춘 <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> 모델을 사용합니다. + +!!! 정보 + 데이터를 보내기 위해, (좀 더 보편적인) `POST`, `PUT`, `DELETE` 혹은 `PATCH` 중에 하나를 사용하는 것이 좋습니다. + + `GET` 요청에 본문을 담아 보내는 것은 명세서에 정의되지 않은 행동입니다. 그럼에도 불구하고, 이 방식은 아주 복잡한/극한의 사용 상황에서만 FastAPI에 의해 지원됩니다. + + `GET` 요청에 본문을 담는 것은 권장되지 않기에, Swagger UI같은 대화형 문서에서는 `GET` 사용시 담기는 본문에 대한 문서를 표시하지 않으며, 중간에 있는 프록시는 이를 지원하지 않을 수도 있습니다. + +## Pydantic의 `BaseModel` 임포트 + +먼저 `pydantic`에서 `BaseModel`를 임포트해야 합니다: + +=== "Python 3.10+" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +## 여러분의 데이터 모델 만들기 + +`BaseModel`를 상속받은 클래스로 여러분의 데이터 모델을 선언합니다. + +모든 어트리뷰트에 대해 표준 파이썬 타입을 사용합니다: + +=== "Python 3.10+" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +쿼리 매개변수를 선언할 때와 같이, 모델 어트리뷰트가 기본 값을 가지고 있어도 이는 필수가 아닙니다. 그외에는 필수입니다. 그저 `None`을 사용하여 선택적으로 만들 수 있습니다. + +예를 들면, 위의 이 모델은 JSON "`object`" (혹은 파이썬 `dict`)을 다음과 같이 선언합니다: + +```JSON +{ + "name": "Foo", + "description": "선택적인 설명란", + "price": 45.2, + "tax": 3.5 +} +``` + +...`description`과 `tax`는 (기본 값이 `None`으로 되어 있어) 선택적이기 때문에, 이 JSON "`object`"는 다음과 같은 상황에서도 유효합니다: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## 매개변수로서 선언하기 + +여러분의 *경로 작동*에 추가하기 위해, 경로 매개변수 그리고 쿼리 매개변수에서 선언했던 것과 같은 방식으로 선언하면 됩니다. + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +...그리고 만들어낸 모델인 `Item`으로 타입을 선언합니다. + +## 결과 + +위에서의 단순한 파이썬 타입 선언으로, **FastAPI**는 다음과 같이 동작합니다: + +* 요청의 본문을 JSON으로 읽어 들입니다. +* (필요하다면) 대응되는 타입으로 변환합니다. +* 데이터를 검증합니다. + * 만약 데이터가 유효하지 않다면, 정확히 어떤 것이 그리고 어디에서 데이터가 잘 못 되었는지 지시하는 친절하고 명료한 에러를 반환할 것입니다. +* 매개변수 `item`에 포함된 수신 데이터를 제공합니다. + * 함수 내에서 매개변수를 `Item` 타입으로 선언했기 때문에, 모든 어트리뷰트와 그에 대한 타입에 대한 편집기 지원(완성 등)을 또한 받을 수 있습니다. +* 여러분의 모델을 위한 <a href="https://json-schema.org" class="external-link" target="_blank">JSON 스키마</a> 정의를 생성합니다. 여러분의 프로젝트에 적합하다면 여러분이 사용하고 싶은 곳 어디에서나 사용할 수 있습니다. +* 이러한 스키마는, 생성된 OpenAPI 스키마 일부가 될 것이며, 자동 문서화 <abbr title="사용자 인터페이스">UI</abbr>에 사용됩니다. + +## 자동 문서화 + +모델의 JSON 스키마는 생성된 OpenAPI 스키마에 포함되며 대화형 API 문서에 표시됩니다: + +<img src="/img/tutorial/body/image01.png"> + +이를 필요로 하는 각각의 *경로 작동*내부의 API 문서에도 사용됩니다: + +<img src="/img/tutorial/body/image02.png"> + +## 편집기 지원 + +편집기에서, 함수 내에서 타입 힌트와 완성을 어디서나 (만약 Pydantic model 대신에 `dict`을 받을 경우 나타나지 않을 수 있습니다) 받을 수 있습니다: + +<img src="/img/tutorial/body/image03.png"> + +잘못된 타입 연산에 대한 에러 확인도 받을 수 있습니다: + +<img src="/img/tutorial/body/image04.png"> + +단순한 우연이 아닙니다. 프레임워크 전체가 이러한 디자인을 중심으로 설계되었습니다. + +그 어떤 실행 전에, 모든 편집기에서 작동할 수 있도록 보장하기 위해 설계 단계에서 혹독하게 테스트되었습니다. + +이를 지원하기 위해 Pydantic 자체에서 몇몇 변경점이 있었습니다. + +이전 스크린샷은 <a href="https://code.visualstudio.com" class="external-link" target="_blank">Visual Studio Code</a>를 찍은 것입니다. + +하지만 똑같은 편집기 지원을 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>에서 받을 수 있거나, 대부분의 다른 편집기에서도 받을 수 있습니다: + +<img src="/img/tutorial/body/image05.png"> + +!!! 팁 + 만약 <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a>를 편집기로 사용한다면, <a href="https://github.com/koxudaxi/pydantic-pycharm-plugin/" class="external-link" target="_blank">Pydantic PyCharm Plugin</a>을 사용할 수 있습니다. + + 다음 사항을 포함해 Pydantic 모델에 대한 편집기 지원을 향상시킵니다: + + * 자동 완성 + * 타입 확인 + * 리팩토링 + * 검색 + * 점검 + +## 모델 사용하기 + +함수 안에서 모델 객체의 모든 어트리뷰트에 직접 접근 가능합니다: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +## 요청 본문 + 경로 매개변수 + +경로 매개변수와 요청 본문을 동시에 선언할 수 있습니다. + +**FastAPI**는 경로 매개변수와 일치하는 함수 매개변수가 **경로에서 가져와야 한다**는 것을 인지하며, Pydantic 모델로 선언된 그 함수 매개변수는 **요청 본문에서 가져와야 한다**는 것을 인지할 것입니다. + +=== "Python 3.10+" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +## 요청 본문 + 경로 + 쿼리 매개변수 + +**본문**, **경로** 그리고 **쿼리** 매개변수 모두 동시에 선언할 수도 있습니다. + +**FastAPI**는 각각을 인지하고 데이터를 옳바른 위치에 가져올 것입니다. + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +함수 매개변수는 다음을 따라서 인지하게 됩니다: + +* 만약 매개변수가 **경로**에도 선언되어 있다면, 이는 경로 매개변수로 사용될 것입니다. +* 만약 매개변수가 (`int`, `float`, `str`, `bool` 등과 같은) **유일한 타입**으로 되어있으면, **쿼리** 매개변수로 해석될 것입니다. +* 만약 매개변수가 **Pydantic 모델** 타입으로 선언되어 있으면, 요청 **본문**으로 해석될 것입니다. + +!!! 참고 + FastAPI는 `q`의 값이 필요없음을 알게 될 것입니다. 기본 값이 `= None`이기 때문입니다. + + `Union[str, None]`에 있는 `Union`은 FastAPI에 의해 사용된 것이 아니지만, 편집기로 하여금 더 나은 지원과 에러 탐지를 지원할 것입니다. + +## Pydantic없이 + +만약 Pydantic 모델을 사용하고 싶지 않다면, **Body** 매개변수를 사용할 수도 있습니다. [Body - 다중 매개변수: 본문에 있는 유일한 값](body-multiple-params.md#singular-values-in-body){.internal-link target=_blank} 문서를 확인하세요. From 00395f3eeb0bc2627e9829ff0d62e70548c19a7f Mon Sep 17 00:00:00 2001 From: Kani Kim <kkh5428@gmail.com> Date: Sat, 27 Jan 2024 18:12:44 +0900 Subject: [PATCH 1791/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/dependencies/index.md`=20(#?= =?UTF-8?q?10989)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/dependencies/index.md | 353 ++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 docs/ko/docs/tutorial/dependencies/index.md diff --git a/docs/ko/docs/tutorial/dependencies/index.md b/docs/ko/docs/tutorial/dependencies/index.md new file mode 100644 index 000000000..d5d113860 --- /dev/null +++ b/docs/ko/docs/tutorial/dependencies/index.md @@ -0,0 +1,353 @@ +# 의존성 + +**FastAPI**는 아주 강력하지만 직관적인 **<abbr title="컴포넌트, 자원, 제공자, 서비스, 인젝터블로 알려져 있습니다">의존성 주입</abbr>** 시스템을 가지고 있습니다. + +이는 사용하기 아주 쉽게 설계했으며, 어느 개발자나 다른 컴포넌트와 **FastAPI**를 쉽게 통합할 수 있도록 만들었습니다. + +## "의존성 주입"은 무엇입니까? + +**"의존성 주입"**은 프로그래밍에서 여러분의 코드(이 경우, 경로 동작 함수)가 작동하고 사용하는 데 필요로 하는 것, 즉 "의존성"을 선언할 수 있는 방법을 의미합니다. + +그 후에, 시스템(이 경우 FastAPI)은 여러분의 코드가 요구하는 의존성을 제공하기 위해 필요한 모든 작업을 처리합니다.(의존성을 "주입"합니다) + +이는 여러분이 다음과 같은 사항을 필요로 할 때 매우 유용합니다: + +* 공용된 로직을 가졌을 경우 (같은 코드 로직이 계속 반복되는 경우). +* 데이터베이스 연결을 공유하는 경우. +* 보안, 인증, 역할 요구 사항 등을 강제하는 경우. +* 그리고 많은 다른 사항... + +이 모든 사항을 할 때 코드 반복을 최소화합니다. + +## 첫번째 단계 + +아주 간단한 예제를 봅시다. 너무 간단할 것이기에 지금 당장은 유용하지 않을 수 있습니다. + +하지만 이를 통해 **의존성 주입** 시스템이 어떻게 작동하는지에 중점을 둘 것입니다. + +### 의존성 혹은 "디펜더블" 만들기 + +의존성에 집중해 봅시다. + +*경로 작동 함수*가 가질 수 있는 모든 매개변수를 갖는 단순한 함수입니다: + +=== "Python 3.10+" + + ```Python hl_lines="8-9" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-12" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="6-7" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="8-11" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +이게 다입니다. + +**단 두 줄입니다**. + +그리고, 이 함수는 여러분의 모든 *경로 작동 함수*가 가지고 있는 것과 같은 형태와 구조를 가지고 있습니다. + +여러분은 이를 "데코레이터"가 없는 (`@app.get("/some-path")`가 없는) *경로 작동 함수*라고 생각할 수 있습니다. + +그리고 여러분이 원하는 무엇이든 반환할 수 있습니다. + +이 경우, 이 의존성은 다음과 같은 경우를 기대합니다: + +* 선택적인 쿼리 매개변수 `q`, `str`을 자료형으로 가집니다. +* 선택적인 쿼리 매개변수 `skip`, `int`를 자료형으로 가지며 기본 값은 `0`입니다. +* 선택적인 쿼리 매개변수 `limit`,`int`를 자료형으로 가지며 기본 값은 `100`입니다. + +그 후 위의 값을 포함한 `dict` 자료형으로 반환할 뿐입니다. + +!!! 정보 + FastAPI는 0.95.0 버전부터 `Annotated`에 대한 지원을 (그리고 이를 사용하기 권장합니다) 추가했습니다. + + 옛날 버전을 가지고 있는 경우, `Annotated`를 사용하려 하면 에러를 맞이하게 될 것입니다. + + `Annotated`를 사용하기 전에 최소 0.95.1로 [FastAPI 버전 업그레이드](../../deployment/versions.md#upgrading-the-fastapi-versions){.internal-link target=_blank}를 확실하게 하세요. + +### `Depends` 불러오기 + +=== "Python 3.10+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="1" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="3" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +### "의존자"에 의존성 명시하기 + +*경로 작동 함수*의 매개변수로 `Body`, `Query` 등을 사용하는 방식과 같이 새로운 매개변수로 `Depends`를 사용합니다: + +=== "Python 3.10+" + + ```Python hl_lines="13 18" + {!> ../../../docs_src/dependencies/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="16 21" + {!> ../../../docs_src/dependencies/tutorial001_an.py!} + ``` + +=== "Python 3.10+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="11 16" + {!> ../../../docs_src/dependencies/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ Annotated가 없는 경우" + + !!! 팁 + 가능하다면 `Annotated`가 달린 버전을 권장합니다. + + ```Python hl_lines="15 20" + {!> ../../../docs_src/dependencies/tutorial001.py!} + ``` + +비록 `Body`, `Query` 등을 사용하는 것과 같은 방식으로 여러분의 함수의 매개변수에 있는 `Depends`를 사용하지만, `Depends`는 약간 다르게 작동합니다. + +`Depends`에 단일 매개변수만 전달했습니다. + +이 매개변수는 함수같은 것이어야 합니다. + +여러분은 직접 **호출하지 않았습니다** (끝에 괄호를 치지 않았습니다), 단지 `Depends()`에 매개변수로 넘겨 줬을 뿐입니다. + +그리고 그 함수는 *경로 작동 함수*가 작동하는 것과 같은 방식으로 매개변수를 받습니다. + +!!! 팁 + 여러분은 다음 장에서 함수를 제외하고서, "다른 것들"이 어떻게 의존성으로 사용되는지 알게 될 것입니다. + +새로운 요청이 도착할 때마다, **FastAPI**는 다음을 처리합니다: + +* 올바른 매개변수를 가진 의존성("디펜더블") 함수를 호출합니다. +* 함수에서 결과를 받아옵니다. +* *경로 작동 함수*에 있는 매개변수에 그 결과를 할당합니다 + +```mermaid +graph TB + +common_parameters(["common_parameters"]) +read_items["/items/"] +read_users["/users/"] + +common_parameters --> read_items +common_parameters --> read_users +``` + +이렇게 하면 공용 코드를 한번만 적어도 되며, **FastAPI**는 *경로 작동*을 위해 이에 대한 호출을 처리합니다. + +!!! 확인 + 특별한 클래스를 만들지 않아도 되며, 이러한 것 혹은 비슷한 종류를 **FastAPI**에 "등록"하기 위해 어떤 곳에 넘겨주지 않아도 됩니다. + + 단순히 `Depends`에 넘겨주기만 하면 되며, **FastAPI**는 나머지를 어찌할지 알고 있습니다. + +## `Annotated`인 의존성 공유하기 + +위의 예제에서 몇몇 작은 **코드 중복**이 있다는 것을 보았을 겁니다. + +`common_parameters()`의존을 사용해야 한다면, 타입 명시와 `Depends()`와 함께 전체 매개변수를 적어야 합니다: + +```Python +commons: Annotated[dict, Depends(common_parameters)] +``` + +하지만 `Annotated`를 사용하고 있기에, `Annotated` 값을 변수에 저장하고 여러 장소에서 사용할 수 있습니다: + +=== "Python 3.10+" + + ```Python hl_lines="12 16 21" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14 18 23" + {!> ../../../docs_src/dependencies/tutorial001_02_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15 19 24" + {!> ../../../docs_src/dependencies/tutorial001_02_an.py!} + ``` + +!!! 팁 + 이는 그저 표준 파이썬이고 "type alias"라고 부르며 사실 **FastAPI**에 국한되는 것은 아닙니다. + + 하지만, `Annotated`를 포함하여, **FastAPI**가 파이썬 표준을 기반으로 하고 있기에, 이를 여러분의 코드 트릭으로 사용할 수 있습니다. 😎 + +이 의존성은 계속해서 예상한대로 작동할 것이며, **제일 좋은 부분**은 **타입 정보가 보존된다는 것입니다**. 즉 여러분의 편집기가 **자동 완성**, **인라인 에러** 등을 계속해서 제공할 수 있다는 것입니다. `mypy`같은 다른 도구도 마찬가지입니다. + +이는 특히 **많은 *경로 작동***에서 **같은 의존성**을 계속해서 사용하는 **거대 코드 기반**안에서 사용하면 유용할 것입니다. + +## `async`하게, 혹은 `async`하지 않게 + +의존성이 (*경로 작동 함수*에서 처럼 똑같이) **FastAPI**에 의해 호출될 수 있으며, 함수를 정의할 때 동일한 규칙이 적용됩니다. + +`async def`을 사용하거나 혹은 일반적인 `def`를 사용할 수 있습니다. + +그리고 일반적인 `def` *경로 작동 함수* 안에 `async def`로 의존성을 선언할 수 있으며, `async def` *경로 작동 함수* 안에 `def`로 의존성을 선언하는 등의 방법이 있습니다. + +아무 문제 없습니다. **FastAPI**는 무엇을 할지 알고 있습니다. + +!!! 참고 + 잘 모르시겠다면, [Async: *"In a hurry?"*](../../async.md){.internal-link target=_blank} 문서에서 `async`와 `await`에 대해 확인할 수 있습니다. + +## OpenAPI와 통합 + +모든 요청 선언, 검증과 의존성(및 하위 의존성)에 대한 요구 사항은 동일한 OpenAPI 스키마에 통합됩니다. + +따라서 대화형 문서에 이러한 의존성에 대한 모든 정보 역시 포함하고 있습니다: + +<img src="/img/tutorial/dependencies/image01.png"> + +## 간단한 사용법 + +이를 보면, *경로 작동 함수*는 *경로*와 *작동*이 매칭되면 언제든지 사용되도록 정의되었으며, **FastAPI**는 올바른 매개변수를 가진 함수를 호출하고 해당 요청에서 데이터를 추출합니다. + +사실, 모든 (혹은 대부분의) 웹 프레임워크는 이와 같은 방식으로 작동합니다. + +여러분은 이러한 함수들을 절대 직접 호출하지 않습니다. 프레임워크(이 경우 **FastAPI**)에 의해 호출됩니다. + +의존성 주입 시스템과 함께라면 **FastAPI**에게 여러분의 *경로 작동 함수*가 실행되기 전에 실행되어야 하는 무언가에 여러분의 *경로 작동 함수* 또한 "의존"하고 있음을 알릴 수 있으며, **FastAPI**는 이를 실행하고 결과를 "주입"할 것입니다. + +"의존성 주입"이라는 동일한 아이디어에 대한 다른 일반적인 용어는 다음과 같습니다: + +* 리소스 +* 제공자 +* 서비스 +* 인젝터블 +* 컴포넌트 + +## **FastAPI** 플러그인 + +통합과 "플러그인"은 **의존성 주입** 시스템을 사용하여 구축할 수 있습니다. 하지만 실제로 **"플러그인"을 만들 필요는 없습니다**, 왜냐하면 의존성을 사용함으로써 여러분의 *경로 작동 함수*에 통합과 상호 작용을 무한대로 선언할 수 있기 때문입니다. + +그리고 "말 그대로", 그저 필요로 하는 파이썬 패키지를 임포트하고 단 몇 줄의 코드로 여러분의 API 함수와 통합함으로써, 의존성을 아주 간단하고 직관적인 방법으로 만들 수 있습니다. + +관계형 및 NoSQL 데이터베이스, 보안 등, 이에 대한 예시를 다음 장에서 볼 수 있습니다. + +## **FastAPI** 호환성 + +의존성 주입 시스템의 단순함은 **FastAPI**를 다음과 같은 요소들과 호환할 수 있게 합니다: + +* 모든 관계형 데이터베이스 +* NoSQL 데이터베이스 +* 외부 패키지 +* 외부 API +* 인증 및 권한 부여 시스템 +* API 사용 모니터링 시스템 +* 응답 데이터 주입 시스템 +* 기타 등등. + +## 간편하고 강력하다 + +계층적인 의존성 주입 시스템은 정의하고 사용하기 쉽지만, 여전히 매우 강력합니다. + +여러분은 스스로를 의존하는 의존성을 정의할 수 있습니다. + +끝에는, 계층적인 나무로 된 의존성이 만들어지며, 그리고 **의존성 주입** 시스템은 (하위 의존성도 마찬가지로) 이러한 의존성들을 처리하고 각 단계마다 결과를 제공합니다(주입합니다). + +예를 들면, 여러분이 4개의 API 엔드포인트(*경로 작동*)를 가지고 있다고 해봅시다: + +* `/items/public/` +* `/items/private/` +* `/users/{user_id}/activate` +* `/items/pro/` + +그 다음 각각에 대해 그저 의존성과 하위 의존성을 사용하여 다른 권한 요구 사항을 추가할 수 있을 겁니다: + +```mermaid +graph TB + +current_user(["current_user"]) +active_user(["active_user"]) +admin_user(["admin_user"]) +paying_user(["paying_user"]) + +public["/items/public/"] +private["/items/private/"] +activate_user["/users/{user_id}/activate"] +pro_items["/items/pro/"] + +current_user --> active_user +active_user --> admin_user +active_user --> paying_user + +current_user --> public +active_user --> private +admin_user --> activate_user +paying_user --> pro_items +``` + +## **OpenAPI**와의 통합 + +이 모든 의존성은 각각의 요구사항을 선언하는 동시에, *경로 작동*에 매개변수, 검증 등을 추가합니다. + +**FastAPI**는 이 모든 것을 OpenAPI 스키마에 추가할 것이며, 이를 통해 대화형 문서 시스템에 나타날 것입니다. From b110cd62a029d0672bd7bff12e81518da265d0dd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:12:59 +0000 Subject: [PATCH 1792/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a778d7fbf..60a7ae361 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -47,6 +47,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `/docs/ko/docs/tutorial/body.md`. PR [#11000](https://github.com/tiangolo/fastapi/pull/11000) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/schema-extra-example.md`. PR [#4065](https://github.com/tiangolo/fastapi/pull/4065) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Turkish translation for `docs/tr/docs/history-design-future.md`. PR [#11012](https://github.com/tiangolo/fastapi/pull/11012) by [@hasansezertasan](https://github.com/hasansezertasan). * 🌐 Add Turkish translation for `docs/tr/docs/resources/index.md`. PR [#11020](https://github.com/tiangolo/fastapi/pull/11020) by [@hasansezertasan](https://github.com/hasansezertasan). From 2ccc0ccf01d57c3ca5d6900ba0433cc089516466 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:13:27 +0000 Subject: [PATCH 1793/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 60a7ae361..a87e4d044 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -47,6 +47,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/index.md`. PR [#10989](https://github.com/tiangolo/fastapi/pull/10989) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `/docs/ko/docs/tutorial/body.md`. PR [#11000](https://github.com/tiangolo/fastapi/pull/11000) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/schema-extra-example.md`. PR [#4065](https://github.com/tiangolo/fastapi/pull/4065) by [@luccasmmg](https://github.com/luccasmmg). * 🌐 Add Turkish translation for `docs/tr/docs/history-design-future.md`. PR [#11012](https://github.com/tiangolo/fastapi/pull/11012) by [@hasansezertasan](https://github.com/hasansezertasan). From 4c0d12497fe75a180792f75d3a944d6d33d8836b Mon Sep 17 00:00:00 2001 From: Alper <itsc0508@gmail.com> Date: Sat, 27 Jan 2024 12:14:47 +0300 Subject: [PATCH 1794/1881] :globe_with_meridians: Add Turkish translation for `docs/tr/docs/alternatives.md` (#10502) --- docs/tr/docs/alternatives.md | 409 +++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 docs/tr/docs/alternatives.md diff --git a/docs/tr/docs/alternatives.md b/docs/tr/docs/alternatives.md new file mode 100644 index 000000000..9c69503c9 --- /dev/null +++ b/docs/tr/docs/alternatives.md @@ -0,0 +1,409 @@ +# Alternatifler, İlham Kaynakları ve Karşılaştırmalar + +**FastAPI**'ya neler ilham verdi? Diğer alternatiflerle karşılaştırıldığında farkları neler? **FastAPI** diğer alternatiflerinden neler öğrendi? + +## Giriş + +Başkalarının daha önceki çalışmaları olmasaydı, **FastAPI** var olmazdı. + +Geçmişte oluşturulan pek çok araç **FastAPI**'a ilham kaynağı olmuştur. + +Yıllardır yeni bir framework oluşturmaktan kaçınıyordum. Başlangıçta **FastAPI**'ın çözdüğü sorunları çözebilmek için pek çok farklı framework, <abbr title="Eklenti: Plug-In">eklenti</abbr> ve araç kullanmayı denedim. + +Ancak bir noktada, geçmişteki diğer araçlardan en iyi fikirleri alarak bütün bu çözümleri kapsayan, ayrıca bütün bunları Python'ın daha önce mevcut olmayan özelliklerini (Python 3.6+ ile gelen <abbr title="Tip belirteçleri: Type Hints">tip belirteçleri</abbr>) kullanarak yapan bir şey üretmekten başka seçenek kalmamıştı. + +## Daha Önce Geliştirilen Araçlar + +### <a href="https://www.djangoproject.com/" class="external-link" target="_blank">Django</a> + +Django geniş çapta güvenilen, Python ekosistemindeki en popüler web framework'üdür. Instagram gibi sistemleri geliştirmede kullanılmıştır. + +MySQL ve PostgreSQL gibi ilişkisel veritabanlarıyla nispeten sıkı bir şekilde bağlantılıdır. Bu nedenle Couchbase, MongoDB ve Cassandra gibi NoSQL veritabanlarını ana veritabanı motoru olarak kullanmak pek de kolay değil. + +Modern ön uçlarda (React, Vue.js ve Angular gibi) veya diğer sistemler (örneğin <abbr title="Nesnelerin interneti: IoT (Internet of Things)">nesnelerin interneti</abbr> cihazları) tarafından kullanılan API'lar yerine arka uçta HTML üretmek için oluşturuldu. + +### <a href="https://www.django-rest-framework.org/" class="external-link" target="_blank">Django REST Framework</a> + +Django REST framework'ü, Django'nun API kabiliyetlerini arttırmak için arka planda Django kullanan esnek bir araç grubu olarak oluşturuldu. + +Üstelik Mozilla, Red Hat ve Eventbrite gibi pek çok şirket tarafından kullanılıyor. + +**Otomatik API dökümantasyonu**nun ilk örneklerinden biri olduğu için, **FastAPI** arayışına ilham veren ilk fikirlerden biri oldu. + +!!! note "Not" + Django REST Framework'ü, aynı zamanda **FastAPI**'ın dayandığı Starlette ve Uvicorn'un da yaratıcısı olan Tom Christie tarafından geliştirildi. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + Kullanıcılar için otomatik API dökümantasyonu sunan bir web arayüzüne sahip olmalı. + +### <a href="https://flask.palletsprojects.com" class="external-link" target="_blank">Flask</a> + +Flask bir <abbr title="Mikro Framework: Micro Framework">mikro framework</abbr> olduğundan Django gibi framework'lerin aksine veritabanı entegrasyonu gibi Django ile gelen pek çok özelliği direkt barındırmaz. + +Sağladığı basitlik ve esneklik NoSQL veritabanlarını ana veritabanı sistemi olarak kullanmak gibi şeyler yapmaya olanak sağlar. + +Yapısı oldukça basit olduğundan öğrenmesi de nispeten basittir, tabii dökümantasyonu bazı noktalarda biraz teknik hale geliyor. + +Ayrıca Django ile birlikte gelen veritabanı, kullanıcı yönetimi ve diğer pek çok özelliğe ihtiyaç duymayan uygulamalarda da yaygın olarak kullanılıyor. Ancak bu tür özelliklerin pek çoğu <abbr title="Eklentiler: Plug-Ins">eklentiler</abbr> ile eklenebiliyor. + +Uygulama parçalarının böyle ayrılıyor oluşu ve istenilen özelliklerle genişletilebilecek bir <abbr title="Mikro Framework: Micro Framework">mikro framework</abbr> olmak tam da benim istediğim bir özellikti. + +Flask'ın basitliği göz önünde bulundurulduğu zaman, API geliştirmek için iyi bir seçim gibi görünüyordu. Sıradaki şey ise Flask için bir "Django REST Framework"! + +!!! check "**FastAPI**'a nasıl ilham verdi?" + Gereken araçları ve parçaları birleştirip eşleştirmeyi kolaylaştıracak bir mikro framework olmalı. + + Basit ve kullanması kolay bir <abbr title="Yönlendirme: Routing">yönlendirme sistemine</abbr> sahip olmalı. + +### <a href="https://requests.readthedocs.io" class="external-link" target="_blank">Requests</a> + +**FastAPI** aslında **Requests**'in bir alternatifi değil. İkisininde kapsamı oldukça farklı. + +Aslında Requests'i bir FastAPI uygulamasının *içinde* kullanmak daha olağan olurdu. + +Ama yine de, FastAPI, Requests'ten oldukça ilham aldı. + +**Requests**, <abbr title="API (Application Programming Interface): Uygulama Programlama Arayüzü">API'lar</abbr> ile bir istemci olarak *etkileşime geçmeyi* sağlayan bir kütüphaneyken **FastAPI** bir sunucu olarak <abbr title="API (Application Programming Interface): Uygulama Programlama Arayüzü">API'lar</abbr> oluşturmaya yarar. + +Öyle ya da böyle zıt uçlarda olmalarına rağmen birbirlerini tamamlıyorlar. + +Requests oldukça basit ve sezgisel bir tasarıma sahip, kullanması da mantıklı varsayılan değerlerle oldukça kolay. Ama aynı zamanda çok güçlü ve gayet özelleştirilebilir. + +Bu yüzden resmi web sitede de söylendiği gibi: + +> Requests, tüm zamanların en çok indirilen Python <abbr title="Paket: Package">paketlerinden</abbr> biridir. + +Kullanım şekli oldukça basit. Örneğin bir `GET` isteği yapmak için aşağıdaki yeterli: + +```Python +response = requests.get("http://example.com/some/url") +``` + +Bunun FastAPI'deki API <abbr title="Yol İşlemi: Path Operation">*yol işlemi*</abbr> şöyle görünür: + +```Python hl_lines="1" +@app.get("/some/url") +def read_url(): + return {"message": "Hello World!"} +``` + +`requests.get(...)` ile `@app.get(...)` arasındaki benzerliklere bakın. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + * Basit ve sezgisel bir API'ya sahip olmalı. + * HTTP metot isimlerini (işlemlerini) anlaşılır olacak bir şekilde, direkt kullanmalı. + * Mantıklı varsayılan değerlere ve buna rağmen güçlü bir özelleştirme desteğine sahip olmalı. + +### <a href="https://swagger.io/" class="external-link" target="_blank">Swagger</a> / <a href="https://github.com/OAI/OpenAPI-Specification/" class="external-link" target="_blank">OpenAPI</a> + +Benim Django REST Framework'ünden istediğim ana özellik otomatik API dökümantasyonuydu. + +Daha sonra API'ları dökümanlamak için Swagger adında JSON (veya JSON'un bir uzantısı olan YAML'ı) kullanan bir standart olduğunu buldum. + +Üstelik Swagger API'ları için zaten halihazırda oluşturulmuş bir web arayüzü vardı. Yani, bir API için Swagger dökümantasyonu oluşturmak bu arayüzü otomatik olarak kullanabilmek demekti. + +Swagger bir noktada Linux Foundation'a verildi ve adı OpenAPI olarak değiştirildi. + +İşte bu yüzden versiyon 2.0 hakkında konuşurken "Swagger", versiyon 3 ve üzeri için ise "OpenAPI" adını kullanmak daha yaygın. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + API spesifikasyonları için özel bir şema yerine bir <abbr title="Open Standard: Açık Standart, Açık kaynak olarak yayınlanan standart">açık standart</abbr> benimseyip kullanmalı. + + Ayrıca standarda bağlı kullanıcı arayüzü araçlarını entegre etmeli: + + * <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> + * <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> + + Yukarıdaki ikisi oldukça popüler ve istikrarlı olduğu için seçildi, ancak hızlı bir araştırma yaparak **FastAPI** ile kullanabileceğiniz pek çok OpenAPI alternatifi arayüz bulabilirsiniz. + +### Flask REST framework'leri + +Pek çok Flask REST framework'ü var, fakat bunları biraz araştırdıktan sonra pek çoğunun artık geliştirilmediğini ve göze batan bazı sorunlarının olduğunu gördüm. + +### <a href="https://marshmallow.readthedocs.io/en/stable/" class="external-link" target="_blank">Marshmallow</a> + +API sistemlerine gereken ana özelliklerden biri de koddan veriyi alıp ağ üzerinde gönderilebilecek bir şeye çevirmek, yani veri <abbr title="Dönüşüm: serialization, marshalling olarak da biliniyor">dönüşümü</abbr>. Bu işleme veritabanındaki veriyi içeren bir objeyi JSON objesine çevirmek, `datetime` objelerini metinlere çevirmek gibi örnekler verilebilir. + +API'lara gereken bir diğer büyük özellik ise veri doğrulamadır, yani verinin çeşitli parametrelere bağlı olarak doğru ve tutarlı olduğundan emin olmaktır. Örneğin bir alanın `int` olmasına karar verdiniz, daha sonra rastgele bir metin değeri almasını istemezsiniz. Bu özellikle sisteme dışarıdan gelen veri için kullanışlı bir özellik oluyor. + +Bir veri doğrulama sistemi olmazsa bütün bu kontrolleri koda dökerek kendiniz yapmak zorunda kalırdınız. + +Marshmallow bu özellikleri sağlamak için geliştirilmişti. Benim de geçmişte oldukça sık kullandığım harika bir kütüphanedir. + +Ama... Python'un tip belirteçleri gelmeden önce oluşturulmuştu. Yani her <abbr title="Verilerin nasıl oluşturulması gerektiğinin tanımı">şemayı</abbr> tanımlamak için Marshmallow'un sunduğu spesifik araçları ve sınıfları kullanmanız gerekiyordu. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + Kod kullanarak otomatik olarak veri tipini ve veri doğrulamayı belirten "şemalar" tanımlamalı. + +### <a href="https://webargs.readthedocs.io/en/latest/" class="external-link" target="_blank">Webargs</a> + +API'ların ihtiyacı olan bir diğer önemli özellik ise gelen isteklerdeki verileri Python objelerine <abbr title="Parsing: dönüştürmek, ayrıştırmak, çözümlemek">ayrıştırabilmektir</abbr>. + +Webargs, Flask gibi bir kaç framework'ün üzerinde bunu sağlamak için geliştirilen bir araçtır. + +Veri doğrulama için arka planda Marshmallow kullanıyor, hatta aynı geliştiriciler tarafından oluşturuldu. + +Webargs da harika bir araç ve onu da geçmişte henüz **FastAPI** yokken çok kullandım. + +!!! info "Bilgi" + Webargs aynı Marshmallow geliştirileri tarafından oluşturuldu. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + Gelen istek verisi için otomatik veri doğrulamaya sahip olmalı. + +### <a href="https://apispec.readthedocs.io/en/stable/" class="external-link" target="_blank">APISpec</a> + +Marshmallow ve Webargs <abbr title="Eklenti: Plug-In">eklentiler</abbr> olarak; veri doğrulama, ayrıştırma ve dönüştürmeyi sağlıyor. + +Ancak dökümantasyondan hala ses seda yok. Daha sonrasında ise APISpec geldi. + +APISpec pek çok framework için bir <abbr title="Eklenti: Plug-In">eklenti</abbr> olarak kullanılıyor (Starlette için de bir <abbr title="Eklenti: Plug-In">eklentisi</abbr> var). + +Şemanın tanımını <abbr title="Route: HTTP isteğinin gittiği yol">yol</abbr>'a istek geldiğinde çalışan her bir fonksiyonun <abbr title="Döküman dizesi: docstring">döküman dizesinin</abbr> içine YAML formatında olacak şekilde yazıyorsunuz o da OpenAPI şemaları üretiyor. + +Flask, Starlette, Responder ve benzerlerinde bu şekilde çalışıyor. + +Fakat sonrasında yine mikro sözdizimi problemiyle karşılaşıyoruz. Python metinlerinin içinde koskoca bir YAML oluyor. + +Editör bu konuda pek yardımcı olamaz. Üstelik eğer parametreleri ya da Marshmallow şemalarını değiştirip YAML kodunu güncellemeyi unutursak artık döküman geçerliliğini yitiriyor. + +!!! info "Bilgi" + APISpec de aynı Marshmallow geliştiricileri tarafından oluşturuldu. + +!!! check "**FastAPI**'a nasıl ilham verdi?" + API'lar için açık standart desteği olmalı (OpenAPI gibi). + +### <a href="https://flask-apispec.readthedocs.io/en/latest/" class="external-link" target="_blank">Flask-apispec</a> + +Flask-apispec ise Webargs, Marshmallow ve APISpec'i birbirine bağlayan bir Flask <abbr title="Eklenti: Plug-In">eklentisi</abbr>. + +Webargs ve Marshmallow'daki bilgiyi APISpec ile otomatik OpenAPI şemaları üretmek için kullanıyor. + +Hak ettiği değeri görmeyen, harika bir araç. Piyasadaki çoğu Flask <abbr title="Eklenti: Plug-In">eklentisinden</abbr> çok daha popüler olmalı. Hak ettiği değeri görmüyor oluşunun sebebi ise dökümantasyonun çok kısa ve soyut olması olabilir. + +Böylece Flask-apispec, Python döküman dizilerine YAML gibi farklı bir syntax yazma sorununu çözmüş oldu. + +**FastAPI**'ı geliştirene dek benim favori arka uç kombinasyonum Flask'in yanında Marshmallow ve Webargs ile birlikte Flask-apispec idi. + +Bunu kullanmak, bir kaç <abbr title="full-stack: Hem ön uç hem de arka uç geliştirme">full-stack</abbr> Flask projesi oluşturucusunun yaratılmasına yol açtı. Bunlar benim (ve bir kaç harici ekibin de) şimdiye kadar kullandığı asıl <abbr title="stack: Projeyi geliştirirken kullanılan araçlar dizisi">stack</abbr>ler: + +* <a href="https://github.com/tiangolo/full-stack" class="external-link" target="_blank">https://github.com/tiangolo/full-stack</a> +* <a href="https://github.com/tiangolo/full-stack-flask-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchbase</a> +* <a href="https://github.com/tiangolo/full-stack-flask-couchdb" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-flask-couchdb</a> + +Aynı full-stack üreticiler [**FastAPI** Proje Üreticileri](project-generation.md){.internal-link target=_blank}'nin de temelini oluşturdu. + +!!! info "Bilgi" + Flask-apispec de aynı Marshmallow geliştiricileri tarafından üretildi. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Veri dönüşümü ve veri doğrulamayı tanımlayan kodu kullanarak otomatik olarak OpenAPI şeması oluşturmalı. + +### <a href="https://nestjs.com/" class="external-link" target="_blank">NestJS</a> (and <a href="https://angular.io/" class="external-link" target="_blank">Angular</a>) + +Bu Python teknolojisi bile değil. NestJS, Angulardan ilham almış olan bir JavaScript (TypeScript) NodeJS framework'ü. + +Flask-apispec ile yapılabileceklere nispeten benzeyen bir şey elde ediyor. + +Angular 2'den ilham alan, içine gömülü bir <abbr title="Bağımlılık enjeksiyonu: Dependency Injection">bağımlılık enjeksiyonu</abbr> sistemi var. Bildiğim diğer tüm bağımlılık enjeksiyonu sistemlerinde olduğu gibi"<abbr title="Injectable: dependency injection sistemi tarafından enjekte edilecek dependency (bağımlılık)">bağımlılık</abbr>"ları önceden kaydetmenizi gerektiriyor. Böylece projeyi daha detaylı hale getiriyor ve kod tekrarını da arttırıyor. + +Parametreler TypeScript tipleri (Python tip belirteçlerine benzer) ile açıklandığından editör desteği oldukça iyi. + +Ama TypeScript verileri kod JavaScript'e derlendikten sonra korunmadığından, bunlara dayanarak aynı anda veri doğrulaması, veri dönüşümü ve dökümantasyon tanımlanamıyor. Bundan ve bazı tasarım tercihlerinden dolayı veri doğrulaması, dönüşümü ve otomatik şema üretimi için pek çok yere dekorator eklemek gerekiyor. Bu da projeyi oldukça detaylandırıyor. + +İç içe geçen derin modelleri pek iyi işleyemiyor. Yani eğer istekteki JSON gövdesi derin bir JSON objesiyse düzgün bir şekilde dökümante edilip doğrulanamıyor. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Güzel bir editör desteği için Python tiplerini kullanmalı. + + Güçlü bir bağımlılık enjeksiyon sistemine sahip olmalı. Kod tekrarını minimuma indirecek bir yol bulmalı. + +### <a href="https://sanic.readthedocs.io/en/latest/" class="external-link" target="_blank">Sanic</a> + +Sanic, `asyncio`'ya dayanan son derece hızlı Python kütüphanelerinden biriydi. Flask'a epey benzeyecek şekilde geliştirilmişti. + +!!! note "Teknik detaylar" + İçerisinde standart Python `asyncio` döngüsü yerine <a href="https://github.com/MagicStack/uvloop" class="external-link" target="_blank">`uvloop`</a> kullanıldı. Hızının asıl kaynağı buydu. + + Uvicorn ve Starlette'e ilham kaynağı olduğu oldukça açık, şu anda ikisi de açık karşılaştırmalarda Sanicten daha hızlı gözüküyor. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Uçuk performans sağlayacak bir yol bulmalı. + + Tam da bu yüzden **FastAPI** Starlette'e dayanıyor, çünkü Starlette şu anda kullanılabilir en hızlı framework. (üçüncü parti karşılaştırmalı testlerine göre) + +### <a href="https://falconframework.org/" class="external-link" target="_blank">Falcon</a> + +Falcon ise bir diğer yüksek performanslı Python framework'ü. Minimal olacak şekilde Hug gibi diğer framework'lerin temeli olabilmek için tasarlandı. + +İki parametre kabul eden fonksiyonlar şeklinde tasarlandı, biri "istek" ve diğeri ise "cevap". Sonra isteğin çeşitli kısımlarını **okuyor**, cevaba ise bir şeyler **yazıyorsunuz**. Bu tasarımdan dolayı istek parametrelerini ve gövdelerini standart Python tip belirteçlerini kullanarak fonksiyon parametreleriyle belirtmek mümkün değil. + +Yani veri doğrulama, veri dönüştürme ve dökümantasyonun hepsi kodda yer almalı, otomatik halledemiyoruz. Ya da Falcon üzerine bir framework olarak uygulanmaları gerekiyor, aynı Hug'da olduğu gibi. Bu ayrım Falcon'un tasarımından esinlenen, istek ve cevap objelerini parametre olarak işleyen diğer kütüphanelerde de yer alıyor. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Harika bir performans'a sahip olmanın yollarını bulmalı. + + Hug ile birlikte (Hug zaten Falcon'a dayandığından) **FastAPI**'ın fonksiyonlarda `cevap` parametresi belirtmesinde ilham kaynağı oldu. + + FastAPI'da opsiyonel olmasına rağmen, daha çok header'lar, çerezler ve alternatif durum kodları belirlemede kullanılıyor. + +### <a href="https://moltenframework.com/" class="external-link" target="_blank">Molten</a> + +**FastAPI**'ı geliştirmenin ilk aşamalarında Molten'ı keşfettim. Pek çok ortak fikrimiz vardı: + +* Python'daki tip belirteçlerini baz alıyordu. +* Bunlara bağlı olarak veri doğrulaması ve dökümantasyon sağlıyordu. +* Bir <abbr title="Bağımlılık enjeksiyonu: Dependency Injection">bağımlılık enjeksiyonu</abbr> sistemi vardı. + +Veri doğrulama, veri dönüştürme ve dökümantasyon için Pydantic gibi bir üçüncü parti kütüphane kullanmıyor, kendi içerisinde bunlara sahip. Yani bu veri tipi tanımlarını tekrar kullanmak pek de kolay değil. + +Biraz daha detaylı ayarlamalara gerek duyuyor. Ayrıca <abbr title="ASGI (Asynchronous Server Gateway Interface): Asenkron Sunucu Ağ Geçidi Arabirimi, asenkron Python web uygulamaları geliştirmek için yeni standart.">ASGI</abbr> yerine <abbr title="WSGI (Web Server Gateway Interface): Web Sunucusu Ağ Geçidi Arabirimi, Pythonda senkron web uygulamaları geliştirmek için eski standart.">WSGI</abbr>'a dayanıyor. Yani Uvicorn, Starlette ve Sanic gibi araçların yüksek performansından faydalanacak şekilde tasarlanmamış. + +<abbr title="Bağımlılık enjeksiyonu: Dependency Injection">Bağımlılık enjeksiyonu</abbr> sistemi bağımlılıkların önceden kaydedilmesini ve sonrasında belirlenen veri tiplerine göre çözülmesini gerektiriyor. Yani spesifik bir tip, birden fazla bileşen ile belirlenemiyor. + +<abbr title="Route: HTTP isteğinin gittiği yol">Yol</abbr>'lar fonksiyonun üstünde endpoint'i işleyen dekoratörler yerine farklı yerlerde tanımlanan fonksiyonlarla belirlenir. Bu Flask (ve Starlette) yerine daha çok Django'nun yaklaşımına daha yakın bir metot. Bu, kodda nispeten birbiriyle sıkı ilişkili olan şeyleri ayırmaya sebep oluyor. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Model özelliklerinin "standart" değerlerini kullanarak veri tipleri için ekstra veri doğrulama koşulları tanımlamalı. Bu editör desteğini geliştiriyor ve daha önceden Pydantic'te yoktu. + + Bu aslında Pydantic'in de aynı doğrulama stiline geçmesinde ilham kaynağı oldu. Şu anda bütün bu özellikler Pydantic'in yapısında yer alıyor. + +### <a href="https://www.hug.rest/" class="external-link" target="_blank">Hug</a> + +Hug, Python tip belirteçlerini kullanarak API parametrelerinin tipini belirlemeyi uygulayan ilk framework'lerdendi. Bu, diğer araçlara da ilham kaynağı olan harika bir fikirdi. + +Tip belirlerken standart Python veri tipleri yerine kendi özel tiplerini kullandı, yine de bu ileriye dönük devasa bir adımdı. + +Hug ayrıca tüm API'ı JSON ile ifade eden özel bir şema oluşturan ilk framework'lerdendir. + +OpenAPI veya JSON Şeması gibi bir standarda bağlı değildi. Yani Swagger UI gibi diğer araçlarla entegre etmek kolay olmazdı. Ama yine de, bu oldukça yenilikçi bir fikirdi. + +Ayrıca ilginç ve çok rastlanmayan bir özelliği vardı: aynı framework'ü kullanarak hem API'lar hem de <abbr title="Command Line Tool (CLI): Komut satırı aracı">CLI</abbr>'lar oluşturmak mümkündü. + +Senkron çalışan Python web framework'lerinin standardına (WSGI) dayandığından dolayı Websocket'leri ve diğer şeyleri işleyemiyor, ancak yine de yüksek performansa sahip. + +!!! info "Bilgi" + Hug, Python dosyalarında bulunan dahil etme satırlarını otomatik olarak sıralayan harika bir araç olan <a href="https://github.com/timothycrosley/isort" class="external-link" target="_blank">`isort`</a>'un geliştiricisi Timothy Crosley tarafından geliştirildi. + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Hug, APIStar'ın çeşitli kısımlarında esin kaynağı oldu ve APIStar'la birlikte en umut verici bulduğum araçlardan biriydi. + + **FastAPI**, Python tip belirteçlerini kullanarak parametre belirlemede ve API'ı otomatık tanımlayan bir şema üretmede de Hug'a esinlendi. + + **FastAPI**'ın header ve çerez tanımlamak için fonksiyonlarda `response` parametresini belirtmesinde de Hug'dan ilham alındı. + +### <a href="https://github.com/encode/apistar" class="external-link" target="_blank">APIStar</a> (<= 0.5) + +**FastAPI**'ı geliştirmeye başlamadan hemen önce **APIStar** sunucusunu buldum. Benim aradığım şeylerin neredeyse hepsine sahipti ve harika bir tasarımı vardı. + +Benim şimdiye kadar gördüğüm Python tip belirteçlerini kullanarak parametre ve istekler belirlemeyi uygulayan ilk framework'lerdendi (Molten ve NestJS'den önce). APIStar'ı da aşağı yukarı Hug ile aynı zamanlarda buldum. Fakat APIStar OpenAPI standardını kullanıyordu. + +Farklı yerlerdeki tip belirteçlerine bağlı olarak otomatik veri doğrulama, veri dönüştürme ve OpenAPI şeması oluşturma desteği sunuyordu. + +Gövde şema tanımları Pydantic ile aynı Python tip belirteçlerini kullanmıyordu, biraz daha Marsmallow'a benziyordu. Dolayısıyla editör desteği de o kadar iyi olmazdı ama APIStar eldeki en iyi seçenekti. + +O dönemlerde karşılaştırmalarda en iyi performansa sahipti (yalnızca Starlette'e kaybediyordu). + +Başlangıçta otomatik API dökümantasyonu sunan bir web arayüzü yoktu, ama ben ona Swagger UI ekleyebileceğimi biliyordum. + +Bağımlılık enjeksiyon sistemi vardı. Yukarıda bahsettiğim diğer araçlar gibi bundaki sistem de bileşenlerin önceden kaydedilmesini gerektiriyordu. Yine de harika bir özellikti. + +Güvenlik entegrasyonu olmadığından dolayı APIStar'ı hiç bir zaman tam bir projede kullanamadım. Bu yüzden Flask-apispec'e bağlı full-stack proje üreticilerde sahip olduğum özellikleri tamamen değiştiremedim. Bu güvenlik entegrasyonunu ekleyen bir <abbr title="Pull request (PR): Git sistemlerinde projenin bir branch'ine yapılan değişikliğin sistemde diğer kullanıcılara ifade edilmesi">PR</abbr> oluşturmak da projelerim arasında yer alıyordu. + +Sonrasında ise projenin odağı değişti. + +Geliştiricinin Starlette'e odaklanması gerekince proje de artık bir API web framework'ü olmayı bıraktı. + +Artık APIStar, OpenAPI özelliklerini doğrulamak için bir dizi araç sunan bir proje haline geldi. + +!!! info "Bilgi" + APIStar, aşağıdaki projeleri de üreten Tom Christie tarafından geliştirildi: + + * Django REST Framework + * **FastAPI**'ın da dayandığı Starlette + * Starlette ve **FastAPI** tarafından da kullanılan Uvicorn + +!!! check "**FastAPI**'a nasıl ilham oldu?" + Var oldu. + + Aynı Python veri tipleriyle birden fazla şeyi belirleme (veri doğrulama, veri dönüştürme ve dökümantasyon), bir yandan da harika bir editör desteği sunma, benim muhteşem bulduğum bir fikirdi. + + Uzunca bir süre boyunca benzer bir framework arayıp pek çok farklı alternatifi denedikten sonra, APIStar en iyi seçenekti. + + Sonra APIStar bir sunucu olmayı bıraktı ve Starlette oluşturuldu. Starlette, böyle bir sunucu sistemi için daha iyi bir temel sunuyordu. Bu da **FastAPI**'ın son esin kaynağıydı. + + Ben bu önceki araçlardan öğrendiklerime dayanarak **FastAPI**'ın özelliklerini arttırıp geliştiriyor, <abbr title="Tip desteği (typing support): kod yapısında parametrelere, argümanlara ve objelerin özelliklerine veri tipi atamak">tip desteği</abbr> sistemi ve diğer kısımları iyileştiriyorum ancak yine de **FastAPI**'ı APIStar'ın "ruhani varisi" olarak görüyorum. + +## **FastAPI** Tarafından Kullanılanlar + +### <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a> + +Pydantic Python tip belirteçlerine dayanan; veri doğrulama, veri dönüştürme ve dökümantasyon tanımlamak (JSON Şema kullanarak) için bir kütüphanedir. + +Tip belirteçleri kullanıyor olması onu aşırı sezgisel yapıyor. + +Marshmallow ile karşılaştırılabilir. Ancak karşılaştırmalarda Marshmallowdan daha hızlı görünüyor. Aynı Python tip belirteçlerine dayanıyor ve editör desteği de harika. + +!!! check "**FastAPI** nerede kullanıyor?" + Bütün veri doğrulama, veri dönüştürme ve JSON Şemasına bağlı otomatik model dökümantasyonunu halletmek için! + + **FastAPI** yaptığı her şeyin yanı sıra bu JSON Şema verisini alıp daha sonra OpenAPI'ya yerleştiriyor. + +### <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a> + +Starlette hafif bir <abbr title="ASGI (Asynchronous Server Gateway Interface): Asenkron Sunucu Ağ Geçidi Arabirimi, asenkron Python web uygulamaları geliştirmek için yeni standart.">ASGI</abbr> framework'ü ve yüksek performanslı asyncio servisleri oluşturmak için ideal. + +Kullanımı çok kolay ve sezgisel, kolaylıkla genişletilebilecek ve modüler bileşenlere sahip olacak şekilde tasarlandı. + +Sahip olduğu bir kaç özellik: + +* Cidden etkileyici bir performans. +* WebSocket desteği. +* İşlem-içi arka plan görevleri. +* Başlatma ve kapatma olayları. +* HTTPX ile geliştirilmiş bir test istemcisi. +* CORS, GZip, Static Files ve Streaming cevapları desteği. +* Session ve çerez desteği. +* Kodun %100'ü test kapsamında. +* Kodun %100'ü tip belirteçleriyle desteklenmiştir. +* Yalnızca bir kaç zorunlu bağımlılığa sahip. + +Starlette şu anda test edilen en hızlı Python framework'ü. Yalnızca bir sunucu olan Uvicorn'a yeniliyor, o da zaten bir framework değil. + +Starlette bütün temel web mikro framework işlevselliğini sağlıyor. + +Ancak otomatik veri doğrulama, veri dönüştürme ve dökümantasyon sağlamyor. + +Bu, **FastAPI**'ın onun üzerine tamamen Python tip belirteçlerine bağlı olarak eklediği (Pydantic ile) ana şeylerden biri. **FastAPI** bunun yanında artı olarak bağımlılık enjeksiyonu sistemi, güvenlik araçları, OpenAPI şema üretimi ve benzeri özellikler de ekliyor. + +!!! note "Teknik Detaylar" + ASGI, Django'nun ana ekibi tarafından geliştirilen yeni bir "standart". Bir "Python standardı" (PEP) olma sürecinde fakat henüz bir standart değil. + + Bununla birlikte, halihazırda birçok araç tarafından bir "standart" olarak kullanılmakta. Bu, Uvicorn'u farklı ASGI sunucularıyla (Daphne veya Hypercorn gibi) değiştirebileceğiniz veya `python-socketio` gibi ASGI ile uyumlu araçları ekleyebileciğiniz için birlikte çalışılabilirliği büyük ölçüde arttırıyor. + +!!! check "**FastAPI** nerede kullanıyor?" + + Tüm temel web kısımlarında üzerine özellikler eklenerek kullanılmakta. + + `FastAPI` sınıfının kendisi direkt olarak `Starlette` sınıfını miras alıyor! + + Yani, Starlette ile yapabileceğiniz her şeyi, Starlette'in bir nevi güçlendirilmiş hali olan **FastAPI** ile doğrudan yapabilirsiniz. + +### <a href="https://www.uvicorn.org/" class="external-link" target="_blank">Uvicorn</a> + +Uvicorn, uvlook ile httptools üzerine kurulu ışık hzında bir ASGI sunucusudur. + +Bir web framework'ünden ziyade bir sunucudur, yani yollara bağlı yönlendirme yapmanızı sağlayan araçları yoktur. Bu daha çok Starlette (ya da **FastAPI**) gibi bir framework'ün sunucuya ek olarak sağladığı bir şeydir. + +Starlette ve **FastAPI** için tavsiye edilen sunucu Uvicorndur. + +!!! check "**FastAPI** neden tavsiye ediyor?" + **FastAPI** uygulamalarını çalıştırmak için ana web sunucusu Uvicorn! + + Gunicorn ile birleştirdiğinizde asenkron ve çoklu işlem destekleyen bir sunucu elde ediyorsunuz! + + Daha fazla detay için [Deployment](deployment/index.md){.internal-link target=_blank} bölümünü inceleyebilirsiniz. + +## Karşılaştırma ve Hız + +Uvicorn, Starlette ve FastAPI arasındakı farkı daha iyi anlamak ve karşılaştırma yapmak için [Kıyaslamalar](benchmarks.md){.internal-link target=_blank} bölümüne bakın! From 2f2a7ad361d2819671c1471fa15a9e5333651872 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:17:26 +0000 Subject: [PATCH 1795/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a87e4d044..6f44d597c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -47,6 +47,7 @@ hide: ### Translations +* :globe_with_meridians: Add Turkish translation for `docs/tr/docs/alternatives.md`. PR [#10502](https://github.com/tiangolo/fastapi/pull/10502) by [@alperiox](https://github.com/alperiox). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/index.md`. PR [#10989](https://github.com/tiangolo/fastapi/pull/10989) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `/docs/ko/docs/tutorial/body.md`. PR [#11000](https://github.com/tiangolo/fastapi/pull/11000) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Portuguese translation for `docs/pt/docs/tutorial/schema-extra-example.md`. PR [#4065](https://github.com/tiangolo/fastapi/pull/4065) by [@luccasmmg](https://github.com/luccasmmg). From 381751499254f5060a69012cb4b4d0e2bb939004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jun-Ah=20=EC=A4=80=EC=95=84?= <junah.dev@gmail.com> Date: Sat, 27 Jan 2024 18:28:49 +0900 Subject: [PATCH 1796/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/background-tasks.md`=20(#59?= =?UTF-8?q?10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/background-tasks.md | 102 ++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 docs/ko/docs/tutorial/background-tasks.md diff --git a/docs/ko/docs/tutorial/background-tasks.md b/docs/ko/docs/tutorial/background-tasks.md new file mode 100644 index 000000000..a951ead16 --- /dev/null +++ b/docs/ko/docs/tutorial/background-tasks.md @@ -0,0 +1,102 @@ +# 백그라운드 작업 + +FastAPI에서는 응답을 반환한 후에 실행할 백그라운드 작업을 정의할 수 있습니다. + +백그라운드 작업은 클라이언트가 응답을 받기 위해 작업이 완료될 때까지 기다릴 필요가 없기 때문에 요청 후에 발생해야하는 작업에 매우 유용합니다. + +이러한 작업에는 다음이 포함됩니다. + +* 작업을 수행한 후 전송되는 이메일 알림 + * 이메일 서버에 연결하고 이메일을 전송하는 것은 (몇 초 정도) "느린" 경향이 있으므로, 응답은 즉시 반환하고 이메일 알림은 백그라운드에서 전송하는 게 가능합니다. +* 데이터 처리: + * 예를 들어 처리에 오랜 시간이 걸리는 데이터를 받았을 때 "Accepted" (HTTP 202)을 반환하고, 백그라운드에서 데이터를 처리할 수 있습니다. + +## `백그라운드 작업` 사용 + +먼저 아래와 같이 `BackgroundTasks`를 임포트하고, `BackgroundTasks`를 _경로 동작 함수_ 에서 매개변수로 가져오고 정의합니다. + +```Python hl_lines="1 13" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +**FastAPI** 는 `BackgroundTasks` 개체를 생성하고, 매개 변수로 전달합니다. + +## 작업 함수 생성 + +백그라운드 작업으로 실행할 함수를 정의합니다. + +이것은 단순히 매개변수를 받을 수 있는 표준 함수일 뿐입니다. + +**FastAPI**는 이것이 `async def` 함수이든, 일반 `def` 함수이든 내부적으로 이를 올바르게 처리합니다. + +이 경우, 아래 작업은 파일에 쓰는 함수입니다. (이메일 보내기 시물레이션) + +그리고 이 작업은 `async`와 `await`를 사용하지 않으므로 일반 `def` 함수로 선언합니다. + +```Python hl_lines="6-9" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +## 백그라운드 작업 추가 + +_경로 동작 함수_ 내에서 작업 함수를 `.add_task()` 함수 통해 _백그라운드 작업_ 개체에 전달합니다. + +```Python hl_lines="14" +{!../../../docs_src/background_tasks/tutorial001.py!} +``` + +`.add_task()` 함수는 다음과 같은 인자를 받습니다 : + +- 백그라운드에서 실행되는 작업 함수 (`write_notification`). +- 작업 함수에 순서대로 전달되어야 하는 일련의 인자 (`email`). +- 작업 함수에 전달되어야하는 모든 키워드 인자 (`message="some notification"`). + +## 의존성 주입 + +`BackgroundTasks`를 의존성 주입 시스템과 함께 사용하면 _경로 동작 함수_, 종속성, 하위 종속성 등 여러 수준에서 BackgroundTasks 유형의 매개변수를 선언할 수 있습니다. + +**FastAPI**는 각 경우에 수행할 작업과 동일한 개체를 내부적으로 재사용하기에, 모든 백그라운드 작업이 함께 병합되고 나중에 백그라운드에서 실행됩니다. + +=== "Python 3.6 and above" + + ```Python hl_lines="13 15 22 25" + {!> ../../../docs_src/background_tasks/tutorial002.py!} + ``` + +=== "Python 3.10 and above" + + ```Python hl_lines="11 13 20 23" + {!> ../../../docs_src/background_tasks/tutorial002_py310.py!} + ``` + +이 예제에서는 응답이 반환된 후에 `log.txt` 파일에 메시지가 기록됩니다. + +요청에 쿼리가 있는 경우 백그라운드 작업의 로그에 기록됩니다. + +그리고 _경로 동작 함수_ 에서 생성된 또 다른 백그라운드 작업은 경로 매개 변수를 활용하여 사용하여 메시지를 작성합니다. + +## 기술적 세부사항 + +`BackgroundTasks` 클래스는 <a href="https://www.starlette.io/background/" class="external-link" target="_blank">`starlette.background`</a>에서 직접 가져옵니다. + +`BackgroundTasks` 클래스는 FastAPI에서 직접 임포트하거나 포함하기 때문에 실수로 `BackgroundTask` (끝에 `s`가 없음)을 임포트하더라도 starlette.background에서 `BackgroundTask`를 가져오는 것을 방지할 수 있습니다. + +(`BackgroundTask`가 아닌) `BackgroundTasks`를 사용하면, _경로 동작 함수_ 매개변수로 사용할 수 있게 되고 나머지는 **FastAPI**가 대신 처리하도록 할 수 있습니다. 이것은 `Request` 객체를 직접 사용하는 것과 같은 방식입니다. + +FastAPI에서 `BackgroundTask`를 단독으로 사용하는 것은 여전히 가능합니다. 하지만 객체를 코드에서 생성하고, 이 객체를 포함하는 Starlette `Response`를 반환해야 합니다. + +<a href="https://www.starlette.io/background/" class="external-link" target="_blank">`Starlette의 공식 문서`</a>에서 백그라운드 작업에 대한 자세한 내용을 확인할 수 있습니다. + +## 경고 + +만약 무거운 백그라운드 작업을 수행해야하고 동일한 프로세스에서 실행할 필요가 없는 경우 (예: 메모리, 변수 등을 공유할 필요가 없음) <a href="https://docs.celeryq.dev" class="external-link" target="_blank">`Celery`</a>와 같은 큰 도구를 사용하면 도움이 될 수 있습니다. + +RabbitMQ 또는 Redis와 같은 메시지/작업 큐 시스템 보다 복잡한 구성이 필요한 경향이 있지만, 여러 작업 프로세스를 특히 여러 서버의 백그라운드에서 실행할 수 있습니다. + +예제를 보시려면 [프로젝트 생성기](../project-generation.md){.internal-link target=\_blank} 를 참고하세요. 해당 예제에는 이미 구성된 `Celery`가 포함되어 있습니다. + +그러나 동일한 FastAPI 앱에서 변수 및 개체에 접근해야햐는 작은 백그라운드 수행이 필요한 경우 (예 : 알림 이메일 보내기) 간단하게 `BackgroundTasks`를 사용해보세요. + +## 요약 + +백그라운드 작업을 추가하기 위해 _경로 동작 함수_ 에 매개변수로 `BackgroundTasks`를 가져오고 사용합니다. From a67f9767a0d651bc296cda221c4c6685cbdeca6d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:30:03 +0000 Subject: [PATCH 1797/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6f44d597c..88db92d51 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -47,6 +47,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/background-tasks.md`. PR [#5910](https://github.com/tiangolo/fastapi/pull/5910) by [@junah201](https://github.com/junah201). * :globe_with_meridians: Add Turkish translation for `docs/tr/docs/alternatives.md`. PR [#10502](https://github.com/tiangolo/fastapi/pull/10502) by [@alperiox](https://github.com/alperiox). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/index.md`. PR [#10989](https://github.com/tiangolo/fastapi/pull/10989) by [@KaniKim](https://github.com/KaniKim). * 🌐 Add Korean translation for `/docs/ko/docs/tutorial/body.md`. PR [#11000](https://github.com/tiangolo/fastapi/pull/11000) by [@KaniKim](https://github.com/KaniKim). From d522cdcb7a9762acaf03b25dc1fa2e500751c228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 27 Jan 2024 10:39:50 +0100 Subject: [PATCH 1798/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20docs=20for=20B?= =?UTF-8?q?ehind=20a=20Proxy=20(#11038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/advanced/behind-a-proxy.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md index 01998cc91..4da2ddefc 100644 --- a/docs/en/docs/advanced/behind-a-proxy.md +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -18,7 +18,11 @@ In this case, the original path `/app` would actually be served at `/api/v1/app` Even though all your code is written assuming there's just `/app`. -And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`. +```Python hl_lines="6" +{!../../../docs_src/behind_a_proxy/tutorial001.py!} +``` + +And the proxy would be **"stripping"** the **path prefix** on the fly before transmitting the request to Uvicorn, keeping your application convinced that it is being served at `/app`, so that you don't have to update all your code to include the prefix `/api/v1`. Up to here, everything would work as normally. From 44645f882f02e98c6cb4e6d88ba035ab2966125a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 09:40:14 +0000 Subject: [PATCH 1799/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 88db92d51..ab5e6a425 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Tweak docs for Behind a Proxy. PR [#11038](https://github.com/tiangolo/fastapi/pull/11038) by [@tiangolo](https://github.com/tiangolo). * 📝 Add External Link: 10 Tips for adding SQLAlchemy to FastAPI. PR [#11036](https://github.com/tiangolo/fastapi/pull/11036) by [@Donnype](https://github.com/Donnype). * 📝 Add External Link: Tips on migrating from Flask to FastAPI and vice-versa. PR [#11029](https://github.com/tiangolo/fastapi/pull/11029) by [@jtemporal](https://github.com/jtemporal). * 📝 Deprecate old tutorials: Peewee, Couchbase, encode/databases. PR [#10979](https://github.com/tiangolo/fastapi/pull/10979) by [@tiangolo](https://github.com/tiangolo). From 23fc06dab919c9067f0f1970f18fb25345030801 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Sat, 27 Jan 2024 05:43:44 -0500 Subject: [PATCH 1800/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/newsletter.md`=20(#10922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/newsletter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/es/docs/newsletter.md diff --git a/docs/es/docs/newsletter.md b/docs/es/docs/newsletter.md new file mode 100644 index 000000000..f4dcfe155 --- /dev/null +++ b/docs/es/docs/newsletter.md @@ -0,0 +1,5 @@ +# Boletín de Noticias de FastAPI y amigos + +<iframe data-w-type="embedded" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="https://xr4n4.mjt.lu/wgt/xr4n4/hj5/form?c=40a44fa4" width="100%" style="height: 0;"></iframe> + +<script type="text/javascript" src="https://app.mailjet.com/pas-nc-embedded-v1.js"></script> From f4e2b6f451a0e7be73aa35bc95a0fae91041532e Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 10:44:06 +0000 Subject: [PATCH 1801/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 ab5e6a425..0759535bc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -48,6 +48,7 @@ hide: ### Translations +* 🌐 Add Spanish translation for `docs/es/docs/newsletter.md`. PR [#10922](https://github.com/tiangolo/fastapi/pull/10922) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/background-tasks.md`. PR [#5910](https://github.com/tiangolo/fastapi/pull/5910) by [@junah201](https://github.com/junah201). * :globe_with_meridians: Add Turkish translation for `docs/tr/docs/alternatives.md`. PR [#10502](https://github.com/tiangolo/fastapi/pull/10502) by [@alperiox](https://github.com/alperiox). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/dependencies/index.md`. PR [#10989](https://github.com/tiangolo/fastapi/pull/10989) by [@KaniKim](https://github.com/KaniKim). From 4b8c822c922d9d353238b5210b25bebb1e9e9252 Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Sat, 27 Jan 2024 05:51:32 -0500 Subject: [PATCH 1802/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Spanish=20tra?= =?UTF-8?q?nslation=20for=20`docs/es/docs/features.md`=20(#10884)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/features.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/es/docs/features.md b/docs/es/docs/features.md index d68791d63..1496628d1 100644 --- a/docs/es/docs/features.md +++ b/docs/es/docs/features.md @@ -1,3 +1,8 @@ +--- +hide: + - navigation +--- + # Características ## Características de FastAPI @@ -164,7 +169,6 @@ Con **FastAPI** obtienes todas las características de **Starlette** (porque Fas * Desempeño realmente impresionante. Es uno <a href="https://github.com/encode/starlette#performance" class="external-link" target="_blank"> de los frameworks de Python más rápidos, a la par con **NodeJS** y **Go**</a>. * Soporte para **WebSocket**. -* Soporte para **GraphQL**. * <abbr title="En español: tareas que se ejecutan en el fondo, sin frenar requests, en el mismo proceso. En ingles: In-process background tasks">Tareas en background</abbr>. * Eventos de startup y shutdown. * Cliente de pruebas construido con HTTPX. @@ -190,8 +194,6 @@ Con **FastAPI** obtienes todas las características de **Pydantic** (dado que Fa * Si sabes tipos de Python, sabes cómo usar Pydantic. * Interactúa bien con tu **<abbr title="en inglés: Integrated Development Environment, similar a editor de código">IDE</abbr>/<abbr title="Un programa que chequea errores en el código">linter</abbr>/cerebro**: * Porque las estructuras de datos de Pydantic son solo <abbr title='En español: ejemplares. Aunque a veces los llaman incorrectamente "instancias"'>instances</abbr> de clases que tu defines, el auto-completado, el linting, mypy y tu intuición deberían funcionar bien con tus datos validados. -* **Rápido**: - * En <a href="https://pydantic-docs.helpmanual.io/benchmarks/" class="external-link" target="_blank">benchmarks</a> Pydantic es más rápido que todas las otras <abbr title='Herramienta, paquete. A veces llamado "librería"'>libraries</abbr> probadas. * Valida **estructuras complejas**: * Usa modelos jerárquicos de modelos de Pydantic, `typing` de Python, `List` y `Dict`, etc. * Los validadores también permiten que se definan fácil y claramente schemas complejos de datos. Estos son chequeados y documentados como JSON Schema. From 8602873d1aefafc66ff69fb6df08d13549b7ce7f Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sat, 27 Jan 2024 10:51:51 +0000 Subject: [PATCH 1803/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0759535bc..2277843e0 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -48,6 +48,7 @@ hide: ### Translations +* 🌐 Update Spanish translation for `docs/es/docs/features.md`. PR [#10884](https://github.com/tiangolo/fastapi/pull/10884) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/newsletter.md`. PR [#10922](https://github.com/tiangolo/fastapi/pull/10922) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/background-tasks.md`. PR [#5910](https://github.com/tiangolo/fastapi/pull/5910) by [@junah201](https://github.com/junah201). * :globe_with_meridians: Add Turkish translation for `docs/tr/docs/alternatives.md`. PR [#10502](https://github.com/tiangolo/fastapi/pull/10502) by [@alperiox](https://github.com/alperiox). From 3b18f1bfc1069e5353c5dcbd6ba9c22063711fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 28 Jan 2024 10:53:45 +0100 Subject: [PATCH 1804/1881] =?UTF-8?q?=F0=9F=92=84=20Fix=20CSS=20breaking?= =?UTF-8?q?=20RTL=20languages=20(erroneously=20introduced=20by=20a=20previ?= =?UTF-8?q?ous=20RTL=20PR)=20(#11039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/css/custom.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/en/docs/css/custom.css b/docs/en/docs/css/custom.css index 187040792..386aa9d7e 100644 --- a/docs/en/docs/css/custom.css +++ b/docs/en/docs/css/custom.css @@ -136,10 +136,6 @@ code { display: inline-block; } -.md-content__inner h1 { - direction: ltr !important; -} - .illustration { margin-top: 2em; margin-bottom: 2em; From 04de371a3acf82a2434209c32225332bfca82978 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 09:54:03 +0000 Subject: [PATCH 1805/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2277843e0..2b443bb79 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -123,6 +123,7 @@ hide: ### Internal +* 💄 Fix CSS breaking RTL languages (erroneously introduced by a previous RTL PR). PR [#11039](https://github.com/tiangolo/fastapi/pull/11039) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Italian to `mkdocs.yml`. PR [#11016](https://github.com/tiangolo/fastapi/pull/11016) by [@alejsdev](https://github.com/alejsdev). * 🔨 Verify `mkdocs.yml` languages in CI, update `docs.py`. PR [#11009](https://github.com/tiangolo/fastapi/pull/11009) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update config in `label-approved.yml` to accept translations with 1 reviewer. PR [#11007](https://github.com/tiangolo/fastapi/pull/11007) by [@alejsdev](https://github.com/alejsdev). From c7111f67ec75fc9e1b8f5bddbbb97191404a26c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 28 Jan 2024 11:33:07 +0100 Subject: [PATCH 1806/1881] =?UTF-8?q?=F0=9F=93=9D=20Tweak=20wording=20in?= =?UTF-8?q?=20`help-fastapi.md`=20(#11040)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/help-fastapi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/help-fastapi.md b/docs/en/docs/help-fastapi.md index 71c580409..095fc8c58 100644 --- a/docs/en/docs/help-fastapi.md +++ b/docs/en/docs/help-fastapi.md @@ -51,7 +51,7 @@ You can: * Tell me how you use FastAPI (I love to hear that). * Hear when I make announcements or release new tools. * You can also <a href="https://twitter.com/fastapi" class="external-link" target="_blank">follow @fastapi on Twitter</a> (a separate account). -* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">Connect with me on **Linkedin**</a>. +* <a href="https://www.linkedin.com/in/tiangolo/" class="external-link" target="_blank">Follow me on **Linkedin**</a>. * Hear when I make announcements or release new tools (although I use Twitter more often 🤷‍♂). * Read what I write (or follow me) on <a href="https://dev.to/tiangolo" class="external-link" target="_blank">**Dev.to**</a> or <a href="https://medium.com/@tiangolo" class="external-link" target="_blank">**Medium**</a>. * Read other ideas, articles, and read about tools I have created. From 9fd7aa8abe9d8b0deb25f4014a19e547711d4bb6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 10:33:29 +0000 Subject: [PATCH 1807/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2b443bb79..251e16e4c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -22,6 +22,7 @@ hide: ### Docs +* 📝 Tweak wording in `help-fastapi.md`. PR [#11040](https://github.com/tiangolo/fastapi/pull/11040) by [@tiangolo](https://github.com/tiangolo). * 📝 Tweak docs for Behind a Proxy. PR [#11038](https://github.com/tiangolo/fastapi/pull/11038) by [@tiangolo](https://github.com/tiangolo). * 📝 Add External Link: 10 Tips for adding SQLAlchemy to FastAPI. PR [#11036](https://github.com/tiangolo/fastapi/pull/11036) by [@Donnype](https://github.com/Donnype). * 📝 Add External Link: Tips on migrating from Flask to FastAPI and vice-versa. PR [#11029](https://github.com/tiangolo/fastapi/pull/11029) by [@jtemporal](https://github.com/jtemporal). From 4d93299a57f3552b6c338169f3869212ed89bc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 28 Jan 2024 11:38:34 +0100 Subject: [PATCH 1808/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors,=20r?= =?UTF-8?q?emove=20Deta=20(#11041)?= 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/docs/deployment/cloud.md | 1 - docs/ko/docs/deployment/cloud.md | 1 - docs/zh/docs/deployment/cloud.md | 1 - 5 files changed, 7 deletions(-) diff --git a/README.md b/README.md index 2df5cba0b..764cd5a36 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ The key features are: <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> -<a href="https://www.deta.sh/?ref=fastapi" target="_blank" title="The launchpad for all your (team's) ideas"><img src="https://fastapi.tiangolo.com/img/sponsors/deta.svg"></a> <a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> <a href="https://github.com/deepset-ai/haystack/" target="_blank" title="Build powerful search from composable, open source building blocks"><img src="https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 121a3b761..bd5b86e44 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -21,9 +21,6 @@ gold: title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png silver: - - url: https://www.deta.sh/?ref=fastapi - title: The launchpad for all your (team's) ideas - img: https://fastapi.tiangolo.com/img/sponsors/deta.svg - url: https://training.talkpython.fm/fastapi-courses title: FastAPI video courses on demand from people you trust img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index b2836aeb4..29f0ad1f6 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -14,4 +14,3 @@ You might want to try their services and follow their guides: * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> -* <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> diff --git a/docs/ko/docs/deployment/cloud.md b/docs/ko/docs/deployment/cloud.md index f2b965a91..2d6938e20 100644 --- a/docs/ko/docs/deployment/cloud.md +++ b/docs/ko/docs/deployment/cloud.md @@ -14,4 +14,3 @@ * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> -* <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> diff --git a/docs/zh/docs/deployment/cloud.md b/docs/zh/docs/deployment/cloud.md index 398f61372..b086b7b6b 100644 --- a/docs/zh/docs/deployment/cloud.md +++ b/docs/zh/docs/deployment/cloud.md @@ -14,4 +14,3 @@ * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank" >Platform.sh</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> -* <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> From 52df4d0378859404eca24910a59b088f1a7af6ea Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 10:38:55 +0000 Subject: [PATCH 1809/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 251e16e4c..43c7ec244 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -124,6 +124,7 @@ hide: ### Internal +* 🔧 Update sponsors, remove Deta. PR [#11041](https://github.com/tiangolo/fastapi/pull/11041) by [@tiangolo](https://github.com/tiangolo). * 💄 Fix CSS breaking RTL languages (erroneously introduced by a previous RTL PR). PR [#11039](https://github.com/tiangolo/fastapi/pull/11039) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Italian to `mkdocs.yml`. PR [#11016](https://github.com/tiangolo/fastapi/pull/11016) by [@alejsdev](https://github.com/alejsdev). * 🔨 Verify `mkdocs.yml` languages in CI, update `docs.py`. PR [#11009](https://github.com/tiangolo/fastapi/pull/11009) by [@tiangolo](https://github.com/tiangolo). From 38f8181fdc2796c7499f77da11ebf3849c1fe9d9 Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Mon, 29 Jan 2024 02:00:42 +0800 Subject: [PATCH 1810/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/docker.md`=20(#10296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/deployment/docker.md | 728 ++++++++++++++++++++++++++++++ 1 file changed, 728 insertions(+) create mode 100644 docs/zh/docs/deployment/docker.md diff --git a/docs/zh/docs/deployment/docker.md b/docs/zh/docs/deployment/docker.md new file mode 100644 index 000000000..0f8906704 --- /dev/null +++ b/docs/zh/docs/deployment/docker.md @@ -0,0 +1,728 @@ +# 容器中的 FastAPI - Docker + +部署 FastAPI 应用程序时,常见的方法是构建 **Linux 容器镜像**。 通常使用 <a href="https://www.docker.com/" class="external-link" target="_blank">**Docker**</a> 完成。 然后,你可以通过几种可能的方式之一部署该容器镜像。 + +使用 Linux 容器有几个优点,包括**安全性**、**可复制性**、**简单性**等。 + +!!! tip + 赶时间并且已经知道这些东西了? 跳转到下面的 [`Dockerfile` 👇](#为-fastapi-构建-docker-镜像)。 + + +<details> +<summary>Dockerfile Preview 👀</summary> + +```Dockerfile +FROM python:3.9 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +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"] +``` + +</details> + +## 什么是容器 + +容器(主要是 Linux 容器)是一种非常**轻量级**的打包应用程序的方式,其包括所有依赖项和必要的文件,同时它们可以和同一系统中的其他容器(或者其他应用程序/组件)相互隔离。 + +Linux 容器使用宿主机(如物理服务器、虚拟机、云服务器等)的Linux 内核运行。 这意味着它们非常轻量(与模拟整个操作系统的完整虚拟机相比)。 + +通过这样的方式,容器消耗**很少的资源**,与直接运行进程相当(虚拟机会消耗更多)。 + +容器的进程(通常只有一个)、文件系统和网络都运行在隔离的环境,这简化了部署、安全、开发等。 + +## 什么是容器镜像 + +**容器**是从**容器镜像**运行的。 + +容器镜像是容器中文件、环境变量和默认命令/程序的**静态**版本。 **静态**这里的意思是容器**镜像**还没有运行,只是打包的文件和元数据。 + +与存储静态内容的“**容器镜像**”相反,“**容器**”通常指正在运行的实例,即正在**执行的**。 + +当**容器**启动并运行时(从**容器镜像**启动),它可以创建或更改文件、环境变量等。这些更改将仅存在于该容器中,而不会持久化到底层的容器镜像中(不会保存到磁盘)。 + +容器镜像相当于**程序**和文件,例如 `python`命令 和某些文件 如`main.py`。 + +而**容器**本身(与**容器镜像**相反)是镜像的实际运行实例,相当于**进程**。 事实上,容器仅在有**进程运行**时才运行(通常它只是一个单独的进程)。 当容器中没有进程运行时,容器就会停止。 + + + +## 容器镜像 + +Docker 一直是创建和管理**容器镜像**和**容器**的主要工具之一。 + +还有一个公共 <a href="https://hub.docker.com/" class="external-link" target="_blank">Docker Hub</a> ,其中包含预制的 **官方容器镜像**, 适用于许多工具、环境、数据库和应用程序。 + +例如,有一个官方的 <a href="https://hub.docker.com/_/python" class="external-link" target="_blank">Python 镜像</a>。 + +还有许多其他镜像用于不同的需要(例如数据库),例如: + + +* <a href="https://hub.docker.com/_/postgres" class="external-link" target="_blank">PostgreSQL</a> +* <a href="https://hub.docker.com/_/mysql" class="external-link" target="_blank">MySQL</a> +* <a href="https://hub.docker.com/_/mongo" class="external-link" target="_blank">MongoDB</a> +* <a href="https://hub.docker.com/_/redis" class="external-link" target="_blank">Redis</a>, etc. + + +通过使用预制的容器镜像,可以非常轻松地**组合**并使用不同的工具。 例如,尝试一个新的数据库。 在大多数情况下,你可以使用**官方镜像**,只需为其配置环境变量即可。 + +这样,在许多情况下,你可以了解容器和 Docker,并通过许多不同的工具和组件重复使用这些知识。 + +因此,你可以运行带有不同内容的**多个容器**,例如数据库、Python 应用程序、带有 React 前端应用程序的 Web 服务器,并通过内部网络将它们连接在一起。 + +所有容器管理系统(如 Docker 或 Kubernetes)都集成了这些网络功能。 + +## 容器和进程 + +**容器镜像**通常在其元数据中包含启动**容器**时应运行的默认程序或命令以及要传递给该程序的参数。 与在命令行中的情况非常相似。 + +当 **容器** 启动时,它将运行该命令/程序(尽管你可以覆盖它并使其运行不同的命令/程序)。 + +只要**主进程**(命令或程序)在运行,容器就在运行。 + +容器通常有一个**单个进程**,但也可以从主进程启动子进程,这样你就可以在同一个容器中拥有**多个进程**。 + +但是,如果没有**至少一个正在运行的进程**,就不可能有一个正在运行的容器。 如果主进程停止,容器也会停止。 + + +## 为 FastAPI 构建 Docker 镜像 + +好吧,让我们现在构建一些东西! 🚀 + +我将向你展示如何基于 **官方 Python** 镜像 **从头开始** 为 FastAPI 构建 **Docker 镜像**。 + +这是你在**大多数情况**下想要做的,例如: + +* 使用 **Kubernetes** 或类似工具 +* 在 **Raspberry Pi** 上运行时 +* 使用可为你运行容器镜像的云服务等。 + +### 依赖项 + +你通常会在某个文件中包含应用程序的**依赖项**。 + +具体做法取决于你**安装**这些依赖时所使用的工具。 + +最常见的方法是创建一个`requirements.txt`文件,其中每行包含一个包名称和它的版本。 + +你当然也可以使用在[关于 FastAPI 版本](./versions.md){.internal-link target=_blank} 中讲到的方法来设置版本范围。 + +例如,你的`requirements.txt`可能如下所示: + + +``` +fastapi>=0.68.0,<0.69.0 +pydantic>=1.8.0,<2.0.0 +uvicorn>=0.15.0,<0.16.0 +``` + +你通常会使用`pip`安装这些依赖项: + +<div class="termy"> + +```console +$ pip install -r requirements.txt +---> 100% +Successfully installed fastapi pydantic uvicorn +``` + +</div> + +!!! info + 还有其他文件格式和工具来定义和安装依赖项。 + + 我将在下面的部分中向你展示一个使用 Poetry 的示例。 👇 + +### 创建 **FastAPI** 代码 + +* 创建`app`目录并进入。 +* 创建一个空文件`__init__.py`。 +* 创建一个 `main.py` 文件: + + + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +### Dockerfile + +现在在相同的project目录创建一个名为`Dockerfile`的文件: + +```{ .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. 从官方Python基础镜像开始。 + +2. 将当前工作目录设置为`/code`。 + + 这是我们放置`requirements.txt`文件和`app`目录的位置。 + +3. 将符合要求的文件复制到`/code`目录中。 + + 首先仅复制requirements.txt文件,而不复制其余代码。 + + 由于此文件**不经常更改**,Docker 将检测到它并在这一步中使用**缓存**,从而为下一步启用缓存。 + +4. 安装需求文件中的包依赖项。 + + `--no-cache-dir` 选项告诉 `pip` 不要在本地保存下载的包,因为只有当 `pip` 再次运行以安装相同的包时才会这样,但在与容器一起工作时情况并非如此。 + + !!! 笔记 + `--no-cache-dir` 仅与 `pip` 相关,与 Docker 或容器无关。 + + `--upgrade` 选项告诉 `pip` 升级软件包(如果已经安装)。 + + 因为上一步复制文件可以被 **Docker 缓存** 检测到,所以此步骤也将 **使用 Docker 缓存**(如果可用)。 + + 在开发过程中一次又一次构建镜像时,在此步骤中使用缓存将为你节省大量**时间**,而不是**每次**都**下载和安装**所有依赖项。 + + +5. 将“./app”目录复制到“/code”目录中。 + + 由于其中包含**更改最频繁**的所有代码,因此 Docker **缓存**不会轻易用于此操作或任何**后续步骤**。 + + 因此,将其放在`Dockerfile`**接近最后**的位置非常重要,以优化容器镜像的构建时间。 + +6. 设置**命令**来运行 `uvicorn` 服务器。 + + `CMD` 接受一个字符串列表,每个字符串都是你在命令行中输入的内容,并用空格分隔。 + + 该命令将从 **当前工作目录** 运行,即你上面使用`WORKDIR /code`设置的同一`/code`目录。 + + 因为程序将从`/code`启动,并且其中包含你的代码的目录`./app`,所以**Uvicorn**将能够从`app.main`中查看并**import**`app`。 + +!!! tip + 通过单击代码中的每个数字气泡来查看每行的作用。 👆 + +你现在应该具有如下目录结构: +``` +. +├── app +│   ├── __init__.py +│ └── main.py +├── Dockerfile +└── requirements.txt +``` + + +#### 在 TLS 终止代理后面 + +如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请添加选项 `--proxy-headers`,这将告诉 Uvicorn 信任该代理发送的标头,告诉它应用程序正在 HTTPS 后面运行等信息 + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +#### Docker 缓存 + +这个`Dockerfile`中有一个重要的技巧,我们首先只单独复制**包含依赖项的文件**,而不是其余代码。 让我来告诉你这是为什么。 + +```Dockerfile +COPY ./requirements.txt /code/requirements.txt +``` + +Docker之类的构建工具是通过**增量**的方式来构建这些容器镜像的。具体做法是从`Dockerfile`顶部开始,每一条指令生成的文件都是镜像的“一层”,同过把这些“层”一层一层地叠加到基础镜像上,最后我们就得到了最终的镜像。 + +Docker 和类似工具在构建镜像时也会使用**内部缓存**,如果自上次构建容器镜像以来文件没有更改,那么它将**重新使用上次创建的同一层**,而不是再次复制文件并从头开始创建新层。 + +仅仅避免文件的复制不一定会有太多速度提升,但是如果在这一步使用了缓存,那么才可以**在下一步中使用缓存**。 例如,可以使用安装依赖项那条指令的缓存: + +```Dockerfile +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +``` + + +包含包依赖项的文件**不会频繁更改**。 只复制该文件(不复制其他的应用代码),Docker 才能在这一步**使用缓存**。 + +Docker 进而能**使用缓存进行下一步**,即下载并安装这些依赖项。 这才是我们**节省大量时间**的地方。 ✨ ...可以避免无聊的等待。 😪😆 + +下载和安装依赖项**可能需要几分钟**,但使用**缓存**最多**只需要几秒钟**。 + +由于你在开发过程中会一次又一次地构建容器镜像以检查代码更改是否有效,因此可以累计节省大量时间。 + +在`Dockerfile`末尾附近,我们再添加复制代码的指令。 由于代码是**更改最频繁的**,所以将其放在最后,因为这一步之后的内容基本上都是无法使用缓存的。 + +```Dockerfile +COPY ./app /code/app +``` + +### 构建 Docker 镜像 + +现在所有文件都已就位,让我们构建容器镜像。 + +* 转到项目目录(在`Dockerfile`所在的位置,包含`app`目录)。 +* 构建你的 FastAPI 镜像: + + +<div class="termy"> + +```console +$ docker build -t myimage . + +---> 100% +``` + +</div> + + +!!! tip + 注意最后的 `.`,它相当于`./`,它告诉 Docker 用于构建容器镜像的目录。 + + 在本例中,它是相同的当前目录(`.`)。 + +### 启动 Docker 容器 + +* 根据你的镜像运行容器: + +<div class="termy"> + +```console +$ docker run -d --name mycontainer -p 80:80 myimage +``` + +</div> + +## 检查一下 + + +你应该能在Docker容器的URL中检查它,例如: <a href="http://192.168.99.100/items/5?q=somequery" class="external-link" target="_blank">http://192.168.99.100/items/5?q=somequery</a> 或 <a href="http://127.0.0.1/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1/items/5?q=somequery</a> (或其他等价的,使用 Docker 主机). + +你会看到类似内容: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +## 交互式 API 文档 + +现在你可以转到 <a href="http://192.168.99.100/docs" class="external-link" target="_blank">http://192.168.99.100/docs</a> 或 <a href ="http://127.0.0.1/docs" class="external-link" target="_blank">http://127.0.0.1/docs</a> (或其他等价的,使用 Docker 主机)。 + +你将看到自动交互式 API 文档(由 <a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a 提供) >): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +## 备选的 API 文档 + +你还可以访问 <a href="http://192.168.99.100/redoc" class="external-link" target="_blank">http://192.168.99.100/redoc</a> 或 <a href="http://127.0.0.1/redoc" class="external-link" target="_blank">http://127.0.0.1/redoc</a> (或其他等价的,使用 Docker 主机)。 + +你将看到备选的自动文档(由 <a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> 提供): + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## 使用单文件 FastAPI 构建 Docker 镜像 + +如果你的 FastAPI 是单个文件,例如没有`./app`目录的`main.py`,则你的文件结构可能如下所示: + +``` +. +├── Dockerfile +├── main.py +└── requirements.txt +``` + +然后你只需更改相应的路径即可将文件复制到`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. 直接将`main.py`文件复制到`/code`目录中(不包含任何`./app`目录)。 + +2. 运行 Uvicorn 并告诉它从 `main` 导入 `app` 对象(而不是从 `app.main` 导入)。 + +然后调整Uvicorn命令使用新模块`main`而不是`app.main`来导入FastAPI 实例`app`。 + +## 部署概念 + +我们再谈谈容器方面的一些相同的[部署概念](./concepts.md){.internal-link target=_blank}。 + +容器主要是一种简化**构建和部署**应用程序的过程的工具,但它们并不强制执行特定的方法来处理这些**部署概念**,并且有几种可能的策略。 + +**好消息**是,对于每种不同的策略,都有一种方法可以涵盖所有部署概念。 🎉 + +让我们从容器的角度回顾一下这些**部署概念**: + +* HTTPS +* 启动时运行 +* 重新启动 +* 复制(运行的进程数) +* 内存 +* 开始前的先前步骤 + + +## HTTPS + +如果我们只关注 FastAPI 应用程序的 **容器镜像**(以及稍后运行的 **容器**),HTTPS 通常会由另一个工具在 **外部** 处理。 + +它可以是另一个容器,例如使用 <a href="https://traefik.io/" class="external-link" target="_blank">Traefik</a>,处理 **HTTPS** 和 **自动**获取**证书**。 + +!!! tip + Traefik可以与 Docker、Kubernetes 等集成,因此使用它为容器设置和配置 HTTPS 非常容易。 + +或者,HTTPS 可以由云服务商作为其服务之一进行处理(同时仍在容器中运行应用程序)。 + +## 在启动和重新启动时运行 + +通常还有另一个工具负责**启动和运行**你的容器。 + +它可以直接是**Docker**, 或者**Docker Compose**、**Kubernetes**、**云服务**等。 + +在大多数(或所有)情况下,有一个简单的选项可以在启动时运行容器并在失败时重新启动。 例如,在 Docker 中,它是命令行选项 `--restart`。 + +如果不使用容器,让应用程序在启动时运行并重新启动可能会很麻烦且困难。 但在大多数情况下,当**使用容器**时,默认情况下会包含该功能。 ✨ + +## 复制 - 进程数 + +如果你有一个 <abbr title="一组配置为以某种方式连接并协同工作的计算机。">集群</abbr>, 比如 **Kubernetes**、Docker Swarm、Nomad 或其他类似的复杂系统来管理多台机器上的分布式容器,那么你可能希望在**集群级别**处理复制**,而不是在每个容器中使用**进程管理器**(如带有Worker的 Gunicorn) 。 + +像 Kubernetes 这样的分布式容器管理系统通常有一些集成的方法来处理**容器的复制**,同时仍然支持传入请求的**负载均衡**。 全部都在**集群级别**。 + +在这些情况下,你可能希望从头开始构建一个 **Docker 镜像**,如[上面所解释](#dockerfile)的那样,安装依赖项并运行 **单个 Uvicorn 进程**,而不是运行 Gunicorn 和 Uvicorn workers这种。 + + +### 负载均衡器 + +使用容器时,通常会有一些组件**监听主端口**。 它可能是处理 **HTTPS** 的 **TLS 终止代理** 或一些类似的工具的另一个容器。 + +由于该组件将接受请求的**负载**并(希望)以**平衡**的方式在worker之间分配该请求,因此它通常也称为**负载均衡器**。 + +!!! tip + 用于 HTTPS **TLS 终止代理** 的相同组件也可能是 **负载均衡器**。 + +当使用容器时,你用来启动和管理容器的同一系统已经具有内部工具来传输来自该**负载均衡器**(也可以是**TLS 终止代理**) 的**网络通信**(例如HTTP请求)到你的应用程序容器。 + +### 一个负载均衡器 - 多个worker容器 + +当使用 **Kubernetes** 或类似的分布式容器管理系统时,使用其内部网络机制将允许单个在主 **端口** 上侦听的 **负载均衡器** 将通信(请求)传输到可能的 **多个** 运行你应用程序的容器。 + +运行你的应用程序的每个容器通常**只有一个进程**(例如,运行 FastAPI 应用程序的 Uvicorn 进程)。 它们都是**相同的容器**,运行相同的东西,但每个容器都有自己的进程、内存等。这样你就可以在 CPU 的**不同核心**, 甚至在**不同的机器**充分利用**并行化(parallelization)**。 + +具有**负载均衡器**的分布式容器系统将**将请求轮流分配**给你的应用程序的每个容器。 因此,每个请求都可以由运行你的应用程序的多个**复制容器**之一来处理。 + +通常,这个**负载均衡器**能够处理发送到集群中的*其他*应用程序的请求(例如发送到不同的域,或在不同的 URL 路径前缀下),并正确地将该通信传输到在集群中运行的*其他*应用程序的对应容器。 + + + + + + +### 每个容器一个进程 + +在这种类型的场景中,你可能希望**每个容器有一个(Uvicorn)进程**,因为你已经在集群级别处理复制。 + +因此,在这种情况下,你**不会**希望拥有像 Gunicorn 和 Uvicorn worker一样的进程管理器,或者 Uvicorn 使用自己的 Uvicorn worker。 你可能希望每个容器(但可能有多个容器)只有一个**单独的 Uvicorn 进程**。 + +在容器内拥有另一个进程管理器(就像使用 Gunicorn 或 Uvicorn 管理 Uvicorn 工作线程一样)只会增加**不必要的复杂性**,而你很可能已经在集群系统中处理这些复杂性了。 + +### 具有多个进程的容器 + +当然,在某些**特殊情况**,你可能希望拥有 **一个容器**,其中包含 **Gunicorn 进程管理器**,并在其中启动多个 **Uvicorn worker进程**。 + +在这些情况下,你可以使用 **官方 Docker 镜像**,其中包含 **Gunicorn** 作为运行多个 **Uvicorn 工作进程** 的进程管理器,以及一些默认设置来根据当前情况调整工作进程数量 自动CPU核心。 我将在下面的 [Gunicorn - Uvicorn 官方 Docker 镜像](#official-docker-image-with-gunicorn-uvicorn) 中告诉你更多相关信息。 + +下面一些什么时候这种做法有意义的示例: + + +#### 一个简单的应用程序 + +如果你的应用程序**足够简单**,你不需要(至少现在不需要)过多地微调进程数量,并且你可以使用自动默认值,那么你可能需要容器中的进程管理器 (使用官方 Docker 镜像),并且你在**单个服务器**而不是集群上运行它。 + +#### Docker Compose + +你可以使用 **Docker Compose** 部署到**单个服务器**(而不是集群),因此你没有一种简单的方法来管理容器的复制(使用 Docker Compose),同时保留共享网络和 **负载均衡**。 + +然后,你可能希望拥有一个**单个容器**,其中有一个**进程管理器**,在其中启动**多个worker进程**。 + +#### Prometheus和其他原因 + +你还可能有**其他原因**,这将使你更容易拥有一个带有**多个进程**的**单个容器**,而不是拥有每个容器中都有**单个进程**的**多个容器**。 + +例如(取决于你的设置)你可以在同一个容器中拥有一些工具,例如 Prometheus exporter,该工具应该有权访问**每个请求**。 + +在这种情况下,如果你有**多个容器**,默认情况下,当 Prometheus 来**读取metrics**时,它每次都会获取**单个容器**的metrics(对于处理该特定请求的容器),而不是获取所有复制容器的**累积metrics**。 + +在这种情况, 这种做法会更加简单:让**一个容器**具有**多个进程**,并在同一个容器上使用本地工具(例如 Prometheus exporter)收集所有内部进程的 Prometheus 指标并公开单个容器上的这些指标。 + +--- + +要点是,这些都**不是**你必须盲目遵循的**一成不变的规则**。 你可以根据这些思路**评估你自己的场景**并决定什么方法是最适合你的的系统,考虑如何管理以下概念: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* 复制(运行的进程数) +* 内存 +* 开始前的先前步骤 + +## 内存 + +如果你**每个容器运行一个进程**,那么每个容器所消耗的内存或多或少是定义明确的、稳定的且有限的(如果它们是复制的,则不止一个)。 + +然后,你可以在容器管理系统的配置中设置相同的内存限制和要求(例如在 **Kubernetes** 中)。 这样,它将能够在**可用机器**中**复制容器**,同时考虑容器所需的内存量以及集群中机器中的可用内存量。 + +如果你的应用程序很**简单**,这可能**不是问题**,并且你可能不需要指定内存限制。 但是,如果你**使用大量内存**(例如使用**机器学习**模型),则应该检查你消耗了多少内存并调整**每台机器**中运行的**容器数量**(也许可以向集群添加更多机器)。 + +如果你**每个容器运行多个进程**(例如使用官方 Docker 镜像),你必须确保启动的进程数量不会消耗比可用内存**更多的内存**。 + +## 启动之前的步骤和容器 + +如果你使用容器(例如 Docker、Kubernetes),那么你可以使用两种主要方法。 + + +### 多个容器 + +如果你有 **多个容器**,可能每个容器都运行一个 **单个进程**(例如,在 **Kubernetes** 集群中),那么你可能希望有一个 **单独的容器** 执行以下操作: 在单个容器中运行单个进程执行**先前步骤**,即运行复制的worker容器之前。 + +!!! info + 如果你使用 Kubernetes,这可能是 <a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" class="external-link" target="_blank">Init Container</a>。 + +如果在你的用例中,运行前面的步骤**并行多次**没有问题(例如,如果你没有运行数据库迁移,而只是检查数据库是否已准备好),那么你也可以将它们放在开始主进程之前在每个容器中。 + +### 单容器 + +如果你有一个简单的设置,使用一个**单个容器**,然后启动多个**工作进程**(或者也只是一个进程),那么你可以在启动进程之前在应用程序同一个容器中运行先前的步骤。 官方 Docker 镜像内部支持这一点。 + +## 带有 Gunicorn 的官方 Docker 镜像 - Uvicorn + +有一个官方 Docker 镜像,其中包含与 Uvicorn worker一起运行的 Gunicorn,如上一章所述:[服务器工作线程 - Gunicorn 与 Uvicorn](./server-workers.md){.internal-link target=_blank}。 + +该镜像主要在上述情况下有用:[具有多个进程和特殊情况的容器](#containers-with-multiple-processes-and-special-cases)。 + + + +* <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank">tiangolo/uvicorn-gunicorn-fastapi</a>. + + +!!! warning + 你很有可能不需要此基础镜像或任何其他类似的镜像,最好从头开始构建镜像,如[上面所述:为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。 + +该镜像包含一个**自动调整**机制,用于根据可用的 CPU 核心设置**worker进程数**。 + +它具有**合理的默认值**,但你仍然可以使用**环境变量**或配置文件更改和更新所有配置。 + +它还支持通过一个脚本运行<a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker#pre_start_path" class="external-link" target="_blank">**开始前的先前步骤** </a>。 + +!!! tip + 要查看所有配置和选项,请转到 Docker 镜像页面: <a href="https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker" class="external-link" target="_blank" >tiangolo/uvicorn-gunicorn-fastapi</a>。 + +### 官方 Docker 镜像上的进程数 + +此镜像上的**进程数**是根据可用的 CPU **核心**自动计算的。 + +这意味着它将尝试尽可能多地**榨取**CPU 的**性能**。 + +你还可以使用 **环境变量** 等配置来调整它。 + +但这也意味着,由于进程数量取决于容器运行的 CPU,因此**消耗的内存量**也将取决于该数量。 + +因此,如果你的应用程序消耗大量内存(例如机器学习模型),并且你的服务器有很多 CPU 核心**但内存很少**,那么你的容器最终可能会尝试使用比实际情况更多的内存 可用,并且性能会下降很多(甚至崩溃)。 🚨 + +### 创建一个`Dockerfile` + +以下是如何根据此镜像创建`Dockerfile`: + + +```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 +``` + +### 更大的应用程序 + +如果你按照有关创建[具有多个文件的更大应用程序](../tutorial/bigger-applications.md){.internal-link target=_blank}的部分进行操作,你的`Dockerfile`可能看起来这样: + +```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 +``` + +### 何时使用 + +如果你使用 **Kubernetes** (或其他)并且你已经在集群级别设置 **复制**,并且具有多个 **容器**。 在这些情况下,你最好按照上面的描述 **从头开始构建镜像**:[为 FastAPI 构建 Docker 镜像](#build-a-docker-image-for-fastapi)。 + +该镜像主要在[具有多个进程的容器和特殊情况](#containers-with-multiple-processes-and-special-cases)中描述的特殊情况下有用。 例如,如果你的应用程序**足够简单**,基于 CPU 设置默认进程数效果很好,你不想在集群级别手动配置复制,并且不会运行更多进程, 或者你使用 **Docker Compose** 进行部署,在单个服务器上运行等。 + +## 部署容器镜像 + +拥有容器(Docker)镜像后,有多种方法可以部署它。 + +例如: + +* 在单个服务器中使用 **Docker Compose** +* 使用 **Kubernetes** 集群 +* 使用 Docker Swarm 模式集群 +* 使用Nomad等其他工具 +* 使用云服务获取容器镜像并部署它 + +## Docker 镜像与Poetry + +如果你使用 <a href="https://python-poetry.org/" class="external-link" target="_blank">Poetry</a> 来管理项目的依赖项,你可以使用 Docker 多阶段构建: + + + +```{ .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. 这是第一阶段,称为`requirements-stage`。 + +2. 将 `/tmp` 设置为当前工作目录。 + + 这是我们生成文件`requirements.txt`的地方 + +3. 在此阶段安装Poetry。 + +4. 将`pyproject.toml`和`poetry.lock`文件复制到`/tmp`目录。 + + 因为它使用 `./poetry.lock*` (以 `*` 结尾),所以如果该文件尚不可用,它不会崩溃。 + +5. 生成`requirements.txt`文件。 + +6. 这是最后阶段,这里的任何内容都将保留在最终的容器镜像中。 + +7. 将当前工作目录设置为`/code`。 + +8. 将 `requirements.txt` 文件复制到 `/code` 目录。 + + 该文件仅存在于前一个阶段,这就是为什么我们使用 `--from-requirements-stage` 来复制它。 + +9. 安装生成的`requirements.txt`文件中的依赖项。 + +10. 将`app`目录复制到`/code`目录。 + +11. 运行`uvicorn`命令,告诉它使用从`app.main`导入的`app`对象。 + +!!! tip + 单击气泡数字可查看每行的作用。 + +**Docker stage** 是 `Dockerfile` 的一部分,用作 **临时容器镜像**,仅用于生成一些稍后使用的文件。 + +第一阶段仅用于 **安装 Poetry** 并使用 Poetry 的 `pyproject.toml` 文件中的项目依赖项 **生成 `requirements.txt`**。 + +此`requirements.txt`文件将在**下一阶段**与`pip`一起使用。 + +在最终的容器镜像中**仅保留最后阶段**。 之前的阶段将被丢弃。 + +使用 Poetry 时,使用 **Docker 多阶段构建** 是有意义的,因为你实际上并不需要在最终的容器镜像中安装 Poetry 及其依赖项,你 **只需要** 生成用于安装项目依赖项的`requirements.txt`文件。 + +然后,在下一个(也是最后一个)阶段,你将或多或少地以与前面描述的相同的方式构建镜像。 + +### 在TLS 终止代理后面 - Poetry + +同样,如果你在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请将选项`--proxy-headers`添加到命令中: + + +```Dockerfile +CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"] +``` + +## 回顾 + +使用容器系统(例如使用**Docker**和**Kubernetes**),处理所有**部署概念**变得相当简单: + +* HTTPS +* 启动时运行 +* 重新启动 +* 复制(运行的进程数) +* 内存 +* 开始前的先前步骤 + +在大多数情况下,你可能不想使用任何基础镜像,而是基于官方 Python Docker 镜像 **从头开始构建容器镜像** 。 + +处理好`Dockerfile`和 **Docker 缓存**中指令的**顺序**,你可以**最小化构建时间**,从而最大限度地提高生产力(并避免无聊)。 😎 + +在某些特殊情况下,你可能需要使用 FastAPI 的官方 Docker 镜像。 🤓 From 008be03f31d16699c7319313df998365243b3dcd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:01:03 +0000 Subject: [PATCH 1811/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 43c7ec244..b07c911d3 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/docker.md`. PR [#10296](https://github.com/tiangolo/fastapi/pull/10296) by [@xzmeng](https://github.com/xzmeng). * 🌐 Update Spanish translation for `docs/es/docs/features.md`. PR [#10884](https://github.com/tiangolo/fastapi/pull/10884) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/newsletter.md`. PR [#10922](https://github.com/tiangolo/fastapi/pull/10922) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/background-tasks.md`. PR [#5910](https://github.com/tiangolo/fastapi/pull/5910) by [@junah201](https://github.com/junah201). From 49113c35be8c51624da0f74260bd96cb5ff29bc5 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:03:58 +0800 Subject: [PATCH 1812/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/project-generation.md`=20(#3831)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/project-generation.md | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/zh/docs/project-generation.md diff --git a/docs/zh/docs/project-generation.md b/docs/zh/docs/project-generation.md new file mode 100644 index 000000000..feafa5333 --- /dev/null +++ b/docs/zh/docs/project-generation.md @@ -0,0 +1,84 @@ +# 项目生成 - 模板 + +项目生成器一般都会提供很多初始设置、安全措施、数据库,甚至还准备好了第一个 API 端点,能帮助您快速上手。 + +项目生成器的设置通常都很主观,您可以按需更新或修改,但对于您的项目来说,它是非常好的起点。 + +## 全栈 FastAPI + PostgreSQL + +GitHub:<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-postgresql</a> + +### 全栈 FastAPI + PostgreSQL - 功能 + +* 完整的 **Docker** 集成(基于 Docker) +* Docker Swarm 开发模式 +* **Docker Compose** 本地开发集成与优化 +* **生产可用**的 Python 网络服务器,使用 Uvicorn 或 Gunicorn +* Python <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">**FastAPI**</a> 后端: +* * **速度快**:可与 **NodeJS** 和 **Go** 比肩的极高性能(归功于 Starlette 和 Pydantic) + * **直观**:强大的编辑器支持,处处皆可<abbr title="也叫自动完成、智能感知">自动补全</abbr>,减少调试时间 + * **简单**:易学、易用,阅读文档所需时间更短 + * **简短**:代码重复最小化,每次参数声明都可以实现多个功能 + * **健壮**: 生产级别的代码,还有自动交互文档 + * **基于标准**:完全兼容并基于 API 开放标准:<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> 和 <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a> + * <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**更多功能**</a>包括自动验证、序列化、交互文档、OAuth2 JWT 令牌身份验证等 +* **安全密码**,默认使用密码哈希 +* **JWT 令牌**身份验证 +* **SQLAlchemy** 模型(独立于 Flask 扩展,可直接用于 Celery Worker) +* 基础的用户模型(可按需修改或删除) +* **Alembic** 迁移 +* **CORS**(跨域资源共享) +* **Celery** Worker 可从后端其它部分有选择地导入并使用模型和代码 +* REST 后端测试基于 Pytest,并与 Docker 集成,可独立于数据库实现完整的 API 交互测试。因为是在 Docker 中运行,每次都可从头构建新的数据存储(使用 ElasticSearch、MongoDB、CouchDB 等数据库,仅测试 API 运行) +* Python 与 **Jupyter Kernels** 集成,用于远程或 Docker 容器内部开发,使用 Atom Hydrogen 或 Visual Studio Code 的 Jupyter 插件 +* **Vue** 前端: + * 由 Vue CLI 生成 + * **JWT 身份验证**处理 + * 登录视图 + * 登录后显示主仪表盘视图 + * 主仪表盘支持用户创建与编辑 + * 用户信息编辑 + * **Vuex** + * **Vue-router** + * **Vuetify** 美化组件 + * **TypeScript** + * 基于 **Nginx** 的 Docker 服务器(优化了 Vue-router 配置) + * Docker 多阶段构建,无需保存或提交编译的代码 + * 在构建时运行前端测试(可禁用) + * 尽量模块化,开箱即用,但仍可使用 Vue CLI 重新生成或创建所需项目,或复用所需内容 +* 使用 **PGAdmin** 管理 PostgreSQL 数据库,可轻松替换为 PHPMyAdmin 或 MySQL +* 使用 **Flower** 监控 Celery 任务 +* 使用 **Traefik** 处理前后端负载平衡,可把前后端放在同一个域下,按路径分隔,但在不同容器中提供服务 +* Traefik 集成,包括自动生成 Let's Encrypt **HTTPS** 凭证 +* GitLab **CI**(持续集成),包括前后端测试 + +## 全栈 FastAPI + Couchbase + +GitHub:<a href="https://github.com/tiangolo/full-stack-fastapi-couchbase" class="external-link" target="_blank">https://github.com/tiangolo/full-stack-fastapi-couchbase</a> + +⚠️ **警告** ⚠️ + +如果您想从头开始创建新项目,建议使用以下备选方案。 + +例如,项目生成器<a href="https://github.com/tiangolo/full-stack-fastapi-postgresql" class="external-link" target="_blank">全栈 FastAPI + PostgreSQL </a>会更适用,这个项目的维护积极,用的人也多,还包括了所有新功能和改进内容。 + +当然,您也可以放心使用这个基于 Couchbase 的生成器,它也能正常使用。就算用它生成项目也没有任何问题(为了更好地满足需求,您可以自行更新这个项目)。 + +详见资源仓库中的文档。 + +## 全栈 FastAPI + MongoDB + +……敬请期待,得看我有没有时间做这个项目。😅 🎉 + +## FastAPI + spaCy 机器学习模型 + +GitHub:<a href="https://github.com/microsoft/cookiecutter-spacy-fastapi" class="external-link" target="_blank">https://github.com/microsoft/cookiecutter-spacy-fastapi</a> + +### FastAPI + spaCy 机器学习模型 - 功能 + +* 集成 **spaCy** NER 模型 +* 内置 **Azure 认知搜索**请求格式 +* **生产可用**的 Python 网络服务器,使用 Uvicorn 与 Gunicorn +* 内置 **Azure DevOps** Kubernetes (AKS) CI/CD 开发 +* **多语**支持,可在项目设置时选择 spaCy 内置的语言 +* 不仅局限于 spaCy,可**轻松扩展**至其它模型框架(Pytorch、TensorFlow) From e1119a16cb2e8ac12895945b9f1d20e8f78d3ca6 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:05:04 +0000 Subject: [PATCH 1813/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b07c911d3..2bf05c1ab 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#3831](https://github.com/tiangolo/fastapi/pull/3831) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/docker.md`. PR [#10296](https://github.com/tiangolo/fastapi/pull/10296) by [@xzmeng](https://github.com/xzmeng). * 🌐 Update Spanish translation for `docs/es/docs/features.md`. PR [#10884](https://github.com/tiangolo/fastapi/pull/10884) by [@pablocm83](https://github.com/pablocm83). * 🌐 Add Spanish translation for `docs/es/docs/newsletter.md`. PR [#10922](https://github.com/tiangolo/fastapi/pull/10922) by [@pablocm83](https://github.com/pablocm83). From 5b0bff3e934754dcad520fdcb3d0167ee6c6b100 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:05:57 +0800 Subject: [PATCH 1814/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/history-design-future.md`=20(#3832)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/history-design-future.md | 78 +++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/zh/docs/history-design-future.md diff --git a/docs/zh/docs/history-design-future.md b/docs/zh/docs/history-design-future.md new file mode 100644 index 000000000..56a15d003 --- /dev/null +++ b/docs/zh/docs/history-design-future.md @@ -0,0 +1,78 @@ +# 历史、设计、未来 + +不久前,<a href="https://github.com/tiangolo/fastapi/issues/3#issuecomment-454956920" class="external-link" target="_blank">曾有 **FastAPI** 用户问过</a>: + +> 这个项目有怎样的历史?好像它只用了几周就从默默无闻变得众所周知…… + +在此,我们简单回顾一下 **FastAPI** 的历史。 + +## 备选方案 + +有那么几年,我曾领导数个开发团队为诸多复杂需求创建各种 API,这些需求包括机器学习、分布系统、异步任务、NoSQL 数据库等领域。 + +作为工作的一部分,我需要调研很多备选方案、还要测试并且使用这些备选方案。 + +**FastAPI** 其实只是延续了这些前辈的历史。 + +正如[备选方案](alternatives.md){.internal-link target=_blank}一章所述: + +<blockquote markdown="1"> +没有大家之前所做的工作,**FastAPI** 就不会存在。 + +以前创建的这些工具为它的出现提供了灵感。 + +在那几年中,我一直回避创建新的框架。首先,我尝试使用各种框架、插件、工具解决 **FastAPI** 现在的功能。 + +但到了一定程度之后,我别无选择,只能从之前的工具中汲取最优思路,并以尽量好的方式把这些思路整合在一起,使用之前甚至是不支持的语言特性(Python 3.6+ 的类型提示),从而创建一个能满足我所有需求的框架。 + +</blockquote> + +## 调研 + +通过使用之前所有的备选方案,我有机会从它们之中学到了很多东西,获取了很多想法,并以我和我的开发团队能想到的最好方式把这些思路整合成一体。 + +例如,大家都清楚,在理想状态下,它应该基于标准的 Python 类型提示。 + +而且,最好的方式是使用现有的标准。 + +因此,甚至在开发 **FastAPI** 前,我就花了几个月的时间研究 OpenAPI、JSON Schema、OAuth2 等规范。深入理解它们之间的关系、重叠及区别之处。 + +## 设计 + +然后,我又花了一些时间从用户角度(使用 FastAPI 的开发者)设计了开发者 **API**。 + +同时,我还在最流行的 Python 代码编辑器中测试了很多思路,包括 PyCharm、VS Code、基于 Jedi 的编辑器。 + +根据最新 <a href="https://www.jetbrains.com/research/python-developers-survey-2018/#development-tools" class="external-link" target="_blank">Python 开发者调研报告</a>显示,这几种编辑器覆盖了约 80% 的用户。 + +也就是说,**FastAPI** 针对差不多 80% 的 Python 开发者使用的编辑器进行了测试,而且其它大多数编辑器的工作方式也与之类似,因此,**FastAPI** 的优势几乎能在所有编辑器上体现。 + +通过这种方式,我就能找到尽可能减少代码重复的最佳方式,进而实现处处都有自动补全、类型提示与错误检查等支持。 + +所有这些都是为了给开发者提供最佳的开发体验。 + +## 需求项 + +经过测试多种备选方案,我最终决定使用 <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">**Pydantic**</a>,并充分利用它的优势。 + +我甚至为它做了不少贡献,让它完美兼容了 JSON Schema,支持多种方式定义约束声明,并基于多个编辑器,改进了它对编辑器支持(类型检查、自动补全)。 + +在开发期间,我还为 <a href="https://www.starlette.io/" class="external-link" target="_blank">**Starlette**</a> 做了不少贡献,这是另一个关键需求项。 + +## 开发 + +当我启动 **FastAPI** 开发的时候,绝大多数部件都已经就位,设计已经定义,需求项和工具也已经准备就绪,相关标准与规范的知识储备也非常清晰而新鲜。 + +## 未来 + +至此,**FastAPI** 及其理念已经为很多人所用。 + +对于很多用例,它比以前很多备选方案都更适用。 + +很多开发者和开发团队已经依赖 **FastAPI** 开发他们的项目(包括我和我的团队)。 + +但,**FastAPI** 仍有很多改进的余地,也还需要添加更多的功能。 + +总之,**FastAPI** 前景光明。 + +在此,我们衷心感谢[您的帮助](help-fastapi.md){.internal-link target=_blank}。 From f81bedd853b2255b0f91107180a336448c422768 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:06:55 +0800 Subject: [PATCH 1815/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/deta.md`=20(#3837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/deployment/deta.md | 244 ++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 docs/zh/docs/deployment/deta.md diff --git a/docs/zh/docs/deployment/deta.md b/docs/zh/docs/deployment/deta.md new file mode 100644 index 000000000..a7390f786 --- /dev/null +++ b/docs/zh/docs/deployment/deta.md @@ -0,0 +1,244 @@ +# 在 Deta 上部署 FastAPI + +本节介绍如何使用 <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> 免费方案部署 **FastAPI** 应用。🎁 + +部署操作需要大约 10 分钟。 + +!!! info "说明" + + <a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">Deta</a> 是 **FastAPI** 的赞助商。 🎉 + +## 基础 **FastAPI** 应用 + +* 创建应用文件夹,例如 `./fastapideta/`,进入文件夹 + +### FastAPI 代码 + +* 创建包含如下代码的 `main.py`: + +```Python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int): + return {"item_id": item_id} +``` + +### 需求项 + +在文件夹里新建包含如下内容的 `requirements.txt` 文件: + +```text +fastapi +``` + +!!! tip "提示" + + 在 Deta 上部署时无需安装 Uvicorn,虽然在本地测试应用时需要安装。 + +### 文件夹架构 + +`./fastapideta/` 文件夹中现在有两个文件: + +``` +. +└── main.py +└── requirements.txt +``` + +## 创建免费 Deta 账号 + +创建<a href="https://www.deta.sh/?ref=fastapi" class="external-link" target="_blank">免费的 Deta 账号</a>,只需要电子邮件和密码。 + +甚至不需要信用卡。 + +## 安装 CLI + +创建账号后,安装 Deta <abbr title="Command Line Interface application">CLI</abbr>: + +=== "Linux, macOS" + + <div class="termy"> + + ```console + $ curl -fsSL https://get.deta.dev/cli.sh | sh + ``` + + </div> + +=== "Windows PowerShell" + + <div class="termy"> + + ```console + $ iwr https://get.deta.dev/cli.ps1 -useb | iex + ``` + + </div> + +安装完 CLI 后,打开新的 Terminal,就能检测到刚安装的 CLI。 + +在新的 Terminal 里,用以下命令确认 CLI 是否正确安装: + +<div class="termy"> + +```console +$ deta --help + +Deta command line interface for managing deta micros. +Complete documentation available at https://docs.deta.sh + +Usage: + deta [flags] + deta [command] + +Available Commands: + auth Change auth settings for a deta micro + +... +``` + +</div> + +!!! tip "提示" + + 安装 CLI 遇到问题时,请参阅 <a href="https://docs.deta.sh/docs/micros/getting_started?ref=fastapi" class="external-link" target="_blank">Deta 官档</a>。 + +## 使用 CLI 登录 + +现在,使用 CLI 登录 Deta: + +<div class="termy"> + +```console +$ deta login + +Please, log in from the web page. Waiting.. +Logged in successfully. +``` + +</div> + +这个命令会打开浏览器并自动验证身份。 + +## 使用 Deta 部署 + +接下来,使用 Deta CLI 部署应用: + +<div class="termy"> + +```console +$ deta new + +Successfully created a new micro + +// Notice the "endpoint" 🔍 + +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} + +Adding dependencies... + + +---> 100% + + +Successfully installed fastapi-0.61.1 pydantic-1.7.2 starlette-0.13.6 +``` + +</div> + +您会看到如下 JSON 信息: + +```JSON hl_lines="4" +{ + "name": "fastapideta", + "runtime": "python3.7", + "endpoint": "https://qltnci.deta.dev", + "visor": "enabled", + "http_auth": "enabled" +} +``` + +!!! tip "提示" + + 您部署时的 `"endpoint"` URL 可能会有所不同。 + +## 查看效果 + +打开浏览器,跳转到 `endpoint` URL。本例中是 `https://qltnci.deta.dev`,但您的链接可能与此不同。 + +FastAPI 应用会返回如下 JSON 响应: + +```JSON +{ + "Hello": "World" +} +``` + +接下来,跳转到 API 文档 `/docs`,本例中是 `https://qltnci.deta.dev/docs`。 + +文档显示如下: + +<img src="/img/deployment/deta/image01.png"> + +## 启用公开访问 + +默认情况下,Deta 使用您的账号 Cookies 处理身份验证。 + +应用一切就绪之后,使用如下命令让公众也能看到您的应用: + +<div class="termy"> + +```console +$ deta auth disable + +Successfully disabled http auth +``` + +</div> + +现在,就可以把 URL 分享给大家,他们就能访问您的 API 了。🚀 + +## HTTPS + +恭喜!您已经在 Deta 上部署了 FastAPI 应用!🎉 🍰 + +还要注意,Deta 能够正确处理 HTTPS,因此您不必操心 HTTPS,您的客户端肯定能有安全加密的连接。 ✅ 🔒 + +## 查看 Visor + +从 API 文档(URL 是 `https://gltnci.deta.dev/docs`)发送请求至*路径操作* `/items/{item_id}`。 + +例如,ID `5`。 + +现在跳转至 <a href="https://web.deta.sh/" class="external-link" target="_blank">https://web.deta.sh。</a> + +左边栏有个 <abbr title="it comes from Micro(server)">"Micros"</abbr> 标签,里面是所有的应用。 + +还有一个 **Details** 和 **Visor** 标签,跳转到 **Visor** 标签。 + +在这里查看最近发送给应用的请求。 + +您可以编辑或重新使用这些请求。 + +<img src="/img/deployment/deta/image02.png"> + +## 更多内容 + +如果要持久化保存应用数据,可以使用提供了**免费方案**的 <a href="https://docs.deta.sh/docs/base/py_tutorial?ref=fastapi" class="external-link" target="_blank">Deta Base</a>。 + +详见 <a href="https://docs.deta.sh?ref=fastapi" class="external-link" target="_blank">Deta 官档</a>。 From 6008c04e2e05a75accd6f61ee67ebe48e9fe0ac9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:07:45 +0000 Subject: [PATCH 1816/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 2bf05c1ab..9edf728b7 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/history-design-future.md`. PR [#3832](https://github.com/tiangolo/fastapi/pull/3832) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#3831](https://github.com/tiangolo/fastapi/pull/3831) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/docker.md`. PR [#10296](https://github.com/tiangolo/fastapi/pull/10296) by [@xzmeng](https://github.com/xzmeng). * 🌐 Update Spanish translation for `docs/es/docs/features.md`. PR [#10884](https://github.com/tiangolo/fastapi/pull/10884) by [@pablocm83](https://github.com/pablocm83). From a54ca1487607b71f94ab721b5117abf41460620a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:08:47 +0000 Subject: [PATCH 1817/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 9edf728b7..189e217ea 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/deta.md`. PR [#3837](https://github.com/tiangolo/fastapi/pull/3837) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/history-design-future.md`. PR [#3832](https://github.com/tiangolo/fastapi/pull/3832) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#3831](https://github.com/tiangolo/fastapi/pull/3831) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/docker.md`. PR [#10296](https://github.com/tiangolo/fastapi/pull/10296) by [@xzmeng](https://github.com/xzmeng). From 1e8b44f3e4c48528480367b4a8f44b1d7cfb8462 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:09:26 +0800 Subject: [PATCH 1818/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/testing-database.md`=20(#3?= =?UTF-8?q?821)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/testing-database.md | 97 +++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/zh/docs/advanced/testing-database.md diff --git a/docs/zh/docs/advanced/testing-database.md b/docs/zh/docs/advanced/testing-database.md new file mode 100644 index 000000000..8dc95c25f --- /dev/null +++ b/docs/zh/docs/advanced/testing-database.md @@ -0,0 +1,97 @@ +# 测试数据库 + +您还可以使用[测试依赖项](testing-dependencies.md){.internal-link target=_blank}中的覆盖依赖项方法变更测试的数据库。 + +实现设置其它测试数据库、在测试后回滚数据、或预填测试数据等操作。 + +本章的主要思路与上一章完全相同。 + +## 为 SQL 应用添加测试 + +为了使用测试数据库,我们要升级 [SQL 关系型数据库](../tutorial/sql-databases.md){.internal-link target=_blank} 一章中的示例。 + +应用的所有代码都一样,直接查看那一章的示例代码即可。 + +本章只是新添加了测试文件。 + +正常的依赖项 `get_db()` 返回数据库会话。 + +测试时使用覆盖依赖项返回自定义数据库会话代替正常的依赖项。 + +本例中,要创建仅用于测试的临时数据库。 + +## 文件架构 + +创建新文件 `sql_app/tests/test_sql_app.py`。 + +因此,新的文件架构如下: + +``` hl_lines="9-11" +. +└── sql_app + ├── __init__.py + ├── crud.py + ├── database.py + ├── main.py + ├── models.py + ├── schemas.py + └── tests + ├── __init__.py + └── test_sql_app.py +``` + +## 创建新的数据库会话 + +首先,为新建数据库创建新的数据库会话。 + +测试时,使用 `test.db` 替代 `sql_app.db`。 + +但其余的会话代码基本上都是一样的,只要复制就可以了。 + +```Python hl_lines="8-13" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip "提示" + + 为减少代码重复,最好把这段代码写成函数,在 `database.py` 与 `tests/test_sql_app.py`中使用。 + + 为了把注意力集中在测试代码上,本例只是复制了这段代码。 + +## 创建数据库 + +因为现在是想在新文件中使用新数据库,所以要使用以下代码创建数据库: + +```Python +Base.metadata.create_all(bind=engine) +``` + +一般是在 `main.py` 中调用这行代码,但在 `main.py` 里,这行代码用于创建 `sql_app.db`,但是现在要为测试创建 `test.db`。 + +因此,要在测试代码中添加这行代码创建新的数据库文件。 + +```Python hl_lines="16" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +## 覆盖依赖项 + +接下来,创建覆盖依赖项,并为应用添加覆盖内容。 + +```Python hl_lines="19-24 27" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +!!! tip "提示" + + `overrider_get_db()` 与 `get_db` 的代码几乎完全一样,只是 `overrider_get_db` 中使用测试数据库的 `TestingSessionLocal`。 + +## 测试应用 + +然后,就可以正常测试了。 + +```Python hl_lines="32-47" +{!../../../docs_src/sql_databases/sql_app/tests/test_sql_app.py!} +``` + +测试期间,所有在数据库中所做的修改都在 `test.db` 里,不会影响主应用的 `sql_app.db`。 From 539e032b2d93150a2101a102ed6020b96bd8e325 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:11:59 +0000 Subject: [PATCH 1819/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 189e217ea..c36c220b9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-database.md`. PR [#3821](https://github.com/tiangolo/fastapi/pull/3821) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/deta.md`. PR [#3837](https://github.com/tiangolo/fastapi/pull/3837) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/history-design-future.md`. PR [#3832](https://github.com/tiangolo/fastapi/pull/3832) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/project-generation.md`. PR [#3831](https://github.com/tiangolo/fastapi/pull/3831) by [@jaystone776](https://github.com/jaystone776). From 4b7fa89f4ed09fc447c7d82c46c0f1843393639d Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:12:29 +0800 Subject: [PATCH 1820/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/testing-websockets.md`=20(?= =?UTF-8?q?#3817)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/testing-websockets.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/zh/docs/advanced/testing-websockets.md diff --git a/docs/zh/docs/advanced/testing-websockets.md b/docs/zh/docs/advanced/testing-websockets.md new file mode 100644 index 000000000..f303e1d67 --- /dev/null +++ b/docs/zh/docs/advanced/testing-websockets.md @@ -0,0 +1,13 @@ +# 测试 WebSockets + +测试 WebSockets 也使用 `TestClient`。 + +为此,要在 `with` 语句中使用 `TestClient` 连接 WebSocket。 + +```Python hl_lines="27-31" +{!../../../docs_src/app_testing/tutorial002.py!} +``` + +!!! note "笔记" + + 更多细节详见 <a href="https://www.starlette.io/testclient/#testing-websocket-sessions" class="external-link" target="_blank">Starlette 官档 - 测试 WebSockets</a>。 From 92cf191f1ff0989b85126c1cd2860b511b86539a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:17:01 +0000 Subject: [PATCH 1821/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c36c220b9..c9a2fc3da 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-websockets.md`. PR [#3817](https://github.com/tiangolo/fastapi/pull/3817) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-database.md`. PR [#3821](https://github.com/tiangolo/fastapi/pull/3821) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/deta.md`. PR [#3837](https://github.com/tiangolo/fastapi/pull/3837) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/history-design-future.md`. PR [#3832](https://github.com/tiangolo/fastapi/pull/3832) by [@jaystone776](https://github.com/jaystone776). From eaf394d3646aef4e73eaf84168506a9dc78f76d1 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:21:02 +0800 Subject: [PATCH 1822/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/testing-events.md`=20(#381?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/testing-events.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/zh/docs/advanced/testing-events.md diff --git a/docs/zh/docs/advanced/testing-events.md b/docs/zh/docs/advanced/testing-events.md new file mode 100644 index 000000000..222a67c8c --- /dev/null +++ b/docs/zh/docs/advanced/testing-events.md @@ -0,0 +1,7 @@ +# 测试事件:启动 - 关闭 + +使用 `TestClient` 和 `with` 语句,在测试中运行事件处理器(`startup` 与 `shutdown`)。 + +```Python hl_lines="9-12 20-24" +{!../../../docs_src/app_testing/tutorial003.py!} +``` From fc4606e1d005549c324641e882c91cce795b9687 Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:22:37 +0800 Subject: [PATCH 1823/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/behind-a-proxy.md`=20(#382?= =?UTF-8?q?0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/behind-a-proxy.md | 351 ++++++++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 docs/zh/docs/advanced/behind-a-proxy.md diff --git a/docs/zh/docs/advanced/behind-a-proxy.md b/docs/zh/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..738bd7119 --- /dev/null +++ b/docs/zh/docs/advanced/behind-a-proxy.md @@ -0,0 +1,351 @@ +# 使用代理 + +有些情况下,您可能要使用 Traefik 或 Nginx 等**代理**服务器,并添加应用不能识别的附加路径前缀配置。 + +此时,要使用 `root_path` 配置应用。 + +`root_path` 是 ASGI 规范提供的机制,FastAPI 就是基于此规范开发的(通过 Starlette)。 + +`root_path` 用于处理这些特定情况。 + +在挂载子应用时,也可以在内部使用。 + +## 移除路径前缀的代理 + +本例中,移除路径前缀的代理是指在代码中声明路径 `/app`,然后在应用顶层添加代理,把 **FastAPI** 应用放在 `/api/v1` 路径下。 + +本例的原始路径 `/app` 实际上是在 `/api/v1/app` 提供服务。 + +哪怕所有代码都假设只有 `/app`。 + +代理只在把请求传送给 Uvicorn 之前才会**移除路径前缀**,让应用以为它是在 `/app` 提供服务,因此不必在代码中加入前缀 `/api/v1`。 + +但之后,在(前端)打开 API 文档时,代理会要求在 `/openapi.json`,而不是 `/api/v1/openapi.json` 中提取 OpenAPI 概图。 + +因此, (运行在浏览器中的)前端会尝试访问 `/openapi.json`,但没有办法获取 OpenAPI 概图。 + +这是因为应用使用了以 `/api/v1` 为路径前缀的代理,前端要从 `/api/v1/openapi.json` 中提取 OpenAPI 概图。 + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +!!! tip "提示" + + IP `0.0.0.0` 常用于指程序监听本机或服务器上的所有有效 IP。 + +API 文档还需要 OpenAPI 概图声明 API `server` 位于 `/api/v1`(使用代理时的 URL)。例如: + +```JSON hl_lines="4-8" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + } + ], + "paths": { + // More stuff here + } +} +``` + +本例中的 `Proxy` 是 **Traefik**,`server` 是运行 FastAPI 应用的 **Uvicorn**。 + +### 提供 `root_path` + +为此,要以如下方式使用命令行选项 `--root-path`: + +<div class="termy"> + +```console +$ uvicorn main:app --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +Hypercorn 也支持 `--root-path `选项。 + +!!! note "技术细节" + + ASGI 规范定义的 `root_path` 就是为了这种用例。 + + 并且 `--root-path` 命令行选项支持 `root_path`。 + +### 查看当前的 `root_path` + +获取应用为每个请求使用的当前 `root_path`,这是 `scope` 字典的内容(也是 ASGI 规范的内容)。 + +我们在这里的信息里包含 `roo_path` 只是为了演示。 + +```Python hl_lines="8" +{!../../../docs_src/behind_a_proxy/tutorial001.py!} +``` + +然后,用以下命令启动 Uvicorn: + +<div class="termy"> + +```console +$ uvicorn main:app --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +返回的响应如下: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +### 在 FastAPI 应用里设置 `root_path` + +还有一种方案,如果不能提供 `--root-path` 或等效的命令行选项,则在创建 FastAPI 应用时要设置 `root_path` 参数。 + +```Python hl_lines="3" +{!../../../docs_src/behind_a_proxy/tutorial002.py!} +``` + +传递 `root_path` 给 `FastAPI` 与传递 `--root-path` 命令行选项给 Uvicorn 或 Hypercorn 一样。 + +### 关于 `root_path` + +注意,服务器(Uvicorn)只是把 `root_path` 传递给应用。 + +在浏览器中输入 <a href="http://127.0.0.1:8000" class="external-link" target="_blank">http://127.0.0.1:8000/app 时能看到标准响应:</a> + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +它不要求访问 `http://127.0.0.1:800/api/v1/app`。 + +Uvicorn 预期代理在 `http://127.0.0.1:8000/app` 访问 Uvicorn,而在顶部添加 `/api/v1` 前缀是代理要做的事情。 + +## 关于移除路径前缀的代理 + +注意,移除路径前缀的代理只是配置代理的方式之一。 + +大部分情况下,代理默认都不会移除路径前缀。 + +(未移除路径前缀时)代理监听 `https://myawesomeapp.com` 等对象,如果浏览器跳转到 `https://myawesomeapp.com/api/v1/app`,且服务器(例如 Uvicorn)监听 `http://127.0.0.1:8000` 代理(未移除路径前缀) 会在同样的路径:`http://127.0.0.1:8000/api/v1/app` 访问 Uvicorn。 + +## 本地测试 Traefik + +您可以轻易地在本地使用 <a href="https://docs.traefik.io/" class="external-link" target="_blank">Traefik</a> 运行移除路径前缀的试验。 + +<a href="https://github.com/containous/traefik/releases" class="external-link" target="_blank">下载 Traefik</a>,这是一个二进制文件,需要解压文件,并在 Terminal 中直接运行。 + +然后创建包含如下内容的 `traefik.toml` 文件: + +```TOML hl_lines="3" +[entryPoints] + [entryPoints.http] + address = ":9999" + +[providers] + [providers.file] + filename = "routes.toml" +``` + +这个文件把 Traefik 监听端口设置为 `9999`,并设置要使用另一个文件 `routes.toml`。 + +!!! tip "提示" + + 使用端口 9999 代替标准的 HTTP 端口 80,这样就不必使用管理员权限运行(`sudo`)。 + +接下来,创建 `routes.toml`: + +```TOML hl_lines="5 12 20" +[http] + [http.middlewares] + + [http.middlewares.api-stripprefix.stripPrefix] + prefixes = ["/api/v1"] + + [http.routers] + + [http.routers.app-http] + entryPoints = ["http"] + service = "app" + rule = "PathPrefix(`/api/v1`)" + middlewares = ["api-stripprefix"] + + [http.services] + + [http.services.app] + [http.services.app.loadBalancer] + [[http.services.app.loadBalancer.servers]] + url = "http://127.0.0.1:8000" +``` + +这个文件配置 Traefik 使用路径前缀 `/api/v1`。 + +然后,它把请求重定位到运行在 `http://127.0.0.1:8000` 上的 Uvicorn。 + +现在,启动 Traefik: + +<div class="termy"> + +```console +$ ./traefik --configFile=traefik.toml + +INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml +``` + +</div> + +接下来,使用 Uvicorn 启动应用,并使用 `--root-path` 选项: + +<div class="termy"> + +```console +$ uvicorn main:app --root-path /api/v1 + +<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +``` + +</div> + +### 查看响应 + +访问含 Uvicorn 端口的 URL:<a href="http://127.0.0.1:8000/app" class="external-link" target="_blank">http://127.0.0.1:8000/app,就能看到标准响应:</a> + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +!!! tip "提示" + + 注意,就算访问 `http://127.0.0.1:8000/app`,也显示从选项 `--root-path` 中提取的 `/api/v1`,这是 `root_path` 的值。 + +打开含 Traefik 端口的 URL,包含路径前缀:<a href="http://127.0.0.1:9999/api/v1/app" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/app。</a> + +得到同样的响应: + +```JSON +{ + "message": "Hello World", + "root_path": "/api/v1" +} +``` + +但这一次 URL 包含了代理提供的路径前缀:`/api/v1`。 + +当然,这是通过代理访问应用的方式,因此,路径前缀 `/app/v1` 版本才是**正确**的。 + +而不带路径前缀的版本(`http://127.0.0.1:8000/app`),则由 Uvicorn 直接提供,专供*代理*(Traefik)访问。 + +这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用选项 `--root-path` 中的 `root_path`。 + +### 查看文档 + +但这才是有趣的地方 ✨ + +访问应用的**官方**方式是通过含路径前缀的代理。因此,不出所料,如果没有在 URL 中添加路径前缀,直接访问通过 Uvicorn 运行的 API 文档,不能正常访问,因为需要通过代理才能访问。 + +输入 <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs 查看 API 文档:</a> + +<img src="/img/tutorial/behind-a-proxy/image01.png"> + +但输入**官方**链接 `/api/v1/docs`,并使用端口 `9999` 访问 API 文档,就能正常运行了!🎉 + +输入 <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs 查看文档:</a> + +<img src="/img/tutorial/behind-a-proxy/image02.png"> + +一切正常。 ✔️ + +这是因为 FastAPI 在 OpenAPI 里使用 `root_path` 提供的 URL 创建默认 `server`。 + +## 附加的服务器 + +!!! warning "警告" + + 此用例较难,可以跳过。 + +默认情况下,**FastAPI** 使用 `root_path` 的链接在 OpenAPI 概图中创建 `server`。 + +但也可以使用其它备选 `servers`,例如,需要同一个 API 文档与 staging 和生产环境交互。 + +如果传递自定义 `servers` 列表,并有 `root_path`( 因为 API 使用了代理),**FastAPI** 会在列表开头使用这个 `root_path` 插入**服务器**。 + +例如: + +```Python hl_lines="4-7" +{!../../../docs_src/behind_a_proxy/tutorial003.py!} +``` + +这段代码生产如下 OpenAPI 概图: + +```JSON hl_lines="5-7" +{ + "openapi": "3.0.2", + // More stuff here + "servers": [ + { + "url": "/api/v1" + }, + { + "url": "https://stag.example.com", + "description": "Staging environment" + }, + { + "url": "https://prod.example.com", + "description": "Production environment" + } + ], + "paths": { + // More stuff here + } +} +``` + +!!! tip "提示" + + 注意,自动生成服务器时,`url` 的值 `/api/v1` 提取自 `roog_path`。 + +<a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs 的 API 文档所示如下:</a> + +<img src="/img/tutorial/behind-a-proxy/image03.png"> + +!!! tip "提示" + + API 文档与所选的服务器进行交互。 + +### 从 `root_path` 禁用自动服务器 + +如果不想让 **FastAPI** 包含使用 `root_path` 的自动服务器,则要使用参数 `root_path_in_servers=False`: + +```Python hl_lines="9" +{!../../../docs_src/behind_a_proxy/tutorial004.py!} +``` + +这样,就不会在 OpenAPI 概图中包含服务器了。 + +## 挂载子应用 + +如需挂载子应用(详见 [子应用 - 挂载](./sub-applications.md){.internal-link target=_blank}),也要通过 `root_path` 使用代理,这与正常应用一样,别无二致。 + +FastAPI 在内部使用 `root_path`,因此子应用也可以正常运行。✨ From 13c6eb2db00a51ef66f533eb60ff47d133c4084e Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:23:10 +0800 Subject: [PATCH 1824/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/events.md`=20(#3815)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/advanced/events.md | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/zh/docs/advanced/events.md diff --git a/docs/zh/docs/advanced/events.md b/docs/zh/docs/advanced/events.md new file mode 100644 index 000000000..6017b8ef0 --- /dev/null +++ b/docs/zh/docs/advanced/events.md @@ -0,0 +1,51 @@ +# 事件:启动 - 关闭 + +**FastAPI** 支持定义在应用启动前,或应用关闭后执行的事件处理器(函数)。 + +事件函数既可以声明为异步函数(`async def`),也可以声明为普通函数(`def`)。 + +!!! warning "警告" + + **FastAPI** 只执行主应用中的事件处理器,不执行[子应用 - 挂载](./sub-applications.md){.internal-link target=_blank}中的事件处理器。 + +## `startup` 事件 + +使用 `startup` 事件声明 `app` 启动前运行的函数: + +```Python hl_lines="8" +{!../../../docs_src/events/tutorial001.py!} +``` + +本例中,`startup` 事件处理器函数为项目数据库(只是**字典**)提供了一些初始值。 + +**FastAPI** 支持多个事件处理器函数。 + +只有所有 `startup` 事件处理器运行完毕,**FastAPI** 应用才开始接收请求。 + +## `shutdown` 事件 + +使用 `shutdown` 事件声明 `app` 关闭时运行的函数: + +```Python hl_lines="6" +{!../../../docs_src/events/tutorial002.py!} +``` + +此处,`shutdown` 事件处理器函数在 `log.txt` 中写入一行文本 `Application shutdown`。 + +!!! info "说明" + + `open()` 函数中,`mode="a"` 指的是**追加**。因此这行文本会添加在文件已有内容之后,不会覆盖之前的内容。 + +!!! tip "提示" + + 注意,本例使用 Python `open()` 标准函数与文件交互。 + + 这个函数执行 I/O(输入/输出)操作,需要等待内容写进磁盘。 + + 但 `open()` 函数不支持使用 `async` 与 `await`。 + + 因此,声明事件处理函数要使用 `def`,不能使用 `asnyc def`。 + +!!! info "说明" + + 有关事件处理器的详情,请参阅 <a href="https://www.starlette.io/events/" class="external-link" target="_blank">Starlette 官档 - 事件</a>。 From 5f194ddcc01c06a02a179d74055a95c7f79b772a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:23:59 +0000 Subject: [PATCH 1825/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 c9a2fc3da..dc2b8ab3c 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-events.md`. PR [#3818](https://github.com/tiangolo/fastapi/pull/3818) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-websockets.md`. PR [#3817](https://github.com/tiangolo/fastapi/pull/3817) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-database.md`. PR [#3821](https://github.com/tiangolo/fastapi/pull/3821) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/deta.md`. PR [#3837](https://github.com/tiangolo/fastapi/pull/3837) by [@jaystone776](https://github.com/jaystone776). From 39d26f3491c2e93cfada205a59d56238932ed096 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:26:17 +0000 Subject: [PATCH 1826/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 dc2b8ab3c..484b4bed4 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/behind-a-proxy.md`. PR [#3820](https://github.com/tiangolo/fastapi/pull/3820) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-events.md`. PR [#3818](https://github.com/tiangolo/fastapi/pull/3818) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-websockets.md`. PR [#3817](https://github.com/tiangolo/fastapi/pull/3817) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-database.md`. PR [#3821](https://github.com/tiangolo/fastapi/pull/3821) by [@jaystone776](https://github.com/jaystone776). From 1f9d5a1db9aa6ab0fe4f8a849fba873487ef3c8f Mon Sep 17 00:00:00 2001 From: jaystone776 <jilei776@gmail.com> Date: Mon, 29 Jan 2024 02:26:57 +0800 Subject: [PATCH 1827/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/advanced/advanced-dependencies.md`?= =?UTF-8?q?=20(#3798)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zh/docs/advanced/advanced-dependencies.md | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/zh/docs/advanced/advanced-dependencies.md diff --git a/docs/zh/docs/advanced/advanced-dependencies.md b/docs/zh/docs/advanced/advanced-dependencies.md new file mode 100644 index 000000000..b2f6e3559 --- /dev/null +++ b/docs/zh/docs/advanced/advanced-dependencies.md @@ -0,0 +1,71 @@ +# 高级依赖项 + +## 参数化的依赖项 + +我们之前看到的所有依赖项都是写死的函数或类。 + +但也可以为依赖项设置参数,避免声明多个不同的函数或类。 + +假设要创建校验查询参数 `q` 是否包含固定内容的依赖项。 + +但此处要把待检验的固定内容定义为参数。 + +## **可调用**实例 + +Python 可以把类实例变为**可调用项**。 + +这里说的不是类本身(类本就是可调用项),而是类实例。 + +为此,需要声明 `__call__` 方法: + +```Python hl_lines="10" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +本例中,**FastAPI** 使用 `__call__` 检查附加参数及子依赖项,稍后,还要调用它向*路径操作函数*传递值。 + +## 参数化实例 + +接下来,使用 `__init__` 声明用于**参数化**依赖项的实例参数: + +```Python hl_lines="7" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +本例中,**FastAPI** 不使用 `__init__`,我们要直接在代码中使用。 + +## 创建实例 + +使用以下代码创建类实例: + +```Python hl_lines="16" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +这样就可以**参数化**依赖项,它包含 `checker.fixed_content` 的属性 - `"bar"`。 + +## 把实例作为依赖项 + +然后,不要再在 `Depends(checker)` 中使用 `Depends(FixedContentQueryChecker)`, 而是要使用 `checker`,因为依赖项是类实例 - `checker`,不是类。 + +处理依赖项时,**FastAPI** 以如下方式调用 `checker`: + +```Python +checker(q="somequery") +``` + +……并用*路径操作函数*的参数 `fixed_content_included` 返回依赖项的值: + +```Python hl_lines="20" +{!../../../docs_src/dependencies/tutorial011.py!} +``` + +!!! tip "提示" + + 本章示例有些刻意,也看不出有什么用处。 + + 这个简例只是为了说明高级依赖项的运作机制。 + + 在有关安全的章节中,工具函数将以这种方式实现。 + + 只要能理解本章内容,就能理解安全工具背后的运行机制。 From 0cf8c74e464656df601cc644311bdb021d3331bb Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:27:02 +0000 Subject: [PATCH 1828/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 484b4bed4..7a1b061ef 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/events.md`. PR [#3815](https://github.com/tiangolo/fastapi/pull/3815) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/behind-a-proxy.md`. PR [#3820](https://github.com/tiangolo/fastapi/pull/3820) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-events.md`. PR [#3818](https://github.com/tiangolo/fastapi/pull/3818) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-websockets.md`. PR [#3817](https://github.com/tiangolo/fastapi/pull/3817) by [@jaystone776](https://github.com/jaystone776). From b2faa22f42714ffa808f05912e4e1f35fb6d754c Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:30:52 +0000 Subject: [PATCH 1829/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 7a1b061ef..853330031 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/advanced/advanced-dependencies.md`. PR [#3798](https://github.com/tiangolo/fastapi/pull/3798) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/events.md`. PR [#3815](https://github.com/tiangolo/fastapi/pull/3815) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/behind-a-proxy.md`. PR [#3820](https://github.com/tiangolo/fastapi/pull/3820) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/testing-events.md`. PR [#3818](https://github.com/tiangolo/fastapi/pull/3818) by [@jaystone776](https://github.com/jaystone776). From aae14c5379ff18f9e328bfe415f4d5275578f97b Mon Sep 17 00:00:00 2001 From: Sho Nakamura <sh0nk.developer@gmail.com> Date: Mon, 29 Jan 2024 03:36:35 +0900 Subject: [PATCH 1830/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Japanese=20trans?= =?UTF-8?q?lation=20for=20`docs/ja/docs/tutorial/security/get-current-user?= =?UTF-8?q?.md`=20(#2681)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com> --- .../tutorial/security/get-current-user.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/ja/docs/tutorial/security/get-current-user.md diff --git a/docs/ja/docs/tutorial/security/get-current-user.md b/docs/ja/docs/tutorial/security/get-current-user.md new file mode 100644 index 000000000..7f8dcaad2 --- /dev/null +++ b/docs/ja/docs/tutorial/security/get-current-user.md @@ -0,0 +1,114 @@ +# 現在のユーザーの取得 + +一つ前の章では、(依存性注入システムに基づいた)セキュリティシステムは、 *path operation関数* に `str` として `token` を与えていました: + +```Python hl_lines="10" +{!../../../docs_src/security/tutorial001.py!} +``` + +しかし、それはまだそんなに有用ではありません。 + +現在のユーザーを取得するようにしてみましょう。 + +## ユーザーモデルの作成 + +まずは、Pydanticのユーザーモデルを作成しましょう。 + +ボディを宣言するのにPydanticを使用するのと同じやり方で、Pydanticを別のどんなところでも使うことができます: + +```Python hl_lines="5 12-16" +{!../../../docs_src/security/tutorial002.py!} +``` + +## 依存関係 `get_current_user` を作成 + +依存関係 `get_current_user` を作ってみましょう。 + +依存関係はサブ依存関係を持つことができるのを覚えていますか? + +`get_current_user` は前に作成した `oauth2_scheme` と同じ依存関係を持ちます。 + +以前直接 *path operation* の中でしていたのと同じように、新しい依存関係である `get_current_user` は `str` として `token` を受け取るようになります: + +```Python hl_lines="25" +{!../../../docs_src/security/tutorial002.py!} +``` + +## ユーザーの取得 + +`get_current_user` は作成した(偽物の)ユーティリティ関数を使って、 `str` としてトークンを受け取り、先ほどのPydanticの `User` モデルを返却します: + +```Python hl_lines="19-22 26-27" +{!../../../docs_src/security/tutorial002.py!} +``` + +## 現在のユーザーの注入 + +ですので、 `get_current_user` に対して同様に *path operation* の中で `Depends` を利用できます。 + +```Python hl_lines="31" +{!../../../docs_src/security/tutorial002.py!} +``` + +Pydanticモデルの `User` として、 `current_user` の型を宣言することに注意してください。 + +その関数の中ですべての入力補完や型チェックを行う際に役に立ちます。 + +!!! tip "豆知識" + リクエストボディはPydanticモデルでも宣言できることを覚えているかもしれません。 + + ここでは `Depends` を使っているおかげで、 **FastAPI** が混乱することはありません。 + + +!!! check "確認" + 依存関係システムがこのように設計されているおかげで、 `User` モデルを返却する別の依存関係(別の"dependables")を持つことができます。 + + 同じデータ型を返却する依存関係は一つだけしか持てない、という制約が入ることはないのです。 + + +## 別のモデル + +これで、*path operation関数* の中で現在のユーザーを直接取得し、`Depends` を使って、 **依存性注入** レベルでセキュリティメカニズムを処理できるようになりました。 + +そして、セキュリティ要件のためにどんなモデルやデータでも利用することができます。(この場合は、 Pydanticモデルの `User`) + +しかし、特定のデータモデルやクラス、型に制限されることはありません。 + +モデルを、 `id` と `email` は持つが、 `username` は全く持たないようにしたいですか? わかりました。同じ手段でこうしたこともできます。 + +ある `str` だけを持ちたい? あるいはある `dict` だけですか? それとも、データベースクラスのモデルインスタンスを直接持ちたいですか? すべて同じやり方で機能します。 + +実際には、あなたのアプリケーションにはログインするようなユーザーはおらず、単にアクセストークンを持つロボットやボット、別のシステムがありますか?ここでも、全く同じようにすべて機能します。 + +あなたのアプリケーションに必要なのがどんな種類のモデル、どんな種類のクラス、どんな種類のデータベースであったとしても、 **FastAPI** は依存性注入システムでカバーしてくれます。 + + +## コードサイズ + +この例は冗長に見えるかもしれません。セキュリティとデータモデルユーティリティ関数および *path operations* が同じファイルに混在しているということを覚えておいてください。 + +しかし、ここに重要なポイントがあります。 + +セキュリティと依存性注入に関するものは、一度だけ書きます。 + +そして、それは好きなだけ複雑にすることができます。それでも、一箇所に、一度だけ書くのです。すべての柔軟性を備えます。 + +しかし、同じセキュリティシステムを使って何千ものエンドポイント(*path operations*)を持つことができます。 + +そして、それらエンドポイントのすべて(必要な、どの部分でも)がこうした依存関係や、あなたが作成する別の依存関係を再利用する利点を享受できるのです。 + +さらに、こうした何千もの *path operations* は、たった3行で表現できるのです: + +```Python hl_lines="30-32" +{!../../../docs_src/security/tutorial002.py!} +``` + +## まとめ + +これで、 *path operation関数* の中で直接現在のユーザーを取得できるようになりました。 + +既に半分のところまで来ています。 + +あとは、 `username` と `password` を実際にそのユーザーやクライアントに送る、 *path operation* を追加する必要があるだけです。 + +次はそれを説明します。 From ab22b795903bb9a782ccfc3d2b4e450b565f6bfc Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 28 Jan 2024 18:43:09 +0000 Subject: [PATCH 1831/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 853330031..b4337c742 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/get-current-user.md`. PR [#2681](https://github.com/tiangolo/fastapi/pull/2681) by [@sh0nk](https://github.com/sh0nk). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/advanced-dependencies.md`. PR [#3798](https://github.com/tiangolo/fastapi/pull/3798) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/events.md`. PR [#3815](https://github.com/tiangolo/fastapi/pull/3815) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/behind-a-proxy.md`. PR [#3820](https://github.com/tiangolo/fastapi/pull/3820) by [@jaystone776](https://github.com/jaystone776). From 26ab83e1571af3b229dc748e68972e446edf47ca Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Mon, 29 Jan 2024 18:32:43 +0100 Subject: [PATCH 1832/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/body-multiple-params.md`=20?= =?UTF-8?q?(#10308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/body-multiple-params.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/de/docs/tutorial/body-multiple-params.md diff --git a/docs/de/docs/tutorial/body-multiple-params.md b/docs/de/docs/tutorial/body-multiple-params.md new file mode 100644 index 000000000..6a237243e --- /dev/null +++ b/docs/de/docs/tutorial/body-multiple-params.md @@ -0,0 +1,308 @@ +# Body – Mehrere Parameter + +Jetzt, da wir gesehen haben, wie `Path` und `Query` verwendet werden, schauen wir uns fortgeschrittenere Verwendungsmöglichkeiten von Requestbody-Deklarationen an. + +## `Path`-, `Query`- und Body-Parameter vermischen + +Zuerst einmal, Sie können `Path`-, `Query`- und Requestbody-Parameter-Deklarationen frei mischen und **FastAPI** wird wissen, was zu tun ist. + +Und Sie können auch Body-Parameter als optional kennzeichnen, indem Sie den Defaultwert auf `None` setzen: + +=== "Python 3.10+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="18-20" + {!> ../../../docs_src/body_multiple_params/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="17-19" + {!> ../../../docs_src/body_multiple_params/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="19-21" + {!> ../../../docs_src/body_multiple_params/tutorial001.py!} + ``` + +!!! note "Hinweis" + Beachten Sie, dass in diesem Fall das `item`, welches vom Body genommen wird, optional ist. Da es `None` als Defaultwert hat. + +## Mehrere Body-Parameter + +Im vorherigen Beispiel erwartete die *Pfadoperation* einen JSON-Body mit den Attributen eines `Item`s, etwa: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +Aber Sie können auch mehrere Body-Parameter deklarieren, z. B. `item` und `user`: + +=== "Python 3.10+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial002.py!} + ``` + +In diesem Fall wird **FastAPI** bemerken, dass es mehr als einen Body-Parameter in der Funktion gibt (zwei Parameter, die Pydantic-Modelle sind). + +Es wird deshalb die Parameternamen als Schlüssel (Feldnamen) im Body verwenden, und erwartet einen Body wie folgt: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + } +} +``` + +!!! note "Hinweis" + Beachten Sie, dass, obwohl `item` wie zuvor deklariert wurde, es nun unter einem Schlüssel `item` im Body erwartet wird. + +**FastAPI** wird die automatische Konvertierung des Requests übernehmen, sodass der Parameter `item` seinen spezifischen Inhalt bekommt, genau so wie der Parameter `user`. + +Es wird die Validierung dieser zusammengesetzten Daten übernehmen, und sie im OpenAPI-Schema und der automatischen Dokumentation dokumentieren. + +## Einzelne Werte im Body + +So wie `Query` und `Path` für Query- und Pfad-Parameter, hat **FastAPI** auch das Äquivalent `Body`, um Extra-Daten für Body-Parameter zu definieren. + +Zum Beispiel, das vorherige Modell erweiternd, könnten Sie entscheiden, dass Sie einen weiteren Schlüssel <abbr title="Wichtigkeit">`importance`</abbr> haben möchten, im selben Body, Seite an Seite mit `item` und `user`. + +Wenn Sie diesen Parameter einfach so hinzufügen, wird **FastAPI** annehmen, dass es ein Query-Parameter ist. + +Aber Sie können **FastAPI** instruieren, ihn als weiteren Body-Schlüssel zu erkennen, indem Sie `Body` verwenden: + +=== "Python 3.10+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="23" + {!> ../../../docs_src/body_multiple_params/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="24" + {!> ../../../docs_src/body_multiple_params/tutorial003_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="20" + {!> ../../../docs_src/body_multiple_params/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="22" + {!> ../../../docs_src/body_multiple_params/tutorial003.py!} + ``` + +In diesem Fall erwartet **FastAPI** einen Body wie: + +```JSON +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + }, + "user": { + "username": "dave", + "full_name": "Dave Grohl" + }, + "importance": 5 +} +``` + +Wiederum wird es die Daten konvertieren, validieren, dokumentieren, usw. + +## Mehrere Body-Parameter und Query-Parameter + +Natürlich können Sie auch, wann immer Sie das brauchen, weitere Query-Parameter hinzufügen, zusätzlich zu den Body-Parametern. + +Da einfache Werte standardmäßig als Query-Parameter interpretiert werden, müssen Sie `Query` nicht explizit hinzufügen, Sie können einfach schreiben: + +```Python +q: Union[str, None] = None +``` + +Oder in Python 3.10 und darüber: + +```Python +q: str | None = None +``` + +Zum Beispiel: + +=== "Python 3.10+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="28" + {!> ../../../docs_src/body_multiple_params/tutorial004_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="25" + {!> ../../../docs_src/body_multiple_params/tutorial004_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="27" + {!> ../../../docs_src/body_multiple_params/tutorial004.py!} + ``` + +!!! info + `Body` hat die gleichen zusätzlichen Validierungs- und Metadaten-Parameter wie `Query` und `Path` und andere, die Sie später kennenlernen. + +## Einen einzelnen Body-Parameter einbetten + +Nehmen wir an, Sie haben nur einen einzelnen `item`-Body-Parameter, ein Pydantic-Modell `Item`. + +Normalerweise wird **FastAPI** dann seinen JSON-Body direkt erwarten. + +Aber wenn Sie möchten, dass es einen JSON-Body erwartet, mit einem Schlüssel `item` und darin den Inhalt des Modells, so wie es das tut, wenn Sie mehrere Body-Parameter deklarieren, dann können Sie den speziellen `Body`-Parameter `embed` setzen: + +```Python +item: Item = Body(embed=True) +``` + +so wie in: + +=== "Python 3.10+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_multiple_params/tutorial005_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="15" + {!> ../../../docs_src/body_multiple_params/tutorial005_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="17" + {!> ../../../docs_src/body_multiple_params/tutorial005.py!} + ``` + +In diesem Fall erwartet **FastAPI** einen Body wie: + +```JSON hl_lines="2" +{ + "item": { + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 + } +} +``` + +statt: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2 +} +``` + +## Zusammenfassung + +Sie können mehrere Body-Parameter zu ihrer *Pfadoperation-Funktion* hinzufügen, obwohl ein Request nur einen einzigen Body enthalten kann. + +**FastAPI** wird sich darum kümmern, Ihnen korrekte Daten in Ihrer Funktion zu überreichen, und das korrekte Schema in der *Pfadoperation* zu validieren und zu dokumentieren. + +Sie können auch einzelne Werte deklarieren, die als Teil des Bodys empfangen werden. + +Und Sie können **FastAPI** instruieren, den Body in einem Schlüssel unterzubringen, selbst wenn nur ein einzelner Body-Parameter deklariert ist. From b180d39d7e03e9ae778a1cfe42d11e909131026d Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 17:33:04 +0000 Subject: [PATCH 1833/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 b4337c742..a4f9a4593 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/body-multiple-params.md`. PR [#10308](https://github.com/tiangolo/fastapi/pull/10308) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/get-current-user.md`. PR [#2681](https://github.com/tiangolo/fastapi/pull/2681) by [@sh0nk](https://github.com/sh0nk). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/advanced-dependencies.md`. PR [#3798](https://github.com/tiangolo/fastapi/pull/3798) by [@jaystone776](https://github.com/jaystone776). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/events.md`. PR [#3815](https://github.com/tiangolo/fastapi/pull/3815) by [@jaystone776](https://github.com/jaystone776). From 32e5a37d1d2f56f496fb4cc9f9ef0d4919953ed1 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Mon, 29 Jan 2024 18:35:23 +0100 Subject: [PATCH 1834/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/body.md`=20(#10295)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/body.md | 213 ++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/de/docs/tutorial/body.md diff --git a/docs/de/docs/tutorial/body.md b/docs/de/docs/tutorial/body.md new file mode 100644 index 000000000..97215a780 --- /dev/null +++ b/docs/de/docs/tutorial/body.md @@ -0,0 +1,213 @@ +# Requestbody + +Wenn Sie Daten von einem <abbr title="Client: Eine Software, die sich mit einem Server verbindet.">Client</abbr> (sagen wir, einem Browser) zu Ihrer API senden, dann senden Sie diese als einen **Requestbody** (Deutsch: Anfragekörper). + +Ein **Request**body sind Daten, die vom Client zu Ihrer API gesendet werden. Ein **Response**body (Deutsch: Antwortkörper) sind Daten, die Ihre API zum Client sendet. + +Ihre API sendet fast immer einen **Response**body. Aber Clients senden nicht unbedingt immer **Request**bodys (sondern nur Metadaten). + +Um einen **Request**body zu deklarieren, verwenden Sie <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>-Modelle mit allen deren Fähigkeiten und Vorzügen. + +!!! info + Um Daten zu versenden, sollten Sie eines von: `POST` (meistverwendet), `PUT`, `DELETE` oder `PATCH` verwenden. + + Senden Sie einen Body mit einem `GET`-Request, dann führt das laut Spezifikation zu undefiniertem Verhalten. Trotzdem wird es von FastAPI unterstützt, für sehr komplexe/extreme Anwendungsfälle. + + Da aber davon abgeraten wird, zeigt die interaktive Dokumentation mit Swagger-Benutzeroberfläche die Dokumentation für den Body auch nicht an, wenn `GET` verwendet wird. Dazwischengeschaltete Proxys unterstützen es möglicherweise auch nicht. + +## Importieren Sie Pydantics `BaseModel` + +Zuerst müssen Sie `BaseModel` von `pydantic` importieren: + +=== "Python 3.10+" + + ```Python hl_lines="2" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +## Erstellen Sie Ihr Datenmodell + +Dann deklarieren Sie Ihr Datenmodell als eine Klasse, die von `BaseModel` erbt. + +Verwenden Sie Standard-Python-Typen für die Klassenattribute: + +=== "Python 3.10+" + + ```Python hl_lines="5-9" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="7-11" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +Wie auch bei Query-Parametern gilt, wenn ein Modellattribut einen Defaultwert hat, ist das Attribut nicht erforderlich. Ansonsten ist es erforderlich. Verwenden Sie `None`, um es als optional zu kennzeichnen. + +Zum Beispiel deklariert das obige Modell ein JSON "`object`" (oder Python-`dict`) wie dieses: + +```JSON +{ + "name": "Foo", + "description": "An optional description", + "price": 45.2, + "tax": 3.5 +} +``` + +Da `description` und `tax` optional sind (mit `None` als Defaultwert), wäre folgendes JSON "`object`" auch gültig: + +```JSON +{ + "name": "Foo", + "price": 45.2 +} +``` + +## Deklarieren Sie es als Parameter + +Um es zu Ihrer *Pfadoperation* hinzuzufügen, deklarieren Sie es auf die gleiche Weise, wie Sie Pfad- und Query-Parameter deklariert haben: + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial001.py!} + ``` + +... und deklarieren Sie seinen Typ als das Modell, welches Sie erstellt haben, `Item`. + +## Resultate + +Mit nur dieser Python-Typdeklaration, wird **FastAPI**: + +* Den Requestbody als JSON lesen. +* Die entsprechenden Typen konvertieren (falls nötig). +* Diese Daten validieren. + * Wenn die Daten ungültig sind, einen klar lesbaren Fehler zurückgeben, der anzeigt, wo und was die inkorrekten Daten waren. +* Ihnen die erhaltenen Daten im Parameter `item` übergeben. + * Da Sie diesen in der Funktion als vom Typ `Item` deklariert haben, erhalten Sie die ganze Editor-Unterstützung (Autovervollständigung, usw.) für alle Attribute und deren Typen. +* Eine <a href="https://json-schema.org" class="external-link" target="_blank">JSON Schema</a> Definition für Ihr Modell generieren, welche Sie überall sonst verwenden können, wenn es für Ihr Projekt Sinn macht. +* Diese Schemas werden Teil des generierten OpenAPI-Schemas und werden von den <abbr title="User Interface – Benutzeroberfläche">UIs</abbr> der automatischen Dokumentation verwendet. + +## Automatische Dokumentation + +Die JSON-Schemas Ihrer Modelle werden Teil ihrer OpenAPI-generierten Schemas und werden in der interaktiven API Dokumentation angezeigt: + +<img src="/img/tutorial/body/image01.png"> + +Und werden auch verwendet in der API-Dokumentation innerhalb jeder *Pfadoperation*, welche sie braucht: + +<img src="/img/tutorial/body/image02.png"> + +## Editor Unterstützung + +In Ihrem Editor, innerhalb Ihrer Funktion, erhalten Sie Typhinweise und Code-Vervollständigung überall (was nicht der Fall wäre, wenn Sie ein `dict` anstelle eines Pydantic Modells erhalten hätten): + +<img src="/img/tutorial/body/image03.png"> + +Sie bekommen auch Fehler-Meldungen für inkorrekte Typoperationen: + +<img src="/img/tutorial/body/image04.png"> + +Das ist nicht zufällig so, das ganze Framework wurde um dieses Design herum aufgebaut. + +Und es wurde in der Designphase gründlich getestet, vor der Implementierung, um sicherzustellen, dass es mit jedem Editor funktioniert. + +Es gab sogar ein paar Änderungen an Pydantic selbst, um das zu unterstützen. + +Die vorherigen Screenshots zeigten <a href="https://code.visualstudio.com" class="external-link" target="_blank">Visual Studio Code</a>. + +Aber Sie bekommen die gleiche Editor-Unterstützung in <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> und in den meisten anderen Python-Editoren: + +<img src="/img/tutorial/body/image05.png"> + +!!! tip "Tipp" + Wenn Sie <a href="https://www.jetbrains.com/pycharm/" class="external-link" target="_blank">PyCharm</a> als Ihren Editor verwenden, probieren Sie das <a href="https://github.com/koxudaxi/pydantic-pycharm-plugin/" class="external-link" target="_blank">Pydantic PyCharm Plugin</a> aus. + + Es verbessert die Editor-Unterstützung für Pydantic-Modelle, mit: + + * Code-Vervollständigung + * Typüberprüfungen + * Refaktorisierung + * Suchen + * Inspektionen + +## Das Modell verwenden + +Innerhalb der Funktion können Sie alle Attribute des Modells direkt verwenden: + +=== "Python 3.10+" + + ```Python hl_lines="19" + {!> ../../../docs_src/body/tutorial002_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="21" + {!> ../../../docs_src/body/tutorial002.py!} + ``` + +## Requestbody- + Pfad-Parameter + +Sie können Pfad- und Requestbody-Parameter gleichzeitig deklarieren. + +**FastAPI** erkennt, dass Funktionsparameter, die mit Pfad-Parametern übereinstimmen, **vom Pfad genommen** werden sollen, und dass Funktionsparameter, welche Pydantic-Modelle sind, **vom Requestbody genommen** werden sollen. + +=== "Python 3.10+" + + ```Python hl_lines="15-16" + {!> ../../../docs_src/body/tutorial003_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-18" + {!> ../../../docs_src/body/tutorial003.py!} + ``` + +## Requestbody- + Pfad- + Query-Parameter + +Sie können auch zur gleichen Zeit **Body-**, **Pfad-** und **Query-Parameter** deklarieren. + +**FastAPI** wird jeden Parameter korrekt erkennen und die Daten vom richtigen Ort holen. + +=== "Python 3.10+" + + ```Python hl_lines="16" + {!> ../../../docs_src/body/tutorial004_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body/tutorial004.py!} + ``` + +Die Funktionsparameter werden wie folgt erkannt: + +* Wenn der Parameter auch im **Pfad** deklariert wurde, wird er als Pfad-Parameter interpretiert. +* Wenn der Parameter ein **einfacher Typ** ist (wie `int`, `float`, `str`, `bool`, usw.), wird er als **Query**-Parameter interpretiert. +* Wenn der Parameter vom Typ eines **Pydantic-Modells** ist, wird er als Request**body** interpretiert. + +!!! note "Hinweis" + FastAPI weiß, dass der Wert von `q` nicht erforderlich ist, wegen des definierten Defaultwertes `= None` + + Das `Union` in `Union[str, None]` wird von FastAPI nicht verwendet, aber es erlaubt Ihrem Editor, Sie besser zu unterstützen und Fehler zu erkennen. + +## Ohne Pydantic + +Wenn Sie keine Pydantic-Modelle verwenden wollen, können Sie auch **Body**-Parameter nehmen. Siehe die Dokumentation unter [Body – Mehrere Parameter: Einfache Werte im Body](body-multiple-params.md#einzelne-werte-im-body){.internal-link target=\_blank}. From 4185f0bd9d9a30935de9bfb2c00b1b9702d9c2c6 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Mon, 29 Jan 2024 18:36:19 +0100 Subject: [PATCH 1835/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/body-fields.md`=20(#10310)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/body-fields.md | 115 +++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/de/docs/tutorial/body-fields.md diff --git a/docs/de/docs/tutorial/body-fields.md b/docs/de/docs/tutorial/body-fields.md new file mode 100644 index 000000000..643be7489 --- /dev/null +++ b/docs/de/docs/tutorial/body-fields.md @@ -0,0 +1,115 @@ +# Body – Felder + +So wie Sie zusätzliche Validation und Metadaten in Parametern der **Pfadoperation-Funktion** mittels `Query`, `Path` und `Body` deklarieren, können Sie auch innerhalb von Pydantic-Modellen zusätzliche Validation und Metadaten deklarieren, mittels Pydantics `Field`. + +## `Field` importieren + +Importieren Sie es zuerst: + +=== "Python 3.10+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="2" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="4" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +!!! warning "Achtung" + Beachten Sie, dass `Field` direkt von `pydantic` importiert wird, nicht von `fastapi`, wie die anderen (`Query`, `Path`, `Body`, usw.) + +## Modellattribute deklarieren + +Dann können Sie `Field` mit Modellattributen deklarieren: + +=== "Python 3.10+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="12-15" + {!> ../../../docs_src/body_fields/tutorial001_an.py!} + ``` + +=== "Python 3.10+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="9-12" + {!> ../../../docs_src/body_fields/tutorial001_py310.py!} + ``` + +=== "Python 3.8+ nicht annotiert" + + !!! tip "Tipp" + Bevorzugen Sie die `Annotated`-Version, falls möglich. + + ```Python hl_lines="11-14" + {!> ../../../docs_src/body_fields/tutorial001.py!} + ``` + +`Field` funktioniert genauso wie `Query`, `Path` und `Body`, es hat die gleichen Parameter, usw. + +!!! note "Technische Details" + Tatsächlich erstellen `Query`, `Path` und andere, die sie kennenlernen werden, Instanzen von Unterklassen einer allgemeinen Klasse `Param`, die ihrerseits eine Unterklasse von Pydantics `FieldInfo`-Klasse ist. + + Und Pydantics `Field` gibt ebenfalls eine Instanz von `FieldInfo` zurück. + + `Body` gibt auch Instanzen einer Unterklasse von `FieldInfo` zurück. Und später werden Sie andere sehen, die Unterklassen der `Body`-Klasse sind. + + Denken Sie daran, dass `Query`, `Path` und andere von `fastapi` tatsächlich Funktionen sind, die spezielle Klassen zurückgeben. + +!!! tip "Tipp" + Beachten Sie, dass jedes Modellattribut mit einem Typ, Defaultwert und `Field` die gleiche Struktur hat wie ein Parameter einer Pfadoperation-Funktion, nur mit `Field` statt `Path`, `Query`, `Body`. + +## Zusätzliche Information hinzufügen + +Sie können zusätzliche Information in `Field`, `Query`, `Body`, usw. deklarieren. Und es wird im generierten JSON-Schema untergebracht. + +Sie werden später mehr darüber lernen, wie man zusätzliche Information unterbringt, wenn Sie lernen, Beispiele zu deklarieren. + +!!! warning "Achtung" + Extra-Schlüssel, die `Field` überreicht werden, werden auch im resultierenden OpenAPI-Schema Ihrer Anwendung gelistet. Da diese Schlüssel nicht notwendigerweise Teil der OpenAPI-Spezifikation sind, könnten einige OpenAPI-Tools, wie etwa [der OpenAPI-Validator](https://validator.swagger.io/), nicht mit Ihrem generierten Schema funktionieren. + +## Zusammenfassung + +Sie können Pydantics `Field` verwenden, um zusätzliche Validierungen und Metadaten für Modellattribute zu deklarieren. + +Sie können auch Extra-Schlüssel verwenden, um zusätzliche JSON-Schema-Metadaten zu überreichen. From 2d886c0e7563cec5a98a823f3eb0932e3e3a394e Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 17:36:44 +0000 Subject: [PATCH 1836/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 a4f9a4593..f6685ed7a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/body.md`. PR [#10295](https://github.com/tiangolo/fastapi/pull/10295) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body-multiple-params.md`. PR [#10308](https://github.com/tiangolo/fastapi/pull/10308) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/get-current-user.md`. PR [#2681](https://github.com/tiangolo/fastapi/pull/2681) by [@sh0nk](https://github.com/sh0nk). * 🌐 Add Chinese translation for `docs/zh/docs/advanced/advanced-dependencies.md`. PR [#3798](https://github.com/tiangolo/fastapi/pull/3798) by [@jaystone776](https://github.com/jaystone776). From 7c5c29de9ee7362155665cacfd93e6d76a282242 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 17:37:16 +0000 Subject: [PATCH 1837/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f6685ed7a..03319666a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/body-fields.md`. PR [#10310](https://github.com/tiangolo/fastapi/pull/10310) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body.md`. PR [#10295](https://github.com/tiangolo/fastapi/pull/10295) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body-multiple-params.md`. PR [#10308](https://github.com/tiangolo/fastapi/pull/10308) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Japanese translation for `docs/ja/docs/tutorial/security/get-current-user.md`. PR [#2681](https://github.com/tiangolo/fastapi/pull/2681) by [@sh0nk](https://github.com/sh0nk). From 11a1268fe29b0aeb8bcb69c2c0aa1721fa95fd65 Mon Sep 17 00:00:00 2001 From: Reza Rohani <theonlykingpin@gmail.com> Date: Mon, 29 Jan 2024 21:18:49 +0330 Subject: [PATCH 1838/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Farsi=20trans?= =?UTF-8?q?lation=20for=20`docs/fa/docs/index.md`=20(#10216)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fa/docs/index.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/fa/docs/index.md b/docs/fa/docs/index.md index 248084389..cc211848b 100644 --- a/docs/fa/docs/index.md +++ b/docs/fa/docs/index.md @@ -32,9 +32,9 @@ FastAPI یک وب فریم‌ورک مدرن و سریع (با کارایی با * **<abbr title="Fast">سرعت</abbr>**: کارایی بسیار بالا و قابل مقایسه با **NodeJS** و **Go** (با تشکر از Starlette و Pydantic). [یکی از سریع‌ترین فریم‌ورک‌های پایتونی موجود](#performance). -* **<abbr title="Fast to code">کدنویسی سریع</abbr>**: افزایش ۲۰۰ تا ۳۰۰ درصدی سرعت توسعه فابلیت‌های جدید. * +* **<abbr title="Fast to code">کدنویسی سریع</abbr>**: افزایش ۲۰۰ تا ۳۰۰ درصدی سرعت توسعه قابلیت‌های جدید. * * **<abbr title="Fewer bugs">باگ کمتر</abbr>**: کاهش ۴۰ درصدی خطاهای انسانی (برنامه‌نویسی). * -* **<abbr title="Intuitive">غریزی</abbr>**: پشتیبانی فوق‌العاده در محیط‌های توسعه یکپارچه (IDE). <abbr title="یا اتوکامپلیت، اتوکامپلشن، اینتلیسنس">تکمیل</abbr> در همه بخش‌های کد. کاهش زمان رفع باگ. +* **<abbr title="Intuitive">هوشمندانه</abbr>**: پشتیبانی فوق‌العاده در محیط‌های توسعه یکپارچه (IDE). <abbr title="یا اتوکامپلیت، اتوکامپلشن، اینتلیسنس">تکمیل</abbr> در همه بخش‌های کد. کاهش زمان رفع باگ. * **<abbr title="Easy">آسان</abbr>**: طراحی شده برای یادگیری و استفاده آسان. کاهش زمان مورد نیاز برای مراجعه به مستندات. * **<abbr title="Short">کوچک</abbr>**: کاهش تکرار در کد. چندین قابلیت برای هر پارامتر (منظور پارامترهای ورودی تابع هندلر می‌باشد، به بخش <a href="https://fastapi.tiangolo.com/#recap">خلاصه</a> در همین صفحه مراجعه شود). باگ کمتر. * **<abbr title="Robust">استوار</abbr>**: ایجاد کدی آماده برای استفاده در محیط پروداکشن و تولید خودکار <abbr title="Interactive documentation">مستندات تعاملی</abbr> @@ -140,7 +140,7 @@ $ pip install "uvicorn[standard]" ## مثال ### ایجاد کنید -* فایلی به نام `main.py` با محتوای زیر ایجاد کنید : +* فایلی به نام `main.py` با محتوای زیر ایجاد کنید: ```Python from typing import Union @@ -163,7 +163,7 @@ def read_item(item_id: int, q: Union[str, None] = None): <details markdown="1"> <summary>همچنین می‌توانید از <code>async def</code>... نیز استفاده کنید</summary> -اگر در کدتان از `async` / `await` استفاده می‌کنید, از `async def` برای تعریف تابع خود استفاده کنید: +اگر در کدتان از `async` / `await` استفاده می‌کنید، از `async def` برای تعریف تابع خود استفاده کنید: ```Python hl_lines="9 14" from typing import Optional @@ -211,7 +211,7 @@ INFO: Application startup complete. <details markdown="1"> <summary>درباره دستور <code>uvicorn main:app --reload</code>...</summary> -دستور `uvicorn main:app` شامل موارد زیر است: +دستور `uvicorn main:app` شامل موارد زیر است: * `main`: فایل `main.py` (ماژول پایتون ایجاد شده). * `app`: شیء ایجاد شده در فایل `main.py` در خط `app = FastAPI()`. @@ -232,7 +232,7 @@ INFO: Application startup complete. تا اینجا شما APIای ساختید که: * درخواست‌های HTTP به _مسیرهای_ `/` و `/items/{item_id}` را دریافت می‌کند. -* هردو _مسیر_ <abbr title="operations در OpenAPI">عملیات</abbr> (یا HTTP _متد_) `GET` را پشتیبانی می‌کنند. +* هردو _مسیر_ <abbr title="operations در OpenAPI">عملیات</abbr> (یا HTTP _متد_) `GET` را پشتیبانی می‌کند. * _مسیر_ `/items/{item_id}` شامل <abbr title="Path Parameter">_پارامتر مسیر_</abbr> `item_id` از نوع `int` است. * _مسیر_ `/items/{item_id}` شامل <abbr title="Query Parameter">_پارامتر پرسمان_</abbr> اختیاری `q` از نوع `str` است. @@ -254,7 +254,7 @@ INFO: Application startup complete. ## تغییر مثال -حال فایل `main.py` را مطابق زیر ویرایش کنید تا بتوانید <abbr title="Body">بدنه</abbr> یک درخواست `PUT` را دریافت کنید. +حال فایل `main.py` را مطابق زیر ویرایش کنید تا بتوانید <abbr title="Body">بدنه</abbr> یک درخواست `PUT` را دریافت کنید. به کمک Pydantic بدنه درخواست را با <abbr title="Type">انواع</abbr> استاندارد پایتون تعریف کنید. @@ -298,11 +298,11 @@ def update_item(item_id: int, item: Item): ![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) -* روی دکمه "Try it out" کلیک کنید, اکنون می‌توانید پارامترهای مورد نیاز هر API را مشخص کرده و به صورت مستقیم با آنها تعامل کنید: +* روی دکمه "Try it out" کلیک کنید، اکنون می‌توانید پارامترهای مورد نیاز هر API را مشخص کرده و به صورت مستقیم با آنها تعامل کنید: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) -* سپس روی دکمه "Execute" کلیک کنید, خواهید دید که واسط کاریری با APIهای تعریف شده ارتباط برقرار کرده، پارامترهای مورد نیاز را به آن‌ها ارسال می‌کند، سپس نتایج را دریافت کرده و در صفحه نشان می‌دهد: +* سپس روی دکمه "Execute" کلیک کنید، خواهید دید که واسط کاربری با APIهای تعریف شده ارتباط برقرار کرده، پارامترهای مورد نیاز را به آن‌ها ارسال می‌کند، سپس نتایج را دریافت کرده و در صفحه نشان می‌دهد: ![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) @@ -342,7 +342,7 @@ item: Item * تکمیل کد. * بررسی انواع داده. * اعتبارسنجی داده: - * خطاهای خودکار و مشخص در هنگام نامعتبر بودن داده + * خطاهای خودکار و مشخص در هنگام نامعتبر بودن داده. * اعتبارسنجی، حتی برای اشیاء JSON تو در تو. * <abbr title="serialization, parsing, marshalling">تبدیل</abbr> داده ورودی: که از شبکه رسیده به انواع و داد‌ه‌ پایتونی. این داده‌ شامل: * JSON. @@ -366,22 +366,22 @@ item: Item به مثال قبلی باز می‌گردیم، در این مثال **FastAPI** موارد زیر را انجام می‌دهد: -* اعتبارسنجی اینکه پارامتر `item_id` در مسیر درخواست‌های `GET` و `PUT` موجود است . +* اعتبارسنجی اینکه پارامتر `item_id` در مسیر درخواست‌های `GET` و `PUT` موجود است. * اعتبارسنجی اینکه پارامتر `item_id` در درخواست‌های `GET` و `PUT` از نوع `int` است. * اگر غیر از این موارد باشد، سرویس‌گیرنده خطای مفید و مشخصی دریافت خواهد کرد. * بررسی وجود پارامتر پرسمان اختیاری `q` (مانند `http://127.0.0.1:8000/items/foo?q=somequery`) در درخواست‌های `GET`. - * از آنجا که پارامتر `q` با `= None` مقداردهی شده است, این پارامتر اختیاری است. + * از آنجا که پارامتر `q` با `= None` مقداردهی شده است، این پارامتر اختیاری است. * اگر از مقدار اولیه `None` استفاده نکنیم، این پارامتر الزامی خواهد بود (همانند بدنه درخواست در درخواست `PUT`). -* برای درخواست‌های `PUT` به آدرس `/items/{item_id}`, بدنه درخواست باید از نوع JSON تعریف شده باشد: +* برای درخواست‌های `PUT` به آدرس `/items/{item_id}`، بدنه درخواست باید از نوع JSON تعریف شده باشد: * بررسی اینکه بدنه شامل فیلدی با نام `name` و از نوع `str` است. * بررسی اینکه بدنه شامل فیلدی با نام `price` و از نوع `float` است. - * بررسی اینکه بدنه شامل فیلدی اختیاری با نام `is_offer` است, که در صورت وجود باید از نوع `bool` باشد. + * بررسی اینکه بدنه شامل فیلدی اختیاری با نام `is_offer` است، که در صورت وجود باید از نوع `bool` باشد. * تمامی این موارد برای اشیاء JSON در هر عمقی قابل بررسی می‌باشد. * تبدیل از/به JSON به صورت خودکار. -* مستندسازی همه چیز با استفاده از OpenAPI, که می‌توان از آن برای موارد زیر استفاده کرد: +* مستندسازی همه چیز با استفاده از OpenAPI، که می‌توان از آن برای موارد زیر استفاده کرد: * سیستم مستندات تعاملی. * تولید خودکار کد سرویس‌گیرنده‌ در زبان‌های برنامه‌نویسی بیشمار. -* فراهم سازی ۲ مستند تعاملی مبتنی بر وب به صورت پیش‌فرض . +* فراهم سازی ۲ مستند تعاملی مبتنی بر وب به صورت پیش‌فرض. --- @@ -413,7 +413,7 @@ item: Item **هشدار اسپویل**: بخش آموزش - راهنمای کاربر شامل موارد زیر است: -* اعلان **پارامترهای** موجود در بخش‌های دیگر درخواست، شامل: **سرآیند‌ (هدر)ها**, **کوکی‌ها**, **فیلد‌های فرم** و **فایل‌ها**. +* اعلان **پارامترهای** موجود در بخش‌های دیگر درخواست، شامل: **سرآیند‌ (هدر)ها**، **کوکی‌ها**، **فیلد‌های فرم** و **فایل‌ها**. * چگونگی تنظیم **<abbr title="Validation Constraints">محدودیت‌های اعتبارسنجی</abbr>** به عنوان مثال `maximum_length` یا `regex`. * سیستم **<abbr title="also known as components, resources, providers, services, injectables">Dependency Injection</abbr>** قوی و کاربردی. * امنیت و تایید هویت, شامل پشتیبانی از **OAuth2** مبتنی بر **JWT tokens** و **HTTP Basic**. @@ -428,7 +428,7 @@ item: Item ## کارایی -معیار (بنچمارک‌)های مستقل TechEmpower حاکی از آن است که برنامه‌های **FastAPI** که تحت Uvicorn اجرا می‌شود، <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">یکی از سریع‌ترین فریم‌ورک‌های مبتنی بر پایتون</a>, است که کمی ضعیف‌تر از Starlette و Uvicorn عمل می‌کند (فریم‌ورک و سروری که FastAPI بر اساس آنها ایجاد شده است) (*) +معیار (بنچمارک‌)های مستقل TechEmpower حاکی از آن است که برنامه‌های **FastAPI** که تحت Uvicorn اجرا می‌شود، <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">یکی از سریع‌ترین فریم‌ورک‌های مبتنی بر پایتون</a>، است که کمی ضعیف‌تر از Starlette و Uvicorn عمل می‌کند (فریم‌ورک و سروری که FastAPI بر اساس آنها ایجاد شده است) (*) برای درک بهتری از این موضوع به بخش <a href="https://fastapi.tiangolo.com/benchmarks/" class="internal-link" target="_blank">بنچ‌مارک‌ها</a> مراجعه کنید. @@ -445,7 +445,7 @@ item: Item * <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - در صورتی که بخواهید از پیکربندی پیش‌فرض برای قالب‌ها استفاده کنید. * <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - در صورتی که بخواهید با استفاده از `request.form()` از قابلیت <abbr title="تبدیل رشته متنی موجود در درخواست HTTP به انواع داده پایتون">"تجزیه (parse)"</abbr> فرم استفاده کنید. * <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - در صورتی که بخواید از `SessionMiddleware` پشتیبانی کنید. -* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - برای پشتیبانی `SchemaGenerator` در Starlet (به احتمال زیاد برای کار کردن با FastAPI به آن نیازی پیدا نمی‌کنید.). +* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - برای پشتیبانی `SchemaGenerator` در Starlet (به احتمال زیاد برای کار کردن با FastAPI به آن نیازی پیدا نمی‌کنید). * <a href="https://graphene-python.org/" target="_blank"><code>graphene</code></a> - در صورتی که از `GraphQLApp` پشتیبانی می‌کنید. * <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - در صورتی که بخواهید از `UJSONResponse` استفاده کنید. From 08b98adea61dd424c41e00b60fd04b1c7bf6df52 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 17:49:07 +0000 Subject: [PATCH 1839/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 03319666a..412b55fee 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Update Farsi translation for `docs/fa/docs/index.md`. PR [#10216](https://github.com/tiangolo/fastapi/pull/10216) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add German translation for `docs/de/docs/tutorial/body-fields.md`. PR [#10310](https://github.com/tiangolo/fastapi/pull/10310) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body.md`. PR [#10295](https://github.com/tiangolo/fastapi/pull/10295) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body-multiple-params.md`. PR [#10308](https://github.com/tiangolo/fastapi/pull/10308) by [@nilslindemann](https://github.com/nilslindemann). From e3728489fad1cdf871faa537472e9028b42304ff Mon Sep 17 00:00:00 2001 From: mojtaba <121169359+mojtabapaso@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:23:46 +0330 Subject: [PATCH 1840/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Persian=20transl?= =?UTF-8?q?ation=20for=20`docs/fa/docs/tutorial/middleware.md`=20(#9695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/fa/docs/tutorial/middleware.md | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/fa/docs/tutorial/middleware.md diff --git a/docs/fa/docs/tutorial/middleware.md b/docs/fa/docs/tutorial/middleware.md new file mode 100644 index 000000000..c5752a4b5 --- /dev/null +++ b/docs/fa/docs/tutorial/middleware.md @@ -0,0 +1,59 @@ +# میان‌افزار - middleware + +شما میتوانید میان‌افزارها را در **FastAPI** اضافه کنید. + +"میان‌افزار" یک تابع است که با هر درخواست(request) قبل از پردازش توسط هر path operation (عملیات مسیر) خاص کار می‌کند. همچنین با هر پاسخ(response) قبل از بازگشت آن نیز کار می‌کند. + +* هر **درخواستی (request)** که به برنامه شما می آید را می گیرد. +* سپس می تواند کاری برای آن **درخواست** انجام دهید یا هر کد مورد نیازتان را اجرا کنید. +* سپس **درخواست** را به بخش دیگری از برنامه (توسط یک path operation مشخص) برای پردازش ارسال می کند. +* سپس **پاسخ** تولید شده توسط برنامه را (توسط یک path operation مشخص) دریافت می‌کند. +* می تواند کاری با **پاسخ** انجام دهید یا هر کد مورد نیازتان را اجرا کند. +* سپس **پاسخ** را برمی گرداند. + +!!! توجه "جزئیات فنی" + در صورت وجود وابستگی هایی با `yield`، کد خروجی **پس از** اجرای میان‌‌افزار اجرا خواهد شد. + + در صورت وجود هر گونه وظایف پس زمینه (که در ادامه توضیح داده می‌شوند)، تمام میان‌افزارها *پس از آن* اجرا خواهند شد. + +## ساخت یک میان افزار + +برای ایجاد یک میان‌افزار، از دکوریتور `@app.middleware("http")` در بالای یک تابع استفاده می‌شود. + +تابع میان افزار دریافت می کند: +* `درخواست` +* تابع `call_next` که `درخواست` را به عنوان پارامتر دریافت می کند + * این تابع `درخواست` را به *path operation* مربوطه ارسال می کند. + * سپس `پاسخ` تولید شده توسط *path operation* مربوطه را برمی‌گرداند. +* شما می‌توانید سپس `پاسخ` را تغییر داده و پس از آن را برگردانید. + +```Python hl_lines="8-9 11 14" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +!!! نکته به خاطر داشته باشید که هدرهای اختصاصی سفارشی را می توان با استفاده از پیشوند "X-" اضافه کرد. + + اما اگر هدرهای سفارشی دارید که می‌خواهید مرورگر کاربر بتواند آنها را ببیند، باید آنها را با استفاده از پارامتر `expose_headers` که در مستندات <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">CORS از Starlette</a> توضیح داده شده است، به پیکربندی CORS خود اضافه کنید. + +!!! توجه "جزئیات فنی" + شما همچنین می‌توانید از `from starlette.requests import Request` استفاده کنید. + + **FastAPI** این را به عنوان یک سهولت برای شما به عنوان برنامه‌نویس فراهم می‌کند. اما این مستقیما از Starlette به دست می‌آید. + +### قبل و بعد از `پاسخ` + +شما می‌توانید کدی را برای اجرا با `درخواست`، قبل از اینکه هر *path operation* آن را دریافت کند، اضافه کنید. + +همچنین پس از تولید `پاسخ`، قبل از بازگشت آن، می‌توانید کدی را اضافه کنید. + +به عنوان مثال، می‌توانید یک هدر سفارشی به نام `X-Process-Time` که شامل زمان پردازش درخواست و تولید پاسخ به صورت ثانیه است، اضافه کنید. + +```Python hl_lines="10 12-13" +{!../../../docs_src/middleware/tutorial001.py!} +``` + + ## سایر میان افزار + +شما می‌توانید بعداً در مورد میان‌افزارهای دیگر در [راهنمای کاربر پیشرفته: میان‌افزار پیشرفته](../advanced/middleware.md){.internal-link target=_blank} بیشتر بخوانید. + +شما در بخش بعدی در مورد این که چگونه با استفاده از یک میان‌افزار، <abbr title="Cross-Origin Resource Sharing">CORS</abbr> را مدیریت کنید، خواهید خواند. From 4f8eec808f7edebec85cc976a42e2b7f7f70a400 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 17:55:19 +0000 Subject: [PATCH 1841/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 412b55fee..15798ec4b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Persian translation for `docs/fa/docs/tutorial/middleware.md`. PR [#9695](https://github.com/tiangolo/fastapi/pull/9695) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Update Farsi translation for `docs/fa/docs/index.md`. PR [#10216](https://github.com/tiangolo/fastapi/pull/10216) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add German translation for `docs/de/docs/tutorial/body-fields.md`. PR [#10310](https://github.com/tiangolo/fastapi/pull/10310) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add German translation for `docs/de/docs/tutorial/body.md`. PR [#10295](https://github.com/tiangolo/fastapi/pull/10295) by [@nilslindemann](https://github.com/nilslindemann). From 653a3579ca807f55e128084e3833ed6324f902a6 Mon Sep 17 00:00:00 2001 From: Nils Lindemann <nilslindemann@tutanota.com> Date: Mon, 29 Jan 2024 19:10:09 +0100 Subject: [PATCH 1842/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20German=20transla?= =?UTF-8?q?tion=20for=20`docs/de/docs/tutorial/body-nested-models.md`=20(#?= =?UTF-8?q?10313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/de/docs/tutorial/body-nested-models.md | 382 ++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 docs/de/docs/tutorial/body-nested-models.md diff --git a/docs/de/docs/tutorial/body-nested-models.md b/docs/de/docs/tutorial/body-nested-models.md new file mode 100644 index 000000000..976f3f924 --- /dev/null +++ b/docs/de/docs/tutorial/body-nested-models.md @@ -0,0 +1,382 @@ +# Body – Verschachtelte Modelle + +Mit **FastAPI** können Sie (dank Pydantic) beliebig tief verschachtelte Modelle definieren, validieren und dokumentieren. + +## Listen als Felder + +Sie können ein Attribut als Kindtyp definieren, zum Beispiel eine Python-`list`e. + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial001_py310.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial001.py!} + ``` + +Das bewirkt, dass `tags` eine Liste ist, wenngleich es nichts über den Typ der Elemente der Liste aussagt. + +## Listen mit Typ-Parametern als Felder + +Aber Python erlaubt es, Listen mit inneren Typen, auch „Typ-Parameter“ genannt, zu deklarieren. + +### `List` von `typing` importieren + +In Python 3.9 oder darüber können Sie einfach `list` verwenden, um diese Typannotationen zu deklarieren, wie wir unten sehen werden. 💡 + +In Python-Versionen vor 3.9 (3.6 und darüber), müssen Sie zuerst `List` von Pythons Standardmodul `typing` importieren. + +```Python hl_lines="1" +{!> ../../../docs_src/body_nested_models/tutorial002.py!} +``` + +### Eine `list`e mit einem Typ-Parameter deklarieren + +Um Typen wie `list`, `dict`, `tuple` mit inneren Typ-Parametern (inneren Typen) zu deklarieren: + +* Wenn Sie eine Python-Version kleiner als 3.9 verwenden, importieren Sie das Äquivalent zum entsprechenden Typ vom `typing`-Modul +* Überreichen Sie den/die inneren Typ(en) von eckigen Klammern umschlossen, `[` und `]`, als „Typ-Parameter“ + +In Python 3.9 wäre das: + +```Python +my_list: list[str] +``` + +Und in Python-Versionen vor 3.9: + +```Python +from typing import List + +my_list: List[str] +``` + +Das ist alles Standard-Python-Syntax für Typdeklarationen. + +Verwenden Sie dieselbe Standardsyntax für Modellattribute mit inneren Typen. + +In unserem Beispiel können wir also bewirken, dass `tags` spezifisch eine „Liste von Strings“ ist: + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial002_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial002.py!} + ``` + +## Set-Typen + +Aber dann denken wir darüber nach und stellen fest, dass sich die Tags nicht wiederholen sollen, es sollen eindeutige Strings sein. + +Python hat einen Datentyp speziell für Mengen eindeutiger Dinge: das <abbr title="Menge">`set`</abbr>. + +Deklarieren wir also `tags` als Set von Strings. + +=== "Python 3.10+" + + ```Python hl_lines="12" + {!> ../../../docs_src/body_nested_models/tutorial003_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="14" + {!> ../../../docs_src/body_nested_models/tutorial003_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="1 14" + {!> ../../../docs_src/body_nested_models/tutorial003.py!} + ``` + +Jetzt, selbst wenn Sie einen Request mit duplizierten Daten erhalten, werden diese zu einem Set eindeutiger Dinge konvertiert. + +Und wann immer Sie diese Daten ausgeben, selbst wenn die Quelle Duplikate hatte, wird es als Set von eindeutigen Dingen ausgegeben. + +Und es wird entsprechend annotiert/dokumentiert. + +## Verschachtelte Modelle + +Jedes Attribut eines Pydantic-Modells hat einen Typ. + +Aber dieser Typ kann selbst ein anderes Pydantic-Modell sein. + +Sie können also tief verschachtelte JSON-„Objekte“ deklarieren, mit spezifischen Attributnamen, -typen, und -validierungen. + +Alles das beliebig tief verschachtelt. + +### Ein Kindmodell definieren + +Wir können zum Beispiel ein `Image`-Modell definieren. + +=== "Python 3.10+" + + ```Python hl_lines="7-9" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9-11" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +### Das Kindmodell als Typ verwenden + +Und dann können wir es als Typ eines Attributes verwenden. + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial004_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial004.py!} + ``` + +Das würde bedeuten, dass **FastAPI** einen Body erwartet wie: + +```JSON +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": ["rock", "metal", "bar"], + "image": { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + } +} +``` + +Wiederum, nur mit dieser Deklaration erhalten Sie von **FastAPI**: + +* Editor-Unterstützung (Codevervollständigung, usw.), selbst für verschachtelte Modelle +* Datenkonvertierung +* Datenvalidierung +* Automatische Dokumentation + +## Spezielle Typen und Validierungen + +Abgesehen von normalen einfachen Typen, wie `str`, `int`, `float`, usw. können Sie komplexere einfache Typen verwenden, die von `str` erben. + +Um alle Optionen kennenzulernen, die Sie haben, schauen Sie sich <a href="https://pydantic-docs.helpmanual.io/usage/types/" class="external-link" target="_blank">Pydantics Typübersicht</a> an. Sie werden im nächsten Kapitel ein paar Beispiele kennenlernen. + +Da wir zum Beispiel im `Image`-Modell ein Feld `url` haben, können wir deklarieren, dass das eine Instanz von Pydantics `HttpUrl` sein soll, anstelle eines `str`: + +=== "Python 3.10+" + + ```Python hl_lines="2 8" + {!> ../../../docs_src/body_nested_models/tutorial005_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 10" + {!> ../../../docs_src/body_nested_models/tutorial005.py!} + ``` + +Es wird getestet, ob der String eine gültige URL ist, und als solche wird er in JSON Schema / OpenAPI dokumentiert. + +## Attribute mit Listen von Kindmodellen + +Sie können Pydantic-Modelle auch als Typen innerhalb von `list`, `set`, usw. verwenden: + +=== "Python 3.10+" + + ```Python hl_lines="18" + {!> ../../../docs_src/body_nested_models/tutorial006_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="20" + {!> ../../../docs_src/body_nested_models/tutorial006.py!} + ``` + +Das wird einen JSON-Body erwarten (konvertieren, validieren, dokumentieren), wie: + +```JSON hl_lines="11" +{ + "name": "Foo", + "description": "The pretender", + "price": 42.0, + "tax": 3.2, + "tags": [ + "rock", + "metal", + "bar" + ], + "images": [ + { + "url": "http://example.com/baz.jpg", + "name": "The Foo live" + }, + { + "url": "http://example.com/dave.jpg", + "name": "The Baz" + } + ] +} +``` + +!!! info + Beachten Sie, dass der `images`-Schlüssel jetzt eine Liste von Bild-Objekten hat. + +## Tief verschachtelte Modelle + +Sie können beliebig tief verschachtelte Modelle definieren: + +=== "Python 3.10+" + + ```Python hl_lines="7 12 18 21 25" + {!> ../../../docs_src/body_nested_models/tutorial007_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9 14 20 23 27" + {!> ../../../docs_src/body_nested_models/tutorial007.py!} + ``` + +!!! info + Beachten Sie, wie `Offer` eine Liste von `Item`s hat, von denen jedes seinerseits eine optionale Liste von `Image`s hat. + +## Bodys aus reinen Listen + +Wenn Sie möchten, dass das äußerste Element des JSON-Bodys ein JSON-`array` (eine Python-`list`e) ist, können Sie den Typ im Funktionsparameter deklarieren, mit der gleichen Syntax wie in Pydantic-Modellen: + +```Python +images: List[Image] +``` + +oder in Python 3.9 und darüber: + +```Python +images: list[Image] +``` + +so wie in: + +=== "Python 3.9+" + + ```Python hl_lines="13" + {!> ../../../docs_src/body_nested_models/tutorial008_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="15" + {!> ../../../docs_src/body_nested_models/tutorial008.py!} + ``` + +## Editor-Unterstützung überall + +Und Sie erhalten Editor-Unterstützung überall. + +Selbst für Dinge in Listen: + +<img src="/img/tutorial/body-nested-models/image01.png"> + +Sie würden diese Editor-Unterstützung nicht erhalten, wenn Sie direkt mit `dict`, statt mit Pydantic-Modellen arbeiten würden. + +Aber Sie müssen sich auch nicht weiter um die Modelle kümmern, hereinkommende Dicts werden automatisch in sie konvertiert. Und was Sie zurückgeben, wird automatisch nach JSON konvertiert. + +## Bodys mit beliebigen `dict`s + +Sie können einen Body auch als `dict` deklarieren, mit Schlüsseln eines Typs und Werten eines anderen Typs. + +So brauchen Sie vorher nicht zu wissen, wie die Feld-/Attribut-Namen lauten (wie es bei Pydantic-Modellen der Fall wäre). + +Das ist nützlich, wenn Sie Schlüssel empfangen, deren Namen Sie nicht bereits kennen. + +--- + +Ein anderer nützlicher Anwendungsfall ist, wenn Sie Schlüssel eines anderen Typs haben wollen, z. B. `int`. + +Das schauen wir uns mal an. + +Im folgenden Beispiel akzeptieren Sie irgendein `dict`, solange es `int`-Schlüssel und `float`-Werte hat. + +=== "Python 3.9+" + + ```Python hl_lines="7" + {!> ../../../docs_src/body_nested_models/tutorial009_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="9" + {!> ../../../docs_src/body_nested_models/tutorial009.py!} + ``` + +!!! tip "Tipp" + Bedenken Sie, dass JSON nur `str` als Schlüssel unterstützt. + + Aber Pydantic hat automatische Datenkonvertierung. + + Das bedeutet, dass Ihre API-Clients nur Strings senden können, aber solange diese Strings nur Zahlen enthalten, wird Pydantic sie konvertieren und validieren. + + Und das `dict` welches Sie als `weights` erhalten, wird `int`-Schlüssel und `float`-Werte haben. + +## Zusammenfassung + +Mit **FastAPI** haben Sie die maximale Flexibilität von Pydantic-Modellen, während Ihr Code einfach, kurz und elegant bleibt. + +Aber mit all den Vorzügen: + +* Editor-Unterstützung (Codevervollständigung überall) +* Datenkonvertierung (auch bekannt als Parsen, Serialisierung) +* Datenvalidierung +* Schema-Dokumentation +* Automatische Dokumentation From a235d93002b925b0d2d7aa650b7ab6d7bb4b24dd Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Mon, 29 Jan 2024 18:10:30 +0000 Subject: [PATCH 1843/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 15798ec4b..1e3af4714 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add German translation for `docs/de/docs/tutorial/body-nested-models.md`. PR [#10313](https://github.com/tiangolo/fastapi/pull/10313) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/middleware.md`. PR [#9695](https://github.com/tiangolo/fastapi/pull/9695) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Update Farsi translation for `docs/fa/docs/index.md`. PR [#10216](https://github.com/tiangolo/fastapi/pull/10216) by [@theonlykingpin](https://github.com/theonlykingpin). * 🌐 Add German translation for `docs/de/docs/tutorial/body-fields.md`. PR [#10310](https://github.com/tiangolo/fastapi/pull/10310) by [@nilslindemann](https://github.com/nilslindemann). From 1b824e0c2352ec67e45a139d22aba9382c2d2622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 30 Jan 2024 10:58:10 +0100 Subject: [PATCH 1844/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20T?= =?UTF-8?q?alkPython=20badge=20image=20(#11048)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/en/data/sponsors.yml | 2 +- docs/en/docs/img/sponsors/talkpython-v2.jpg | Bin 0 -> 18712 bytes 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/en/docs/img/sponsors/talkpython-v2.jpg diff --git a/README.md b/README.md index 764cd5a36..968ccf7a7 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ The key features are: <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> -<a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython.png"></a> +<a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython-v2.jpg"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> <a href="https://github.com/deepset-ai/haystack/" target="_blank" title="Build powerful search from composable, open source building blocks"><img src="https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg"></a> <a href="https://careers.powens.com/" target="_blank" title="Powens is hiring!"><img src="https://fastapi.tiangolo.com/img/sponsors/powens.png"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index bd5b86e44..0ce434b5e 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -23,7 +23,7 @@ gold: silver: - url: https://training.talkpython.fm/fastapi-courses title: FastAPI video courses on demand from people you trust - img: https://fastapi.tiangolo.com/img/sponsors/talkpython.png + img: https://fastapi.tiangolo.com/img/sponsors/talkpython-v2.jpg - 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 diff --git a/docs/en/docs/img/sponsors/talkpython-v2.jpg b/docs/en/docs/img/sponsors/talkpython-v2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a1e492a162458847861035ce010dfde35734c97 GIT binary patch literal 18712 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 literal 0 HcmV?d00001 From efee7a407d39e422f614a306a24b9acbf8e3f360 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 30 Jan 2024 09:58:29 +0000 Subject: [PATCH 1845/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1e3af4714..3c207f98d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -141,6 +141,7 @@ hide: ### Internal +* 🔧 Update sponsors: TalkPython badge image. PR [#11048](https://github.com/tiangolo/fastapi/pull/11048) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Deta. PR [#11041](https://github.com/tiangolo/fastapi/pull/11041) by [@tiangolo](https://github.com/tiangolo). * 💄 Fix CSS breaking RTL languages (erroneously introduced by a previous RTL PR). PR [#11039](https://github.com/tiangolo/fastapi/pull/11039) by [@tiangolo](https://github.com/tiangolo). * 🔧 Add Italian to `mkdocs.yml`. PR [#11016](https://github.com/tiangolo/fastapi/pull/11016) by [@alejsdev](https://github.com/alejsdev). From 2a21dfba0ee0334825aecfd9ef1f85dc02b92e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 30 Jan 2024 15:24:35 +0100 Subject: [PATCH 1846/1881] =?UTF-8?q?=F0=9F=8D=B1=20Update=20sponsors:=20T?= =?UTF-8?q?alkPython=20badge=20(#11052)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/img/sponsors/talkpython-v2.jpg | Bin 18712 -> 9778 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/en/docs/img/sponsors/talkpython-v2.jpg b/docs/en/docs/img/sponsors/talkpython-v2.jpg index 4a1e492a162458847861035ce010dfde35734c97..adb0152489fa8dea5594a78a835e0cbfa817e840 100644 GIT binary patch literal 9778 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 literal 18712 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 From ffbe83d4d090add910561ce42d2202d8df9f2702 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 30 Jan 2024 14:24:57 +0000 Subject: [PATCH 1847/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3c207f98d..6838bfa2b 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -141,6 +141,7 @@ hide: ### Internal +* 🍱 Update sponsors: TalkPython badge. PR [#11052](https://github.com/tiangolo/fastapi/pull/11052) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: TalkPython badge image. PR [#11048](https://github.com/tiangolo/fastapi/pull/11048) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Deta. PR [#11041](https://github.com/tiangolo/fastapi/pull/11041) by [@tiangolo](https://github.com/tiangolo). * 💄 Fix CSS breaking RTL languages (erroneously introduced by a previous RTL PR). PR [#11039](https://github.com/tiangolo/fastapi/pull/11039) by [@tiangolo](https://github.com/tiangolo). From fb7af9ec72656048df07dd40bab8721075f967a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Tue, 30 Jan 2024 19:46:56 +0100 Subject: [PATCH 1848/1881] =?UTF-8?q?=F0=9F=91=B7=20Upgrade=20GitHub=20Act?= =?UTF-8?q?ion=20issue-manager=20(#11056)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-manager.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue-manager.yml b/.github/workflows/issue-manager.yml index d1aad28fd..0f564d721 100644 --- a/.github/workflows/issue-manager.yml +++ b/.github/workflows/issue-manager.yml @@ -23,7 +23,7 @@ jobs: env: GITHUB_CONTEXT: ${{ toJson(github) }} run: echo "$GITHUB_CONTEXT" - - uses: tiangolo/issue-manager@0.4.0 + - uses: tiangolo/issue-manager@0.5.0 with: token: ${{ secrets.FASTAPI_ISSUE_MANAGER }} config: > From ec5e08251d77ea81f7e5d5ccebb1fa55950add7a Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 30 Jan 2024 18:47:20 +0000 Subject: [PATCH 1849/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 6838bfa2b..0f0dbb1c9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -141,6 +141,7 @@ hide: ### Internal +* 👷 Upgrade GitHub Action issue-manager. PR [#11056](https://github.com/tiangolo/fastapi/pull/11056) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update sponsors: TalkPython badge. PR [#11052](https://github.com/tiangolo/fastapi/pull/11052) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: TalkPython badge image. PR [#11048](https://github.com/tiangolo/fastapi/pull/11048) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors, remove Deta. PR [#11041](https://github.com/tiangolo/fastapi/pull/11041) by [@tiangolo](https://github.com/tiangolo). From 7178eb4fb1b88cdd69d356c8d34c03905262c745 Mon Sep 17 00:00:00 2001 From: JeongHyeongKim <maladroit1@likelion.org> Date: Wed, 31 Jan 2024 23:35:27 +0900 Subject: [PATCH 1850/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Korean=20transla?= =?UTF-8?q?tion=20for=20`docs/ko/docs/tutorial/middleware.md`=20(#2829)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/middleware.md | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/ko/docs/tutorial/middleware.md diff --git a/docs/ko/docs/tutorial/middleware.md b/docs/ko/docs/tutorial/middleware.md new file mode 100644 index 000000000..f35b446a6 --- /dev/null +++ b/docs/ko/docs/tutorial/middleware.md @@ -0,0 +1,61 @@ +# 미들웨어 + +미들웨어를 **FastAPI** 응용 프로그램에 추가할 수 있습니다. + +"미들웨어"는 특정 *경로 작동*에 의해 처리되기 전, 모든 **요청**에 대해서 동작하는 함수입니다. 또한 모든 **응답**이 반환되기 전에도 동일하게 동작합니다. + +* 미들웨어는 응용 프로그램으로 오는 **요청**를 가져옵니다. +* **요청** 또는 다른 필요한 코드를 실행 시킬 수 있습니다. +* **요청**을 응용 프로그램의 *경로 작동*으로 전달하여 처리합니다. +* 애플리케이션의 *경로 작업*에서 생성한 **응답**를 받습니다. +* **응답** 또는 다른 필요한 코드를 실행시키는 동작을 할 수 있습니다. +* **응답**를 반환합니다. + +!!! note "기술 세부사항" + 만약 `yield`를 사용한 의존성을 가지고 있다면, 미들웨어가 실행되고 난 후에 exit이 실행됩니다. + + 만약 (나중에 문서에서 다룰) 백그라운드 작업이 있다면, 모든 미들웨어가 실행되고 *난 후에* 실행됩니다. + +## 미들웨어 만들기 + +미들웨어를 작성하기 위해서 함수 상단에 `@app.middleware("http")` 데코레이터를 사용할 수 있습니다. + +미들웨어 함수는 다음 항목들을 받습니다: + +* `request`. +* `request`를 매개변수로 받는 `call_next` 함수. + * 이 함수는 `request`를 해당하는 *경로 작업*으로 전달합니다. + * 그런 다음, *경로 작업*에 의해 생성된 `response` 를 반환합니다. +* `response`를 반환하기 전에 추가로 `response`를 수정할 수 있습니다. + +```Python hl_lines="8-9 11 14" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +!!! tip "팁" + 사용자 정의 헤더는 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers" class="external-link" target="_blank">'X-' 접두사를 사용</a>하여 추가할 수 있습니다. + + 그러나 만약 클라이언트의 브라우저에서 볼 수 있는 사용자 정의 헤더를 가지고 있다면, 그것들을 CORS 설정([CORS (Cross-Origin Resource Sharing)](cors.md){.internal-link target=_blank})에 <a href="https://www.starlette.io/middleware/#corsmiddleware" class="external-link" target="_blank">Starlette CORS 문서</a>에 명시된 `expose_headers` 매개변수를 이용하여 헤더들을 추가하여야합니다. + +!!! note "기술적 세부사항" + `from starlette.requests import request`를 사용할 수도 있습니다. + + **FastAPI**는 개발자에게 편의를 위해 이를 제공합니다. 그러나 Starlette에서 직접 파생되었습니다. + +### `response`의 전과 후 + +*경로 작동*을 받기 전 `request`와 함께 작동할 수 있는 코드를 추가할 수 있습니다. + +그리고 `response` 또한 생성된 후 반환되기 전에 코드를 추가 할 수 있습니다. + +예를 들어, 요청을 수행하고 응답을 생성하는데 까지 걸린 시간 값을 가지고 있는 `X-Process-Time` 같은 사용자 정의 헤더를 추가할 수 있습니다. + +```Python hl_lines="10 12-13" +{!../../../docs_src/middleware/tutorial001.py!} +``` + +## 다른 미들웨어 + +미들웨어에 대한 더 많은 정보는 [숙련된 사용자 안내서: 향상된 미들웨어](../advanced/middleware.md){.internal-link target=\_blank}에서 확인할 수 있습니다. + +다음 부분에서 미들웨어와 함께 <abbr title="교차-출처 리소스 공유">CORS</abbr>를 어떻게 다루는지에 대해 확인할 것입니다. From 531b0d5e035be7de800b541828023084a79cf4ad Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 31 Jan 2024 14:35:50 +0000 Subject: [PATCH 1851/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0f0dbb1c9..d7dba1721 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Korean translation for `docs/ko/docs/tutorial/middleware.md`. PR [#2829](https://github.com/tiangolo/fastapi/pull/2829) by [@JeongHyeongKim](https://github.com/JeongHyeongKim). * 🌐 Add German translation for `docs/de/docs/tutorial/body-nested-models.md`. PR [#10313](https://github.com/tiangolo/fastapi/pull/10313) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/middleware.md`. PR [#9695](https://github.com/tiangolo/fastapi/pull/9695) by [@mojtabapaso](https://github.com/mojtabapaso). * 🌐 Update Farsi translation for `docs/fa/docs/index.md`. PR [#10216](https://github.com/tiangolo/fastapi/pull/10216) by [@theonlykingpin](https://github.com/theonlykingpin). From 67494c2b5eaf27f372812e21850c204e2bc79cc6 Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov <88669260+aykhans@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:45:57 +0400 Subject: [PATCH 1852/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Azerbaijani=20tr?= =?UTF-8?q?anslation=20for=20`docs/az/docs/index.md`=20(#11047)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/az/docs/index.md | 469 ++++++++++++++++++++++++++++++++++++++++++ docs/az/mkdocs.yml | 1 + docs/en/mkdocs.yml | 2 + 3 files changed, 472 insertions(+) create mode 100644 docs/az/docs/index.md create mode 100644 docs/az/mkdocs.yml diff --git a/docs/az/docs/index.md b/docs/az/docs/index.md new file mode 100644 index 000000000..a22706512 --- /dev/null +++ b/docs/az/docs/index.md @@ -0,0 +1,469 @@ +<p align="center"> + <a href="https://fastapi.tiangolo.com"><img src="https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png" alt="FastAPI"></a> +</p> +<p align="center"> + <em>FastAPI framework, yüksək məshuldarlı, öyrənməsi asan, çevik kodlama, istifadəyə hazırdır</em> +</p> +<p align="center"> +<a href="https://github.com/tiangolo/fastapi/actions?query=workflow%3ATest+event%3Apush+branch%3Amaster" target="_blank"> + <img src="https://github.com/tiangolo/fastapi/workflows/Test/badge.svg?event=push&branch=master" alt="Test"> +</a> +<a href="https://coverage-badge.samuelcolvin.workers.dev/redirect/tiangolo/fastapi" target="_blank"> + <img src="https://coverage-badge.samuelcolvin.workers.dev/tiangolo/fastapi.svg" alt="Əhatə"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/v/fastapi?color=%2334D058&label=pypi%20package" alt="Paket versiyası"> +</a> +<a href="https://pypi.org/project/fastapi" target="_blank"> + <img src="https://img.shields.io/pypi/pyversions/fastapi.svg?color=%2334D058" alt="Dəstəklənən Python versiyaları"> +</a> +</p> + +--- + +**Sənədlər**: <a href="https://fastapi.tiangolo.com" target="_blank">https://fastapi.tiangolo.com</a> + +**Qaynaq Kodu**: <a href="https://github.com/tiangolo/fastapi" target="_blank">https://github.com/tiangolo/fastapi</a> + +--- + +FastAPI Python 3.8+ ilə API yaratmaq üçün standart Python <abbr title="Tip Məsləhətləri: Type Hints">tip məsləhətlərinə</abbr> əsaslanan, müasir, sürətli (yüksək performanslı) framework-dür. + +Əsas xüsusiyyətləri bunlardır: + +* **Sürətli**: Çox yüksək performans, **NodeJS** və **Go** səviyyəsində (Starlette və Pydantic-ə təşəkkürlər). [Ən sürətli Python frameworklərindən biridir](#performans). +* **Çevik kodlama**: Funksiyanallıqları inkişaf etdirmək sürətini təxminən 200%-dən 300%-ə qədər artırın. * +* **Daha az xəta**: İnsan (developer) tərəfindən törədilən səhvlərin təxminən 40% -ni azaldın. * +* **İntuitiv**: Əla redaktor dəstəyi. Hər yerdə <abbr title="auto-complete, autocompletion, IntelliSense olaraq da bilinir">otomatik tamamlama</abbr>. Xətaları müəyyənləşdirməyə daha az vaxt sərf edəcəksiniz. +* **Asan**: İstifadəsi və öyrənilməsi asan olması üçün nəzərdə tutulmuşdur. Sənədləri oxumaq üçün daha az vaxt ayıracaqsınız. +* **Qısa**: Kod təkrarlanmasını minimuma endirin. Hər bir parametr tərifində birdən çox xüsusiyyət ilə və daha az səhvlə qarşılaşacaqsınız. +* **Güclü**: Avtomatik və interaktiv sənədlərlə birlikdə istifadəyə hazır kod əldə edə bilərsiniz. +* **Standartlara əsaslanan**: API-lar üçün açıq standartlara əsaslanır (və tam uyğun gəlir): <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> (əvvəlki adı ilə Swagger) və <a href="https://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>. + +<small>* Bu fikirlər daxili development komandasının hazırladıqları məhsulların sınaqlarına əsaslanır.</small> + +## Sponsorlar + +<!-- sponsors --> + +{% if sponsors %} +{% for sponsor in sponsors.gold -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor -%}` +{%- for sponsor in sponsors.silver -%} +<a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> +{% endfor %} +{% endif %} + +<!-- /sponsors --> + +<a href="https://fastapi.tiangolo.com/az/fastapi-people/#sponsors" class="external-link" target="_blank">Digər sponsorlar</a> + +## Rəylər + +"_[...] Son günlərdə **FastAPI**-ı çox istifadə edirəm. [...] Əslində onu komandamın bütün **Microsoftda ML sevislərində** istifadə etməyi planlayıram. Onların bəziləri **windows**-un əsas məhsuluna və bəzi **Office** məhsullarına inteqrasiya olunurlar._" + +<div style="text-align: right; margin-right: 10%;">Kabir Khan - <strong>Microsoft</strong> <a href="https://github.com/tiangolo/fastapi/pull/26" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**FastAPI** kitabxanasını **Proqnozlar** əldə etmək üçün sorğulana bilən **REST** serverini yaratmaqda istifadə etdik._" + +<div style="text-align: right; margin-right: 10%;">Piero Molino, Yaroslav Dudin, and Sai Sumanth Miryala - <strong>Uber</strong> <a href="https://eng.uber.com/ludwig-v0-2/" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**Netflix** **böhran idarəçiliyi** orkestrləşmə framework-nün açıq qaynaqlı buraxılışını elan etməkdən məmnundur: **Dispatch**! [**FastAPI** ilə quruldu]_" + +<div style="text-align: right; margin-right: 10%;">Kevin Glisson, Marc Vilanova, Forest Monsen - <strong>Netflix</strong> <a href="https://netflixtechblog.com/introducing-dispatch-da4b8a2a8072" target="_blank"><small>(ref)</small></a></div> + +--- + +"_**FastAPI** üçün həyəcanlıyam. Çox əyləncəlidir!_" + +<div style="text-align: right; margin-right: 10%;">Brian Okken - <strong><a href="https://pythonbytes.fm/episodes/show/123/time-to-right-the-py-wrongs?time_in_sec=855" target="_blank">Python Bytes</a> podcast host</strong> <a href="https://twitter.com/brianokken/status/1112220079972728832" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Düzünü desəm, sizin qurduğunuz şey həqiqətən möhkəm və peşəkar görünür. Bir çox cəhətdən **Hug**-un olmasını istədiyim kimdir - kiminsə belə bir şey qurduğunu görmək həqiqətən ruhlandırıcıdır._" + +<div style="text-align: right; margin-right: 10%;">Timothy Crosley - <strong><a href="https://www.hug.rest/" target="_blank">Hug</a> creator</strong> <a href="https://news.ycombinator.com/item?id=19455465" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Əgər REST API-lər yaratmaq üçün **müasir framework** öyrənmək istəyirsinizsə, **FastAPI**-a baxın [...] Sürətli, istifadəsi və öyrənməsi asandır. [...]_" + +"_**API** xidmətlərimizi **FastAPI**-a köçürdük [...] Sizin də bəyənəcəyinizi düşünürük._" + +<div style="text-align: right; margin-right: 10%;">Ines Montani - Matthew Honnibal - <strong><a href="https://explosion.ai" target="_blank">Explosion AI</a> founders - <a href="https://spacy.io" target="_blank">spaCy</a> creators</strong> <a href="https://twitter.com/_inesmontani/status/1144173225322143744" target="_blank"><small>(ref)</small></a> - <a href="https://twitter.com/honnibal/status/1144031421859655680" target="_blank"><small>(ref)</small></a></div> + +--- + +"_Python ilə istifadəyə hazır API qurmaq istəyən hər kəsə **FastAPI**-ı tövsiyə edirəm. **Möhtəşəm şəkildə dizayn edilmiş**, **istifadəsi asan** və **yüksək dərəcədə genişlənə bilən**-dir, API əsaslı inkişaf strategiyamızın **əsas komponentinə** çevrilib və Virtual TAC Engineer kimi bir çox avtomatlaşdırma və servisləri idarə edir._" + +<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/" target="_blank"><small>(ref)</small></a></div> + +--- + +## **Typer**, CLI-ların FastAPI-ı + +<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a> + +Əgər siz veb API əvəzinə terminalda istifadə ediləcək <abbr title="Command Line Interface">CLI</abbr> proqramı qurursunuzsa, <a href="https://typer.tiangolo.com/" class="external-link" target="_blank">**Typer**</a>-a baxa bilərsiniz. + +**Typer** FastAPI-ın kiçik qardaşıdır. Və o, CLI-lərin **FastAPI**-ı olmaq üçün nəzərdə tutulub. ⌨️ 🚀 + +## Tələblər + +Python 3.8+ + +FastAPI nəhənglərin çiyinlərində dayanır: + +* Web tərəfi üçün <a href="https://www.starlette.io/" class="external-link" target="_blank">Starlette</a>. +* Data tərəfi üçün <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>. + +## Quraşdırma + +<div class="termy"> + +```console +$ pip install fastapi + +---> 100% +``` + +</div> + +Tətbiqimizi əlçatan etmək üçün bizə <a href="https://www.uvicorn.org" class="external-link" target="_blank">Uvicorn</a> və ya <a href="https://github.com/pgjones/hypercorn" class="external-link" target="_blank">Hypercorn</a> kimi ASGI server lazımdır. + +<div class="termy"> + +```console +$ pip install "uvicorn[standard]" + +---> 100% +``` + +</div> + +## Nümunə + +### Kodu yaradaq + +* `main.py` adlı fayl yaradaq və ona aşağıdakı kodu yerləşdirək: + +```Python +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +<details markdown="1"> +<summary>Və ya <code>async def</code>...</summary> + +Əgər kodunuzda `async` və ya `await` vardırsa `async def` istifadə edə bilərik: + +```Python hl_lines="9 14" +from typing import Union + +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +async def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} +``` + +**Qeyd**: + +Əgər bu mövzu haqqında məlumatınız yoxdursa <a href="https://fastapi.tiangolo.com/az/async/#in-a-hurry" target="_blank">`async` və `await` sənədindəki</a> _"Tələsirsən?"_ bölməsinə baxa bilərsiniz. + +</details> + +### Kodu işə salaq + +Serveri aşağıdakı əmr ilə işə salaq: + +<div class="termy"> + +```console +$ uvicorn main:app --reload + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +</div> + +<details markdown="1"> +<summary><code>uvicorn main:app --reload</code> əmri haqqında...</summary> + +`uvicorn main:app` əmri aşağıdakılara instinad edir: + +* `main`: `main.py` faylı (yəni Python "modulu"). +* `app`: `main.py` faylında `app = FastAPI()` sətrində yaratdığımız `FastAPI` obyektidir. +* `--reload`: kod dəyişikliyindən sonra avtomatik olaraq serveri yenidən işə salır. Bu parametrdən yalnız development mərhələsində istifadə etməliyik. + +</details> + +### İndi yoxlayaq + +Bu linki brauzerimizdə açaq <a href="http://127.0.0.1:8000/items/5?q=somequery" class="external-link" target="_blank">http://127.0.0.1:8000/items/5?q=somequery</a>. + +Aşağıdakı kimi bir JSON cavabı görəcəksiniz: + +```JSON +{"item_id": 5, "q": "somequery"} +``` + +Siz artıq bir API yaratmısınız, hansı ki: + +* `/` və `/items/{item_id}` <abbr title="Yol: Path ">_yollarında_</abbr> HTTP sorğularını qəbul edir. +* Hər iki _yolda_ `GET` <em>əməliyyatlarını</em> (həmçinin HTTP _metodları_ kimi bilinir) aparır. +* `/items/{item_id}` _yolu_ `item_id` adlı `int` qiyməti almalı olan _yol parametrinə_ sahibdir. +* `/items/{item_id}` _yolunun_ `q` adlı yol parametri var və bu parametr istəyə bağlı olsa da, `str` qiymətini almalıdır. + +### İnteraktiv API Sənədləri + +İndi <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> ünvanına daxil olun. + +Avtomatik interaktiv API sənədlərini görəcəksiniz (<a href="https://github.com/swagger-api/swagger-ui" class="external-link" target="_blank">Swagger UI</a> tərəfindən təmin edilir): + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-01-swagger-ui-simple.png) + +### Alternativ API sənədləri + +İndi isə <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> ünvanına daxil olun. + +<a href="https://github.com/Rebilly/ReDoc" class="external-link" target="_blank">ReDoc</a> tərəfindən təqdim edilən avtomatik sənədləri görəcəksiniz: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-02-redoc-simple.png) + +## Nümunəni Yeniləyək + +İndi gəlin `main.py` faylını `PUT` sorğusu ilə birlikdə <abbr title="Gövdə: Body ">gövdə</abbr> qəbul edəcək şəkildə dəyişdirək. + +Pydantic sayəsində standart Python tiplərindən istifadə edərək <abbr title="Gövdə: Body ">gövdə</abbr>ni müəyyən edək. + +```Python hl_lines="4 9-12 25-27" +from typing import Union + +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + price: float + is_offer: Union[bool, None] = None + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +@app.get("/items/{item_id}") +def read_item(item_id: int, q: Union[str, None] = None): + return {"item_id": item_id, "q": q} + + +@app.put("/items/{item_id}") +def update_item(item_id: int, item: Item): + return {"item_name": item.name, "item_id": item_id} +``` +Server avtomatik olaraq yenidən işə salınmalı idi (çünki biz yuxarıda `uvicorn` əmri ilə `--reload` parametrindən istifadə etmişik). + +### İnteraktiv API sənədlərindəki dəyişikliyə baxaq + +Yenidən <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a> ünvanına daxil olun. + +* İnteraktiv API sənədləri yeni gövdə də daxil olmaq ilə avtomatik olaraq yenilənəcək: + +![Swagger UI](https://fastapi.tiangolo.com/img/index/index-03-swagger-02.png) + +* "Try it out" düyməsini klikləyin, bu, parametrləri doldurmağa və API ilə birbaşa əlaqə saxlamağa imkan verir: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-04-swagger-03.png) + +* Sonra "Execute" düyməsini klikləyin, istifadəçi interfeysi API ilə əlaqə quracaq, parametrləri göndərəcək, nəticələri əldə edəcək və onları ekranda göstərəcək: + +![Swagger UI interaction](https://fastapi.tiangolo.com/img/index/index-05-swagger-04.png) + +### Alternativ API Sənədlərindəki Dəyişikliyə Baxaq + +İndi isə yenidən <a href="http://127.0.0.1:8000/redoc" class="external-link" target="_blank">http://127.0.0.1:8000/redoc</a> ünvanına daxil olun. + +* Alternativ sənədlər həm də yeni sorğu parametri və gövdəsini əks etdirəcək: + +![ReDoc](https://fastapi.tiangolo.com/img/index/index-06-redoc-02.png) + +### Xülasə + +Ümumiləşdirsək, parametrlər, gövdə və s. Biz məlumat növlərini **bir dəfə** funksiya parametrləri kimi təyin edirik. + +Bunu standart müasir Python tipləri ilə edirsiniz. + +Yeni sintaksis, müəyyən bir kitabxananın metodlarını və ya siniflərini və s. öyrənmək məcburiyyətində deyilsiniz. + +Sadəcə standart **Python 3.8+**. + +Məsələn, `int` üçün: + +```Python +item_id: int +``` + +və ya daha mürəkkəb `Item` modeli üçün: + +```Python +item: Item +``` + +...və yalnız parametr tipini təyin etməklə bunları əldə edirsiniz: + +* Redaktor dəstəyi ilə: + * Avtomatik tamamlama. + * Tip yoxlanması. +* Məlumatların Təsdiqlənməsi: + * Məlumat etibarsız olduqda avtomatik olaraq aydın xətalar göstərir. + * Hətta çox dərin JSON obyektlərində belə doğrulama aparır. +* Daxil olan məlumatları <abbr title="Çevrilmə: serialization, parsing, marshalling olaraq da bilinir">çevirmək</abbr> üçün aşağıdakı məlumat növlərindən istifadə edilir: + * JSON. + * <abbr title="Yol: Path">Yol</abbr> parametrləri. + * <abbr title="Sorğu: Query">Sorğu</abbr> parametrləri. + * <abbr title="Çərəz: Cookie">Çərəzlər</abbr>. + * <abbr title="Başlıq: Header">Başlıqlaq</abbr>. + * <abbr title="Forma: Form">Formalar</abbr>. + * Fayllar. +* Daxil olan məlumatları <abbr title="Çevrilmə: serialization, parsing, marshalling olaraq da bilinir">çevirmək</abbr> üçün aşağıdakı məlumat növlərindən istifadə edilir (JSON olaraq): + * Python tiplərinin (`str`, `int`, `float`, `bool`, `list`, və s) çevrilməsi. + * `datetime` obyektləri. + * `UUID` obyektləri. + * Verilənlər bazası modelləri. + * və daha çoxu... +* 2 alternativ istifadəçi interfeysi daxil olmaqla avtomatik interaktiv API sənədlərini təmin edir: + * Swagger UI. + * ReDoc. + +--- + +Gəlin əvvəlki nümunəyə qayıdaq və **FastAPI**-nin nələr edəcəyinə nəzər salaq: + +* `GET` və `PUT` sorğuları üçün `item_id`-nin <abbr title="Yol: Path">yolda</abbr> olub-olmadığını yoxlayacaq. +* `item_id`-nin `GET` və `PUT` sorğuları üçün növünün `int` olduğunu yoxlayacaq. + * Əgər `int` deyilsə, səbəbini göstərən bir xəta mesajı göstərəcəkdir. +* <abbr title="Məcburi olmayan: Optional">məcburi olmayan</abbr> `q` parametrinin `GET` (`http://127.0.0.1:8000/items/foo?q=somequery` burdakı kimi) sorğusu içərisində olub olmadığını yoxlayacaq. + * `q` parametrini `= None` ilə yaratdığımız üçün, <abbr title="Məcburi olmayan: Optional">məcburi olmayan</abbr> parametr olacaq. + * Əgər `None` olmasaydı, bu məcburi parametr olardı (`PUT` metodunun gövdəsində olduğu kimi). +* `PUT` sorğusu üçün, `/items/{item_id}` gövdəsini JSON olaraq oxuyacaq: + * `name` adında məcburi bir parametr olub olmadığını və əgər varsa, tipinin `str` olub olmadığını yoxlayacaq. + * `price` adında məcburi bir parametr olub olmadığını və əgər varsa, tipinin `float` olub olmadığını yoxlayacaq. + * `is_offer` adında <abbr title="Məcburi olmayan: Optional">məcburi olmayan</abbr> bir parametr olub olmadığını və əgər varsa, tipinin `float` olub olmadığını yoxlayacaq. + * Bütün bunlar ən dərin JSON obyektlərində belə işləyəcək. +* Məlumatların JSON-a və JSON-un Python obyektinə çevrilməsi avtomatik həyata keçiriləcək. +* Hər şeyi OpenAPI ilə uyğun olacaq şəkildə avtomatik olaraq sənədləşdirəcək və onları aşağıdakı kimi istifadə edə biləcək: + * İnteraktiv sənədləşmə sistemləri. + * Bir çox proqramlaşdırma dilləri üçün avtomatlaşdırılmış <abbr title="Müştəri: Client">müştəri</abbr> kodu yaratma sistemləri. +* 2 interaktiv sənədləşmə veb interfeysini birbaşa təmin edəcək. + +--- + +Yeni başlamışıq, amma siz artıq işin məntiqini başa düşmüsünüz. + +İndi aşağıdakı sətri dəyişdirməyə çalışın: + +```Python + return {"item_name": item.name, "item_id": item_id} +``` + +...bundan: + +```Python + ... "item_name": item.name ... +``` + +...buna: + +```Python + ... "item_price": item.price ... +``` + +...və redaktorun məlumat tiplərini bildiyini və avtomatik tamaladığını görəcəksiniz: + +![editor support](https://fastapi.tiangolo.com/img/vscode-completion.png) + +Daha çox funksiyaya malik daha dolğun nümunə üçün <a href="https://fastapi.tiangolo.com/az/tutorial/">Öyrədici - İstifadəçi Təlimatı</a> səhifəsinə baxa bilərsiniz. + +**Spoiler xəbərdarlığı**: Öyrədici - istifadəçi təlimatına bunlar daxildir: + +* **Parametrlərin**, <abbr title="Başlıq: Header">**başlıqlar**</abbr>, <abbr title="Çərəz: Cookie">çərəzlər</abbr>, **forma sahələri** və **fayllar** olaraq müəyyən edilməsi. +* `maximum_length` və ya `regex` kimi **doğrulama məhdudiyyətlərinin** necə təyin ediləcəyi. +* Çox güclü və istifadəsi asan **<abbr title="components, resources, providers, services, injectables olaraq da bilinir">Dependency Injection</abbr>** sistemi. +* Təhlükəsizlik və autentifikasiya, **JWT tokenləri** ilə **OAuth2** dəstəyi və **HTTP Basic** autentifikasiyası. +* **çox dərin JSON modellərini** müəyyən etmək üçün daha irəli səviyyə (lakin eyni dərəcədə asan) üsullar (Pydantic sayəsində). +* <a href="https://strawberry.rocks" class="external-link" target="_blank">Strawberry</a> və digər kitabxanalar ilə **GraphQL** inteqrasiyası. +* Digər əlavə xüsusiyyətlər (Starlette sayəsində): + * **WebSockets** + * HTTPX və `pytest` sayəsində çox asan testlər + * **CORS** + * **Cookie Sessions** + * ...və daha çoxu. + +## Performans + +Müstəqil TechEmpower meyarları göstərir ki, Uvicorn üzərində işləyən **FastAPI** proqramları <a href="https://www.techempower.com/benchmarks/#section=test&runid=7464e520-0dc2-473d-bd34-dbdfd7e85911&hw=ph&test=query&l=zijzen-7" class="external-link" target="_blank">ən sürətli Python kitabxanalarından biridir</a>, yalnız Starlette və Uvicorn-un özündən yavaşdır, ki FastAPI bunların üzərinə qurulmuş bir framework-dür. (*) + +Ətraflı məlumat üçün bu bölməyə nəzər salın <a href="https://fastapi.tiangolo.com/az/benchmarks/" class="internal-link" target="_blank"><abbr title="Müqayisələr: Benchmarks">Müqayisələr</abbr></a>. + +## Məcburi Olmayan Tələblər + +Pydantic tərəfindən istifadə olunanlar: + +* <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - e-poçtun yoxlanılması üçün. +* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - parametrlərin idarə edilməsi üçün. +* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - Pydantic ilə istifadə edilə bilən əlavə tiplər üçün. + +Starlette tərəfindən istifadə olunanlar: + +* <a href="https://www.python-httpx.org" target="_blank"><code>httpx</code></a> - Əgər `TestClient` strukturundan istifadə edəcəksinizsə, tələb olunur. +* <a href="https://jinja.palletsprojects.com" target="_blank"><code>jinja2</code></a> - Standart <abbr title="Şablon: Template">şablon</abbr> konfiqurasiyasından istifadə etmək istəyirsinizsə, tələb olunur. +* <a href="https://andrew-d.github.io/python-multipart/" target="_blank"><code>python-multipart</code></a> - `request.form()` ilə forma <abbr title="HTTP sorğusu ilə alınan string məlumatın Python obyektinə çevrilməsi">"çevirmə"</abbr> dəstəyindən istifadə etmək istəyirsinizsə, tələb olunur. +* <a href="https://pythonhosted.org/itsdangerous/" target="_blank"><code>itsdangerous</code></a> - `SessionMiddleware` dəstəyi üçün tələb olunur. +* <a href="https://pyyaml.org/wiki/PyYAMLDocumentation" target="_blank"><code>pyyaml</code></a> - `SchemaGenerator` dəstəyi üçün tələb olunur (Çox güman ki, FastAPI istifadə edərkən buna ehtiyacınız olmayacaq). +* <a href="https://github.com/esnme/ultrajson" target="_blank"><code>ujson</code></a> - `UJSONResponse` istifadə etmək istəyirsinizsə, tələb olunur. + +Həm FastAPI, həm də Starlette tərəfindən istifadə olunur: + +* <a href="https://www.uvicorn.org" target="_blank"><code>uvicorn</code></a> - Yaratdığımız proqramı servis edəcək veb server kimi fəaliyyət göstərir. +* <a href="https://github.com/ijl/orjson" target="_blank"><code>orjson</code></a> - `ORJSONResponse` istifadə edəcəksinizsə tələb olunur. + +Bütün bunları `pip install fastapi[all]` ilə quraşdıra bilərsiniz. + +## Lisenziya + +Bu layihə MIT lisenziyasının şərtlərinə əsasən lisenziyalaşdırılıb. diff --git a/docs/az/mkdocs.yml b/docs/az/mkdocs.yml new file mode 100644 index 000000000..de18856f4 --- /dev/null +++ b/docs/az/mkdocs.yml @@ -0,0 +1 @@ +INHERIT: ../en/mkdocs.yml diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index 2b843e026..9e22e3a22 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -267,6 +267,8 @@ extra: alternate: - link: / name: en - English + - link: /az/ + name: az - azərbaycan dili - link: /bn/ name: bn - বাংলা - link: /de/ From c8c9ae475c33b6989c02ab72c20630b2fcb9b5ec Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 31 Jan 2024 15:46:19 +0000 Subject: [PATCH 1853/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 d7dba1721..3a4c6f170 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Azerbaijani translation for `docs/az/docs/index.md`. PR [#11047](https://github.com/tiangolo/fastapi/pull/11047) by [@aykhans](https://github.com/aykhans). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/middleware.md`. PR [#2829](https://github.com/tiangolo/fastapi/pull/2829) by [@JeongHyeongKim](https://github.com/JeongHyeongKim). * 🌐 Add German translation for `docs/de/docs/tutorial/body-nested-models.md`. PR [#10313](https://github.com/tiangolo/fastapi/pull/10313) by [@nilslindemann](https://github.com/nilslindemann). * 🌐 Add Persian translation for `docs/fa/docs/tutorial/middleware.md`. PR [#9695](https://github.com/tiangolo/fastapi/pull/9695) by [@mojtabapaso](https://github.com/mojtabapaso). From f43e18562b0dfd7b29305a1ee4751e5de1a0841e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Wed, 31 Jan 2024 23:13:52 +0100 Subject: [PATCH 1854/1881] =?UTF-8?q?=F0=9F=94=A7=20Update=20sponsors:=20a?= =?UTF-8?q?dd=20Coherence=20(#11066)?= 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 | 5 +++++ docs/en/docs/deployment/cloud.md | 1 + docs/en/docs/img/sponsors/coherence-banner.png | Bin 0 -> 18388 bytes docs/en/docs/img/sponsors/coherence.png | Bin 0 -> 27886 bytes docs/en/overrides/main.html | 6 ++++++ 7 files changed, 16 insertions(+) create mode 100644 docs/en/docs/img/sponsors/coherence-banner.png create mode 100644 docs/en/docs/img/sponsors/coherence.png diff --git a/README.md b/README.md index 968ccf7a7..874abf8c6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The key features are: <a href="https://reflex.dev" target="_blank" title="Reflex"><img src="https://fastapi.tiangolo.com/img/sponsors/reflex.png"></a> <a href="https://github.com/scalar/scalar/?utm_source=fastapi&utm_medium=website&utm_campaign=main-badge" target="_blank" title="Scalar: Beautiful Open-Source API References from Swagger/OpenAPI files"><img src="https://fastapi.tiangolo.com/img/sponsors/scalar.svg"></a> <a href="https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge" target="_blank" title="Auth, user management and more for your B2B product"><img src="https://fastapi.tiangolo.com/img/sponsors/propelauth.png"></a> +<a href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=banner%20january%2024" target="_blank" title="Coherence"><img src="https://fastapi.tiangolo.com/img/sponsors/coherence.png"></a> <a href="https://training.talkpython.fm/fastapi-courses" target="_blank" title="FastAPI video courses on demand from people you trust"><img src="https://fastapi.tiangolo.com/img/sponsors/talkpython-v2.jpg"></a> <a href="https://testdriven.io/courses/tdd-fastapi/" target="_blank" title="Learn to build high-quality web apps with best practices"><img src="https://fastapi.tiangolo.com/img/sponsors/testdriven.svg"></a> <a href="https://github.com/deepset-ai/haystack/" target="_blank" title="Build powerful search from composable, open source building blocks"><img src="https://fastapi.tiangolo.com/img/sponsors/haystack-fastapi.svg"></a> diff --git a/docs/en/data/sponsors.yml b/docs/en/data/sponsors.yml index 0ce434b5e..fd8518ce3 100644 --- a/docs/en/data/sponsors.yml +++ b/docs/en/data/sponsors.yml @@ -20,6 +20,9 @@ gold: - url: https://www.propelauth.com/?utm_source=fastapi&utm_campaign=1223&utm_medium=mainbadge title: Auth, user management and more for your B2B product img: https://fastapi.tiangolo.com/img/sponsors/propelauth.png + - url: https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=banner%20january%2024 + title: Coherence + img: https://fastapi.tiangolo.com/img/sponsors/coherence.png silver: - url: https://training.talkpython.fm/fastapi-courses title: FastAPI video courses on demand from people you trust diff --git a/docs/en/data/sponsors_badge.yml b/docs/en/data/sponsors_badge.yml index 4078454a8..00cbec7d2 100644 --- a/docs/en/data/sponsors_badge.yml +++ b/docs/en/data/sponsors_badge.yml @@ -23,3 +23,8 @@ logins: - svixhq - Alek99 - codacy + - zanfaruqui + - scalar + - bump-sh + - andrew-propelauth + - svix diff --git a/docs/en/docs/deployment/cloud.md b/docs/en/docs/deployment/cloud.md index 29f0ad1f6..d34fbe2f7 100644 --- a/docs/en/docs/deployment/cloud.md +++ b/docs/en/docs/deployment/cloud.md @@ -14,3 +14,4 @@ You might want to try their services and follow their guides: * <a href="https://docs.platform.sh/languages/python.html?utm_source=fastapi-signup&utm_medium=banner&utm_campaign=FastAPI-signup-June-2023" class="external-link" target="_blank">Platform.sh</a> * <a href="https://docs.porter.run/language-specific-guides/fastapi" class="external-link" target="_blank">Porter</a> +* <a href="https://docs.withcoherence.com/docs/configuration/frameworks?utm_medium=advertising&utm_source=fastapi&utm_campaign=banner%20january%2024#fast-api-example" class="external-link" target="_blank">Coherence</a> diff --git a/docs/en/docs/img/sponsors/coherence-banner.png b/docs/en/docs/img/sponsors/coherence-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..1d495965920e7fce5127637b50fd9530ce447de4 GIT binary patch literal 18388 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 literal 0 HcmV?d00001 diff --git a/docs/en/docs/img/sponsors/coherence.png b/docs/en/docs/img/sponsors/coherence.png new file mode 100644 index 0000000000000000000000000000000000000000..d48c4edc4df9693ef0d847fcd1b6c3055cb6c609 GIT binary patch literal 27886 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 GLSTZd%u3<_ literal 0 HcmV?d00001 diff --git a/docs/en/overrides/main.html b/docs/en/overrides/main.html index 476b43676..eaab6b630 100644 --- a/docs/en/overrides/main.html +++ b/docs/en/overrides/main.html @@ -64,6 +64,12 @@ <img class="sponsor-image" src="/img/sponsors/propelauth-banner.png" /> </a> </div> + <div class="item"> + <a title="Coherence" style="display: block; position: relative;" href="https://www.withcoherence.com/?utm_medium=advertising&utm_source=fastapi&utm_campaign=banner%20january%2024" target="_blank"> + <span class="sponsor-badge">sponsor</span> + <img class="sponsor-image" src="/img/sponsors/coherence-banner.png" /> + </a> + </div> </div> </div> {% endblock %} From df674d5b21c50c5f4932df32bfb0cecfc75bbac8 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Wed, 31 Jan 2024 22:14:15 +0000 Subject: [PATCH 1855/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 3a4c6f170..1777dad70 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -143,6 +143,7 @@ hide: ### Internal +* 🔧 Update sponsors: add Coherence. PR [#11066](https://github.com/tiangolo/fastapi/pull/11066) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade GitHub Action issue-manager. PR [#11056](https://github.com/tiangolo/fastapi/pull/11056) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update sponsors: TalkPython badge. PR [#11052](https://github.com/tiangolo/fastapi/pull/11052) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: TalkPython badge image. PR [#11048](https://github.com/tiangolo/fastapi/pull/11048) by [@tiangolo](https://github.com/tiangolo). From fd330fc45234a2b4220a3c2dd4f779b5f7b83021 Mon Sep 17 00:00:00 2001 From: xzmeng <aumo@foxmail.com> Date: Thu, 1 Feb 2024 16:56:55 +0800 Subject: [PATCH 1856/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/deployment/concepts.md`=20(#10282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh/docs/deployment/concepts.md | 323 ++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) create mode 100644 docs/zh/docs/deployment/concepts.md diff --git a/docs/zh/docs/deployment/concepts.md b/docs/zh/docs/deployment/concepts.md new file mode 100644 index 000000000..9c4aaa64b --- /dev/null +++ b/docs/zh/docs/deployment/concepts.md @@ -0,0 +1,323 @@ +# 部署概念 + +在部署 **FastAPI** 应用程序或任何类型的 Web API 时,有几个概念值得了解,通过掌握这些概念您可以找到**最合适的**方法来**部署您的应用程序**。 + +一些重要的概念是: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* 复制(运行的进程数) +* 内存 +* 开始前的先前步骤 + +我们接下来了解它们将如何影响**部署**。 + +我们的最终目标是能够以**安全**的方式**为您的 API 客户端**提供服务,同时要**避免中断**,并且尽可能高效地利用**计算资源**( 例如服务器CPU资源)。 🚀 + +我将在这里告诉您更多关于这些**概念**的信息,希望能给您提供**直觉**来决定如何在非常不同的环境中部署 API,甚至在是尚不存在的**未来**的环境里。 + +通过考虑这些概念,您将能够**评估和设计**部署**您自己的 API**的最佳方式。 + +在接下来的章节中,我将为您提供更多部署 FastAPI 应用程序的**具体方法**。 + +但现在,让我们仔细看一下这些重要的**概念**。 这些概念也适用于任何其他类型的 Web API。 💡 + +## 安全性 - HTTPS + +在[上一章有关 HTTPS](./https.md){.internal-link target=_blank} 中,我们了解了 HTTPS 如何为您的 API 提供加密。 + +我们还看到,HTTPS 通常由应用程序服务器的**外部**组件(**TLS 终止代理**)提供。 + +并且必须有某个东西负责**更新 HTTPS 证书**,它可以是相同的组件,也可以是不同的组件。 + + +### HTTPS 示例工具 + +您可以用作 TLS 终止代理的一些工具包括: + +* Traefik + * 自动处理证书更新 ✨ +* Caddy + * 自动处理证书更新 ✨ +* Nginx + * 使用 Certbot 等外部组件进行证书更新 +* HAProxy + * 使用 Certbot 等外部组件进行证书更新 +* 带有 Ingress Controller(如Nginx) 的 Kubernetes + * 使用诸如 cert-manager 之类的外部组件来进行证书更新 +* 由云服务商内部处理,作为其服务的一部分(请阅读下文👇) + +另一种选择是您可以使用**云服务**来完成更多工作,包括设置 HTTPS。 它可能有一些限制或向您收取更多费用等。但在这种情况下,您不必自己设置 TLS 终止代理。 + +我将在接下来的章节中向您展示一些具体示例。 + +--- + +接下来要考虑的概念都是关于运行实际 API 的程序(例如 Uvicorn)。 + +## 程序和进程 + +我们将讨论很多关于正在运行的“**进程**”的内容,因此弄清楚它的含义以及与“**程序**”这个词有什么区别是很有用的。 + +### 什么是程序 + +**程序**这个词通常用来描述很多东西: + +* 您编写的 **代码**,**Python 文件**。 +* 操作系统可以**执行**的**文件**,例如:`python`、`python.exe`或`uvicorn`。 +* 在操作系统上**运行**、使用CPU 并将内容存储在内存上的特定程序。 这也被称为**进程**。 + +### 什么是进程 + +**进程** 这个词通常以更具体的方式使用,仅指在操作系统中运行的东西(如上面的最后一点): + +* 在操作系统上**运行**的特定程序。 + * 这不是指文件,也不是指代码,它**具体**指的是操作系统正在**执行**和管理的东西。 +* 任何程序,任何代码,**只有在执行时才能做事**。 因此,是当有**进程正在运行**时。 +* 该进程可以由您或操作系统**终止**(或“杀死”)。 那时,它停止运行/被执行,并且它可以**不再做事情**。 +* 您计算机上运行的每个应用程序背后都有一些进程,每个正在运行的程序,每个窗口等。并且通常在计算机打开时**同时**运行许多进程。 +* **同一程序**可以有**多个进程**同时运行。 + +如果您检查操作系统中的“任务管理器”或“系统监视器”(或类似工具),您将能够看到许多正在运行的进程。 + +例如,您可能会看到有多个进程运行同一个浏览器程序(Firefox、Chrome、Edge 等)。 他们通常每个tab运行一个进程,再加上一些其他额外的进程。 + +<img class="shadow" src="/img/deployment/concepts/image01.png"> + +--- + +现在我们知道了术语“进程”和“程序”之间的区别,让我们继续讨论部署。 + +## 启动时运行 + +在大多数情况下,当您创建 Web API 时,您希望它**始终运行**、不间断,以便您的客户端始终可以访问它。 这是当然的,除非您有特定原因希望它仅在某些情况下运行,但大多数时候您希望它不断运行并且**可用**。 + +### 在远程服务器中 + +当您设置远程服务器(云服务器、虚拟机等)时,您可以做的最简单的事情就是手动运行 Uvicorn(或类似的),就像本地开发时一样。 + +它将会在**开发过程中**发挥作用并发挥作用。 + +但是,如果您与服务器的连接丢失,**正在运行的进程**可能会终止。 + +如果服务器重新启动(例如更新后或从云提供商迁移后),您可能**不会注意到它**。 因此,您甚至不知道必须手动重新启动该进程。 所以,你的 API 将一直处于挂掉的状态。 😱 + + +### 启动时自动运行 + +一般来说,您可能希望服务器程序(例如 Uvicorn)在服务器启动时自动启动,并且不需要任何**人为干预**,让进程始终与您的 API 一起运行(例如 Uvicorn 运行您的 FastAPI 应用程序) 。 + +### 单独的程序 + +为了实现这一点,您通常会有一个**单独的程序**来确保您的应用程序在启动时运行。 在许多情况下,它还可以确保其他组件或应用程序也运行,例如数据库。 + +### 启动时运行的示例工具 + +可以完成这项工作的工具的一些示例是: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm Mode +* Systemd +* Supervisor +* 作为其服务的一部分由云提供商内部处理 +* 其他的... + +我将在接下来的章节中为您提供更具体的示例。 + + +## 重新启动 + +与确保应用程序在启动时运行类似,您可能还想确保它在挂掉后**重新启动**。 + +### 我们会犯错误 + +作为人类,我们总是会犯**错误**。 软件几乎*总是*在不同的地方隐藏着**bug**。 🐛 + +作为开发人员,当我们发现这些bug并实现新功能(也可能添加新bug😅)时,我们会不断改进代码。 + +### 自动处理小错误 + +使用 FastAPI 构建 Web API 时,如果我们的代码中存在错误,FastAPI 通常会将其包含到触发错误的单个请求中。 🛡 + +对于该请求,客户端将收到 **500 内部服务器错误**,但应用程序将继续处理下一个请求,而不是完全崩溃。 + +### 更大的错误 - 崩溃 + +尽管如此,在某些情况下,我们编写的一些代码可能会导致整个应用程序崩溃,从而导致 Uvicorn 和 Python 崩溃。 💥 + +尽管如此,您可能不希望应用程序因为某个地方出现错误而保持死机状态,您可能希望它**继续运行**,至少对于未破坏的*路径操作*。 + +### 崩溃后重新启动 + +但在那些严重错误导致正在运行的**进程**崩溃的情况下,您需要一个外部组件来负责**重新启动**进程,至少尝试几次...... + +!!! tip + + ...尽管如果整个应用程序只是**立即崩溃**,那么永远重新启动它可能没有意义。 但在这些情况下,您可能会在开发过程中注意到它,或者至少在部署后立即注意到它。 + + 因此,让我们关注主要情况,在**未来**的某些特定情况下,它可能会完全崩溃,但重新启动它仍然有意义。 + +您可能希望让这个东西作为 **外部组件** 负责重新启动您的应用程序,因为到那时,使用 Uvicorn 和 Python 的同一应用程序已经崩溃了,因此同一应用程序的相同代码中没有东西可以对此做出什么。 + +### 自动重新启动的示例工具 + +在大多数情况下,用于**启动时运行程序**的同一工具也用于处理自动**重新启动**。 + +例如,可以通过以下方式处理: + +* Docker +* Kubernetes +* Docker Compose +* Docker in Swarm mode +* Systemd +* Supervisor +* 作为其服务的一部分由云提供商内部处理 +* 其他的... + +## 复制 - 进程和内存 + +对于 FastAPI 应用程序,使用像 Uvicorn 这样的服务器程序,在**一个进程**中运行一次就可以同时为多个客户端提供服务。 + +但在许多情况下,您会希望同时运行多个工作进程。 + +### 多进程 - Workers + +如果您的客户端数量多于单个进程可以处理的数量(例如,如果虚拟机不是太大),并且服务器的 CPU 中有 **多个核心**,那么您可以让 **多个进程** 运行 同时处理同一个应用程序,并在它们之间分发所有请求。 + +当您运行同一 API 程序的**多个进程**时,它们通常称为 **workers**。 + +### 工作进程和端口 + +还记得文档 [About HTTPS](./https.md){.internal-link target=_blank} 中只有一个进程可以侦听服务器中的端口和 IP 地址的一种组合吗? + +现在仍然是对的。 + +因此,为了能够同时拥有**多个进程**,必须有一个**单个进程侦听端口**,然后以某种方式将通信传输到每个工作进程。 + +### 每个进程的内存 + +现在,当程序将内容加载到内存中时,例如,将机器学习模型加载到变量中,或者将大文件的内容加载到变量中,所有这些都会消耗服务器的一点内存 (RAM) 。 + +多个进程通常**不共享任何内存**。 这意味着每个正在运行的进程都有自己的东西、变量和内存。 如果您的代码消耗了大量内存,**每个进程**将消耗等量的内存。 + +### 服务器内存 + +例如,如果您的代码加载 **1 GB 大小**的机器学习模型,则当您使用 API 运行一个进程时,它将至少消耗 1 GB RAM。 如果您启动 **4 个进程**(4 个工作进程),每个进程将消耗 1 GB RAM。 因此,您的 API 总共将消耗 **4 GB RAM**。 + +如果您的远程服务器或虚拟机只有 3 GB RAM,尝试加载超过 4 GB RAM 将导致问题。 🚨 + + +### 多进程 - 一个例子 + +在此示例中,有一个 **Manager Process** 启动并控制两个 **Worker Processes**。 + +该管理器进程可能是监听 IP 中的 **端口** 的进程。 它将所有通信传输到工作进程。 + +这些工作进程将是运行您的应用程序的进程,它们将执行主要计算以接收 **请求** 并返回 **响应**,并且它们将加载您放入 RAM 中的变量中的任何内容。 + +<img src="/img/deployment/concepts/process-ram.svg"> + +当然,除了您的应用程序之外,同一台机器可能还运行**其他进程**。 + +一个有趣的细节是,随着时间的推移,每个进程使用的 **CPU 百分比可能会发生很大变化,但内存 (RAM) 通常会或多或少保持稳定**。 + +如果您有一个每次执行相当数量的计算的 API,并且您有很多客户端,那么 **CPU 利用率** 可能也会保持稳定(而不是不断快速上升和下降)。 + +### 复制工具和策略示例 + +可以通过多种方法来实现这一目标,我将在接下来的章节中向您详细介绍具体策略,例如在谈论 Docker 和容器时。 + +要考虑的主要限制是必须有一个**单个**组件来处理**公共IP**中的**端口**。 然后它必须有一种方法将通信**传输**到复制的**进程/worker**。 + +以下是一些可能的组合和策略: + +* **Gunicorn** 管理 **Uvicorn workers** + * Gunicorn 将是监听 **IP** 和 **端口** 的 **进程管理器**,复制将通过 **多个 Uvicorn 工作进程** 进行 +* **Uvicorn** 管理 **Uvicorn workers** + * 一个 Uvicorn **进程管理器** 将监听 **IP** 和 **端口**,并且它将启动 **多个 Uvicorn 工作进程** +* **Kubernetes** 和其他分布式 **容器系统** + * **Kubernetes** 层中的某些东西将侦听 **IP** 和 **端口**。 复制将通过拥有**多个容器**,每个容器运行**一个 Uvicorn 进程** +* **云服务** 为您处理此问题 + * 云服务可能**为您处理复制**。 它可能会让您定义 **要运行的进程**,或要使用的 **容器映像**,在任何情况下,它很可能是 **单个 Uvicorn 进程**,并且云服务将负责复制它。 + + + +!!! tip + + 如果这些关于 **容器**、Docker 或 Kubernetes 的内容还没有多大意义,请不要担心。 + + 我将在以后的章节中向您详细介绍容器镜像、Docker、Kubernetes 等:[容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank}。 + +## 启动之前的步骤 + +在很多情况下,您希望在**启动**应用程序之前执行一些步骤。 + +例如,您可能想要运行**数据库迁移**。 + +但在大多数情况下,您只想执行这些步骤**一次**。 + +因此,在启动应用程序之前,您将需要一个**单个进程**来执行这些**前面的步骤**。 + +而且您必须确保它是运行前面步骤的单个进程, *即使*之后您为应用程序本身启动**多个进程**(多个worker)。 如果这些步骤由**多个进程**运行,它们会通过在**并行**运行来**重复**工作,并且如果这些步骤像数据库迁移一样需要小心处理,它们可能会导致每个进程和其他进程发生冲突。 + +当然,也有一些情况,多次运行前面的步骤也没有问题,这样的话就好办多了。 + +!!! tip + + 另外,请记住,根据您的设置,在某些情况下,您在开始应用程序之前**可能甚至不需要任何先前的步骤**。 + + 在这种情况下,您就不必担心这些。 🤷 + + +### 前面步骤策略的示例 + +这将在**很大程度上取决于您部署系统的方式**,并且可能与您启动程序、处理重启等的方式有关。 + +以下是一些可能的想法: + +* Kubernetes 中的“Init Container”在应用程序容器之前运行 +* 一个 bash 脚本,运行前面的步骤,然后启动您的应用程序 + * 您仍然需要一种方法来启动/重新启动 bash 脚本、检测错误等。 + +!!! tip + + 我将在以后的章节中为您提供使用容器执行此操作的更具体示例:[容器中的 FastAPI - Docker](./docker.md){.internal-link target=_blank}。 + +## 资源利用率 + +您的服务器是一个**资源**,您可以通过您的程序消耗或**利用**CPU 上的计算时间以及可用的 RAM 内存。 + +您想要消耗/利用多少系统资源? 您可能很容易认为“不多”,但实际上,您可能希望在不崩溃的情况下**尽可能多地消耗**。 + +如果您支付了 3 台服务器的费用,但只使用了它们的一点点 RAM 和 CPU,那么您可能**浪费金钱** 💸,并且可能 **浪费服务器电力** 🌎,等等。 + +在这种情况下,最好只拥有 2 台服务器并使用更高比例的资源(CPU、内存、磁盘、网络带宽等)。 + +另一方面,如果您有 2 台服务器,并且正在使用 **100% 的 CPU 和 RAM**,则在某些时候,一个进程会要求更多内存,并且服务器将不得不使用磁盘作为“内存” (这可能会慢数千倍),甚至**崩溃**。 或者一个进程可能需要执行一些计算,并且必须等到 CPU 再次空闲。 + +在这种情况下,最好购买**一台额外的服务器**并在其上运行一些进程,以便它们都有**足够的 RAM 和 CPU 时间**。 + +由于某种原因,您的 API 的使用量也有可能出现**激增**。 也许它像病毒一样传播开来,或者也许其他一些服务或机器人开始使用它。 在这些情况下,您可能需要额外的资源来保证安全。 + +您可以将一个**任意数字**设置为目标,例如,资源利用率**在 50% 到 90%** 之间。 重点是,这些可能是您想要衡量和用来调整部署的主要内容。 + +您可以使用“htop”等简单工具来查看服务器中使用的 CPU 和 RAM 或每个进程使用的数量。 或者您可以使用更复杂的监控工具,这些工具可能分布在服务器等上。 + + +## 回顾 + +您在这里阅读了一些在决定如何部署应用程序时可能需要牢记的主要概念: + +* 安全性 - HTTPS +* 启动时运行 +* 重新启动 +* 复制(运行的进程数) +* 内存 +* 开始前的先前步骤 + +了解这些想法以及如何应用它们应该会给您足够的直觉在配置和调整部署时做出任何决定。 🤓 + +在接下来的部分中,我将为您提供更具体的示例,说明您可以遵循的可能策略。 🚀 From e94575c2834777c2e723808f6e041727b51647f0 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 1 Feb 2024 08:57:17 +0000 Subject: [PATCH 1857/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 1777dad70..0fece2963 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/deployment/concepts.md`. PR [#10282](https://github.com/tiangolo/fastapi/pull/10282) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Azerbaijani translation for `docs/az/docs/index.md`. PR [#11047](https://github.com/tiangolo/fastapi/pull/11047) by [@aykhans](https://github.com/aykhans). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/middleware.md`. PR [#2829](https://github.com/tiangolo/fastapi/pull/2829) by [@JeongHyeongKim](https://github.com/JeongHyeongKim). * 🌐 Add German translation for `docs/de/docs/tutorial/body-nested-models.md`. PR [#10313](https://github.com/tiangolo/fastapi/pull/10313) by [@nilslindemann](https://github.com/nilslindemann). From 60130ed5f26e9039be928f22615223b434d48343 Mon Sep 17 00:00:00 2001 From: zqc <zhiquanchi@qq.com> Date: Fri, 2 Feb 2024 05:11:05 +0800 Subject: [PATCH 1858/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Chinese=20transl?= =?UTF-8?q?ation=20for=20`docs/zh/docs/tutorial/dependencies/dependencies-?= =?UTF-8?q?with-yield.md`=20(#10870)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dependencies/dependencies-with-yield.md | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md diff --git a/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md b/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md new file mode 100644 index 000000000..e24b9409f --- /dev/null +++ b/docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md @@ -0,0 +1,253 @@ +# 使用yield的依赖项 + +FastAPI支持在完成后执行一些<abbr title='有时也被称为“退出”("exit"),“清理”("cleanup"),“拆卸”("teardown"),“关闭”("close"),“上下文管理器”("context managers")。 ...'>额外步骤</abbr>的依赖项. + +为此,请使用 `yield` 而不是 `return`,然后再编写额外的步骤(代码)。 + +!!! 提示 + 确保只使用一次 `yield` 。 + +!!! 注意 "技术细节" + + 任何一个可以与以下内容一起使用的函数: + + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a> 或者 + * <a href="https://docs.python.org/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> + + 都可以作为 **FastAPI** 的依赖项。 + + 实际上,FastAPI内部就使用了这两个装饰器。 + + +## 使用 `yield` 的数据库依赖项 + +例如,您可以使用这种方式创建一个数据库会话,并在完成后关闭它。 + +在发送响应之前,只会执行 `yield` 语句及之前的代码: + +```Python hl_lines="2-4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +生成的值会注入到*路径操作*和其他依赖项中: + +```Python hl_lines="4" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +"yield"语句后面的代码会在发送响应后执行:: + +```Python hl_lines="5-6" +{!../../../docs_src/dependencies/tutorial007.py!} +``` + +!!! 提示 + + 您可以使用 `async` 或普通函数。 + + **FastAPI** 会像处理普通依赖关系一样,对每个依赖关系做正确的处理。 + +## 同时包含了 `yield` 和 `try` 的依赖项 + +如果在带有 `yield` 的依赖关系中使用 `try` 代码块,就会收到使用依赖关系时抛出的任何异常。 + +例如,如果中间某个代码在另一个依赖中或在*路径操作*中使数据库事务 "回滚 "或产生任何其他错误,您就会在依赖中收到异常。 + +因此,你可以使用 `except SomeException` 在依赖关系中查找特定的异常。 + +同样,您也可以使用 `finally` 来确保退出步骤得到执行,无论是否存在异常。 + +```Python hl_lines="3 5" +{!../../../docs_src/dependencies/tutorial007.py!} +``` +## 使用`yield`的子依赖项 + +你可以拥有任意大小和形状的子依赖和子依赖的“树”,而且它们中的任何一个或所有的都可以使用`yield`。 + +**FastAPI** 会确保每个带有`yield`的依赖中的“退出代码”按正确顺序运行。 + +例如,`dependency_c` 可以依赖于 `dependency_b`,而 `dependency_b` 则依赖于 `dependency_a`。 + +=== "Python 3.9+" + + ```Python hl_lines="6 14 22" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="5 13 21" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 如果可能,请尽量使用“ Annotated”版本。 + + ```Python hl_lines="4 12 20" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` + +所有这些依赖都可以使用`yield`。 + +在这种情况下,`dependency_c` 在执行其退出代码时需要`dependency_b`(此处称为 `dep_b`)的值仍然可用。 + +而`dependency_b` 反过来则需要`dependency_a`(此处称为 `dep_a`)的值在其退出代码中可用。 + +=== "Python 3.9+" + + ```Python hl_lines="18-19 26-27" + {!> ../../../docs_src/dependencies/tutorial008_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="17-18 25-26" + {!> ../../../docs_src/dependencies/tutorial008_an.py!} + ``` + +=== "Python 3.8+ non-Annotated" + + !!! tip + 如果可能,请尽量使用“ Annotated”版本。 + + ```Python hl_lines="16-17 24-25" + {!> ../../../docs_src/dependencies/tutorial008.py!} + ``` + +同样,你可以有混合了`yield`和`return`的依赖。 + +你也可以有一个单一的依赖需要多个其他带有`yield`的依赖,等等。 + +你可以拥有任何你想要的依赖组合。 + +**FastAPI** 将确保按正确的顺序运行所有内容。 + +!!! note "技术细节" + + 这是由 Python 的<a href="https://docs.python.org/3/library/contextlib.html" class="external-link" target="_blank">上下文管理器</a>完成的。 + + **FastAPI** 在内部使用它们来实现这一点。 + + +## 使用 `yield` 和 `HTTPException` 的依赖项 + +您看到可以使用带有 `yield` 的依赖项,并且具有捕获异常的 `try` 块。 + +在 `yield` 后抛出 `HTTPException` 或类似的异常是很诱人的,但是**这不起作用**。 + +带有`yield`的依赖中的退出代码在响应发送之后执行,因此[异常处理程序](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}已经运行过。没有任何东西可以捕获退出代码(在`yield`之后)中的依赖抛出的异常。 + +所以,如果在`yield`之后抛出`HTTPException`,默认(或任何自定义)异常处理程序捕获`HTTPException`并返回HTTP 400响应的机制将不再能够捕获该异常。 + +这就是允许在依赖中设置的任何东西(例如数据库会话(DB session))可以被后台任务使用的原因。 + +后台任务在响应发送之后运行。因此,无法触发`HTTPException`,因为甚至没有办法更改*已发送*的响应。 + +但如果后台任务产生了数据库错误,至少你可以在带有`yield`的依赖中回滚或清理关闭会话,并且可能记录错误或将其报告给远程跟踪系统。 + +如果你知道某些代码可能会引发异常,那就做最“Pythonic”的事情,就是在代码的那部分添加一个`try`块。 + +如果你有自定义异常,希望在返回响应之前处理,并且可能修改响应甚至触发`HTTPException`,可以创建[自定义异常处理程序](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}。 + +!!! tip + + 在`yield`之前仍然可以引发包括`HTTPException`在内的异常,但在`yield`之后则不行。 + +执行的顺序大致如下图所示。时间从上到下流动。每列都是相互交互或执行代码的其中一部分。 + +```mermaid +sequenceDiagram + +participant client as Client +participant handler as Exception handler +participant dep as Dep with yield +participant operation as Path Operation +participant tasks as Background tasks + + Note over client,tasks: Can raise exception for dependency, handled after response is sent + Note over client,operation: Can raise HTTPException and can change the response + client ->> dep: Start request + Note over dep: Run code up to yield + opt raise + dep -->> handler: Raise HTTPException + handler -->> client: HTTP error response + dep -->> dep: Raise other exception + end + dep ->> operation: Run dependency, e.g. DB session + opt raise + operation -->> dep: Raise HTTPException + dep -->> handler: Auto forward exception + handler -->> client: HTTP error response + operation -->> dep: Raise other exception + dep -->> handler: Auto forward exception + end + operation ->> client: Return response to client + Note over client,operation: Response is already sent, can't change it anymore + opt Tasks + operation -->> tasks: Send background tasks + end + opt Raise other exception + tasks -->> dep: Raise other exception + end + Note over dep: After yield + opt Handle other exception + dep -->> dep: Handle exception, can't change response. E.g. close DB session. + end +``` + +!!! info + 只会向客户端发送**一次响应**,可能是一个错误响应之一,也可能是来自*路径操作*的响应。 + + 在发送了其中一个响应之后,就无法再发送其他响应了。 + +!!! tip + 这个图表展示了`HTTPException`,但你也可以引发任何其他你创建了[自定义异常处理程序](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}的异常。 + + 如果你引发任何异常,它将传递给带有`yield`的依赖,包括`HTTPException`,然后**再次**传递给异常处理程序。如果没有针对该异常的异常处理程序,那么它将被默认的内部`ServerErrorMiddleware`处理,返回500 HTTP状态码,告知客户端服务器发生了错误。 + +## 上下文管理器 + +### 什么是“上下文管理器” + +“上下文管理器”是您可以在`with`语句中使用的任何Python对象。 + +例如,<a href="https://docs.python.org/zh-cn/3/tutorial/inputoutput.html#reading-and-writing-files" class="external-link" target="_blank">您可以使用`with`读取文件</a>: + +```Python +with open("./somefile.txt") as f: + contents = f.read() + print(contents) +``` + +在底层,`open("./somefile.txt")`创建了一个被称为“上下文管理器”的对象。 + +当`with`块结束时,它会确保关闭文件,即使发生了异常也是如此。 + +当你使用`yield`创建一个依赖项时,**FastAPI**会在内部将其转换为上下文管理器,并与其他相关工具结合使用。 + +### 在依赖项中使用带有`yield`的上下文管理器 + +!!! warning + 这是一个更为“高级”的想法。 + + 如果您刚开始使用**FastAPI**,您可能暂时可以跳过它。 + +在Python中,你可以通过<a href="https://docs.python.org/3/reference/datamodel.html#context-managers" class="external-link" target="_blank">创建一个带有`__enter__()`和`__exit__()`方法的类</a>来创建上下文管理器。 + +你也可以在**FastAPI**的依赖项中使用带有`yield`的`with`或`async with`语句,通过在依赖函数内部使用它们。 + +```Python hl_lines="1-9 13" +{!../../../docs_src/dependencies/tutorial010.py!} +``` + +!!! tip + 另一种创建上下文管理器的方法是: + + * <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.contextmanager" class="external-link" target="_blank">`@contextlib.contextmanager`</a>或者 + * <a href="https://docs.python.org/zh-cn/3/library/contextlib.html#contextlib.asynccontextmanager" class="external-link" target="_blank">`@contextlib.asynccontextmanager`</a> + + 使用上下文管理器装饰一个只有单个`yield`的函数。这就是**FastAPI**在内部用于带有`yield`的依赖项的方式。 + + 但是你不需要为FastAPI的依赖项使用这些装饰器(而且也不应该)。FastAPI会在内部为你处理这些。 From 6f6e786979eb3156b85e7c2f44d0b8af5cf0af17 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Thu, 1 Feb 2024 21:11:27 +0000 Subject: [PATCH 1859/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 0fece2963..f1b63b023 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10870](https://github.com/tiangolo/fastapi/pull/10870) by [@zhiquanchi](https://github.com/zhiquanchi). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/concepts.md`. PR [#10282](https://github.com/tiangolo/fastapi/pull/10282) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Azerbaijani translation for `docs/az/docs/index.md`. PR [#11047](https://github.com/tiangolo/fastapi/pull/11047) by [@aykhans](https://github.com/aykhans). * 🌐 Add Korean translation for `docs/ko/docs/tutorial/middleware.md`. PR [#2829](https://github.com/tiangolo/fastapi/pull/2829) by [@JeongHyeongKim](https://github.com/JeongHyeongKim). From d254e2f6ad773cf6694b7c1917c5792e9f805fd0 Mon Sep 17 00:00:00 2001 From: Soonho Kwon <percy3368@gmail.com> Date: Sat, 3 Feb 2024 02:39:46 +0900 Subject: [PATCH 1860/1881] =?UTF-8?q?=F0=9F=8C=90=20Update=20Korean=20tran?= =?UTF-8?q?slation=20for=20`docs/ko/docs/tutorial/first-steps.md`,=20`docs?= =?UTF-8?q?/ko/docs/tutorial/index.md`,=20`docs/ko/docs/tutorial/path-para?= =?UTF-8?q?ms.md`,=20and=20`docs/ko/docs/tutorial/query-params.md`=20(#421?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ko/docs/tutorial/first-steps.md | 90 +++++++++++++-------------- docs/ko/docs/tutorial/index.md | 27 ++++---- docs/ko/docs/tutorial/path-params.md | 74 +++++++++++----------- docs/ko/docs/tutorial/query-params.md | 16 ++--- 4 files changed, 104 insertions(+), 103 deletions(-) diff --git a/docs/ko/docs/tutorial/first-steps.md b/docs/ko/docs/tutorial/first-steps.md index a669cb2ba..0eb4d6bd5 100644 --- a/docs/ko/docs/tutorial/first-steps.md +++ b/docs/ko/docs/tutorial/first-steps.md @@ -1,12 +1,12 @@ # 첫걸음 -가장 단순한 FastAPI 파일은 다음과 같이 보일 겁니다: +가장 단순한 FastAPI 파일은 다음과 같이 보일 것입니다: ```Python {!../../../docs_src/first_steps/tutorial001.py!} ``` -위를 `main.py`에 복사합니다. +위 코드를 `main.py`에 복사합니다. 라이브 서버를 실행합니다: @@ -29,9 +29,9 @@ $ uvicorn main:app --reload * `main`: 파일 `main.py` (파이썬 "모듈"). * `app`: `main.py` 내부의 `app = FastAPI()` 줄에서 생성한 오브젝트. - * `--reload`: 코드 변경 후 서버 재시작. 개발에만 사용. + * `--reload`: 코드 변경 시 자동으로 서버 재시작. 개발 시에만 사용. -출력에 아래와 같은 줄이 있습니다: +출력되는 줄들 중에는 아래와 같은 내용이 있습니다: ```hl_lines="4" INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) @@ -75,7 +75,7 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) #### API "스키마" -이 경우, <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>는 API의 스키마를 어떻게 정의하는지 지시하는 규격입니다. +<a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a>는 API의 스키마를 어떻게 정의하는지 지시하는 규격입니다. 이 스키마 정의는 API 경로, 가능한 매개변수 등을 포함합니다. @@ -87,13 +87,13 @@ INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) #### OpenAPI와 JSON 스키마 -OpenAPI는 API에 대한 API 스키마를 정의합니다. 또한 이 스키마에는 JSON 데이터 스키마의 표준인 **JSON 스키마**를 사용하여 API에서 보내고 받은 데이터의 정의(또는 "스키마")를 포함합니다. +OpenAPI는 당신의 API에 대한 API 스키마를 정의합니다. 또한 이 스키마는 JSON 데이터 스키마의 표준인 **JSON 스키마**를 사용하여 당신의 API가 보내고 받는 데이터의 정의(또는 "스키마")를 포함합니다. #### `openapi.json` 확인 -가공되지 않은 OpenAPI 스키마가 어떻게 생겼는지 궁금하다면, FastAPI는 자동으로 API의 설명과 함께 JSON (스키마)를 생성합니다. +FastAPI는 자동으로 API의 설명과 함께 JSON (스키마)를 생성합니다. -여기에서 직접 볼 수 있습니다: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. +가공되지 않은 OpenAPI 스키마가 어떻게 생겼는지 궁금하다면, 여기에서 직접 볼 수 있습니다: <a href="http://127.0.0.1:8000/openapi.json" class="external-link" target="_blank">http://127.0.0.1:8000/openapi.json</a>. 다음과 같이 시작하는 JSON을 확인할 수 있습니다: @@ -124,7 +124,7 @@ OpenAPI 스키마는 포함된 두 개의 대화형 문서 시스템을 제공 그리고 OpenAPI의 모든 것을 기반으로 하는 수십 가지 대안이 있습니다. **FastAPI**로 빌드한 애플리케이션에 이러한 대안을 쉽게 추가 할 수 있습니다. -API와 통신하는 클라이언트를 위해 코드를 자동으로 생성하는 데도 사용할 수 있습니다. 예로 프론트엔드, 모바일, IoT 애플리케이션이 있습니다. +API와 통신하는 클라이언트(프론트엔드, 모바일, IoT 애플리케이션 등)를 위해 코드를 자동으로 생성하는 데도 사용할 수 있습니다. ## 단계별 요약 @@ -134,7 +134,7 @@ API와 통신하는 클라이언트를 위해 코드를 자동으로 생성하 {!../../../docs_src/first_steps/tutorial001.py!} ``` -`FastAPI`는 API에 대한 모든 기능을 제공하는 파이썬 클래스입니다. +`FastAPI`는 당신의 API를 위한 모든 기능을 제공하는 파이썬 클래스입니다. !!! note "기술 세부사항" `FastAPI`는 `Starlette`를 직접 상속하는 클래스입니다. @@ -147,11 +147,11 @@ API와 통신하는 클라이언트를 위해 코드를 자동으로 생성하 {!../../../docs_src/first_steps/tutorial001.py!} ``` -여기 있는 `app` 변수는 `FastAPI` 클래스의 "인스턴스"가 됩니다. +여기에서 `app` 변수는 `FastAPI` 클래스의 "인스턴스"가 됩니다. -이것은 모든 API를 생성하기 위한 상호작용의 주요 지점이 될 것입니다. +이것은 당신의 모든 API를 생성하기 위한 상호작용의 주요 지점이 될 것입니다. -이 `app`은 다음 명령에서 `uvicorn`이 참조하고 것과 동일합니다: +이 `app`은 다음 명령에서 `uvicorn`이 참조하고 있는 것과 동일합니다: <div class="termy"> @@ -181,11 +181,11 @@ $ uvicorn main:my_awesome_api --reload </div> -### 3 단계: *경로 동작* 생성 +### 3 단계: *경로 작동* 생성 #### 경로 -여기서 "경로"는 첫 번째 `/`에서 시작하는 URL의 마지막 부분을 나타냅니다. +여기서 "경로"는 첫 번째 `/`부터 시작하는 URL의 뒷부분을 의미합니다. 그러므로 아래와 같은 URL에서: @@ -200,13 +200,13 @@ https://example.com/items/foo ``` !!! info "정보" - "경로"는 일반적으로 "앤드포인트" 또는 "라우트"라고도 불립니다. + "경로"는 일반적으로 "엔드포인트" 또는 "라우트"라고도 불립니다. -API를 빌드하는 동안 "경로"는 "관심사"와 "리소스"를 분리하는 주요 방법입니다. +API를 설계할 때 "경로"는 "관심사"와 "리소스"를 분리하기 위한 주요한 방법입니다. -#### 동작 +#### 작동 -여기서 "동작(Operation)"은 HTTP "메소드" 중 하나를 나타냅니다. +"작동(Operation)"은 HTTP "메소드" 중 하나를 나타냅니다. 다음 중 하나이며: @@ -215,7 +215,7 @@ API를 빌드하는 동안 "경로"는 "관심사"와 "리소스"를 분리하 * `PUT` * `DELETE` -...이국적인 것들도 있습니다: +...흔히 사용되지 않는 것들도 있습니다: * `OPTIONS` * `HEAD` @@ -226,20 +226,20 @@ HTTP 프로토콜에서는 이러한 "메소드"를 하나(또는 이상) 사용 --- -API를 빌드하는 동안 일반적으로 특정 행동을 수행하기 위해 특정 HTTP 메소드를 사용합니다. +API를 설계할 때 일반적으로 특정 행동을 수행하기 위해 특정 HTTP 메소드를 사용합니다. -일반적으로 다음을 사용합니다: +일반적으로 다음과 같습니다: * `POST`: 데이터를 생성하기 위해. * `GET`: 데이터를 읽기 위해. -* `PUT`: 데이터를 업데이트하기 위해. +* `PUT`: 데이터를 수정하기 위해. * `DELETE`: 데이터를 삭제하기 위해. -그래서 OpenAPI에서는 각 HTTP 메소드들을 "동작"이라 부릅니다. +그래서 OpenAPI에서는 각 HTTP 메소드들을 "작동"이라 부릅니다. -이제부터 우리는 메소드를 "**동작**"이라고도 부를겁니다. +우리 역시 이제부터 메소드를 "**작동**"이라고 부를 것입니다. -#### *경로 동작 데코레이터* 정의 +#### *경로 작동 데코레이터* 정의 ```Python hl_lines="6" {!../../../docs_src/first_steps/tutorial001.py!} @@ -248,26 +248,26 @@ API를 빌드하는 동안 일반적으로 특정 행동을 수행하기 위해 `@app.get("/")`은 **FastAPI**에게 바로 아래에 있는 함수가 다음으로 이동하는 요청을 처리한다는 것을 알려줍니다. * 경로 `/` -* <abbr title="HTTP GET 메소드"><code>get</code> 동작</abbr> 사용 +* <abbr title="HTTP GET 메소드"><code>get</code> 작동</abbr> 사용 !!! info "`@decorator` 정보" 이 `@something` 문법은 파이썬에서 "데코레이터"라 부릅니다. - 함수 맨 위에 놓습니다. 마치 예쁜 장식용(Decorative) 모자처럼(개인적으로 이 용어가 여기서 유래한거 같습니다). + 마치 예쁜 장식용(Decorative) 모자처럼(개인적으로 이 용어가 여기서 유래한 것 같습니다) 함수 맨 위에 놓습니다. - "데코레이터" 아래 있는 함수를 받고 그걸 이용해 무언가 합니다. + "데코레이터"는 아래 있는 함수를 받아 그것으로 무언가를 합니다. - 우리의 경우, 이 데코레이터는 **FastAPI**에게 아래 함수가 **경로** `/`에 해당하는 `get` **동작**하라고 알려줍니다. + 우리의 경우, 이 데코레이터는 **FastAPI**에게 아래 함수가 **경로** `/`의 `get` **작동**에 해당한다고 알려줍니다. - 이것이 "**경로 동작 데코레이터**"입니다. + 이것이 "**경로 작동 데코레이터**"입니다. -다른 동작도 쓸 수 있습니다: +다른 작동도 사용할 수 있습니다: * `@app.post()` * `@app.put()` * `@app.delete()` -이국적인 것들도 있습니다: +흔히 사용되지 않는 것들도 있습니다: * `@app.options()` * `@app.head()` @@ -275,20 +275,20 @@ API를 빌드하는 동안 일반적으로 특정 행동을 수행하기 위해 * `@app.trace()` !!! tip "팁" - 각 동작(HTTP 메소드)을 원하는 대로 사용해도 됩니다. + 각 작동(HTTP 메소드)을 원하는 대로 사용해도 됩니다. **FastAPI**는 특정 의미를 강제하지 않습니다. - 여기서 정보는 지침서일뿐 요구사항이 아닙니다. + 여기서 정보는 지침서일뿐 강제사항이 아닙니다. - 예를 들어 GraphQL을 사용할때 일반적으로 `POST` 동작만 사용하여 모든 행동을 수행합니다. + 예를 들어 GraphQL을 사용하는 경우, 일반적으로 `POST` 작동만 사용하여 모든 행동을 수행합니다. -### 4 단계: **경로 동작 함수** 정의 +### 4 단계: **경로 작동 함수** 정의 -다음은 우리의 "**경로 동작 함수**"입니다: +다음은 우리의 "**경로 작동 함수**"입니다: * **경로**: 는 `/`입니다. -* **동작**: 은 `get`입니다. +* **작동**: 은 `get`입니다. * **함수**: 는 "데코레이터" 아래에 있는 함수입니다 (`@app.get("/")` 아래). ```Python hl_lines="7" @@ -297,13 +297,13 @@ API를 빌드하는 동안 일반적으로 특정 행동을 수행하기 위해 이것은 파이썬 함수입니다. -`GET` 동작을 사용하여 URL "`/`"에 대한 요청을 받을 때마다 **FastAPI**에 의해 호출됩니다. +URL "`/`"에 대한 `GET` 작동을 사용하는 요청을 받을 때마다 **FastAPI**에 의해 호출됩니다. -위의 경우 `async` 함수입니다. +위의 예시에서 이 함수는 `async`(비동기) 함수입니다. --- -`async def` 대신 일반 함수로 정의할 수 있습니다: +`async def`을 이용하는 대신 일반 함수로 정의할 수 있습니다: ```Python hl_lines="7" {!../../../docs_src/first_steps/tutorial003.py!} @@ -322,12 +322,12 @@ API를 빌드하는 동안 일반적으로 특정 행동을 수행하기 위해 Pydantic 모델을 반환할 수도 있습니다(나중에 더 자세히 살펴봅니다). -JSON으로 자동 변환되는 객체들과 모델들이 많이 있습니다(ORM 등을 포함해서요). 가장 마음에 드는 것을 사용하세요, 이미 지원되고 있을 겁니다. +JSON으로 자동 변환되는 객체들과 모델들(ORM 등을 포함해서)이 많이 있습니다. 가장 마음에 드는 것을 사용하십시오, 이미 지원되고 있을 것입니다. ## 요약 * `FastAPI` 임포트. * `app` 인스턴스 생성. -* (`@app.get("/")`처럼) **경로 동작 데코레이터** 작성. -* (위에 있는 `def root(): ...`처럼) **경로 동작 함수** 작성. +* (`@app.get("/")`처럼) **경로 작동 데코레이터** 작성. +* (위에 있는 `def root(): ...`처럼) **경로 작동 함수** 작성. * (`uvicorn main:app --reload`처럼) 개발 서버 실행. diff --git a/docs/ko/docs/tutorial/index.md b/docs/ko/docs/tutorial/index.md index deb5ca8f2..94d6dfb92 100644 --- a/docs/ko/docs/tutorial/index.md +++ b/docs/ko/docs/tutorial/index.md @@ -1,16 +1,16 @@ # 자습서 - 사용자 안내서 -이 자습서는 **FastAPI**의 대부분의 기능을 단계별로 사용하는 방법을 보여줍니다. +이 자습서는 단계별로 **FastAPI**의 대부분의 기능에 대해 설명합니다. -각 섹션은 이전 섹션을 기반해서 점진적으로 만들어 졌지만, 주제에 따라 다르게 구성되었기 때문에 특정 API 요구사항을 해결하기 위해서라면 어느 특정 항목으로던지 직접 이동할 수 있습니다. +각 섹션은 이전 섹션에 기반하는 순차적인 구조로 작성되었지만, 각 주제로 구분되어 있기 때문에 필요에 따라 특정 섹션으로 바로 이동하여 필요한 내용을 바로 확인할 수 있습니다. -또한 향후 참조가 될 수 있도록 만들어졌습니다. +또한 향후에도 참조 자료로 쓰일 수 있도록 작성되었습니다. -그러므로 다시 돌아와서 정확히 필요한 것을 확인할 수 있습니다. +그러므로 필요할 때에 다시 돌아와서 원하는 것을 정확히 찾을 수 있습니다. ## 코드 실행하기 -모든 코드 블록은 복사하고 직접 사용할 수 있습니다(실제로 테스트한 파이썬 파일입니다). +모든 코드 블록은 복사하여 바로 사용할 수 있습니다(실제로 테스트된 파이썬 파일입니다). 예제를 실행하려면 코드를 `main.py` 파일에 복사하고 다음을 사용하여 `uvicorn`을 시작합니다: @@ -28,17 +28,18 @@ $ uvicorn main:app --reload </div> -코드를 작성하거나 복사, 편집할 때, 로컬에서 실행하는 것을 **강력히 장려**합니다. +코드를 작성하거나 복사, 편집할 때, 로컬 환경에서 실행하는 것을 **강력히 권장**합니다. + +로컬 편집기에서 사용한다면, 모든 타입 검사와 자동완성 등 작성해야 하는 코드가 얼마나 적은지 보면서 FastAPI의 비로소 경험할 수 있습니다. -편집기에서 이렇게 사용한다면, 모든 타입 검사와 자동완성 등 작성해야 하는 코드가 얼마나 적은지 보면서 FastAPI의 장점을 실제로 확인할 수 있습니다. --- ## FastAPI 설치 -첫 번째 단계는 FastAPI 설치입니다. +첫 번째 단계는 FastAPI를 설치하는 것입니다. -자습시에는 모든 선택적인 의존성 및 기능을 사용하여 설치할 수 있습니다: +자습시에는 모든 선택적인 의존성 및 기능을 함께 설치하는 것을 추천합니다: <div class="termy"> @@ -50,7 +51,7 @@ $ pip install "fastapi[all]" </div> -...코드를 실행하는 서버로 사용할 수 있는 `uvicorn` 역시 포함하고 있습니다. +...이는 코드를 실행하는 서버로 사용할 수 있는 `uvicorn` 또한 포함하고 있습니다. !!! note "참고" 부분적으로 설치할 수도 있습니다. @@ -73,8 +74,8 @@ $ pip install "fastapi[all]" 이 **자습서 - 사용자 안내서** 다음에 읽을 수 있는 **고급 사용자 안내서**도 있습니다. -**고급 사용자 안내서**는 현재 문서를 기반으로 하고, 동일한 개념을 사용하며, 추가 기능들을 알려줍니다. +**고급 사용자 안내서**는 현재 문서를 기반으로 하고, 동일한 개념을 사용하며, 추가적인 기능들에 대해 설명합니다. -하지만 (지금 읽고 있는) **자습서 - 사용자 안내서**를 먼저 읽는게 좋습니다. +하지만 (지금 읽고 있는) **자습서 - 사용자 안내서**를 먼저 읽는 것을 권장합니다. -**자습서 - 사용자 안내서**만으로도 완전한 애플리케이션을 구축할 수 있으며, 필요에 따라 **고급 사용자 안내서**에서 제공하는 몇 가지 추가적인 기능을 사용하여 다양한 방식으로 확장할 수 있도록 설계되었습니다. +**자습서 - 사용자 안내서**만으로도 완전한 애플리케이션을 구축할 수 있도록 작성되었으며, 필요에 따라 **고급 사용자 안내서**의 추가적인 아이디어를 적용하여 다양한 방식으로 확장할 수 있습니다. diff --git a/docs/ko/docs/tutorial/path-params.md b/docs/ko/docs/tutorial/path-params.md index 8ebd0804e..6d5d37352 100644 --- a/docs/ko/docs/tutorial/path-params.md +++ b/docs/ko/docs/tutorial/path-params.md @@ -1,6 +1,6 @@ # 경로 매개변수 -파이썬 포맷 문자열이 사용하는 동일한 문법으로 "매개변수" 또는 "변수"를 경로에 선언할 수 있습니다: +파이썬의 포맷 문자열 리터럴에서 사용되는 문법을 이용하여 경로 "매개변수" 또는 "변수"를 선언할 수 있습니다: ```Python hl_lines="6-7" {!../../../docs_src/path_params/tutorial001.py!} @@ -22,10 +22,10 @@ {!../../../docs_src/path_params/tutorial002.py!} ``` -지금과 같은 경우, `item_id`는 `int`로 선언 되었습니다. +위의 예시에서, `item_id`는 `int`로 선언되었습니다. !!! check "확인" - 이 기능은 함수 내에서 오류 검사, 자동완성 등을 편집기를 지원합니다 + 이 기능은 함수 내에서 오류 검사, 자동완성 등의 편집기 기능을 활용할 수 있게 해줍니다. ## 데이터 <abbr title="다음으로도 알려져 있습니다: 직렬화, 파싱, 마샬링">변환</abbr> @@ -42,7 +42,7 @@ ## 데이터 검증 -하지만 브라우저에서 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>로 이동하면, 멋진 HTTP 오류를 볼 수 있습니다: +하지만 브라우저에서 <a href="http://127.0.0.1:8000/items/foo" class="external-link" target="_blank">http://127.0.0.1:8000/items/foo</a>로 이동하면, HTTP 오류가 잘 뜨는 것을 확인할 수 있습니다: ```JSON { @@ -61,12 +61,12 @@ 경로 매개변수 `item_id`는 `int`가 아닌 `"foo"` 값이기 때문입니다. -`int` 대신 `float`을 전달하면 동일한 오류가 나타납니다: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> +`int`가 아닌 `float`을 전달하는 경우에도 동일한 오류가 나타납니다: <a href="http://127.0.0.1:8000/items/4.2" class="external-link" target="_blank">http://127.0.0.1:8000/items/4.2</a> !!! check "확인" 즉, 파이썬 타입 선언을 하면 **FastAPI**는 데이터 검증을 합니다. - 오류는 검증을 통과하지 못한 지점도 정확하게 명시합니다. + 오류에는 정확히 어느 지점에서 검증을 통과하지 못했는지 명시됩니다. 이는 API와 상호 작용하는 코드를 개발하고 디버깅하는 데 매우 유용합니다. @@ -77,11 +77,11 @@ <img src="/img/tutorial/path-params/image01.png"> !!! check "확인" - 다시 한번, 그저 파이썬 타입 선언을 하기만 하면 **FastAPI**는 자동 대화식 API 문서(Swagger UI 통합)를 제공합니다. + 그저 파이썬 타입 선언을 하기만 하면 **FastAPI**는 자동 대화형 API 문서(Swagger UI)를 제공합니다. - 경로 매개변수는 정수형으로 선언됐음을 주목하세요. + 경로 매개변수가 정수형으로 명시된 것을 확인할 수 있습니다. -## 표준 기반의 이점, 대체 문서화 +## 표준 기반의 이점, 대체 문서 그리고 생성된 스키마는 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md" class="external-link" target="_blank">OpenAPI</a> 표준에서 나온 것이기 때문에 호환되는 도구가 많이 있습니다. @@ -89,53 +89,53 @@ <img src="/img/tutorial/path-params/image02.png"> -이와 마찬가지로 호환되는 도구가 많이 있습니다. 다양한 언어에 대한 코드 생성 도구를 포함합니다. +이와 마찬가지로 다양한 언어에 대한 코드 생성 도구를 포함하여 여러 호환되는 도구가 있습니다. ## Pydantic -모든 데이터 검증은 <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>에 의해 내부적으로 수행되므로 이로 인한 모든 이점을 얻을 수 있습니다. 여러분은 관리를 잘 받고 있음을 느낄 수 있습니다. +모든 데이터 검증은 <a href="https://pydantic-docs.helpmanual.io/" class="external-link" target="_blank">Pydantic</a>에 의해 내부적으로 수행되므로 이로 인한 이점을 모두 얻을 수 있습니다. 여러분은 관리를 잘 받고 있음을 느낄 수 있습니다. -`str`, `float`, `bool`과 다른 복잡한 데이터 타입 선언을 할 수 있습니다. +`str`, `float`, `bool`, 그리고 다른 여러 복잡한 데이터 타입 선언을 할 수 있습니다. -이 중 몇 가지는 자습서의 다음 장에서 살펴봅니다. +이 중 몇 가지는 자습서의 다음 장에 설명되어 있습니다. ## 순서 문제 -*경로 동작*을 만들때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. +*경로 작동*을 만들때 고정 경로를 갖고 있는 상황들을 맞닥뜨릴 수 있습니다. `/users/me`처럼, 현재 사용자의 데이터를 가져온다고 합시다. 사용자 ID를 이용해 특정 사용자의 정보를 가져오는 경로 `/users/{user_id}`도 있습니다. -*경로 동작*은 순차적으로 평가되기 때문에 `/users/{user_id}` 이전에 `/users/me`를 먼저 선언해야 합니다: +*경로 작동*은 순차적으로 실행되기 때문에 `/users/{user_id}` 이전에 `/users/me`를 먼저 선언해야 합니다: ```Python hl_lines="6 11" {!../../../docs_src/path_params/tutorial003.py!} ``` -그렇지 않으면 `/users/{user_id}`는 매개변수 `user_id`의 값을 `"me"`라고 "생각하여" `/users/me`도 연결합니다. +그렇지 않으면 `/users/{user_id}`는 `/users/me` 요청 또한 매개변수 `user_id`의 값이 `"me"`인 것으로 "생각하게" 됩니다. ## 사전정의 값 -만약 *경로 매개변수*를 받는 *경로 동작*이 있지만, 유효하고 미리 정의할 수 있는 *경로 매개변수* 값을 원한다면 파이썬 표준 <abbr title="열거형(Enumeration)">`Enum`</abbr>을 사용할 수 있습니다. +만약 *경로 매개변수*를 받는 *경로 작동*이 있지만, *경로 매개변수*로 가능한 값들을 미리 정의하고 싶다면 파이썬 표준 <abbr title="열거형(Enumeration)">`Enum`</abbr>을 사용할 수 있습니다. ### `Enum` 클래스 생성 `Enum`을 임포트하고 `str`과 `Enum`을 상속하는 서브 클래스를 만듭니다. -`str`을 상속함으로써 API 문서는 값이 `string` 형이어야 하는 것을 알게 되고 제대로 렌더링 할 수 있게 됩니다. +`str`을 상속함으로써 API 문서는 값이 `string` 형이어야 하는 것을 알게 되고 이는 문서에 제대로 표시됩니다. -고정값으로 사용할 수 있는 유효한 클래스 어트리뷰트를 만듭니다: +가능한 값들에 해당하는 고정된 값의 클래스 어트리뷰트들을 만듭니다: ```Python hl_lines="1 6-9" {!../../../docs_src/path_params/tutorial005.py!} ``` !!! info "정보" - <a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">열거형(또는 enums)</a>은 파이썬 버전 3.4 이후로 사용가능합니다. + <a href="https://docs.python.org/3/library/enum.html" class="external-link" target="_blank">열거형(또는 enums)</a>은 파이썬 버전 3.4 이후로 사용 가능합니다. !!! tip "팁" - 혹시 헷갈린다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 기계 학습 <abbr title="기술적으로 정확히는 딥 러닝 모델 구조">모델</abbr>들의 이름입니다. + 혹시 궁금하다면, "AlexNet", "ResNet", 그리고 "LeNet"은 그저 기계 학습 <abbr title="기술적으로 정확히는 딥 러닝 모델 구조">모델</abbr>들의 이름입니다. ### *경로 매개변수* 선언 @@ -147,7 +147,7 @@ ### 문서 확인 -*경로 매개변수*에 사용할 수 있는 값은 미리 정의되어 있으므로 대화형 문서에서 멋지게 표시됩니다: +*경로 매개변수*에 사용할 수 있는 값은 미리 정의되어 있으므로 대화형 문서에서 잘 표시됩니다: <img src="/img/tutorial/path-params/image03.png"> @@ -157,7 +157,7 @@ #### *열거형 멤버* 비교 -열거체 `ModelName`의 *열거형 멤버*를 비교할 수 있습니다: +열거형 `ModelName`의 *열거형 멤버*를 비교할 수 있습니다: ```Python hl_lines="17" {!../../../docs_src/path_params/tutorial005.py!} @@ -165,7 +165,7 @@ #### *열거형 값* 가져오기 -`model_name.value` 또는 일반적으로 `your_enum_member.value`를 이용하여 실제값(지금의 경우 `str`)을 가져올 수 있습니다: +`model_name.value` 또는 일반적으로 `your_enum_member.value`를 이용하여 실제 값(위 예시의 경우 `str`)을 가져올 수 있습니다: ```Python hl_lines="20" {!../../../docs_src/path_params/tutorial005.py!} @@ -176,7 +176,7 @@ #### *열거형 멤버* 반환 -*경로 동작*에서 중첩 JSON 본문(예: `dict`) 역시 *열거형 멤버*를 반환할 수 있습니다. +*경로 작동*에서 *열거형 멤버*를 반환할 수 있습니다. 이는 중첩 JSON 본문(예: `dict`)내의 값으로도 가능합니다. 클라이언트에 반환하기 전에 해당 값(이 경우 문자열)으로 변환됩니다: @@ -195,50 +195,50 @@ ## 경로를 포함하는 경로 매개변수 -`/files/{file_path}`가 있는 *경로 동작*이 있다고 해봅시다. +경로를 포함하는 *경로 작동* `/files/{file_path}`이 있다고 해봅시다. -그런데 여러분은 `home/johndoe/myfile.txt`처럼 *path*에 들어있는 `file_path` 자체가 필요합니다. +그런데 이 경우 `file_path` 자체가 `home/johndoe/myfile.txt`와 같은 경로를 포함해야 합니다. -따라서 해당 파일의 URL은 다음처럼 됩니다: `/files/home/johndoe/myfile.txt`. +이때 해당 파일의 URL은 다음처럼 됩니다: `/files/home/johndoe/myfile.txt`. ### OpenAPI 지원 테스트와 정의가 어려운 시나리오로 이어질 수 있으므로 OpenAPI는 *경로*를 포함하는 *경로 매개변수*를 내부에 선언하는 방법을 지원하지 않습니다. -그럼에도 Starlette의 내부 도구중 하나를 사용하여 **FastAPI**에서는 할 수 있습니다. +그럼에도 Starlette의 내부 도구중 하나를 사용하여 **FastAPI**에서는 이가 가능합니다. -매개변수에 경로가 포함되어야 한다는 문서를 추가하지 않아도 문서는 계속 작동합니다. +문서에 매개변수에 경로가 포함되어야 한다는 정보가 명시되지는 않지만 여전히 작동합니다. ### 경로 변환기 -Starlette에서 직접 옵션을 사용하면 다음과 같은 URL을 사용하여 *path*를 포함하는 *경로 매개변수*를 선언 할 수 있습니다: +Starlette의 옵션을 직접 이용하여 다음과 같은 URL을 사용함으로써 *path*를 포함하는 *경로 매개변수*를 선언할 수 있습니다: ``` /files/{file_path:path} ``` -이러한 경우 매개변수의 이름은 `file_path`이고 마지막 부분 `:path`는 매개변수가 *경로*와 일치해야함을 알려줍니다. +이러한 경우 매개변수의 이름은 `file_path`이며, 마지막 부분 `:path`는 매개변수가 *경로*와 일치해야 함을 명시합니다. -그러므로 다음과 같이 사용할 수 있습니다: +따라서 다음과 같이 사용할 수 있습니다: ```Python hl_lines="6" {!../../../docs_src/path_params/tutorial004.py!} ``` !!! tip "팁" - 매개변수가 `/home/johndoe/myfile.txt`를 갖고 있어 슬래시로 시작(`/`)해야 할 수 있습니다. + 매개변수가 가져야 하는 값이 `/home/johndoe/myfile.txt`와 같이 슬래시로 시작(`/`)해야 할 수 있습니다. 이 경우 URL은: `/files//home/johndoe/myfile.txt`이며 `files`과 `home` 사이에 이중 슬래시(`//`)가 생깁니다. ## 요약 -**FastAPI**과 함께라면 짧고 직관적인 표준 파이썬 타입 선언을 사용하여 다음을 얻을 수 있습니다: +**FastAPI**를 이용하면 짧고 직관적인 표준 파이썬 타입 선언을 사용하여 다음을 얻을 수 있습니다: * 편집기 지원: 오류 검사, 자동완성 등 * 데이터 "<abbr title="HTTP 요청에서 전달되는 문자열을 파이썬 데이터로 변환">파싱</abbr>" * 데이터 검증 * API 주석(Annotation)과 자동 문서 -위 사항들을 그저 한번에 선언하면 됩니다. +단 한번의 선언만으로 위 사항들을 모두 선언할 수 있습니다. -이는 (원래 성능과는 별개로) 대체 프레임워크와 비교했을 때 **FastAPI**의 주요 가시적 장점일 것입니다. +이는 대체 프레임워크와 비교했을 때 (엄청나게 빠른 성능 외에도) **FastAPI**의 주요한 장점일 것입니다. diff --git a/docs/ko/docs/tutorial/query-params.md b/docs/ko/docs/tutorial/query-params.md index bb631e6ff..8c7f9167b 100644 --- a/docs/ko/docs/tutorial/query-params.md +++ b/docs/ko/docs/tutorial/query-params.md @@ -1,6 +1,6 @@ # 쿼리 매개변수 -경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언할 때, "쿼리" 매개변수로 자동 해석합니다. +경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언하면 "쿼리" 매개변수로 자동 해석합니다. ```Python hl_lines="9" {!../../../docs_src/query_params/tutorial001.py!} @@ -8,7 +8,7 @@ 쿼리는 URL에서 `?` 후에 나오고 `&`으로 구분되는 키-값 쌍의 집합입니다. -예를 들어, URL에서: +예를 들어, 아래 URL에서: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 @@ -21,7 +21,7 @@ http://127.0.0.1:8000/items/?skip=0&limit=10 URL의 일부이므로 "자연스럽게" 문자열입니다. -하지만 파이썬 타입과 함께 선언할 경우(위 예에서 `int`), 해당 타입으로 변환되고 이에 대해 검증합니다. +하지만 파이썬 타입과 함께 선언할 경우(위 예에서 `int`), 해당 타입으로 변환 및 검증됩니다. 경로 매개변수에 적용된 동일한 프로세스가 쿼리 매개변수에도 적용됩니다: @@ -36,13 +36,13 @@ URL의 일부이므로 "자연스럽게" 문자열입니다. 위 예에서 `skip=0`과 `limit=10`은 기본값을 갖고 있습니다. -그러므로 URL로 이동하면: +그러므로 URL로 이동하는 것은: ``` http://127.0.0.1:8000/items/ ``` -아래로 이동한 것과 같습니다: +아래로 이동하는 것과 같습니다: ``` http://127.0.0.1:8000/items/?skip=0&limit=10 @@ -136,7 +136,7 @@ http://127.0.0.1:8000/items/foo?short=yes 특정값을 추가하지 않고 선택적으로 만들기 위해선 기본값을 `None`으로 설정하면 됩니다. -그러나 쿼리 매개변수를 필수로 만들려면 기본값을 선언할 수 없습니다: +그러나 쿼리 매개변수를 필수로 만들려면 단순히 기본값을 선언하지 않으면 됩니다: ```Python hl_lines="6-7" {!../../../docs_src/query_params/tutorial005.py!} @@ -144,7 +144,7 @@ http://127.0.0.1:8000/items/foo?short=yes 여기 쿼리 매개변수 `needy`는 `str`형인 필수 쿼리 매개변수입니다. -브라우저에서 URL을 아래처럼 연다면: +브라우저에서 아래와 같은 URL을 연다면: ``` http://127.0.0.1:8000/items/foo-item @@ -188,7 +188,7 @@ http://127.0.0.1:8000/items/foo-item?needy=sooooneedy {!../../../docs_src/query_params/tutorial006.py!} ``` -이 경우 3가지 쿼리 매개변수가 있습니다: +위 예시에서는 3가지 쿼리 매개변수가 있습니다: * `needy`, 필수적인 `str`. * `skip`, 기본값이 `0`인 `int`. From 6c4a143fd0fb2a9963ca60938c4a2bf69573aeca Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 2 Feb 2024 17:40:08 +0000 Subject: [PATCH 1861/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 f1b63b023..03ab31ce5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Update Korean translation for `docs/ko/docs/tutorial/first-steps.md`, `docs/ko/docs/tutorial/index.md`, `docs/ko/docs/tutorial/path-params.md`, and `docs/ko/docs/tutorial/query-params.md`. PR [#4218](https://github.com/tiangolo/fastapi/pull/4218) by [@SnowSuno](https://github.com/SnowSuno). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10870](https://github.com/tiangolo/fastapi/pull/10870) by [@zhiquanchi](https://github.com/zhiquanchi). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/concepts.md`. PR [#10282](https://github.com/tiangolo/fastapi/pull/10282) by [@xzmeng](https://github.com/xzmeng). * 🌐 Add Azerbaijani translation for `docs/az/docs/index.md`. PR [#11047](https://github.com/tiangolo/fastapi/pull/11047) by [@aykhans](https://github.com/aykhans). From 3c81e622f3783920ef3e3d4ecfae12c5511747bf Mon Sep 17 00:00:00 2001 From: pablocm83 <pablocm83@gmail.com> Date: Fri, 2 Feb 2024 13:09:12 -0500 Subject: [PATCH 1862/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/external-links.md`=20(#10933)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/es/docs/external-links.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/es/docs/external-links.md diff --git a/docs/es/docs/external-links.md b/docs/es/docs/external-links.md new file mode 100644 index 000000000..cfbdd68a6 --- /dev/null +++ b/docs/es/docs/external-links.md @@ -0,0 +1,33 @@ +# Enlaces Externos y Artículos + +**FastAPI** tiene una gran comunidad en constante crecimiento. + +Hay muchas publicaciones, artículos, herramientas y proyectos relacionados con **FastAPI**. + +Aquí hay una lista incompleta de algunos de ellos. + +!!! tip "Consejo" + Si tienes un artículo, proyecto, herramienta o cualquier cosa relacionada con **FastAPI** que aún no aparece aquí, crea un <a href="https://github.com/tiangolo/fastapi/edit/master/docs/en/data/external_links.yml" class="external-link" target="_blank">Pull Request agregándolo</a>. + +{% for section_name, section_content in external_links.items() %} + +## {{ section_name }} + +{% for lang_name, lang_content in section_content.items() %} + +### {{ lang_name }} + +{% for item in lang_content %} + +* <a href="{{ item.link }}" class="external-link" target="_blank">{{ item.title }}</a> by <a href="{{ item.author_link }}" class="external-link" target="_blank">{{ item.author }}</a>. + +{% endfor %} +{% endfor %} +{% endfor %} + +## Projects + +Últimos proyectos de GitHub con el tema `fastapi`: + +<div class="github-topic-projects"> +</div> From 063d7ffb15c4d1fe88745a8d2d2c9202d44046c5 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 2 Feb 2024 18:09:33 +0000 Subject: [PATCH 1863/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 03ab31ce5..97b777ce9 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -49,6 +49,7 @@ hide: ### Translations +* 🌐 Add Spanish translation for `docs/es/docs/external-links.md`. PR [#10933](https://github.com/tiangolo/fastapi/pull/10933) by [@pablocm83](https://github.com/pablocm83). * 🌐 Update Korean translation for `docs/ko/docs/tutorial/first-steps.md`, `docs/ko/docs/tutorial/index.md`, `docs/ko/docs/tutorial/path-params.md`, and `docs/ko/docs/tutorial/query-params.md`. PR [#4218](https://github.com/tiangolo/fastapi/pull/4218) by [@SnowSuno](https://github.com/SnowSuno). * 🌐 Add Chinese translation for `docs/zh/docs/tutorial/dependencies/dependencies-with-yield.md`. PR [#10870](https://github.com/tiangolo/fastapi/pull/10870) by [@zhiquanchi](https://github.com/zhiquanchi). * 🌐 Add Chinese translation for `docs/zh/docs/deployment/concepts.md`. PR [#10282](https://github.com/tiangolo/fastapi/pull/10282) by [@xzmeng](https://github.com/xzmeng). From 8590d0c2ec301d01da68ad3032f9b119d616a644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Fri, 2 Feb 2024 20:52:13 +0100 Subject: [PATCH 1864/1881] =?UTF-8?q?=F0=9F=91=A5=20Update=20FastAPI=20Peo?= =?UTF-8?q?ple=20(#11074)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/data/github_sponsors.yml | 706 +++++++++++++++---------------- docs/en/data/people.yml | 210 +++++---- 2 files changed, 464 insertions(+), 452 deletions(-) diff --git a/docs/en/data/github_sponsors.yml b/docs/en/data/github_sponsors.yml index 713f229cf..259a67f8f 100644 --- a/docs/en/data/github_sponsors.yml +++ b/docs/en/data/github_sponsors.yml @@ -1,44 +1,35 @@ sponsors: -- - login: scalar - avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 - url: https://github.com/scalar - - login: codacy - avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 - url: https://github.com/codacy - - login: bump-sh +- - login: bump-sh avatarUrl: https://avatars.githubusercontent.com/u/33217836?v=4 url: https://github.com/bump-sh - login: Alek99 avatarUrl: https://avatars.githubusercontent.com/u/38776361?u=bd6c163fe787c2de1a26c881598e54b67e2482dd&v=4 url: https://github.com/Alek99 - - login: cryptapi - avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 - url: https://github.com/cryptapi - login: porter-dev avatarUrl: https://avatars.githubusercontent.com/u/62078005?v=4 url: https://github.com/porter-dev - login: andrew-propelauth avatarUrl: https://avatars.githubusercontent.com/u/89474256?u=1188c27cb744bbec36447a2cfd4453126b2ddb5c&v=4 url: https://github.com/andrew-propelauth -- - login: nihpo - avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 - url: https://github.com/nihpo - - login: ObliviousAI + - login: zanfaruqui + avatarUrl: https://avatars.githubusercontent.com/u/104461687?v=4 + url: https://github.com/zanfaruqui + - login: cryptapi + avatarUrl: https://avatars.githubusercontent.com/u/44925437?u=61369138589bc7fee6c417f3fbd50fbd38286cc4&v=4 + url: https://github.com/cryptapi + - login: codacy + avatarUrl: https://avatars.githubusercontent.com/u/1834093?v=4 + url: https://github.com/codacy + - login: scalar + avatarUrl: https://avatars.githubusercontent.com/u/301879?v=4 + url: https://github.com/scalar +- - login: ObliviousAI avatarUrl: https://avatars.githubusercontent.com/u/65656077?v=4 url: https://github.com/ObliviousAI -- - login: mikeckennedy - avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=ce6165b799ea3164cb6f5ff54ea08042057442af&v=4 - url: https://github.com/mikeckennedy - - login: ndimares - avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 - url: https://github.com/ndimares - - login: deta - avatarUrl: https://avatars.githubusercontent.com/u/47275976?v=4 - url: https://github.com/deta - - login: deepset-ai - avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 - url: https://github.com/deepset-ai - - login: databento + - login: nihpo + avatarUrl: https://avatars.githubusercontent.com/u/1841030?u=0264956d7580f7e46687a762a7baa629f84cf97c&v=4 + url: https://github.com/nihpo +- - login: databento avatarUrl: https://avatars.githubusercontent.com/u/64141749?v=4 url: https://github.com/databento - login: svix @@ -47,10 +38,16 @@ sponsors: - login: VincentParedes avatarUrl: https://avatars.githubusercontent.com/u/103889729?v=4 url: https://github.com/VincentParedes -- - login: acsone - avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 - url: https://github.com/acsone - - login: takashi-yoneya + - login: deepset-ai + avatarUrl: https://avatars.githubusercontent.com/u/51827949?v=4 + url: https://github.com/deepset-ai + - login: mikeckennedy + avatarUrl: https://avatars.githubusercontent.com/u/2035561?u=ce6165b799ea3164cb6f5ff54ea08042057442af&v=4 + url: https://github.com/mikeckennedy + - login: ndimares + avatarUrl: https://avatars.githubusercontent.com/u/6267663?u=cfb27efde7a7212be8142abb6c058a1aeadb41b1&v=4 + url: https://github.com/ndimares +- - login: takashi-yoneya avatarUrl: https://avatars.githubusercontent.com/u/33813153?u=2d0522bceba0b8b69adf1f2db866503bd96f944e&v=4 url: https://github.com/takashi-yoneya - login: xoflare @@ -65,144 +62,72 @@ sponsors: - login: jina-ai avatarUrl: https://avatars.githubusercontent.com/u/60539444?v=4 url: https://github.com/jina-ai -- - login: Trivie + - login: acsone + avatarUrl: https://avatars.githubusercontent.com/u/7601056?v=4 + url: https://github.com/acsone +- - login: FOSS-Community + avatarUrl: https://avatars.githubusercontent.com/u/103304813?v=4 + url: https://github.com/FOSS-Community + - login: Trivie avatarUrl: https://avatars.githubusercontent.com/u/8161763?v=4 url: https://github.com/Trivie -- - login: birkjernstrom - avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 - url: https://github.com/birkjernstrom - - login: yasyf - avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 - url: https://github.com/yasyf - - login: AccentDesign - avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 - url: https://github.com/AccentDesign - - login: americanair +- - login: americanair avatarUrl: https://avatars.githubusercontent.com/u/12281813?v=4 url: https://github.com/americanair + - login: 84adam + avatarUrl: https://avatars.githubusercontent.com/u/13172004?u=293f3cc6bb7e6f6ecfcdd64489a3202468321254&v=4 + url: https://github.com/84adam + - login: CanoaPBC + avatarUrl: https://avatars.githubusercontent.com/u/64223768?v=4 + url: https://github.com/CanoaPBC - login: mainframeindustries avatarUrl: https://avatars.githubusercontent.com/u/55092103?v=4 url: https://github.com/mainframeindustries - login: doseiai avatarUrl: https://avatars.githubusercontent.com/u/57115726?v=4 url: https://github.com/doseiai - - login: CanoaPBC - avatarUrl: https://avatars.githubusercontent.com/u/64223768?v=4 - url: https://github.com/CanoaPBC -- - login: povilasb - avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 - url: https://github.com/povilasb - - login: primer-io + - login: AccentDesign + avatarUrl: https://avatars.githubusercontent.com/u/2429332?v=4 + url: https://github.com/AccentDesign + - login: birkjernstrom + avatarUrl: https://avatars.githubusercontent.com/u/281715?u=4be14b43f76b4bd497b1941309bb390250b405e6&v=4 + url: https://github.com/birkjernstrom + - login: yasyf + avatarUrl: https://avatars.githubusercontent.com/u/709645?u=f36736b3c6a85f578886ecc42a740e7b436e7a01&v=4 + url: https://github.com/yasyf +- - login: primer-io avatarUrl: https://avatars.githubusercontent.com/u/62146168?v=4 url: https://github.com/primer-io + - login: povilasb + avatarUrl: https://avatars.githubusercontent.com/u/1213442?u=b11f58ed6ceea6e8297c9b310030478ebdac894d&v=4 + url: https://github.com/povilasb - - login: upciti avatarUrl: https://avatars.githubusercontent.com/u/43346262?v=4 url: https://github.com/upciti -- - login: Kludex - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex - - login: samuelcolvin +- - login: samuelcolvin avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin - - login: koconder - avatarUrl: https://avatars.githubusercontent.com/u/25068?u=582657b23622aaa3dfe68bd028a780f272f456fa&v=4 - url: https://github.com/koconder - - login: jefftriplett - avatarUrl: https://avatars.githubusercontent.com/u/50527?u=af1ddfd50f6afd6d99f333ba2ac8d0a5b245ea74&v=4 - url: https://github.com/jefftriplett - - login: jstanden - avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 - url: https://github.com/jstanden - - login: andreaso - avatarUrl: https://avatars.githubusercontent.com/u/285964?u=837265cc7562c0685f25b2d81cd9de0434fe107c&v=4 - url: https://github.com/andreaso - - login: pamelafox - avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 - url: https://github.com/pamelafox - - login: ericof - avatarUrl: https://avatars.githubusercontent.com/u/306014?u=cf7c8733620397e6584a451505581c01c5d842d7&v=4 - url: https://github.com/ericof - - login: wshayes - avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 - url: https://github.com/wshayes - - login: koxudaxi - avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 - url: https://github.com/koxudaxi - - login: falkben - avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 - url: https://github.com/falkben - - login: mintuhouse - avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 - url: https://github.com/mintuhouse - - login: tcsmith - avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 - url: https://github.com/tcsmith - - login: mickaelandrieu - avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 - url: https://github.com/mickaelandrieu - - login: knallgelb - avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 - url: https://github.com/knallgelb - - login: johannquerne - avatarUrl: https://avatars.githubusercontent.com/u/2736484?u=9b3381546a25679913a2b08110e4373c98840821&v=4 - url: https://github.com/johannquerne - - login: Shark009 - avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 - url: https://github.com/Shark009 - - login: dblackrun - avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 - url: https://github.com/dblackrun - - login: zsinx6 - avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 - url: https://github.com/zsinx6 - - login: kennywakeland - avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 - url: https://github.com/kennywakeland - - login: aacayaco - avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 - url: https://github.com/aacayaco - - login: anomaly - avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 - url: https://github.com/anomaly - - login: jgreys - avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=096820d1ef89877d57d0f68e669ead8b0fde84df&v=4 - url: https://github.com/jgreys - - login: jaredtrog - avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 - url: https://github.com/jaredtrog - - login: oliverxchen - avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 - url: https://github.com/oliverxchen - - login: ennui93 - avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 - url: https://github.com/ennui93 - - login: ternaus - avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=fabc8d75c921b3380126adb5a931c5da6e7db04f&v=4 - url: https://github.com/ternaus - - login: eseglem - avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 - url: https://github.com/eseglem - - login: Yaleesa - avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 - url: https://github.com/Yaleesa - - login: iwpnd - avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=c485eefca5c6329600cae63dd35e4f5682ce6924&v=4 - url: https://github.com/iwpnd - - login: FernandoCelmer - avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=d29fff3fd862fda4ca752079f13f32e84c762ea4&v=4 - url: https://github.com/FernandoCelmer - - login: simw - avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 - url: https://github.com/simw - - login: Rehket - avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 - url: https://github.com/Rehket - - login: hiancdtrsnm - avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 - url: https://github.com/hiancdtrsnm - - login: wdwinslow - avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 - url: https://github.com/wdwinslow + - login: Kludex + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex + - login: ehaca + avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 + url: https://github.com/ehaca + - login: timlrx + avatarUrl: https://avatars.githubusercontent.com/u/28362229?u=9a745ca31372ee324af682715ae88ce8522f9094&v=4 + url: https://github.com/timlrx + - login: Leay15 + avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 + url: https://github.com/Leay15 + - login: ygorpontelo + avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 + url: https://github.com/ygorpontelo + - login: ProteinQure + avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 + url: https://github.com/ProteinQure + - login: RafaelWO + avatarUrl: https://avatars.githubusercontent.com/u/38643099?u=56c676f024667ee416dc8b1cdf0c2611b9dc994f&v=4 + url: https://github.com/RafaelWO - login: drcat101 avatarUrl: https://avatars.githubusercontent.com/u/11951946?u=e714b957185b8cf3d301cced7fc3ad2842122c6a&v=4 url: https://github.com/drcat101 @@ -230,30 +155,21 @@ sponsors: - login: Filimoa avatarUrl: https://avatars.githubusercontent.com/u/21352040?u=0be845711495bbd7b756e13fcaeb8efc1ebd78ba&v=4 url: https://github.com/Filimoa - - login: ehaca - avatarUrl: https://avatars.githubusercontent.com/u/25950317?u=cec1a3e0643b785288ae8260cc295a85ab344995&v=4 - url: https://github.com/ehaca - - login: timlrx - avatarUrl: https://avatars.githubusercontent.com/u/28362229?u=9a745ca31372ee324af682715ae88ce8522f9094&v=4 - url: https://github.com/timlrx - - login: BrettskiPy - avatarUrl: https://avatars.githubusercontent.com/u/30988215?u=d8a94a67e140d5ee5427724b292cc52d8827087a&v=4 - url: https://github.com/BrettskiPy - - login: Leay15 - avatarUrl: https://avatars.githubusercontent.com/u/32212558?u=c4aa9c1737e515959382a5515381757b1fd86c53&v=4 - url: https://github.com/Leay15 - - login: ygorpontelo - avatarUrl: https://avatars.githubusercontent.com/u/32963605?u=35f7103f9c4c4c2589ae5737ee882e9375ef072e&v=4 - url: https://github.com/ygorpontelo - - login: ProteinQure - avatarUrl: https://avatars.githubusercontent.com/u/33707203?v=4 - url: https://github.com/ProteinQure - - login: RafaelWO - avatarUrl: https://avatars.githubusercontent.com/u/38643099?u=56c676f024667ee416dc8b1cdf0c2611b9dc994f&v=4 - url: https://github.com/RafaelWO - - login: arleybri18 - avatarUrl: https://avatars.githubusercontent.com/u/39681546?u=5c028f81324b0e8c73b3c15bc4e7b0218d2ba0c3&v=4 - url: https://github.com/arleybri18 + - login: anthonycepeda + avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=60bdf46240cff8fca482ff0fc07d963fd5e1a27c&v=4 + url: https://github.com/anthonycepeda + - login: patricioperezv + avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 + url: https://github.com/patricioperezv + - login: kaoru0310 + avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 + url: https://github.com/kaoru0310 + - login: DelfinaCare + avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 + url: https://github.com/DelfinaCare + - login: apitally + avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 + url: https://github.com/apitally - login: thenickben avatarUrl: https://avatars.githubusercontent.com/u/40610922?u=1e907d904041b7c91213951a3cb344cd37c14aaf&v=4 url: https://github.com/thenickben @@ -266,9 +182,6 @@ sponsors: - login: dudikbender avatarUrl: https://avatars.githubusercontent.com/u/53487583?u=3a57542938ebfd57579a0111db2b297e606d9681&v=4 url: https://github.com/dudikbender - - login: Amirshox - avatarUrl: https://avatars.githubusercontent.com/u/56707784?u=2a2f8cc243d6f5b29cd63fd2772f7a97aadc6c6b&v=4 - url: https://github.com/Amirshox - login: prodhype avatarUrl: https://avatars.githubusercontent.com/u/60444672?u=3f278cff25ea37ead487d7861d4a984795de819e&v=4 url: https://github.com/prodhype @@ -278,30 +191,228 @@ sponsors: - login: patsatsia avatarUrl: https://avatars.githubusercontent.com/u/61111267?u=3271b85f7a37b479c8d0ae0a235182e83c166edf&v=4 url: https://github.com/patsatsia - - login: anthonycepeda - avatarUrl: https://avatars.githubusercontent.com/u/72019805?u=4252c6b6dc5024af502a823a3ac5e7a03a69963f&v=4 - url: https://github.com/anthonycepeda - - login: patricioperezv - avatarUrl: https://avatars.githubusercontent.com/u/73832292?u=5f471f156e19ee7920e62ae0f4a47b95580e61cf&v=4 - url: https://github.com/patricioperezv - - login: kaoru0310 - avatarUrl: https://avatars.githubusercontent.com/u/80977929?u=1b61d10142b490e56af932ddf08a390fae8ee94f&v=4 - url: https://github.com/kaoru0310 - - login: DelfinaCare - avatarUrl: https://avatars.githubusercontent.com/u/83734439?v=4 - url: https://github.com/DelfinaCare - - login: osawa-koki - avatarUrl: https://avatars.githubusercontent.com/u/94336223?u=59c6fe6945bcbbaff87b2a794238671b060620d2&v=4 - url: https://github.com/osawa-koki - - login: apitally - avatarUrl: https://avatars.githubusercontent.com/u/138365043?v=4 - url: https://github.com/apitally + - login: koconder + avatarUrl: https://avatars.githubusercontent.com/u/25068?u=582657b23622aaa3dfe68bd028a780f272f456fa&v=4 + url: https://github.com/koconder + - login: mickaelandrieu + avatarUrl: https://avatars.githubusercontent.com/u/1247388?u=599f6e73e452a9453f2bd91e5c3100750e731ad4&v=4 + url: https://github.com/mickaelandrieu + - login: dodo5522 + avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 + url: https://github.com/dodo5522 + - login: knallgelb + avatarUrl: https://avatars.githubusercontent.com/u/2358812?u=c48cb6362b309d74cbf144bd6ad3aed3eb443e82&v=4 + url: https://github.com/knallgelb + - login: johannquerne + avatarUrl: https://avatars.githubusercontent.com/u/2736484?u=9b3381546a25679913a2b08110e4373c98840821&v=4 + url: https://github.com/johannquerne + - login: Shark009 + avatarUrl: https://avatars.githubusercontent.com/u/3163309?u=0c6f4091b0eda05c44c390466199826e6dc6e431&v=4 + url: https://github.com/Shark009 + - login: dblackrun + avatarUrl: https://avatars.githubusercontent.com/u/3528486?v=4 + url: https://github.com/dblackrun + - login: zsinx6 + avatarUrl: https://avatars.githubusercontent.com/u/3532625?u=ba75a5dc744d1116ccfeaaf30d41cb2fe81fe8dd&v=4 + url: https://github.com/zsinx6 + - login: kennywakeland + avatarUrl: https://avatars.githubusercontent.com/u/3631417?u=7c8f743f1ae325dfadea7c62bbf1abd6a824fc55&v=4 + url: https://github.com/kennywakeland + - login: jstanden + avatarUrl: https://avatars.githubusercontent.com/u/63288?u=c3658d57d2862c607a0e19c2101c3c51876e36ad&v=4 + url: https://github.com/jstanden + - login: andreaso + avatarUrl: https://avatars.githubusercontent.com/u/285964?u=837265cc7562c0685f25b2d81cd9de0434fe107c&v=4 + url: https://github.com/andreaso + - login: pamelafox + avatarUrl: https://avatars.githubusercontent.com/u/297042?v=4 + url: https://github.com/pamelafox + - login: ericof + avatarUrl: https://avatars.githubusercontent.com/u/306014?u=cf7c8733620397e6584a451505581c01c5d842d7&v=4 + url: https://github.com/ericof + - login: wshayes + avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 + url: https://github.com/wshayes + - login: koxudaxi + avatarUrl: https://avatars.githubusercontent.com/u/630670?u=507d8577b4b3670546b449c4c2ccbc5af40d72f7&v=4 + url: https://github.com/koxudaxi + - login: falkben + avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 + url: https://github.com/falkben + - login: mintuhouse + avatarUrl: https://avatars.githubusercontent.com/u/769950?u=ecfbd79a97d33177e0d093ddb088283cf7fe8444&v=4 + url: https://github.com/mintuhouse + - login: tcsmith + avatarUrl: https://avatars.githubusercontent.com/u/989034?u=7d8d741552b3279e8f4d3878679823a705a46f8f&v=4 + url: https://github.com/tcsmith + - login: aacayaco + avatarUrl: https://avatars.githubusercontent.com/u/3634801?u=eaadda178c964178fcb64886f6c732172c8f8219&v=4 + url: https://github.com/aacayaco + - login: Rehket + avatarUrl: https://avatars.githubusercontent.com/u/7015688?u=3afb0ba200feebbc7f958950e92db34df2a3c172&v=4 + url: https://github.com/Rehket + - login: hiancdtrsnm + avatarUrl: https://avatars.githubusercontent.com/u/7343177?v=4 + url: https://github.com/hiancdtrsnm + - login: TrevorBenson + avatarUrl: https://avatars.githubusercontent.com/u/9167887?u=afdd1766fdb79e04e59094cc6a54cd011ee7f686&v=4 + url: https://github.com/TrevorBenson + - login: pkwarts + avatarUrl: https://avatars.githubusercontent.com/u/10128250?u=151b92c2be8baff34f366cfc7ecf2800867f5e9f&v=4 + url: https://github.com/pkwarts + - login: wdwinslow + avatarUrl: https://avatars.githubusercontent.com/u/11562137?u=dc01daafb354135603a263729e3d26d939c0c452&v=4 + url: https://github.com/wdwinslow + - login: anomaly + avatarUrl: https://avatars.githubusercontent.com/u/3654837?v=4 + url: https://github.com/anomaly + - login: jgreys + avatarUrl: https://avatars.githubusercontent.com/u/4136890?u=096820d1ef89877d57d0f68e669ead8b0fde84df&v=4 + url: https://github.com/jgreys + - login: jaredtrog + avatarUrl: https://avatars.githubusercontent.com/u/4381365?v=4 + url: https://github.com/jaredtrog + - login: oliverxchen + avatarUrl: https://avatars.githubusercontent.com/u/4471774?u=534191f25e32eeaadda22dfab4b0a428733d5489&v=4 + url: https://github.com/oliverxchen + - login: ennui93 + avatarUrl: https://avatars.githubusercontent.com/u/5300907?u=5b5452725ddb391b2caaebf34e05aba873591c3a&v=4 + url: https://github.com/ennui93 + - login: ternaus + avatarUrl: https://avatars.githubusercontent.com/u/5481618?u=fabc8d75c921b3380126adb5a931c5da6e7db04f&v=4 + url: https://github.com/ternaus + - login: eseglem + avatarUrl: https://avatars.githubusercontent.com/u/5920492?u=208d419cf667b8ac594c82a8db01932c7e50d057&v=4 + url: https://github.com/eseglem + - login: Yaleesa + avatarUrl: https://avatars.githubusercontent.com/u/6135475?v=4 + url: https://github.com/Yaleesa + - login: iwpnd + avatarUrl: https://avatars.githubusercontent.com/u/6152183?u=f4ef76a069858f0f37c8737cada5c2cfa9c538b9&v=4 + url: https://github.com/iwpnd + - login: FernandoCelmer + avatarUrl: https://avatars.githubusercontent.com/u/6262214?u=d29fff3fd862fda4ca752079f13f32e84c762ea4&v=4 + url: https://github.com/FernandoCelmer + - login: simw + avatarUrl: https://avatars.githubusercontent.com/u/6322526?v=4 + url: https://github.com/simw - - login: getsentry avatarUrl: https://avatars.githubusercontent.com/u/1396951?v=4 url: https://github.com/getsentry - - login: pawamoy avatarUrl: https://avatars.githubusercontent.com/u/3999221?u=b030e4c89df2f3a36bc4710b925bdeb6745c9856&v=4 url: https://github.com/pawamoy + - login: hoenie-ams + avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 + url: https://github.com/hoenie-ams + - login: joerambo + avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 + url: https://github.com/joerambo + - login: rlnchow + avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 + url: https://github.com/rlnchow + - login: HosamAlmoghraby + avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 + url: https://github.com/HosamAlmoghraby + - login: dvlpjrs + avatarUrl: https://avatars.githubusercontent.com/u/32254642?u=fbd6ad0324d4f1eb6231cf775be1c7bd4404e961&v=4 + url: https://github.com/dvlpjrs + - login: engineerjoe440 + avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 + url: https://github.com/engineerjoe440 + - login: bnkc + avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=fa1dc8db3e920cf5c5636b97180a6f811fa01aaf&v=4 + url: https://github.com/bnkc + - login: curegit + avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 + url: https://github.com/curegit + - login: JimFawkes + avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 + url: https://github.com/JimFawkes + - login: artempronevskiy + avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 + url: https://github.com/artempronevskiy + - login: TheR1D + avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 + url: https://github.com/TheR1D + - login: joshuatz + avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 + url: https://github.com/joshuatz + - login: jangia + avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 + url: https://github.com/jangia + - login: shuheng-liu + avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 + url: https://github.com/shuheng-liu + - login: pers0n4 + avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 + url: https://github.com/pers0n4 + - login: kxzk + avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 + url: https://github.com/kxzk + - login: SebTota + avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 + url: https://github.com/SebTota + - login: nisutec + avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 + url: https://github.com/nisutec + - login: 0417taehyun + avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 + url: https://github.com/0417taehyun + - login: fernandosmither + avatarUrl: https://avatars.githubusercontent.com/u/66154723?u=a76a037b5d674938a75d2cff862fb6dfd63ec214&v=4 + url: https://github.com/fernandosmither + - login: romabozhanovgithub + avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 + url: https://github.com/romabozhanovgithub + - login: PelicanQ + avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 + url: https://github.com/PelicanQ + - login: tim-habitat + avatarUrl: https://avatars.githubusercontent.com/u/86600518?u=7389dd77fe6a0eb8d13933356120b7d2b32d7bb4&v=4 + url: https://github.com/tim-habitat + - login: jugeeem + avatarUrl: https://avatars.githubusercontent.com/u/116043716?u=ae590d79c38ac79c91b9c5caa6887d061e865a3d&v=4 + url: https://github.com/jugeeem + - login: tahmarrrr23 + avatarUrl: https://avatars.githubusercontent.com/u/138208610?u=465a46b0ff72a74252d3e3a71ac7d2f1919cda28&v=4 + url: https://github.com/tahmarrrr23 + - login: kristiangronberg + avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 + url: https://github.com/kristiangronberg + - login: leonardo-holguin + avatarUrl: https://avatars.githubusercontent.com/u/43093055?u=b59013d52fb6c4e0954aaaabc0882bd844985b38&v=4 + url: https://github.com/leonardo-holguin + - login: arrrrrmin + avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4 + url: https://github.com/arrrrrmin + - login: ArtyomVancyan + avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 + url: https://github.com/ArtyomVancyan + - login: hgalytoby + avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 + url: https://github.com/hgalytoby + - login: conservative-dude + avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 + url: https://github.com/conservative-dude + - login: miguelgr + avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 + url: https://github.com/miguelgr + - login: WillHogan + avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 + url: https://github.com/WillHogan + - login: my3 + avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 + url: https://github.com/my3 + - login: leobiscassi + avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 + url: https://github.com/leobiscassi + - login: cbonoz + avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 + url: https://github.com/cbonoz + - login: anthonycorletti + avatarUrl: https://avatars.githubusercontent.com/u/3477132?u=dfe51d2080fbd3fee81e05911cd8d50da9dcc709&v=4 + url: https://github.com/anthonycorletti - login: ddanier avatarUrl: https://avatars.githubusercontent.com/u/113563?u=ed1dc79de72f93bd78581f88ebc6952b62f472da&v=4 url: https://github.com/ddanier @@ -323,57 +434,9 @@ sponsors: - login: securancy avatarUrl: https://avatars.githubusercontent.com/u/606673?v=4 url: https://github.com/securancy - - login: natehouk - avatarUrl: https://avatars.githubusercontent.com/u/805439?u=d8e4be629dc5d7efae7146157e41ee0bd129d9bc&v=4 - url: https://github.com/natehouk - login: browniebroke avatarUrl: https://avatars.githubusercontent.com/u/861044?u=5abfca5588f3e906b31583d7ee62f6de4b68aa24&v=4 url: https://github.com/browniebroke - - login: dodo5522 - avatarUrl: https://avatars.githubusercontent.com/u/1362607?u=9bf1e0e520cccc547c046610c468ce6115bbcf9f&v=4 - url: https://github.com/dodo5522 - - login: miguelgr - avatarUrl: https://avatars.githubusercontent.com/u/1484589?u=54556072b8136efa12ae3b6902032ea2a39ace4b&v=4 - url: https://github.com/miguelgr - - login: WillHogan - avatarUrl: https://avatars.githubusercontent.com/u/1661551?u=7036c064cf29781470573865264ec8e60b6b809f&v=4 - url: https://github.com/WillHogan - - login: my3 - avatarUrl: https://avatars.githubusercontent.com/u/1825270?v=4 - url: https://github.com/my3 - - login: leobiscassi - avatarUrl: https://avatars.githubusercontent.com/u/1977418?u=f9f82445a847ab479bd7223debd677fcac6c49a0&v=4 - url: https://github.com/leobiscassi - - login: cbonoz - avatarUrl: https://avatars.githubusercontent.com/u/2351087?u=fd3e8030b2cc9fbfbb54a65e9890c548a016f58b&v=4 - url: https://github.com/cbonoz - - login: anthonycorletti - avatarUrl: https://avatars.githubusercontent.com/u/3477132?v=4 - url: https://github.com/anthonycorletti - - login: Alisa-lisa - avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 - url: https://github.com/Alisa-lisa - - login: gowikel - avatarUrl: https://avatars.githubusercontent.com/u/4339072?u=0e325ffcc539c38f89d9aa876bd87f9ec06ce0ee&v=4 - url: https://github.com/gowikel - - login: danielunderwood - avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 - url: https://github.com/danielunderwood - - login: yuawn - avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4 - url: https://github.com/yuawn - - login: sdevkota - avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 - url: https://github.com/sdevkota - - login: unredundant - avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4 - url: https://github.com/unredundant - - login: Baghdady92 - avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 - url: https://github.com/Baghdady92 - - login: jakeecolution - avatarUrl: https://avatars.githubusercontent.com/u/5884696?u=4a7c7883fb064b593b50cb6697b54687e6f7aafe&v=4 - url: https://github.com/jakeecolution - login: KentShikama avatarUrl: https://avatars.githubusercontent.com/u/6329898?u=8b236810db9b96333230430837e1f021f9246da1&v=4 url: https://github.com/KentShikama @@ -416,123 +479,48 @@ sponsors: - login: dzoladz avatarUrl: https://avatars.githubusercontent.com/u/10561752?u=5ee314d54aa79592c18566827ad8914debd5630d&v=4 url: https://github.com/dzoladz - - login: JimFawkes - avatarUrl: https://avatars.githubusercontent.com/u/12075115?u=dc58ecfd064d72887c34bf500ddfd52592509acd&v=4 - url: https://github.com/JimFawkes - - login: artempronevskiy - avatarUrl: https://avatars.githubusercontent.com/u/12235104?u=03df6e1e55c9c6fe5d230adabb8dd7d43d8bbe8f&v=4 - url: https://github.com/artempronevskiy - - login: giuliano-oliveira - avatarUrl: https://avatars.githubusercontent.com/u/13181797?u=0ef2dfbf7fc9a9726d45c21d32b5d1038a174870&v=4 - url: https://github.com/giuliano-oliveira - - login: TheR1D - avatarUrl: https://avatars.githubusercontent.com/u/16740832?u=b0dfdbdb27b79729430c71c6128962f77b7b53f7&v=4 - url: https://github.com/TheR1D - - login: joshuatz - avatarUrl: https://avatars.githubusercontent.com/u/17817563?u=f1bf05b690d1fc164218f0b420cdd3acb7913e21&v=4 - url: https://github.com/joshuatz - - login: jangia - avatarUrl: https://avatars.githubusercontent.com/u/17927101?u=9261b9bb0c3e3bb1ecba43e8915dc58d8c9a077e&v=4 - url: https://github.com/jangia - - login: shuheng-liu - avatarUrl: https://avatars.githubusercontent.com/u/22414322?u=813c45f30786c6b511b21a661def025d8f7b609e&v=4 - url: https://github.com/shuheng-liu - - login: salahelfarissi - avatarUrl: https://avatars.githubusercontent.com/u/23387408?u=73222a4be627c1a3dee9736e0da22224eccdc8f6&v=4 - url: https://github.com/salahelfarissi - - login: pers0n4 - avatarUrl: https://avatars.githubusercontent.com/u/24864600?u=f211a13a7b572cbbd7779b9c8d8cb428cc7ba07e&v=4 - url: https://github.com/pers0n4 - - login: kxzk - avatarUrl: https://avatars.githubusercontent.com/u/25046261?u=e185e58080090f9e678192cd214a14b14a2b232b&v=4 - url: https://github.com/kxzk - - login: SebTota - avatarUrl: https://avatars.githubusercontent.com/u/25122511?v=4 - url: https://github.com/SebTota - - login: nisutec - avatarUrl: https://avatars.githubusercontent.com/u/25281462?u=e562484c451fdfc59053163f64405f8eb262b8b0&v=4 - url: https://github.com/nisutec - - login: hoenie-ams - avatarUrl: https://avatars.githubusercontent.com/u/25708487?u=cda07434f0509ac728d9edf5e681117c0f6b818b&v=4 - url: https://github.com/hoenie-ams - - login: joerambo - avatarUrl: https://avatars.githubusercontent.com/u/26282974?v=4 - url: https://github.com/joerambo - - login: rlnchow - avatarUrl: https://avatars.githubusercontent.com/u/28018479?u=a93ca9cf1422b9ece155784a72d5f2fdbce7adff&v=4 - url: https://github.com/rlnchow - - login: White-Mask - avatarUrl: https://avatars.githubusercontent.com/u/31826970?u=8625355dc25ddf9c85a8b2b0b9932826c4c8f44c&v=4 - url: https://github.com/White-Mask - - login: HosamAlmoghraby - avatarUrl: https://avatars.githubusercontent.com/u/32025281?u=aa1b09feabccbf9dc506b81c71155f32d126cefa&v=4 - url: https://github.com/HosamAlmoghraby - - login: engineerjoe440 - avatarUrl: https://avatars.githubusercontent.com/u/33275230?u=eb223cad27017bb1e936ee9b429b450d092d0236&v=4 - url: https://github.com/engineerjoe440 - - login: bnkc - avatarUrl: https://avatars.githubusercontent.com/u/34930566?u=fa1dc8db3e920cf5c5636b97180a6f811fa01aaf&v=4 - url: https://github.com/bnkc - - login: declon - avatarUrl: https://avatars.githubusercontent.com/u/36180226?v=4 - url: https://github.com/declon - - login: curegit - avatarUrl: https://avatars.githubusercontent.com/u/37978051?u=1733c322079118c0cdc573c03d92813f50a9faec&v=4 - url: https://github.com/curegit - - login: kristiangronberg - avatarUrl: https://avatars.githubusercontent.com/u/42678548?v=4 - url: https://github.com/kristiangronberg - - login: arrrrrmin - avatarUrl: https://avatars.githubusercontent.com/u/43553423?u=36a3880a6eb29309c19e6cadbb173bafbe91deb1&v=4 - url: https://github.com/arrrrrmin - - login: ArtyomVancyan - avatarUrl: https://avatars.githubusercontent.com/u/44609997?v=4 - url: https://github.com/ArtyomVancyan - - login: hgalytoby - avatarUrl: https://avatars.githubusercontent.com/u/50397689?u=f4888c2c54929bd86eed0d3971d09fcb306e5088&v=4 - url: https://github.com/hgalytoby - - login: conservative-dude - avatarUrl: https://avatars.githubusercontent.com/u/55538308?u=f250c44942ea6e73a6bd90739b381c470c192c11&v=4 - url: https://github.com/conservative-dude - - login: Calesi19 - avatarUrl: https://avatars.githubusercontent.com/u/58052598?u=273d4fc364c004602c93dd6adeaf5cc915b93cd2&v=4 - url: https://github.com/Calesi19 - - login: 0417taehyun - avatarUrl: https://avatars.githubusercontent.com/u/63915557?u=47debaa860fd52c9b98c97ef357ddcec3b3fb399&v=4 - url: https://github.com/0417taehyun - - login: fernandosmither - avatarUrl: https://avatars.githubusercontent.com/u/66154723?u=a76a037b5d674938a75d2cff862fb6dfd63ec214&v=4 - url: https://github.com/fernandosmither - - login: romabozhanovgithub - avatarUrl: https://avatars.githubusercontent.com/u/67696229?u=e4b921eef096415300425aca249348f8abb78ad7&v=4 - url: https://github.com/romabozhanovgithub - - login: mbukeRepo - avatarUrl: https://avatars.githubusercontent.com/u/70356088?u=d2eb23e2b222a3b316c4183b05a3236b32819dc2&v=4 - url: https://github.com/mbukeRepo - - login: adriiamontoto - avatarUrl: https://avatars.githubusercontent.com/u/75563346?u=eeb1350b82ecb4d96592f9b6cd1a16870c355e38&v=4 - url: https://github.com/adriiamontoto - - login: PelicanQ - avatarUrl: https://avatars.githubusercontent.com/u/77930606?v=4 - url: https://github.com/PelicanQ - - login: tahmarrrr23 - avatarUrl: https://avatars.githubusercontent.com/u/138208610?u=465a46b0ff72a74252d3e3a71ac7d2f1919cda28&v=4 - url: https://github.com/tahmarrrr23 -- - login: ssbarnea - avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 - url: https://github.com/ssbarnea - - login: Patechoc - avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 - url: https://github.com/Patechoc - - login: DazzyMlv - avatarUrl: https://avatars.githubusercontent.com/u/23006212?u=df429da52882b0432e5ac81d4f1b489abc86433c&v=4 - url: https://github.com/DazzyMlv + - login: Alisa-lisa + avatarUrl: https://avatars.githubusercontent.com/u/4137964?u=e7e393504f554f4ff15863a1e01a5746863ef9ce&v=4 + url: https://github.com/Alisa-lisa + - login: gowikel + avatarUrl: https://avatars.githubusercontent.com/u/4339072?u=0e325ffcc539c38f89d9aa876bd87f9ec06ce0ee&v=4 + url: https://github.com/gowikel + - login: danielunderwood + avatarUrl: https://avatars.githubusercontent.com/u/4472301?v=4 + url: https://github.com/danielunderwood + - login: rangulvers + avatarUrl: https://avatars.githubusercontent.com/u/5235430?v=4 + url: https://github.com/rangulvers + - login: sdevkota + avatarUrl: https://avatars.githubusercontent.com/u/5250987?u=4ed9a120c89805a8aefda1cbdc0cf6512e64d1b4&v=4 + url: https://github.com/sdevkota + - login: brizzbuzz + avatarUrl: https://avatars.githubusercontent.com/u/5607577?u=1ffbf39f5bb8736b75c0d235707d6e8f803725c5&v=4 + url: https://github.com/brizzbuzz + - login: Baghdady92 + avatarUrl: https://avatars.githubusercontent.com/u/5708590?v=4 + url: https://github.com/Baghdady92 + - login: jakeecolution + avatarUrl: https://avatars.githubusercontent.com/u/5884696?u=4a7c7883fb064b593b50cb6697b54687e6f7aafe&v=4 + url: https://github.com/jakeecolution +- - login: danburonline + avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=94935cccfbec58083ab1e535212d54f1bf2c978a&v=4 + url: https://github.com/danburonline + - login: Cxx-mlr + avatarUrl: https://avatars.githubusercontent.com/u/37257545?u=7f6296d7bfd4c58e2918576d79e7d2250987e6a4&v=4 + url: https://github.com/Cxx-mlr - login: sadikkuzu avatarUrl: https://avatars.githubusercontent.com/u/23168063?u=d179c06bb9f65c4167fcab118526819f8e0dac17&v=4 url: https://github.com/sadikkuzu - - login: danburonline - avatarUrl: https://avatars.githubusercontent.com/u/34251194?u=94935cccfbec58083ab1e535212d54f1bf2c978a&v=4 - url: https://github.com/danburonline - login: rwxd avatarUrl: https://avatars.githubusercontent.com/u/40308458?u=cd04a39e3655923be4f25c2ba8a5a07b3da3230a&v=4 url: https://github.com/rwxd + - login: Patechoc + avatarUrl: https://avatars.githubusercontent.com/u/2376641?u=23b49e9eda04f078cb74fa3f93593aa6a57bb138&v=4 + url: https://github.com/Patechoc + - login: ssbarnea + avatarUrl: https://avatars.githubusercontent.com/u/102495?u=b4bf6818deefe59952ac22fec6ed8c76de1b8f7c&v=4 + url: https://github.com/ssbarnea + - login: yuawn + avatarUrl: https://avatars.githubusercontent.com/u/5111198?u=5315576f3fe1a70fd2d0f02181588f4eea5d353d&v=4 + url: https://github.com/yuawn diff --git a/docs/en/data/people.yml b/docs/en/data/people.yml index 2877e7938..b21d989f2 100644 --- a/docs/en/data/people.yml +++ b/docs/en/data/people.yml @@ -1,12 +1,12 @@ maintainers: - login: tiangolo - answers: 1870 - prs: 523 + answers: 1874 + prs: 544 avatarUrl: https://avatars.githubusercontent.com/u/1326112?u=740f11212a731f56798f558ceddb0bd07642afa7&v=4 url: https://github.com/tiangolo experts: - login: Kludex - count: 522 + count: 572 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: dmontagu @@ -22,7 +22,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/62724709?u=bba5af018423a2858d49309bed2a899bb5c34ac5&v=4 url: https://github.com/ycd - login: jgould22 - count: 205 + count: 212 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 - login: JarroVGIT @@ -34,7 +34,7 @@ experts: avatarUrl: https://avatars.githubusercontent.com/u/1104190?u=321a2e953e6645a7d09b732786c7a8061e0f8a8b&v=4 url: https://github.com/euri10 - login: iudeen - count: 127 + count: 128 avatarUrl: https://avatars.githubusercontent.com/u/10519440?u=2843b3303282bff8b212dcd4d9d6689452e4470c&v=4 url: https://github.com/iudeen - login: phy25 @@ -45,14 +45,14 @@ experts: count: 83 avatarUrl: https://avatars.githubusercontent.com/u/10202690?u=e6f86f5c0c3026a15d6b51792fa3e532b12f1371&v=4 url: https://github.com/raphaelauv -- login: ArcLightSlavik - count: 71 - avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 - url: https://github.com/ArcLightSlavik - login: ghandic count: 71 avatarUrl: https://avatars.githubusercontent.com/u/23500353?u=e2e1d736f924d9be81e8bfc565b6d8836ba99773&v=4 url: https://github.com/ghandic +- login: ArcLightSlavik + count: 71 + avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 + url: https://github.com/ArcLightSlavik - login: falkben count: 57 avatarUrl: https://avatars.githubusercontent.com/u/653031?u=ad9838e089058c9e5a0bab94c0eec7cc181e0cd0&v=4 @@ -65,6 +65,10 @@ experts: count: 48 avatarUrl: https://avatars.githubusercontent.com/u/37829370?u=da44ca53aefd5c23f346fab8e9fd2e108294c179&v=4 url: https://github.com/yinziyan1206 +- login: acidjunk + count: 46 + avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 + url: https://github.com/acidjunk - login: insomnes count: 45 avatarUrl: https://avatars.githubusercontent.com/u/16958893?u=f8be7088d5076d963984a21f95f44e559192d912&v=4 @@ -73,10 +77,6 @@ experts: count: 45 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 url: https://github.com/adriangb -- login: acidjunk - count: 45 - avatarUrl: https://avatars.githubusercontent.com/u/685002?u=b5094ab4527fc84b006c0ac9ff54367bdebb2267&v=4 - url: https://github.com/acidjunk - login: Dustyposa count: 45 avatarUrl: https://avatars.githubusercontent.com/u/27180793?u=5cf2877f50b3eb2bc55086089a78a36f07042889&v=4 @@ -85,6 +85,10 @@ experts: count: 43 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 url: https://github.com/odiseo0 +- login: n8sty + count: 43 + avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 + url: https://github.com/n8sty - login: frankie567 count: 43 avatarUrl: https://avatars.githubusercontent.com/u/1144727?u=c159fe047727aedecbbeeaa96a1b03ceb9d39add&v=4 @@ -93,10 +97,10 @@ experts: count: 40 avatarUrl: https://avatars.githubusercontent.com/u/11836741?u=8bd5ef7e62fe6a82055e33c4c0e0a7879ff8cfb6&v=4 url: https://github.com/includeamin -- login: n8sty - count: 40 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty +- login: JavierSanchezCastro + count: 39 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: chbndrhnns count: 38 avatarUrl: https://avatars.githubusercontent.com/u/7534547?v=4 @@ -113,10 +117,6 @@ experts: count: 32 avatarUrl: https://avatars.githubusercontent.com/u/41326348?u=ba2fda6b30110411ecbf406d187907e2b420ac19&v=4 url: https://github.com/panla -- login: JavierSanchezCastro - count: 30 - avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 - url: https://github.com/JavierSanchezCastro - login: prostomarkeloff count: 28 avatarUrl: https://avatars.githubusercontent.com/u/28061158?u=72309cc1f2e04e40fa38b29969cb4e9d3f722e7b&v=4 @@ -137,22 +137,22 @@ experts: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/9435877?u=719327b7d2c4c62212456d771bfa7c6b8dbb9eac&v=4 url: https://github.com/SirTelemak +- login: nymous + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 + url: https://github.com/nymous - login: rafsaf count: 21 avatarUrl: https://avatars.githubusercontent.com/u/51059348?u=5fe59a56e1f2f9ccd8005d71752a8276f133ae1a&v=4 url: https://github.com/rafsaf -- login: nymous - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/4216559?u=360a36fb602cded27273cbfc0afc296eece90662&v=4 - url: https://github.com/nymous -- login: nsidnev - count: 20 - avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 - url: https://github.com/nsidnev - login: chris-allnutt count: 20 avatarUrl: https://avatars.githubusercontent.com/u/565544?v=4 url: https://github.com/chris-allnutt +- login: nsidnev + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/22559461?u=a9cc3238217e21dc8796a1a500f01b722adb082c&v=4 + url: https://github.com/nsidnev - login: chrisK824 count: 20 avatarUrl: https://avatars.githubusercontent.com/u/79946379?u=03d85b22d696a58a9603e55fbbbe2de6b0f4face&v=4 @@ -161,6 +161,10 @@ experts: count: 20 avatarUrl: https://avatars.githubusercontent.com/u/100039558?u=e2c672da5a7977fd24d87ce6ab35f8bf5b1ed9fa&v=4 url: https://github.com/ebottos94 +- login: hasansezertasan + count: 18 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan - login: retnikt count: 18 avatarUrl: https://avatars.githubusercontent.com/u/24581770?v=4 @@ -193,48 +197,44 @@ experts: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/26334101?u=071c062d2861d3dd127f6b4a5258cd8ef55d4c50&v=4 url: https://github.com/jonatasoli -- login: ghost - count: 15 - avatarUrl: https://avatars.githubusercontent.com/u/10137?u=b1951d34a583cf12ec0d3b0781ba19be97726318&v=4 - url: https://github.com/ghost last_month_active: -- login: Ventura94 - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/43103937?u=ccb837005aaf212a449c374618c4339089e2f733&v=4 - url: https://github.com/Ventura94 +- login: Kludex + count: 20 + avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 + url: https://github.com/Kludex - login: JavierSanchezCastro - count: 4 + count: 6 avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 url: https://github.com/JavierSanchezCastro - login: jgould22 - count: 3 + count: 6 avatarUrl: https://avatars.githubusercontent.com/u/4335847?u=ed77f67e0bb069084639b24d812dbb2a2b1dc554&v=4 url: https://github.com/jgould22 -- login: Kludex - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 - url: https://github.com/Kludex -- login: n8sty - count: 3 - avatarUrl: https://avatars.githubusercontent.com/u/2964996?v=4 - url: https://github.com/n8sty top_contributors: +- login: jaystone776 + count: 27 + avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 + url: https://github.com/jaystone776 - login: waynerv count: 25 avatarUrl: https://avatars.githubusercontent.com/u/39515546?u=ec35139777597cdbbbddda29bf8b9d4396b429a9&v=4 url: https://github.com/waynerv - login: tokusumi - count: 22 + count: 23 avatarUrl: https://avatars.githubusercontent.com/u/41147016?u=55010621aece725aa702270b54fed829b6a1fe60&v=4 url: https://github.com/tokusumi - login: Kludex - count: 21 + count: 22 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex -- login: jaystone776 - count: 18 - avatarUrl: https://avatars.githubusercontent.com/u/11191137?u=299205a95e9b6817a43144a48b643346a5aac5cc&v=4 - url: https://github.com/jaystone776 +- login: nilslindemann + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann +- login: SwftAlpc + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 + url: https://github.com/SwftAlpc - login: dmontagu count: 17 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 @@ -255,6 +255,22 @@ top_contributors: count: 11 avatarUrl: https://avatars.githubusercontent.com/u/16785985?v=4 url: https://github.com/Smlep +- login: AlertRED + count: 11 + avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 + url: https://github.com/AlertRED +- login: hard-coders + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + url: https://github.com/hard-coders +- login: hasansezertasan + count: 10 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan +- login: xzmeng + count: 9 + avatarUrl: https://avatars.githubusercontent.com/u/40202897?v=4 + url: https://github.com/xzmeng - login: Serrones count: 8 avatarUrl: https://avatars.githubusercontent.com/u/22691749?u=4795b880e13ca33a73e52fc0ef7dc9c60c8fce47&v=4 @@ -267,10 +283,6 @@ top_contributors: count: 7 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 url: https://github.com/RunningIkkyu -- login: hard-coders - count: 7 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders - login: Alexandrhub count: 7 avatarUrl: https://avatars.githubusercontent.com/u/119126536?u=9fc0d48f3307817bafecc5861eb2168401a6cb04&v=4 @@ -283,6 +295,10 @@ top_contributors: count: 6 avatarUrl: https://avatars.githubusercontent.com/u/33462923?u=0fb3d7acb316764616f11e4947faf080e49ad8d9&v=4 url: https://github.com/batlopes +- login: pablocm83 + count: 6 + avatarUrl: https://avatars.githubusercontent.com/u/28315068?u=3310fbb05bb8bfc50d2c48b6cb64ac9ee4a14549&v=4 + url: https://github.com/pablocm83 - login: wshayes count: 5 avatarUrl: https://avatars.githubusercontent.com/u/365303?u=07ca03c5ee811eb0920e633cc3c3db73dbec1aa5&v=4 @@ -291,10 +307,6 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/4039449?u=42eb3b833047c8c4b4f647a031eaef148c16d93f&v=4 url: https://github.com/samuelcolvin -- login: SwftAlpc - count: 5 - avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 - url: https://github.com/SwftAlpc - login: Attsun1031 count: 5 avatarUrl: https://avatars.githubusercontent.com/u/1175560?v=4 @@ -303,10 +315,18 @@ top_contributors: count: 5 avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 url: https://github.com/ComicShrimp +- login: rostik1410 + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 + url: https://github.com/rostik1410 - login: tamtam-fitness count: 5 avatarUrl: https://avatars.githubusercontent.com/u/62091034?u=8da19a6bd3d02f5d6ba30c7247d5b46c98dd1403&v=4 url: https://github.com/tamtam-fitness +- login: KaniKim + count: 5 + avatarUrl: https://avatars.githubusercontent.com/u/19832624?u=4368c4286cc0a122b746f34d4484cef3eed0611f&v=4 + url: https://github.com/KaniKim - login: jekirl count: 4 avatarUrl: https://avatars.githubusercontent.com/u/2546697?u=a027452387d85bd4a14834e19d716c99255fb3b7&v=4 @@ -335,6 +355,10 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/61513630?u=320e43fe4dc7bc6efc64e9b8f325f8075634fd20&v=4 url: https://github.com/lsglucas +- login: BilalAlpaslan + count: 4 + avatarUrl: https://avatars.githubusercontent.com/u/47563997?u=63ed66e304fe8d765762c70587d61d9196e5c82d&v=4 + url: https://github.com/BilalAlpaslan - login: adriangb count: 4 avatarUrl: https://avatars.githubusercontent.com/u/1755071?u=612704256e38d6ac9cbed24f10e4b6ac2da74ecb&v=4 @@ -351,13 +375,13 @@ top_contributors: count: 4 avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 url: https://github.com/ivan-abc -- login: rostik1410 +- login: alejsdev count: 4 - avatarUrl: https://avatars.githubusercontent.com/u/11443899?u=e26a635c2ba220467b308a326a579b8ccf4a8701&v=4 - url: https://github.com/rostik1410 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=1ee3a9fbef27abc9448ef5951350f99c7d76f7af&v=4 + url: https://github.com/alejsdev top_reviewers: - login: Kludex - count: 145 + count: 147 avatarUrl: https://avatars.githubusercontent.com/u/7353520?u=62adc405ef418f4b6c8caa93d3eb8ab107bc4927&v=4 url: https://github.com/Kludex - login: BilalAlpaslan @@ -396,6 +420,10 @@ top_reviewers: count: 41 avatarUrl: https://avatars.githubusercontent.com/u/24587499?u=e772190a051ab0eaa9c8542fcff1892471638f2b&v=4 url: https://github.com/cikay +- login: hasansezertasan + count: 37 + avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 + url: https://github.com/hasansezertasan - login: JarroVGIT count: 34 avatarUrl: https://avatars.githubusercontent.com/u/13659033?u=e8bea32d07a5ef72f7dde3b2079ceb714923ca05&v=4 @@ -404,6 +432,10 @@ top_reviewers: count: 33 avatarUrl: https://avatars.githubusercontent.com/u/1024932?u=b2ea249c6b41ddf98679c8d110d0f67d4a3ebf93&v=4 url: https://github.com/AdrianDeAnda +- login: alejsdev + count: 32 + avatarUrl: https://avatars.githubusercontent.com/u/90076947?u=1ee3a9fbef27abc9448ef5951350f99c7d76f7af&v=4 + url: https://github.com/alejsdev - login: ArcLightSlavik count: 31 avatarUrl: https://avatars.githubusercontent.com/u/31127044?u=b0f2c37142f4b762e41ad65dc49581813422bd71&v=4 @@ -432,14 +464,18 @@ top_reviewers: count: 23 avatarUrl: https://avatars.githubusercontent.com/u/35119617?u=540f30c937a6450812628b9592a1dfe91bbe148e&v=4 url: https://github.com/dmontagu +- login: hard-coders + count: 23 + avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 + url: https://github.com/hard-coders +- login: nilslindemann + count: 21 + avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 + url: https://github.com/nilslindemann - login: rjNemo count: 21 avatarUrl: https://avatars.githubusercontent.com/u/56785022?u=d5c3a02567c8649e146fcfc51b6060ccaf8adef8&v=4 url: https://github.com/rjNemo -- login: hard-coders - count: 21 - avatarUrl: https://avatars.githubusercontent.com/u/9651103?u=95db33927bbff1ed1c07efddeb97ac2ff33068ed&v=4 - url: https://github.com/hard-coders - login: odiseo0 count: 20 avatarUrl: https://avatars.githubusercontent.com/u/87550035?u=241a71f6b7068738b81af3e57f45ffd723538401&v=4 @@ -468,10 +504,6 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/52768429?u=6a3aa15277406520ad37f6236e89466ed44bc5b8&v=4 url: https://github.com/SwftAlpc -- login: nilslindemann - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/6892179?u=1dca6a22195d6cd1ab20737c0e19a4c55d639472&v=4 - url: https://github.com/nilslindemann - login: axel584 count: 16 avatarUrl: https://avatars.githubusercontent.com/u/1334088?u=9667041f5b15dc002b6f9665fda8c0412933ac04&v=4 @@ -484,10 +516,6 @@ top_reviewers: count: 16 avatarUrl: https://avatars.githubusercontent.com/u/87962045?u=08e10fa516e844934f4b3fc7c38b33c61697e4a1&v=4 url: https://github.com/DevDae -- login: hasansezertasan - count: 16 - avatarUrl: https://avatars.githubusercontent.com/u/13135006?u=99f0b0f0fc47e88e8abb337b4447357939ef93e7&v=4 - url: https://github.com/hasansezertasan - login: pedabraham count: 15 avatarUrl: https://avatars.githubusercontent.com/u/16860088?u=abf922a7b920bf8fdb7867d8b43e091f1e796178&v=4 @@ -508,6 +536,10 @@ top_reviewers: count: 13 avatarUrl: https://avatars.githubusercontent.com/u/5357541?u=6428442d875d5d71aaa1bb38bb11c4be1a526bc2&v=4 url: https://github.com/r0b2g1t +- login: Aruelius + count: 13 + avatarUrl: https://avatars.githubusercontent.com/u/25380989?u=574f8cfcda3ea77a3f81884f6b26a97068e36a9d&v=4 + url: https://github.com/Aruelius - login: RunningIkkyu count: 12 avatarUrl: https://avatars.githubusercontent.com/u/31848542?u=494ecc298e3f26197495bb357ad0f57cfd5f7a32&v=4 @@ -516,6 +548,14 @@ top_reviewers: count: 12 avatarUrl: https://avatars.githubusercontent.com/u/36765187?u=c6e0ba571c1ccb6db9d94e62e4b8b5eda811a870&v=4 url: https://github.com/ivan-abc +- login: AlertRED + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/15695000?u=f5a4944c6df443030409c88da7d7fa0b7ead985c&v=4 + url: https://github.com/AlertRED +- login: JavierSanchezCastro + count: 12 + avatarUrl: https://avatars.githubusercontent.com/u/72013291?u=ae5679e6bd971d9d98cd5e76e8683f83642ba950&v=4 + url: https://github.com/JavierSanchezCastro - login: solomein-sv count: 11 avatarUrl: https://avatars.githubusercontent.com/u/46193920?u=789927ee09cfabd752d3bd554fa6baf4850d2777&v=4 @@ -536,19 +576,3 @@ top_reviewers: count: 10 avatarUrl: https://avatars.githubusercontent.com/u/7887703?v=4 url: https://github.com/maoyibo -- login: ComicShrimp - count: 10 - avatarUrl: https://avatars.githubusercontent.com/u/43503750?u=f440bc9062afb3c43b9b9c6cdfdcfe31d58699ef&v=4 - url: https://github.com/ComicShrimp -- login: romashevchenko - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/132477732?v=4 - url: https://github.com/romashevchenko -- login: izaguerreiro - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/2241504?v=4 - url: https://github.com/izaguerreiro -- login: graingert - count: 9 - avatarUrl: https://avatars.githubusercontent.com/u/413772?u=64b77b6aa405c68a9c6bcf45f84257c66eea5f32&v=4 - url: https://github.com/graingert From ebf972349431043170c1f6a4550bd29642f09cd2 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Fri, 2 Feb 2024 19:52:32 +0000 Subject: [PATCH 1865/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= 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 97b777ce9..99b27aa3d 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -147,6 +147,7 @@ hide: ### Internal +* 👥 Update FastAPI People. PR [#11074](https://github.com/tiangolo/fastapi/pull/11074) by [@tiangolo](https://github.com/tiangolo). * 🔧 Update sponsors: add Coherence. PR [#11066](https://github.com/tiangolo/fastapi/pull/11066) by [@tiangolo](https://github.com/tiangolo). * 👷 Upgrade GitHub Action issue-manager. PR [#11056](https://github.com/tiangolo/fastapi/pull/11056) by [@tiangolo](https://github.com/tiangolo). * 🍱 Update sponsors: TalkPython badge. PR [#11052](https://github.com/tiangolo/fastapi/pull/11052) by [@tiangolo](https://github.com/tiangolo). From 9d34ad0ee8a0dfbbcce06f76c2d5d851085024fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 3 Feb 2024 13:32:42 +0100 Subject: [PATCH 1866/1881] Merge pull request from GHSA-qf9m-vfgh-m389 --- pyproject.toml | 2 +- requirements-tests.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e43f35e1..31d9c59b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ Repository = "https://github.com/tiangolo/fastapi" all = [ "httpx >=0.23.0", "jinja2 >=2.11.2", - "python-multipart >=0.0.5", + "python-multipart >=0.0.7", "itsdangerous >=1.1.0", "pyyaml >=5.3.1", "ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0", diff --git a/requirements-tests.txt b/requirements-tests.txt index e1a976c13..a5586c5ce 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -13,7 +13,7 @@ sqlalchemy >=1.3.18,<1.4.43 databases[sqlite] >=0.3.2,<0.7.0 orjson >=3.2.1,<4.0.0 ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0 -python-multipart >=0.0.5,<0.0.7 +python-multipart >=0.0.7,<0.1.0 flask >=1.1.2,<3.0.0 anyio[trio] >=3.2.1,<4.0.0 python-jose[cryptography] >=3.3.0,<4.0.0 From a4de147521bf1546f700aa494d99fce1bed0741b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 3 Feb 2024 13:41:43 +0100 Subject: [PATCH 1867/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 99b27aa3d..bad1686fd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,7 +7,9 @@ hide: ## Latest Changes -* ✏️ Fix Pydantic method name in `docs/en/docs/advanced/path-operation-advanced-configuration.md`. PR [#10826](https://github.com/tiangolo/fastapi/pull/10826) by [@ahmedabdou14](https://github.com/ahmedabdou14). +### Security fixes + +* ⬆️ Upgrade minimum version of `python-multipart` to `>=0.0.7` to fix a vulnerability when using form data with a ReDos attack. You can also simply upgrade `python-multipart`. ### Features @@ -46,6 +48,7 @@ hide: * 📝 Reword in docs, from "have in mind" to "keep in mind". PR [#10376](https://github.com/tiangolo/fastapi/pull/10376) by [@malicious](https://github.com/malicious). * 📝 Add External Link: Talk by Jeny Sadadia. PR [#10265](https://github.com/tiangolo/fastapi/pull/10265) by [@JenySadadia](https://github.com/JenySadadia). * 📝 Add location info to `tutorial/bigger-applications.md`. PR [#10552](https://github.com/tiangolo/fastapi/pull/10552) by [@nilslindemann](https://github.com/nilslindemann). +* ✏️ Fix Pydantic method name in `docs/en/docs/advanced/path-operation-advanced-configuration.md`. PR [#10826](https://github.com/tiangolo/fastapi/pull/10826) by [@ahmedabdou14](https://github.com/ahmedabdou14). ### Translations From 7633d1571cc0c2792b766f67172edb9427c66899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 3 Feb 2024 13:42:30 +0100 Subject: [PATCH 1868/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.?= =?UTF-8?q?109.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index bad1686fd..91627ef62 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.109.1 + ### Security fixes * ⬆️ Upgrade minimum version of `python-multipart` to `>=0.0.7` to fix a vulnerability when using form data with a ReDos attack. You can also simply upgrade `python-multipart`. diff --git a/fastapi/__init__.py b/fastapi/__init__.py index f457fafd4..fedd8b419 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.109.0" +__version__ = "0.109.1" from starlette import status as status From 3f3ee240dd8656962e94e89eceb3838508982068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sat, 3 Feb 2024 13:54:44 +0100 Subject: [PATCH 1869/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 91627ef62..2cbeb74d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -13,6 +13,8 @@ hide: * ⬆️ Upgrade minimum version of `python-multipart` to `>=0.0.7` to fix a vulnerability when using form data with a ReDos attack. You can also simply upgrade `python-multipart`. +Read more in the [advisory: Content-Type Header ReDoS](https://github.com/tiangolo/fastapi/security/advisories/GHSA-qf9m-vfgh-m389). + ### Features * ✨ Include HTTP 205 in status codes with no body. PR [#10969](https://github.com/tiangolo/fastapi/pull/10969) by [@tiangolo](https://github.com/tiangolo). From e239c5637152f9322cbbe56b3b59b19fdd45c324 Mon Sep 17 00:00:00 2001 From: Alper <itsc0508@gmail.com> Date: Sun, 4 Feb 2024 17:28:44 +0300 Subject: [PATCH 1870/1881] :globe_with_meridians: Update Turkish translation for `docs/tr/docs/fastapi-people.md` (#10547) --- docs/tr/docs/fastapi-people.md | 107 +++++++++++++++++---------------- 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/docs/tr/docs/fastapi-people.md b/docs/tr/docs/fastapi-people.md index 3e459036a..4ab43ac00 100644 --- a/docs/tr/docs/fastapi-people.md +++ b/docs/tr/docs/fastapi-people.md @@ -1,10 +1,15 @@ +--- +hide: + - navigation +--- + # FastAPI Topluluğu FastAPI, her kökenden insanı ağırlayan harika bir topluluğa sahip. ## Yazan - Geliştiren -Hey! 👋 +Merhaba! 👋 İşte bu benim: @@ -12,38 +17,37 @@ Hey! 👋 <div class="user-list user-list-center"> {% for user in people.maintainers %} -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Answers: {{ user.answers }}</div><div class="count">Pull Requests: {{ user.prs }}</div></div> +<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Cevaplar: {{ user.answers }}</div><div class="count">Pull Request'ler: {{ user.prs }}</div></div> {% endfor %} </div> {% endif %} -Ben **FastAPI** 'nin yazarı ve geliştiricisiyim. Bununla ilgili daha fazla bilgiyi şurada okuyabilirsiniz: - [FastAPI yardım - yardım al - Yazar ile iletişime geç](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. +Ben **FastAPI**'ın geliştiricisiyim. Bununla ilgili daha fazla bilgiyi şurada okuyabilirsiniz: [FastAPI yardım - yardım al - benimle iletişime geç](help-fastapi.md#connect-with-the-author){.internal-link target=_blank}. -... Burada size harika FastAPI topluluğunu göstermek istiyorum. +...burada size harika FastAPI topluluğunu göstermek istiyorum. --- -**FastAPI** topluluğundan destek alıyor. Ve katkıda bulunanları vurgulamak istiyorum. +**FastAPI**, topluluğundan çok destek alıyor. Ben de onların katkılarını vurgulamak istiyorum. -İşte o mükemmel insanlar: +Bu insanlar: -* [GitHubdaki sorunları (issues) çözmelerinde diğerlerine yardım et](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank}. -* [Pull Requests oluşturun](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. -* Pull Requests 'leri gözden geçirin, [özelliklede çevirileri](contributing.md#translations){.internal-link target=_blank}. +* [GitHubdaki soruları cevaplayarak diğerlerine yardım ediyor](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank}. +* [Pull Request'ler oluşturuyor](help-fastapi.md#create-a-pull-request){.internal-link target=_blank}. +* Pull Request'leri gözden geçiriyorlar, [özellikle çeviriler için bu çok önemli](contributing.md#translations){.internal-link target=_blank}. -Onlara bir alkış. 👏 🙇 +Onları bir alkışlayalım. 👏 🙇 -## Geçen ayın en aktif kullanıcıları +## Geçen Ayın En Aktif Kullanıcıları -Bunlar geçen ay boyunca [GitHub' da başkalarına sorunlarında (issues) en çok yardımcı olan ](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} kullanıcılar ☕ +Geçtiğimiz ay boyunca [GitHub'da diğerlerine en çok yardımcı olan](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} kullanıcılar. ☕ {% if people %} <div class="user-list user-list-center"> {% for user in people.last_month_active %} -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Issues replied: {{ user.count }}</div></div> +<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Cevaplanan soru sayısı: {{ user.count }}</div></div> {% endfor %} </div> @@ -53,57 +57,57 @@ Bunlar geçen ay boyunca [GitHub' da başkalarına sorunlarında (issues) en ço İşte **FastAPI Uzmanları**. 🤓 -Bunlar *tüm zamanlar boyunca* [GitHub' da başkalarına sorunlarında (issues) en çok yardımcı olan](help-fastapi.md#help-others-with-issues-in-github){.internal-link target=_blank} kullanıcılar. +Uzmanlarımız ise *tüm zamanlar boyunca* [GitHub'da insanların sorularına en çok yardımcı olan](help-fastapi.md#help-others-with-questions-in-github){.internal-link target=_blank} insanlar. -Başkalarına yardım ederek uzman olduklarını kanıtladılar. ✨ +Bir çok kullanıcıya yardım ederek uzman olduklarını kanıtladılar! ✨ {% if people %} <div class="user-list user-list-center"> {% for user in people.experts %} -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Issues replied: {{ user.count }}</div></div> +<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Cevaplanan soru sayısı: {{ user.count }}</div></div> {% endfor %} </div> {% endif %} -## En fazla katkıda bulunanlar +## En Fazla Katkıda Bulunanlar -işte **En fazla katkıda bulunanlar**. 👷 +Şimdi ise sıra **en fazla katkıda bulunanlar**da. 👷 -Bu kullanıcılar en çok [Pull Requests oluşturan](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} ve onu kaynak koduna *birleştirenler*. +Bu kullanıcılar en fazla [kaynak koduyla birleştirilen Pull Request'lere](help-fastapi.md#create-a-pull-request){.internal-link target=_blank} sahip! -Kaynak koduna, belgelere, çevirilere vb. katkıda bulundular. 📦 +Kaynak koduna, dökümantasyona, çevirilere ve bir sürü şeye katkıda bulundular. 📦 {% if people %} <div class="user-list user-list-center"> {% for user in people.top_contributors %} -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Pull Requests: {{ user.count }}</div></div> +<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Pull Request sayısı: {{ user.count }}</div></div> {% endfor %} </div> {% endif %} -Çok fazla katkıda bulunan var (binden fazla), hepsini şurda görebilirsin: <a href="https://github.com/tiangolo/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Katkıda Bulunanlar</a>. 👷 +Bunlar dışında katkıda bulunan, yüzden fazla, bir sürü insan var. Hepsini <a href="https://github.com/tiangolo/fastapi/graphs/contributors" class="external-link" target="_blank">FastAPI GitHub Katkıda Bulunanlar</a> sayfasında görebilirsin. 👷 -## En fazla inceleme yapanlar +## En Fazla Değerlendirme Yapanlar -İşte **En fazla inceleme yapanlar**. 🕵️ +İşte **en çok değerlendirme yapanlar**. 🕵️ -### Çeviri için İncelemeler +### Çeviri Değerlendirmeleri -Yalnızca birkaç dil konuşabiliyorum (ve çok da iyi değilim 😅). Bu yüzden döküman çevirilerini [**onaylama yetkisi**](contributing.md#translations){.internal-link target=_blank} siz inceleyenlere aittir. Sizler olmadan diğer birkaç dilde dokümantasyon olmazdı. +Yalnızca birkaç dil konuşabiliyorum (ve çok da iyi değilim 😅). Bu yüzden değerlendirme yapanların da döküman çevirilerini [**onaylama yetkisi**](contributing.md#translations){.internal-link target=_blank} var. Onlar olmasaydı çeşitli dillerde dökümantasyon da olmazdı. --- -**En fazla inceleme yapanlar** 🕵️ kodun, belgelerin ve özellikle **çevirilerin** kalitesini sağlamak için diğerlerinden daha fazla pull requests incelemiştir. +**En fazla değerlendirme yapanlar** 🕵️ kodun, dökümantasyonun ve özellikle **çevirilerin** Pull Request'lerini inceleyerek kalitesinden emin oldular. {% if people %} <div class="user-list user-list-center"> {% for user in people.top_reviewers %} -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Reviews: {{ user.count }}</div></div> +<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a> <div class="count">Değerlendirme sayısı: {{ user.count }}</div></div> {% endfor %} </div> @@ -113,66 +117,67 @@ Yalnızca birkaç dil konuşabiliyorum (ve çok da iyi değilim 😅). Bu yüzde işte **Sponsorlarımız**. 😎 -**FastAPI** ve diğer projelerde çalışmamı destekliyorlar, özellikle de <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub Sponsorları</a>. +Çoğunlukla <a href="https://github.com/sponsors/tiangolo" class="external-link" target="_blank">GitHub Sponsorları</a> aracılığıyla olmak üzere, **FastAPI** ve diğer projelerdeki çalışmalarımı destekliyorlar. + +{% if sponsors %} + +{% if sponsors.gold %} ### Altın Sponsorlar -{% if sponsors %} {% for sponsor in sponsors.gold -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} {% endif %} +{% if sponsors.silver %} + ### Gümüş Sponsorlar -{% if sponsors %} {% for sponsor in sponsors.silver -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} {% endif %} +{% if sponsors.bronze %} + ### Bronz Sponsorlar -{% if sponsors %} {% for sponsor in sponsors.bronze -%} <a href="{{ sponsor.url }}" target="_blank" title="{{ sponsor.title }}"><img src="{{ sponsor.img }}" style="border-radius:15px"></a> {% endfor %} {% endif %} +{% endif %} + ### Bireysel Sponsorlar -{% if people %} -{% if people.sponsors_50 %} +{% if github_sponsors %} +{% for group in github_sponsors.sponsors %} <div class="user-list user-list-center"> -{% for user in people.sponsors_50 %} + +{% for user in group %} +{% if user.login not in sponsors_badge.logins %} <div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a></div> + +{% endif %} {% endfor %} </div> -{% endif %} -{% endif %} - -{% if people %} -<div class="user-list user-list-center"> -{% for user in people.sponsors %} - -<div class="user"><a href="{{ user.url }}" target="_blank"><div class="avatar-wrapper"><img src="{{ user.avatarUrl }}"/></div><div class="title">@{{ user.login }}</div></a></div> {% endfor %} - -</div> {% endif %} -## Veriler hakkında - Teknik detaylar +## Veriler - Teknik detaylar Bu sayfanın temel amacı, topluluğun başkalarına yardım etme çabasını vurgulamaktır. -Özellikle normalde daha az görünür olan ve çoğu durumda daha zahmetli olan, diğerlerine sorunlar konusunda yardımcı olmak ve pull requests'leri gözden geçirmek gibi çabalar dahil. +Özellikle normalde daha az görünür olan ve çoğu durumda daha zahmetli olan, diğerlerine sorularında yardımcı olmak, çevirileri ve Pull Request'leri gözden geçirmek gibi çabalar dahil. -Veriler ayda bir hesaplanır, işte kaynak kodu okuyabilirsin :<a href="https://github.com/tiangolo/fastapi/blob/master/.github/actions/people/app/main.py" class="external-link" target="_blank">source code here</a>. +Veriler ayda bir hesaplanır, <a href="https://github.com/tiangolo/fastapi/blob/master/.github/actions/people/app/main.py" class="external-link" target="_blank">kaynak kodu buradan</a> okuyabilirsin. -Burada sponsorların katkılarını da tekrardan vurgulamak isterim. +Burada sponsorların katkılarını da vurguluyorum. -Ayrıca algoritmayı, bölümleri, eşikleri vb. güncelleme hakkımı da saklı tutarım (her ihtimale karşı 🤷). +Ayrıca algoritmayı, bölümleri, eşikleri vb. güncelleme hakkımı da saklı tutuyorum (her ihtimale karşı 🤷). From 6944ae15a2396ac83ef9f1fa738ac535d24c54d4 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 4 Feb 2024 14:29:04 +0000 Subject: [PATCH 1871/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2cbeb74d2..2dcfac473 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Translations + +* :globe_with_meridians: Update Turkish translation for `docs/tr/docs/fastapi-people.md`. PR [#10547](https://github.com/tiangolo/fastapi/pull/10547) by [@alperiox](https://github.com/alperiox). + ## 0.109.1 ### Security fixes From 739739c9d25d2ae578b540d3a7eb1c446cffde73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 4 Feb 2024 21:56:59 +0100 Subject: [PATCH 1872/1881] =?UTF-8?q?=F0=9F=8D=B1=20Add=20new=20FastAPI=20?= =?UTF-8?q?logo=20(#11090)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/img/favicon.png | Bin 412 -> 5043 bytes docs/en/docs/img/github-social-preview.png | Bin 58136 -> 62301 bytes docs/en/docs/img/github-social-preview.svg | 89 +++++++----------- docs/en/docs/img/icon-transparent-bg.png | Bin 4461 -> 0 bytes docs/en/docs/img/icon-white-bg.png | Bin 5210 -> 0 bytes docs/en/docs/img/icon-white.svg | 33 +++---- .../docs/img/logo-margin/logo-teal-vector.svg | 72 +++++--------- docs/en/docs/img/logo-margin/logo-teal.png | Bin 17680 -> 19843 bytes docs/en/docs/img/logo-margin/logo-teal.svg | 53 ++++++----- .../en/docs/img/logo-margin/logo-white-bg.png | Bin 21502 -> 20841 bytes docs/en/docs/img/logo-teal-vector.svg | 75 ++++++--------- docs/en/docs/img/logo-teal.svg | 59 +++++++----- docs/en/overrides/partials/copyright.html | 11 +++ 13 files changed, 178 insertions(+), 214 deletions(-) mode change 100755 => 100644 docs/en/docs/img/favicon.png delete mode 100644 docs/en/docs/img/icon-transparent-bg.png delete mode 100644 docs/en/docs/img/icon-white-bg.png create mode 100644 docs/en/overrides/partials/copyright.html diff --git a/docs/en/docs/img/favicon.png b/docs/en/docs/img/favicon.png old mode 100755 new mode 100644 index b3dcdd3090e52a3d94affb00a40df2fe8a56d4ba..e5b7c3ada7d093d237711560e03f6a841a4a41b2 GIT binary patch literal 5043 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 JPDHLkV1n0j+0_65 literal 412 zcmV;N0b~A&P)<h;3K|Lk000e1NJLTq001Na001Ni1ONa4O9@aD0004FNkl<Zcmd^= z0Yn2)7=S-v2N6Or5D26Su0Q|;0Ol|TSPKBA)mkH17&U+Z0R*%m1keNxpt!(Bi>PX* z{K4Sv{rBz-z`hUQ_xYc@=P{f3wCEBt;){?j=lD#CN0Dbl@rgY=9LX_2EDp%wKp9cI zMIx3-p{Zs%RFRksEYuLyRV5_kK_@^|uR#rs@lnXoM^snz5JHbR4E7P#D@Y6p1q@!) z!9SL*p^zBsi0UO+L&BQN_xdH98agIITODdKfgP$7A7V2%MCd7*AC`zS&wFg@kqB=} z=nRtsV`@l*p)v5uqQgGIYueH3m>eRA{b(Iyph<ON*0PV#Cc+7lsBirRYDCyf4K472 zC~goVObON8t_T#Yp`?9^7|hYLgzAW5p9~5<V?C6(Pk;`2J;W)Zc!Y%pVz{gB(<Fro z67x>URB#|jeINrI@hBxe*(E%tWQ!K}gpBwmq{{`{%p(7v$68vzm0<1w0000<MNUMn GLSTXsHK*7B diff --git a/docs/en/docs/img/github-social-preview.png b/docs/en/docs/img/github-social-preview.png index a12cbcda560406b5fb5f45b395b94837aecf2b89..4c299c1d6a1ac505e6e997e740a22436f877b5d2 100644 GIT binary patch literal 62301 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 Xck|Gqk}qyd(tOT+b#wNY>QeK+O^kvd literal 58136 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 diff --git a/docs/en/docs/img/github-social-preview.svg b/docs/en/docs/img/github-social-preview.svg index 08450929e..f03a0eefd 100644 --- a/docs/en/docs/img/github-social-preview.svg +++ b/docs/en/docs/img/github-social-preview.svg @@ -1,42 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" id="svg8" version="1.1" viewBox="0 0 338.66665 169.33332" height="169.33333mm" width="338.66666mm" - sodipodi:docname="github-social-preview.svg" - inkscape:version="0.92.3 (2405546, 2018-03-11)" - inkscape:export-filename="/home/user/code/fastapi/docs/img/github-social-preview.png" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96"> - <sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1920" - inkscape:window-height="1025" - id="namedview9" - showgrid="false" - inkscape:zoom="0.52249777" - inkscape:cx="565.37328" - inkscape:cy="403.61034" - inkscape:window-x="0" - inkscape:window-y="27" - inkscape:window-maximized="1" - inkscape:current-layer="svg8" /> + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -47,7 +20,6 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> @@ -57,42 +29,47 @@ width="338.66666" height="169.33333" x="-1.0833333e-05" - y="0.71613133" - inkscape:export-xdpi="96" - inkscape:export-ydpi="96" /> + y="0.71613133" /> <g - transform="matrix(0.73259569,0,0,0.73259569,64.842852,-4.5763945)" - id="layer1"> - <path - style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505" - id="path817" - d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z" - inkscape:connector-curvature="0" /> + id="g2" + transform="matrix(0.73293148,0,0,0.73293148,42.286898,36.073041)"> + <g + id="g2106" + transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)"> + <circle + style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000" + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + cx="964.56165" + cy="-169.22266" + r="33.234192" /> + <path + id="rect1249-6-3-4-4-3-6-6-1-2" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000" + d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> + </g> <text - id="text979" - y="114.91215" - x="52.115433" - style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772" + id="text979-3" + y="59.410606" + x="82.667519" + style="font-style:normal;font-weight:normal;font-size:79.7151px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99288" xml:space="preserve"><tspan - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - y="114.91215" - x="52.115433" - id="tspan977">FastAPI</tspan></text> + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99288" + y="59.410606" + x="82.667519" + id="tspan977-7">FastAPI</tspan></text> </g> <text xml:space="preserve" - style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332" + style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583" x="169.60979" y="119.20409" id="text851"><tspan - sodipodi:role="line" id="tspan849" x="169.60979" y="119.20409" - style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332">High performance, easy to learn,</tspan><tspan - sodipodi:role="line" + style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.264583">High performance, easy to learn,</tspan><tspan x="169.60979" y="132.53661" - style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.26458332" + style="font-style:italic;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Roboto;-inkscape-font-specification:'Roboto Italic';text-align:center;text-anchor:middle;stroke-width:0.264583" id="tspan855">fast to code, ready for production</tspan></text> </svg> diff --git a/docs/en/docs/img/icon-transparent-bg.png b/docs/en/docs/img/icon-transparent-bg.png deleted file mode 100644 index c3481da4ac551129e253450e0fccdc7518dbc31d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4461 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 diff --git a/docs/en/docs/img/icon-white-bg.png b/docs/en/docs/img/icon-white-bg.png deleted file mode 100644 index 00888b5226bf07ace0ddf47f6e3e8b8de5a8603a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5210 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 diff --git a/docs/en/docs/img/icon-white.svg b/docs/en/docs/img/icon-white.svg index cf7c0c7ec..19f0825cc 100644 --- a/docs/en/docs/img/icon-white.svg +++ b/docs/en/docs/img/icon-white.svg @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 6.3499999 6.3499999" height="6.3499999mm" - width="6.3499999mm"> + width="6.3499999mm" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -20,20 +20,15 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> - <g - transform="translate(-87.539286,-84.426191)" - id="layer1"> - <path - id="path815" - d="m 87.539286,84.426191 h 6.35 v 6.35 h -6.35 z" - style="fill:none;stroke-width:0.26458332" /> - <path - style="stroke-width:0.26458332;fill:#ffffff" - id="path817" - d="m 90.714286,84.960649 c -1.457854,0 -2.640542,1.182688 -2.640542,2.640542 0,1.457854 1.182688,2.640542 2.640542,2.640542 1.457854,0 2.640542,-1.182688 2.640542,-2.640542 0,-1.457854 -1.182688,-2.640542 -2.640542,-2.640542 z m -0.137583,4.757209 v -1.656292 h -0.92075 l 1.322916,-2.577042 v 1.656292 h 0.886354 z" /> - </g> + <path + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.0112167;stop-color:#000000" + d="M 3.175 0.53433431 A 2.6405416 2.6320024 0 0 0 0.53433431 3.166215 A 2.6405416 2.6320024 0 0 0 3.175 5.7986125 A 2.6405416 2.6320024 0 0 0 5.8156657 3.166215 A 2.6405416 2.6320024 0 0 0 3.175 0.53433431 z M 2.9925822 1.7259928 L 4.6539795 1.7259928 L 2.9858643 2.8985311 L 4.1263631 2.8985311 L 1.6960205 4.6064372 L 2.2236369 3.4344157 L 2.4649658 2.8985311 L 2.9925822 1.7259928 z " /> + <path + id="path815" + d="M 0,0 H 6.35 V 6.35 H 0 Z" + style="fill:none;stroke-width:0.264583" /> </svg> diff --git a/docs/en/docs/img/logo-margin/logo-teal-vector.svg b/docs/en/docs/img/logo-margin/logo-teal-vector.svg index 02183293c..3eb945c7e 100644 --- a/docs/en/docs/img/logo-margin/logo-teal-vector.svg +++ b/docs/en/docs/img/logo-margin/logo-teal-vector.svg @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" width="451.52316mm" height="162.82199mm" viewBox="0 0 451.52316 162.82198" version="1.1" - id="svg8"> + id="svg8" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -20,10 +20,28 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> + <g + id="g2106" + transform="matrix(0.96564264,0,0,0.96251987,-846.8299,244.29116)"> + <circle + style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000" + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + cx="964.56165" + cy="-169.22266" + r="33.234192" /> + <path + id="rect1249-6-3-4-4-3-6-6-1-2" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000" + d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> + </g> + <path + style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288" + d="M 142.02261,108.83303 V 53.590465 h 33.32092 v 6.616353 h -25.58855 v 16.660457 h 22.71881 v 6.536638 h -22.71881 v 25.429117 z m 52.29297,-5.34091 q 2.6306,0 4.62348,-0.0797 2.07259,-0.15943 3.42774,-0.47829 V 90.578273 q -0.79715,-0.398576 -2.63059,-0.637721 -1.75374,-0.31886 -4.30462,-0.31886 -1.67402,0 -3.58718,0.239145 -1.83345,0.239145 -3.42775,1.036296 -1.51459,0.717436 -2.55088,2.072593 -1.0363,1.275442 -1.0363,3.427749 0,3.985755 2.55089,5.580055 2.55088,1.51459 6.93521,1.51459 z m -0.63772,-37.147239 q 4.46404,0 7.49322,1.195727 3.10889,1.116011 4.94233,3.268319 1.91317,2.072593 2.71032,5.022052 0.79715,2.869743 0.79715,6.377208 V 108.1156 q -0.95658,0.15943 -2.71031,0.47829 -1.67402,0.23914 -3.82633,0.47829 -2.15231,0.23914 -4.70319,0.39857 -2.47117,0.23915 -4.94234,0.23915 -3.50746,0 -6.45692,-0.71744 -2.94946,-0.71743 -5.10177,-2.23202 -2.1523,-1.5943 -3.34803,-4.14519 -1.19573,-2.55088 -1.19573,-6.13806 0,-3.427749 1.35516,-5.898917 1.43487,-2.471168 3.82632,-3.985755 2.39146,-1.514587 5.58006,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19572,0.07972 2.23202,0.31886 1.11601,0.159431 1.91316,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39857,-3.587179 -0.39858,-1.833448 -1.43487,-3.188604 -1.0363,-1.434872 -2.86975,-2.232023 -1.75373,-0.876866 -4.62347,-0.876866 -3.6669,0 -6.45693,0.558005 -2.71031,0.478291 -4.06547,1.036297 l -0.87686,-6.138063 q 1.43487,-0.637721 4.7829,-1.195727 3.34804,-0.637721 7.25408,-0.637721 z m 37.86462,37.147239 q 4.54377,0 6.69607,-1.19573 2.23203,-1.19572 2.23203,-3.826322 0,-2.710314 -2.15231,-4.304616 -2.15231,-1.594302 -7.09465,-3.587179 -2.39145,-0.956581 -4.62347,-1.913163 -2.15231,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95659,-1.913163 -0.95659,-4.703191 0,-5.500342 4.06547,-8.688946 4.06547,-3.26832 11.0804,-3.26832 1.75374,0 3.50747,0.239146 1.75373,0.15943 3.26832,0.47829 1.51458,0.239146 2.6306,0.558006 1.19572,0.31886 1.83344,0.558006 l -1.35515,6.377208 q -1.19573,-0.637721 -3.74661,-1.275442 -2.55089,-0.717436 -6.13807,-0.717436 -3.10889,0 -5.42062,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391453 0.55801,1.036296 1.5943,1.913163 1.11601,0.797151 2.71031,1.514587 1.59431,0.717436 3.82633,1.514587 2.94946,1.116011 5.2612,2.232022 2.31173,1.036297 3.90604,2.471169 1.67401,1.434871 2.55088,3.507464 0.87687,1.992878 0.87687,4.942337 0,5.739482 -4.30462,8.688942 -4.2249,2.94946 -12.1167,2.94946 -5.50034,0 -8.60923,-0.95658 -3.10889,-0.87686 -4.2249,-1.35515 l 1.35516,-6.37721 q 1.27544,0.47829 4.06547,1.43487 2.79003,0.95658 7.4135,0.95658 z m 32.84256,-36.110942 h 15.70387 v 6.217778 h -15.70387 v 19.131625 q 0,3.108889 0.47829,5.181481 0.47829,1.992878 1.43487,3.188608 0.95658,1.11601 2.39145,1.5943 1.43487,0.47829 3.34804,0.47829 3.34803,0 5.34091,-0.71744 2.07259,-0.79715 2.86974,-1.11601 l 1.43487,6.13806 q -1.11601,0.55801 -3.90604,1.35516 -2.79003,0.87687 -6.37721,0.87687 -4.2249,0 -7.01492,-1.0363 -2.71032,-1.11601 -4.38434,-3.26832 -1.67401,-2.15231 -2.39145,-5.2612 -0.63772,-3.188599 -0.63772,-7.333784 V 55.822488 l 7.41351,-1.275442 z m 62.49652,41.451852 q -1.35516,-3.58718 -2.55088,-7.01493 -1.19573,-3.507462 -2.47117,-7.094642 h -25.03054 l -5.02205,14.109572 h -8.05123 q 3.18861,-8.76866 5.97863,-16.182165 2.79003,-7.493219 5.42063,-14.189288 2.71031,-6.696069 5.34091,-12.754416 2.6306,-6.138063 5.50034,-12.116696 h 7.09465 q 2.86974,5.978633 5.50034,12.116696 2.6306,6.058347 5.2612,12.754416 2.71031,6.696069 5.50034,14.189288 2.79003,7.413505 5.97863,16.182165 z m -7.25407,-20.48678 q -2.55089,-6.935214 -5.10177,-13.392137 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456923 -4.94234,13.392137 z m 37.86453,-35.313791 q 11.6384,0 17.85618,4.464046 6.29749,4.384331 6.29749,13.152992 0,4.782906 -1.75373,8.210656 -1.67402,3.348034 -4.94234,5.500342 -3.1886,2.072592 -7.81208,3.029174 -4.62347,0.956581 -10.44268,0.956581 h -6.13806 v 20.48678 h -7.73236 V 54.387616 q 3.26832,-0.797151 7.25407,-1.036296 4.06547,-0.318861 7.41351,-0.318861 z m 0.63772,6.775784 q -4.94234,0 -7.57294,0.239145 v 21.682508 h 5.8192 q 3.98576,0 7.17436,-0.47829 3.18861,-0.558006 5.34092,-1.753733 2.23202,-1.275441 3.42774,-3.427749 1.19573,-2.152308 1.19573,-5.500342 0,-3.188604 -1.27544,-5.261197 -1.19573,-2.072593 -3.34803,-3.268319 -2.0726,-1.275442 -4.86263,-1.753732 -2.79002,-0.478291 -5.89891,-0.478291 z m 33.16146,-6.217778 h 7.73237 v 55.242565 h -7.73237 z" + id="text979-1" + aria-label="FastAPI" /> <rect y="7.1054274e-15" x="0" @@ -33,43 +51,5 @@ style="opacity:0.98000004;fill:none;fill-opacity:1;stroke-width:0.31103656" /> <g id="layer1" - transform="translate(83.131114,-6.0791148)"> - <path - d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z" - id="path817" - style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505" /> - <g - id="text979" - style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772" - aria-label="FastAPI"> - <path - id="path845" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="M 58.970932,114.91215 V 59.669576 H 92.291849 V 66.28593 H 66.703298 v 16.660458 h 22.718807 v 6.536639 H 66.703298 v 25.429123 z" /> - <path - id="path847" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 111.2902,109.57124 q 2.6306,0 4.62348,-0.0797 2.07259,-0.15943 3.42775,-0.47829 V 96.657387 q -0.79715,-0.398575 -2.6306,-0.637721 -1.75373,-0.31886 -4.30462,-0.31886 -1.67401,0 -3.58718,0.239145 -1.83344,0.239145 -3.42775,1.036297 -1.51458,0.717436 -2.55088,2.072592 -1.0363,1.27544 -1.0363,3.42775 0,3.98576 2.55089,5.58006 2.55088,1.51459 6.93521,1.51459 z m -0.63772,-37.147247 q 4.46405,0 7.49322,1.195727 3.10889,1.116012 4.94234,3.26832 1.91316,2.072593 2.71031,5.022052 0.79715,2.869744 0.79715,6.377209 v 25.907409 q -0.95658,0.15943 -2.71031,0.47829 -1.67402,0.23915 -3.82633,0.47829 -2.1523,0.23915 -4.70319,0.39858 -2.47117,0.23914 -4.94233,0.23914 -3.50747,0 -6.45693,-0.71743 -2.94946,-0.71744 -5.101766,-2.23203 -2.152308,-1.5943 -3.348035,-4.14518 -1.195726,-2.55088 -1.195726,-6.13806 0,-3.427754 1.355157,-5.898923 1.434872,-2.471168 3.826325,-3.985755 2.391455,-1.514587 5.580055,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19573,0.07972 2.23202,0.31886 1.11601,0.15943 1.91317,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39858,-3.58718 -0.39857,-1.833447 -1.43487,-3.188604 -1.0363,-1.434872 -2.86974,-2.232023 -1.75374,-0.876867 -4.62348,-0.876867 -3.6669,0 -6.45692,0.558006 -2.71032,0.478291 -4.065475,1.036297 l -0.876866,-6.138064 q 1.434871,-0.637721 4.782911,-1.195727 3.34803,-0.637721 7.25407,-0.637721 z" /> - <path - id="path849" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 148.47606,109.57124 q 4.54376,0 6.69607,-1.19573 2.23202,-1.19573 2.23202,-3.82633 0,-2.71031 -2.1523,-4.30461 -2.15231,-1.594305 -7.09465,-3.587183 -2.39145,-0.956581 -4.62348,-1.913163 -2.1523,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95658,-1.913163 -0.95658,-4.703192 0,-5.500343 4.06547,-8.688947 4.06547,-3.26832 11.0804,-3.26832 1.75373,0 3.50746,0.239146 1.75374,0.15943 3.26832,0.47829 1.51459,0.239146 2.6306,0.558006 1.19573,0.318861 1.83345,0.558006 l -1.35516,6.377209 q -1.19572,-0.637721 -3.74661,-1.275442 -2.55088,-0.717436 -6.13806,-0.717436 -3.10889,0 -5.42063,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391454 0.55801,1.036296 1.59431,1.913162 1.11601,0.797151 2.71031,1.514587 1.5943,0.717436 3.82633,1.514587 2.94946,1.116012 5.26119,2.232024 2.31174,1.036296 3.90604,2.471168 1.67402,1.434872 2.55089,3.507465 0.87686,1.992874 0.87686,4.942334 0,5.73949 -4.30461,8.68895 -4.2249,2.94946 -12.1167,2.94946 -5.50034,0 -8.60923,-0.95658 -3.10889,-0.87687 -4.2249,-1.35516 l 1.35515,-6.37721 q 1.27545,0.47829 4.06548,1.43487 2.79002,0.95659 7.4135,0.95659 z" /> - <path - id="path851" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 181.26388,73.46029 h 15.70387 v 6.217779 h -15.70387 v 19.131626 q 0,3.108885 0.47829,5.181485 0.47829,1.99288 1.43487,3.1886 0.95658,1.11601 2.39145,1.5943 1.43488,0.47829 3.34804,0.47829 3.34803,0 5.34091,-0.71743 2.07259,-0.79715 2.86974,-1.11601 l 1.43488,6.13806 q -1.11601,0.55801 -3.90604,1.35516 -2.79003,0.87686 -6.37721,0.87686 -4.2249,0 -7.01493,-1.03629 -2.71032,-1.11601 -4.38433,-3.26832 -1.67402,-2.15231 -2.39146,-5.2612 -0.63772,-3.1886 -0.63772,-7.33379 V 61.901599 l 7.41351,-1.275442 z" /> - <path - id="path853" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 243.78793,114.91215 q -1.35515,-3.58718 -2.55088,-7.01493 -1.19573,-3.50747 -2.47117,-7.09465 h -25.03054 l -5.02206,14.10958 h -8.05122 q 3.1886,-8.76866 5.97863,-16.18217 2.79003,-7.49322 5.42063,-14.18929 2.71031,-6.696069 5.34091,-12.754417 2.6306,-6.138064 5.50034,-12.116697 h 7.09465 q 2.86974,5.978633 5.50034,12.116697 2.6306,6.058348 5.2612,12.754417 2.71031,6.69607 5.50034,14.18929 2.79003,7.41351 5.97864,16.18217 z m -7.25407,-20.486786 q -2.55089,-6.935215 -5.10177,-13.392139 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456924 -4.94234,13.392139 z" /> - <path - id="path855" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 274.32754,59.11157 q 11.6384,0 17.85618,4.464046 6.2975,4.384331 6.2975,13.152993 0,4.782907 -1.75374,8.210657 -1.67401,3.348035 -4.94233,5.500343 -3.18861,2.072592 -7.81208,3.029174 -4.62348,0.956581 -10.44268,0.956581 h -6.13807 v 20.486786 h -7.73236 V 60.466727 q 3.26832,-0.797151 7.25407,-1.036297 4.06547,-0.31886 7.41351,-0.31886 z m 0.63772,6.775784 q -4.94234,0 -7.57294,0.239146 v 21.68251 h 5.81921 q 3.98575,0 7.17436,-0.478291 3.1886,-0.558006 5.34091,-1.753732 2.23202,-1.275442 3.42775,-3.42775 1.19573,-2.152308 1.19573,-5.500343 0,-3.188604 -1.27545,-5.261197 -1.19572,-2.072593 -3.34803,-3.26832 -2.07259,-1.275441 -4.86262,-1.753732 -2.79003,-0.478291 -5.89892,-0.478291 z" /> - <path - id="path857" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 308.08066,59.669576 h 7.73236 v 55.242574 h -7.73236 z" /> - </g> - </g> + transform="translate(83.131114,-6.0791148)" /> </svg> diff --git a/docs/en/docs/img/logo-margin/logo-teal.png b/docs/en/docs/img/logo-margin/logo-teal.png index 57d9eec137c0458dd83539835397fdd299e650ed..f08144cd92e7ae62f57e34044605c56a92c1504c 100644 GIT binary patch literal 19843 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 DDxOq& literal 17680 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 XuqwmUh9A!VCZQy!E?XsS^7a1$wHKYW diff --git a/docs/en/docs/img/logo-margin/logo-teal.svg b/docs/en/docs/img/logo-margin/logo-teal.svg index 2fad25ef7..ce9db533b 100644 --- a/docs/en/docs/img/logo-margin/logo-teal.svg +++ b/docs/en/docs/img/logo-margin/logo-teal.svg @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" width="451.52316mm" height="162.82199mm" viewBox="0 0 451.52316 162.82198" version="1.1" - id="svg8"> + id="svg8" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -20,10 +20,32 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> + <g + id="g2" + transform="translate(52.499596,49.422425)"> + <g + id="g2106" + transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)"> + <circle + style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000" + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + cx="964.56165" + cy="-169.22266" + r="33.234192" /> + <path + id="rect1249-6-3-4-4-3-6-6-1-2" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000" + d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> + </g> + <path + style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288" + d="M 89.523017,59.410606 V 4.1680399 H 122.84393 V 10.784393 H 97.255382 V 27.44485 h 22.718808 v 6.536638 H 97.255382 v 25.429118 z m 52.292963,-5.340912 q 2.6306,0 4.62348,-0.07972 2.07259,-0.15943 3.42774,-0.47829 V 41.155848 q -0.79715,-0.398576 -2.63059,-0.637721 -1.75374,-0.31886 -4.30462,-0.31886 -1.67402,0 -3.58718,0.239145 -1.83345,0.239145 -3.42775,1.036296 -1.51459,0.717436 -2.55088,2.072593 -1.0363,1.275442 -1.0363,3.427749 0,3.985755 2.55089,5.580058 2.55088,1.514586 6.93521,1.514586 z m -0.63772,-37.147238 q 4.46404,0 7.49322,1.195727 3.10889,1.116011 4.94233,3.268319 1.91317,2.072593 2.71032,5.022052 0.79715,2.869743 0.79715,6.377208 V 58.69317 q -0.95658,0.159431 -2.71031,0.478291 -1.67402,0.239145 -3.82633,0.478291 -2.15231,0.239145 -4.70319,0.398575 -2.47117,0.239146 -4.94234,0.239146 -3.50746,0 -6.45692,-0.717436 -2.94946,-0.717436 -5.10177,-2.232023 -2.1523,-1.594302 -3.34803,-4.145186 -1.19573,-2.550883 -1.19573,-6.138063 0,-3.427749 1.35516,-5.898917 1.43487,-2.471168 3.82632,-3.985755 2.39146,-1.514587 5.58006,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19572,0.07972 2.23202,0.31886 1.11601,0.159431 1.91316,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39857,-3.587179 -0.39858,-1.833448 -1.43487,-3.188604 -1.0363,-1.434872 -2.86975,-2.232023 -1.75373,-0.876866 -4.62347,-0.876866 -3.6669,0 -6.45693,0.558005 -2.71031,0.478291 -4.06547,1.036297 l -0.87686,-6.138063 q 1.43487,-0.637721 4.7829,-1.195727 3.34804,-0.637721 7.25408,-0.637721 z m 37.86462,37.147238 q 4.54377,0 6.69607,-1.195726 2.23203,-1.195727 2.23203,-3.826325 0,-2.710314 -2.15231,-4.304616 -2.15231,-1.594302 -7.09465,-3.587179 -2.39145,-0.956581 -4.62347,-1.913163 -2.15231,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95659,-1.913163 -0.95659,-4.703191 0,-5.500342 4.06547,-8.688946 4.06547,-3.26832 11.0804,-3.26832 1.75374,0 3.50747,0.239146 1.75373,0.15943 3.26832,0.47829 1.51458,0.239146 2.6306,0.558006 1.19572,0.31886 1.83344,0.558006 l -1.35515,6.377208 q -1.19573,-0.637721 -3.74661,-1.275442 -2.55089,-0.717436 -6.13807,-0.717436 -3.10889,0 -5.42062,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391453 0.55801,1.036296 1.5943,1.913163 1.11601,0.797151 2.71031,1.514587 1.59431,0.717436 3.82633,1.514587 2.94946,1.116011 5.2612,2.232022 2.31173,1.036297 3.90604,2.471169 1.67401,1.434871 2.55088,3.507464 0.87687,1.992878 0.87687,4.942337 0,5.739487 -4.30462,8.688946 -4.2249,2.949459 -12.1167,2.949459 -5.50034,0 -8.60923,-0.956582 -3.10889,-0.876866 -4.2249,-1.355156 l 1.35516,-6.377209 q 1.27544,0.478291 4.06547,1.434872 2.79003,0.956581 7.4135,0.956581 z m 32.84256,-36.110941 h 15.70387 v 6.217778 h -15.70387 v 19.131625 q 0,3.108889 0.47829,5.181481 0.47829,1.992878 1.43487,3.188604 0.95658,1.116012 2.39145,1.594302 1.43487,0.478291 3.34804,0.478291 3.34803,0 5.34091,-0.717436 2.07259,-0.797151 2.86974,-1.116011 l 1.43487,6.138063 q -1.11601,0.558005 -3.90604,1.355156 -2.79003,0.876867 -6.37721,0.876867 -4.2249,0 -7.01492,-1.036297 -2.71032,-1.116011 -4.38434,-3.268319 -1.67401,-2.152308 -2.39145,-5.261197 -0.63772,-3.188604 -0.63772,-7.333789 V 6.4000628 l 7.41351,-1.2754417 z m 62.49652,41.451853 q -1.35516,-3.587179 -2.55088,-7.014929 -1.19573,-3.507464 -2.47117,-7.094644 h -25.03054 l -5.02205,14.109573 h -8.05123 q 3.18861,-8.768661 5.97863,-16.182166 2.79003,-7.493219 5.42063,-14.189288 2.71031,-6.696069 5.34091,-12.754416 2.6306,-6.138063 5.50034,-12.1166961 h 7.09465 q 2.86974,5.9786331 5.50034,12.1166961 2.6306,6.058347 5.2612,12.754416 2.71031,6.696069 5.50034,14.189288 2.79003,7.413505 5.97863,16.182166 z m -7.25407,-20.486781 q -2.55089,-6.935214 -5.10177,-13.392137 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456923 -4.94234,13.392137 z M 304.99242,3.6100342 q 11.6384,0 17.85618,4.4640458 6.29749,4.384331 6.29749,13.152992 0,4.782906 -1.75373,8.210656 -1.67402,3.348034 -4.94234,5.500342 -3.1886,2.072592 -7.81208,3.029174 -4.62347,0.956581 -10.44268,0.956581 h -6.13806 v 20.486781 h -7.73236 V 4.9651909 q 3.26832,-0.797151 7.25407,-1.0362963 4.06547,-0.3188604 7.41351,-0.3188604 z m 0.63772,6.7757838 q -4.94234,0 -7.57294,0.239145 v 21.682508 h 5.8192 q 3.98576,0 7.17436,-0.47829 3.18861,-0.558006 5.34092,-1.753733 2.23202,-1.275441 3.42774,-3.427749 1.19573,-2.152308 1.19573,-5.500342 0,-3.188604 -1.27544,-5.261197 -1.19573,-2.072593 -3.34803,-3.268319 -2.0726,-1.275442 -4.86263,-1.753732 -2.79002,-0.478291 -5.89891,-0.478291 z M 338.7916,4.1680399 h 7.73237 V 59.410606 h -7.73237 z" + id="text979-1" + aria-label="FastAPI" /> + </g> <rect y="7.1054274e-15" x="0" @@ -33,20 +55,5 @@ style="opacity:0.98000004;fill:none;fill-opacity:1;stroke-width:0.31103656" /> <g id="layer1" - transform="translate(83.131114,-6.0791148)"> - <path - d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z" - id="path817" - style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505" /> - <text - xml:space="preserve" - style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772" - x="52.115433" - y="114.91215" - id="text979"><tspan - id="tspan977" - x="52.115433" - y="114.91215" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772">FastAPI</tspan></text> - </g> + transform="translate(83.131114,-6.0791148)" /> </svg> diff --git a/docs/en/docs/img/logo-margin/logo-white-bg.png b/docs/en/docs/img/logo-margin/logo-white-bg.png index 89256db06f02eca727a47a373f33a313833b4952..b84fd285ee52d721a29af6e79fafb6ade5987469 100644 GIT binary patch literal 20841 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 Cn`D*% literal 21502 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 diff --git a/docs/en/docs/img/logo-teal-vector.svg b/docs/en/docs/img/logo-teal-vector.svg index c1d1b72e4..d3dad4bec 100644 --- a/docs/en/docs/img/logo-teal-vector.svg +++ b/docs/en/docs/img/logo-teal-vector.svg @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - width="346.36511mm" - height="63.977139mm" - viewBox="0 0 346.36511 63.977134" + id="svg8" version="1.1" - id="svg8"> + viewBox="0 0 346.52395 63.977134" + height="63.977139mm" + width="346.52396mm" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -20,49 +20,32 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g - id="layer1" - transform="translate(30.552089,-55.50154)"> - <path - d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z" - id="path817" - style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505" /> + id="g2149"> <g - id="text979" - style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772" - aria-label="FastAPI"> + id="g2141"> + <g + id="g2106" + transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)"> + <circle + style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000" + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + cx="964.56165" + cy="-169.22266" + r="33.234192" /> + <path + id="rect1249-6-3-4-4-3-6-6-1-2" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000" + d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> + </g> <path - id="path923" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="M 58.970932,114.91215 V 59.669576 H 92.291849 V 66.28593 H 66.703298 v 16.660458 h 22.718807 v 6.536639 H 66.703298 v 25.429123 z" /> - <path - id="path925" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 111.2902,109.57124 q 2.6306,0 4.62348,-0.0797 2.07259,-0.15943 3.42775,-0.47829 V 96.657387 q -0.79715,-0.398575 -2.6306,-0.637721 -1.75373,-0.31886 -4.30462,-0.31886 -1.67401,0 -3.58718,0.239145 -1.83344,0.239145 -3.42775,1.036297 -1.51458,0.717436 -2.55088,2.072592 -1.0363,1.27544 -1.0363,3.42775 0,3.98576 2.55089,5.58006 2.55088,1.51459 6.93521,1.51459 z m -0.63772,-37.147247 q 4.46405,0 7.49322,1.195727 3.10889,1.116012 4.94234,3.26832 1.91316,2.072593 2.71031,5.022052 0.79715,2.869744 0.79715,6.377209 v 25.907409 q -0.95658,0.15943 -2.71031,0.47829 -1.67402,0.23915 -3.82633,0.47829 -2.1523,0.23915 -4.70319,0.39858 -2.47117,0.23914 -4.94233,0.23914 -3.50747,0 -6.45693,-0.71743 -2.94946,-0.71744 -5.101766,-2.23203 -2.152308,-1.5943 -3.348035,-4.14518 -1.195726,-2.55088 -1.195726,-6.13806 0,-3.427754 1.355157,-5.898923 1.434872,-2.471168 3.826325,-3.985755 2.391455,-1.514587 5.580055,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19573,0.07972 2.23202,0.31886 1.11601,0.15943 1.91317,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39858,-3.58718 -0.39857,-1.833447 -1.43487,-3.188604 -1.0363,-1.434872 -2.86974,-2.232023 -1.75374,-0.876867 -4.62348,-0.876867 -3.6669,0 -6.45692,0.558006 -2.71032,0.478291 -4.065475,1.036297 l -0.876866,-6.138064 q 1.434871,-0.637721 4.782911,-1.195727 3.34803,-0.637721 7.25407,-0.637721 z" /> - <path - id="path927" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 148.47606,109.57124 q 4.54376,0 6.69607,-1.19573 2.23202,-1.19573 2.23202,-3.82633 0,-2.71031 -2.1523,-4.30461 -2.15231,-1.594305 -7.09465,-3.587183 -2.39145,-0.956581 -4.62348,-1.913163 -2.1523,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95658,-1.913163 -0.95658,-4.703192 0,-5.500343 4.06547,-8.688947 4.06547,-3.26832 11.0804,-3.26832 1.75373,0 3.50746,0.239146 1.75374,0.15943 3.26832,0.47829 1.51459,0.239146 2.6306,0.558006 1.19573,0.318861 1.83345,0.558006 l -1.35516,6.377209 q -1.19572,-0.637721 -3.74661,-1.275442 -2.55088,-0.717436 -6.13806,-0.717436 -3.10889,0 -5.42063,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391454 0.55801,1.036296 1.59431,1.913162 1.11601,0.797151 2.71031,1.514587 1.5943,0.717436 3.82633,1.514587 2.94946,1.116012 5.26119,2.232024 2.31174,1.036296 3.90604,2.471168 1.67402,1.434872 2.55089,3.507465 0.87686,1.992874 0.87686,4.942334 0,5.73949 -4.30461,8.68895 -4.2249,2.94946 -12.1167,2.94946 -5.50034,0 -8.60923,-0.95658 -3.10889,-0.87687 -4.2249,-1.35516 l 1.35515,-6.37721 q 1.27545,0.47829 4.06548,1.43487 2.79002,0.95659 7.4135,0.95659 z" /> - <path - id="path929" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 181.26388,73.46029 h 15.70387 v 6.217779 h -15.70387 v 19.131626 q 0,3.108885 0.47829,5.181485 0.47829,1.99288 1.43487,3.1886 0.95658,1.11601 2.39145,1.5943 1.43488,0.47829 3.34804,0.47829 3.34803,0 5.34091,-0.71743 2.07259,-0.79715 2.86974,-1.11601 l 1.43488,6.13806 q -1.11601,0.55801 -3.90604,1.35516 -2.79003,0.87686 -6.37721,0.87686 -4.2249,0 -7.01493,-1.03629 -2.71032,-1.11601 -4.38433,-3.26832 -1.67402,-2.15231 -2.39146,-5.2612 -0.63772,-3.1886 -0.63772,-7.33379 V 61.901599 l 7.41351,-1.275442 z" /> - <path - id="path931" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 243.78793,114.91215 q -1.35515,-3.58718 -2.55088,-7.01493 -1.19573,-3.50747 -2.47117,-7.09465 h -25.03054 l -5.02206,14.10958 h -8.05122 q 3.1886,-8.76866 5.97863,-16.18217 2.79003,-7.49322 5.42063,-14.18929 2.71031,-6.696069 5.34091,-12.754417 2.6306,-6.138064 5.50034,-12.116697 h 7.09465 q 2.86974,5.978633 5.50034,12.116697 2.6306,6.058348 5.2612,12.754417 2.71031,6.69607 5.50034,14.18929 2.79003,7.41351 5.97864,16.18217 z m -7.25407,-20.486786 q -2.55089,-6.935215 -5.10177,-13.392139 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456924 -4.94234,13.392139 z" /> - <path - id="path933" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 274.32754,59.11157 q 11.6384,0 17.85618,4.464046 6.2975,4.384331 6.2975,13.152993 0,4.782907 -1.75374,8.210657 -1.67401,3.348035 -4.94233,5.500343 -3.18861,2.072592 -7.81208,3.029174 -4.62348,0.956581 -10.44268,0.956581 h -6.13807 v 20.486786 h -7.73236 V 60.466727 q 3.26832,-0.797151 7.25407,-1.036297 4.06547,-0.31886 7.41351,-0.31886 z m 0.63772,6.775784 q -4.94234,0 -7.57294,0.239146 v 21.68251 h 5.81921 q 3.98575,0 7.17436,-0.478291 3.1886,-0.558006 5.34091,-1.753732 2.23202,-1.275442 3.42775,-3.42775 1.19573,-2.152308 1.19573,-5.500343 0,-3.188604 -1.27545,-5.261197 -1.19572,-2.072593 -3.34803,-3.26832 -2.07259,-1.275441 -4.86262,-1.753732 -2.79003,-0.478291 -5.89892,-0.478291 z" /> - <path - id="path935" - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772" - d="m 308.08066,59.669576 h 7.73236 v 55.242574 h -7.73236 z" /> + style="font-size:79.7151px;line-height:1.25;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;letter-spacing:0px;word-spacing:0px;fill:#009688;stroke-width:1.99288" + d="M 89.523017,59.410606 V 4.1680399 H 122.84393 V 10.784393 H 97.255382 V 27.44485 h 22.718808 v 6.536638 H 97.255382 v 25.429118 z m 52.292963,-5.340912 q 2.6306,0 4.62348,-0.07972 2.07259,-0.15943 3.42774,-0.47829 V 41.155848 q -0.79715,-0.398576 -2.63059,-0.637721 -1.75374,-0.31886 -4.30462,-0.31886 -1.67402,0 -3.58718,0.239145 -1.83345,0.239145 -3.42775,1.036296 -1.51459,0.717436 -2.55088,2.072593 -1.0363,1.275442 -1.0363,3.427749 0,3.985755 2.55089,5.580058 2.55088,1.514586 6.93521,1.514586 z m -0.63772,-37.147238 q 4.46404,0 7.49322,1.195727 3.10889,1.116011 4.94233,3.268319 1.91317,2.072593 2.71032,5.022052 0.79715,2.869743 0.79715,6.377208 V 58.69317 q -0.95658,0.159431 -2.71031,0.478291 -1.67402,0.239145 -3.82633,0.478291 -2.15231,0.239145 -4.70319,0.398575 -2.47117,0.239146 -4.94234,0.239146 -3.50746,0 -6.45692,-0.717436 -2.94946,-0.717436 -5.10177,-2.232023 -2.1523,-1.594302 -3.34803,-4.145186 -1.19573,-2.550883 -1.19573,-6.138063 0,-3.427749 1.35516,-5.898917 1.43487,-2.471168 3.82632,-3.985755 2.39146,-1.514587 5.58006,-2.232023 3.18861,-0.717436 6.69607,-0.717436 1.11601,0 2.31174,0.15943 1.19572,0.07972 2.23202,0.31886 1.11601,0.159431 1.91316,0.318861 0.79715,0.15943 1.11601,0.239145 v -2.072593 q 0,-1.833447 -0.39857,-3.587179 -0.39858,-1.833448 -1.43487,-3.188604 -1.0363,-1.434872 -2.86975,-2.232023 -1.75373,-0.876866 -4.62347,-0.876866 -3.6669,0 -6.45693,0.558005 -2.71031,0.478291 -4.06547,1.036297 l -0.87686,-6.138063 q 1.43487,-0.637721 4.7829,-1.195727 3.34804,-0.637721 7.25408,-0.637721 z m 37.86462,37.147238 q 4.54377,0 6.69607,-1.195726 2.23203,-1.195727 2.23203,-3.826325 0,-2.710314 -2.15231,-4.304616 -2.15231,-1.594302 -7.09465,-3.587179 -2.39145,-0.956581 -4.62347,-1.913163 -2.15231,-1.036296 -3.74661,-2.391453 -1.5943,-1.355157 -2.55088,-3.268319 -0.95659,-1.913163 -0.95659,-4.703191 0,-5.500342 4.06547,-8.688946 4.06547,-3.26832 11.0804,-3.26832 1.75374,0 3.50747,0.239146 1.75373,0.15943 3.26832,0.47829 1.51458,0.239146 2.6306,0.558006 1.19572,0.31886 1.83344,0.558006 l -1.35515,6.377208 q -1.19573,-0.637721 -3.74661,-1.275442 -2.55089,-0.717436 -6.13807,-0.717436 -3.10889,0 -5.42062,1.275442 -2.31174,1.195727 -2.31174,3.826325 0,1.355157 0.47829,2.391453 0.55801,1.036296 1.5943,1.913163 1.11601,0.797151 2.71031,1.514587 1.59431,0.717436 3.82633,1.514587 2.94946,1.116011 5.2612,2.232022 2.31173,1.036297 3.90604,2.471169 1.67401,1.434871 2.55088,3.507464 0.87687,1.992878 0.87687,4.942337 0,5.739487 -4.30462,8.688946 -4.2249,2.949459 -12.1167,2.949459 -5.50034,0 -8.60923,-0.956582 -3.10889,-0.876866 -4.2249,-1.355156 l 1.35516,-6.377209 q 1.27544,0.478291 4.06547,1.434872 2.79003,0.956581 7.4135,0.956581 z m 32.84256,-36.110941 h 15.70387 v 6.217778 h -15.70387 v 19.131625 q 0,3.108889 0.47829,5.181481 0.47829,1.992878 1.43487,3.188604 0.95658,1.116012 2.39145,1.594302 1.43487,0.478291 3.34804,0.478291 3.34803,0 5.34091,-0.717436 2.07259,-0.797151 2.86974,-1.116011 l 1.43487,6.138063 q -1.11601,0.558005 -3.90604,1.355156 -2.79003,0.876867 -6.37721,0.876867 -4.2249,0 -7.01492,-1.036297 -2.71032,-1.116011 -4.38434,-3.268319 -1.67401,-2.152308 -2.39145,-5.261197 -0.63772,-3.188604 -0.63772,-7.333789 V 6.4000628 l 7.41351,-1.2754417 z m 62.49652,41.451853 q -1.35516,-3.587179 -2.55088,-7.014929 -1.19573,-3.507464 -2.47117,-7.094644 h -25.03054 l -5.02205,14.109573 h -8.05123 q 3.18861,-8.768661 5.97863,-16.182166 2.79003,-7.493219 5.42063,-14.189288 2.71031,-6.696069 5.34091,-12.754416 2.6306,-6.138063 5.50034,-12.1166961 h 7.09465 q 2.86974,5.9786331 5.50034,12.1166961 2.6306,6.058347 5.2612,12.754416 2.71031,6.696069 5.50034,14.189288 2.79003,7.413505 5.97863,16.182166 z m -7.25407,-20.486781 q -2.55089,-6.935214 -5.10177,-13.392137 -2.47117,-6.536639 -5.18148,-12.515272 -2.79003,5.978633 -5.34091,12.515272 -2.47117,6.456923 -4.94234,13.392137 z M 304.99242,3.6100342 q 11.6384,0 17.85618,4.4640458 6.29749,4.384331 6.29749,13.152992 0,4.782906 -1.75373,8.210656 -1.67402,3.348034 -4.94234,5.500342 -3.1886,2.072592 -7.81208,3.029174 -4.62347,0.956581 -10.44268,0.956581 h -6.13806 v 20.486781 h -7.73236 V 4.9651909 q 3.26832,-0.797151 7.25407,-1.0362963 4.06547,-0.3188604 7.41351,-0.3188604 z m 0.63772,6.7757838 q -4.94234,0 -7.57294,0.239145 v 21.682508 h 5.8192 q 3.98576,0 7.17436,-0.47829 3.18861,-0.558006 5.34092,-1.753733 2.23202,-1.275441 3.42774,-3.427749 1.19573,-2.152308 1.19573,-5.500342 0,-3.188604 -1.27544,-5.261197 -1.19573,-2.072593 -3.34803,-3.268319 -2.0726,-1.275442 -4.86263,-1.753732 -2.79002,-0.478291 -5.89891,-0.478291 z M 338.7916,4.1680399 h 7.73237 V 59.410606 h -7.73237 z" + id="text979" + aria-label="FastAPI" /> </g> </g> </svg> diff --git a/docs/en/docs/img/logo-teal.svg b/docs/en/docs/img/logo-teal.svg index 0d1136eb4..bc699860f 100644 --- a/docs/en/docs/img/logo-teal.svg +++ b/docs/en/docs/img/logo-teal.svg @@ -1,15 +1,15 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" - viewBox="0 0 346.36511 63.977134" + viewBox="0 0 346.52395 63.977134" height="63.977139mm" - width="346.36511mm"> + width="346.52396mm" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> <defs id="defs2" /> <metadata @@ -20,26 +20,37 @@ <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <g - transform="translate(30.552089,-55.50154)" - id="layer1"> - <path - style="opacity:0.98000004;fill:#009688;fill-opacity:1;stroke-width:3.20526505" - id="path817" - d="m 1.4365174,55.50154 c -17.6610514,0 -31.9886064,14.327532 -31.9886064,31.988554 0,17.661036 14.327555,31.988586 31.9886064,31.988586 17.6609756,0 31.9885196,-14.32755 31.9885196,-31.988586 0,-17.661022 -14.327544,-31.988554 -31.9885196,-31.988554 z m -1.66678692,57.63069 V 93.067264 H -11.384533 L 4.6417437,61.847974 V 81.912929 H 15.379405 Z" /> - <text - id="text979" - y="114.91215" - x="52.115433" - style="font-style:normal;font-weight:normal;font-size:79.71511078px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99287772;" - xml:space="preserve"><tspan - style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99287772;" - y="114.91215" - x="52.115433" - id="tspan977">FastAPI</tspan></text> + id="g2149"> + <g + id="g2141"> + <g + id="g2106" + transform="matrix(0.96564264,0,0,0.96251987,-899.3295,194.86874)"> + <circle + style="fill:#009688;fill-opacity:0.980392;stroke:none;stroke-width:0.141404;stop-color:#000000" + id="path875-5-9-7-3-2-3-9-9-8-0-0-5-87-7" + cx="964.56165" + cy="-169.22266" + r="33.234192" /> + <path + id="rect1249-6-3-4-4-3-6-6-1-2" + style="fill:#ffffff;fill-opacity:0.980392;stroke:none;stroke-width:0.146895;stop-color:#000000" + d="m 962.2685,-187.40837 -6.64403,14.80375 -3.03599,6.76393 -6.64456,14.80375 30.59142,-21.56768 h -14.35312 l 20.99715,-14.80375 z" /> + </g> + <text + id="text979" + y="59.410606" + x="82.667519" + style="font-style:normal;font-weight:normal;font-size:79.7151px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#009688;fill-opacity:1;stroke:none;stroke-width:1.99288" + xml:space="preserve"><tspan + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#009688;fill-opacity:1;stroke-width:1.99288" + y="59.410606" + x="82.667519" + id="tspan977">FastAPI</tspan></text> + </g> </g> </svg> diff --git a/docs/en/overrides/partials/copyright.html b/docs/en/overrides/partials/copyright.html new file mode 100644 index 000000000..dcca89abe --- /dev/null +++ b/docs/en/overrides/partials/copyright.html @@ -0,0 +1,11 @@ +<div class="md-copyright"> + <div class="md-copyright__highlight"> + The FastAPI trademark is owned by <a href="https://tiangolo.com" target="_blank">@tiangolo</a> and is registered in the US and across other regions + </div> + {% if not config.extra.generator == false %} + Made with + <a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener"> + Material for MkDocs + </a> + {% endif %} +</div> From 43f9cbc0fced5be44ac469e1c543620ac42df880 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 4 Feb 2024 20:57:18 +0000 Subject: [PATCH 1873/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 2dcfac473..6e42281cf 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * :globe_with_meridians: Update Turkish translation for `docs/tr/docs/fastapi-people.md`. PR [#10547](https://github.com/tiangolo/fastapi/pull/10547) by [@alperiox](https://github.com/alperiox). +### Internal + +* 🍱 Add new FastAPI logo. PR [#11090](https://github.com/tiangolo/fastapi/pull/11090) by [@tiangolo](https://github.com/tiangolo). + ## 0.109.1 ### Security fixes From 4a2be2abff43260fdd49ecf72cde2425adffdfe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 4 Feb 2024 22:14:53 +0100 Subject: [PATCH 1874/1881] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20versio?= =?UTF-8?q?n=20of=20Starlette=20to=20`>=3D=200.36.3`=20(#11086)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31d9c59b3..c23e82ef4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ "Topic :: Internet :: WWW/HTTP", ] dependencies = [ - "starlette>=0.35.0,<0.36.0", + "starlette>=0.36.3,<0.37.0", "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0", "typing-extensions>=4.8.0", ] From 50e558e944860fb237cb5934e899ce3c5ecc37f9 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Sun, 4 Feb 2024 21:15:15 +0000 Subject: [PATCH 1875/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 6e42281cf..490952466 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Upgrades + +* ⬆️ Upgrade version of Starlette to `>= 0.36.3`. PR [#11086](https://github.com/tiangolo/fastapi/pull/11086) by [@tiangolo](https://github.com/tiangolo). + ### Translations * :globe_with_meridians: Update Turkish translation for `docs/tr/docs/fastapi-people.md`. PR [#10547](https://github.com/tiangolo/fastapi/pull/10547) by [@alperiox](https://github.com/alperiox). From 57b0983948617c6ac051f9c29947bd9db6547e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 4 Feb 2024 22:21:53 +0100 Subject: [PATCH 1876/1881] =?UTF-8?q?=F0=9F=94=96=20Release=20FastAPI=20ve?= =?UTF-8?q?rsion=200.109.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 490952466..71b79bccc 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.109.2 + ### Upgrades * ⬆️ Upgrade version of Starlette to `>= 0.36.3`. PR [#11086](https://github.com/tiangolo/fastapi/pull/11086) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index fedd8b419..3458b9e5b 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.109.1" +__version__ = "0.109.2" from starlette import status as status From 141e34f281ed746431766d712d56d357e306a689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= <tiangolo@gmail.com> Date: Sun, 4 Feb 2024 22:24:21 +0100 Subject: [PATCH 1877/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 71b79bccc..c0e47b9f5 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -15,7 +15,7 @@ hide: ### Translations -* :globe_with_meridians: Update Turkish translation for `docs/tr/docs/fastapi-people.md`. PR [#10547](https://github.com/tiangolo/fastapi/pull/10547) by [@alperiox](https://github.com/alperiox). +* 🌐 Update Turkish translation for `docs/tr/docs/fastapi-people.md`. PR [#10547](https://github.com/tiangolo/fastapi/pull/10547) by [@alperiox](https://github.com/alperiox). ### Internal From 0880a5c6a0d2702b057cef8064690f63ff1afc78 Mon Sep 17 00:00:00 2001 From: Jacob Hayes <jacob.r.hayes@gmail.com> Date: Tue, 6 Feb 2024 12:21:29 -0500 Subject: [PATCH 1878/1881] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fix=20minor=20ty?= =?UTF-8?q?po=20in=20`fastapi/applications.py`=20(#11099)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 597c60a56..ffe9da358 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -297,7 +297,7 @@ class FastAPI(Starlette): browser tabs open). Or if you want to leave fixed the possible URLs. If the servers `list` is not provided, or is an empty `list`, the - default value would be a a `dict` with a `url` value of `/`. + default value would be a `dict` with a `url` value of `/`. Each item in the `list` is a `dict` containing: From d9cacacf7f786e43eb30ec430ca20ca0d9960171 Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 6 Feb 2024 17:21:53 +0000 Subject: [PATCH 1879/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index c0e47b9f5..1f0a9a7d2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### Docs + +* ✏️ Fix minor typo in `fastapi/applications.py`. PR [#11099](https://github.com/tiangolo/fastapi/pull/11099) by [@JacobHayes](https://github.com/JacobHayes). + ## 0.109.2 ### Upgrades From b9766d7ee9af21daf0eaa6caac9634feff487afb Mon Sep 17 00:00:00 2001 From: Alejandra <90076947+alejsdev@users.noreply.github.com> Date: Tue, 6 Feb 2024 14:56:23 -0500 Subject: [PATCH 1880/1881] =?UTF-8?q?=F0=9F=8C=90=20Add=20Spanish=20transl?= =?UTF-8?q?ation=20for=20`docs/es/docs/advanced/response-change-status-cod?= =?UTF-8?q?e.md`=20(#11100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../advanced/response-change-status-code.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/es/docs/advanced/response-change-status-code.md diff --git a/docs/es/docs/advanced/response-change-status-code.md b/docs/es/docs/advanced/response-change-status-code.md new file mode 100644 index 000000000..ecd9fad41 --- /dev/null +++ b/docs/es/docs/advanced/response-change-status-code.md @@ -0,0 +1,33 @@ +# Response - Cambiar el Status Code + +Probablemente ya has leído con anterioridad que puedes establecer un [Response Status Code](../tutorial/response-status-code.md){.internal-link target=_blank} por defecto. + +Pero en algunos casos necesitas retornar un status code diferente al predeterminado. + +## Casos de uso + +Por ejemplo, imagina que quieres retornar un HTTP status code de "OK" `200` por defecto. + +Pero si los datos no existen, quieres crearlos y retornar un HTTP status code de "CREATED" `201`. + +Pero aún quieres poder filtrar y convertir los datos que retornas con un `response_model`. + +Para esos casos, puedes usar un parámetro `Response`. + +## Usar un parámetro `Response` + +Puedes declarar un parámetro de tipo `Response` en tu *función de la operación de path* (como puedes hacer para cookies y headers). + +Y luego puedes establecer el `status_code` en ese objeto de respuesta *temporal*. + +```Python hl_lines="1 9 12" +{!../../../docs_src/response_change_status_code/tutorial001.py!} +``` + +Y luego puedes retornar cualquier objeto que necesites, como normalmente lo harías (un `dict`, un modelo de base de datos, etc). + +Y si declaraste un `response_model`, aún se usará para filtrar y convertir el objeto que retornaste. + +**FastAPI** usará esa respuesta *temporal* para extraer el código de estado (también cookies y headers), y los pondrá en la respuesta final que contiene el valor que retornaste, filtrado por cualquier `response_model`. + +También puedes declarar la dependencia del parámetro `Response`, y establecer el código de estado en ellos. Pero ten en cuenta que el último en establecerse será el que gane. From e79bd168a441b931eb6a9762966e63b40ab3575b Mon Sep 17 00:00:00 2001 From: github-actions <github-actions@github.com> Date: Tue, 6 Feb 2024 19:56:42 +0000 Subject: [PATCH 1881/1881] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 1f0a9a7d2..5ce56a3ac 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * ✏️ Fix minor typo in `fastapi/applications.py`. PR [#11099](https://github.com/tiangolo/fastapi/pull/11099) by [@JacobHayes](https://github.com/JacobHayes). +### Translations + +* 🌐 Add Spanish translation for `docs/es/docs/advanced/response-change-status-code.md`. PR [#11100](https://github.com/tiangolo/fastapi/pull/11100) by [@alejsdev](https://github.com/alejsdev). + ## 0.109.2 ### Upgrades

F$&nSGjm+)B(l^i#fk@M42$y;{?fP5NJM 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Ⓢ<%VQ556rL)Q= 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